From 7d1278ad468b3f0b398e46289ecea0712508c892 Mon Sep 17 00:00:00 2001 From: Alex Kalderimis Date: Wed, 18 Nov 2020 20:32:39 +0000 Subject: [PATCH 1/9] Prepare tests for framework authorization changes This mostly adds connection methods in preparation for the graphql helper changes. None of the changes in this commit should cause any observable effects on application behaviour. This includes - removal of pagination tests from resolver specs (resolvers do not - currently - know enough of their arguments to make this work). At the moment we need to run all our pagination specs at the request integration level. This is the same for N+1 specs must always be run in request specs, since otherwise they are unreliable. --- .../prometheus_integration_type.rb | 2 +- .../lazy_aggregate.rb | 2 + .../ee/resolvers/issues_resolver_spec.rb | 14 ++--- .../board_groupings/epics_resolvers_spec.rb | 7 ++- .../clusters/agents_resolver_spec.rb | 41 +++++++------ .../resolvers/epic_issues_resolver_spec.rb | 26 ++++----- .../graphql/resolvers/epics_resolver_spec.rb | 2 +- .../requirements_resolver_spec.rb | 16 ++---- .../security_report_summary_resolver_spec.rb | 4 +- .../vulnerabilities_grade_resolver_spec.rb | 5 +- .../graphql/group/epic/epic_issues_spec.rb | 57 ++++++++++--------- .../instance_security_dashboard_spec.rb | 38 ++++++------- lib/gitlab/graphql/deferred.rb | 12 ++++ .../graphql/externally_paginated_array.rb | 6 +- lib/gitlab/graphql/laziness.rb | 37 ++++++++++++ lib/gitlab/graphql/lazy.rb | 2 + .../externally_paginated_array_connection.rb | 8 +-- .../alert_management/alert_resolver_spec.rb | 2 +- .../board_list_issues_resolver_spec.rb | 2 +- .../resolvers/ci/jobs_resolver_spec.rb | 20 ++++--- .../commit_pipelines_resolver_spec.rb | 2 +- .../concerns/caching_array_resolver_spec.rb | 42 ++++++++------ .../design_at_version_resolver_spec.rb | 6 +- .../version_in_collection_resolver_spec.rb | 6 +- .../versions_resolver_spec.rb | 18 +++--- .../sentry_errors_resolver_spec.rb | 4 +- .../issue_status_counts_resolver_spec.rb | 11 +--- .../graphql/resolvers/issues_resolver_spec.rb | 48 +++++++++------- .../resolvers/merge_requests_resolver_spec.rb | 22 +++---- .../project_merge_requests_resolver_spec.rb | 6 +- .../projects/snippets_resolver_spec.rb | 26 ++++++--- .../resolvers/releases_resolver_spec.rb | 21 +++++-- .../resolvers/snippets_resolver_spec.rb | 11 ++-- spec/graphql/resolvers/todo_resolver_spec.rb | 4 +- .../resolvers/users/snippets_resolver_spec.rb | 13 ++--- .../prometheus_integration_type_spec.rb | 5 +- .../admin/sidekiq_queues/delete_jobs_spec.rb | 12 ++-- .../graphql/mutations/snippets/create_spec.rb | 8 ++- .../issue/design_collection/version_spec.rb | 3 +- .../api/graphql/project/issue_spec.rb | 6 +- .../api/graphql/project/release_spec.rb | 10 ++-- .../graphql/project/terraform/states_spec.rb | 53 ++++++++--------- .../api/graphql/project_query_spec.rb | 28 ++++----- .../graphql/design_fields_shared_examples.rb | 30 ++++++---- .../jira_import_resolver_shared_examples.rb | 8 +-- .../graphql/mutation_shared_examples.rb | 2 + .../spammable_mutation_fields_examples.rb | 7 ++- .../services_resolver_shared_examples.rb | 2 +- 48 files changed, 411 insertions(+), 306 deletions(-) create mode 100644 lib/gitlab/graphql/deferred.rb create mode 100644 lib/gitlab/graphql/laziness.rb diff --git a/app/graphql/types/alert_management/prometheus_integration_type.rb b/app/graphql/types/alert_management/prometheus_integration_type.rb index f605e325b8bc52..79f265f2f1e1fe 100644 --- a/app/graphql/types/alert_management/prometheus_integration_type.rb +++ b/app/graphql/types/alert_management/prometheus_integration_type.rb @@ -2,7 +2,7 @@ module Types module AlertManagement - class PrometheusIntegrationType < BaseObject + class PrometheusIntegrationType < ::Types::BaseObject include ::Gitlab::Routing graphql_name 'AlertManagementPrometheusIntegration' diff --git a/ee/lib/gitlab/graphql/aggregations/vulnerability_statistics/lazy_aggregate.rb b/ee/lib/gitlab/graphql/aggregations/vulnerability_statistics/lazy_aggregate.rb index 0b3c3a898923a8..76b47bedaed273 100644 --- a/ee/lib/gitlab/graphql/aggregations/vulnerability_statistics/lazy_aggregate.rb +++ b/ee/lib/gitlab/graphql/aggregations/vulnerability_statistics/lazy_aggregate.rb @@ -5,6 +5,8 @@ module Graphql module Aggregations module VulnerabilityStatistics class LazyAggregate + include ::Gitlab::Graphql::Deferred + attr_reader :vulnerable, :lazy_state def initialize(query_ctx, vulnerable, include_subgroups: false) diff --git a/ee/spec/graphql/ee/resolvers/issues_resolver_spec.rb b/ee/spec/graphql/ee/resolvers/issues_resolver_spec.rb index e4e127c45c0ad9..558aee95d33818 100644 --- a/ee/spec/graphql/ee/resolvers/issues_resolver_spec.rb +++ b/ee/spec/graphql/ee/resolvers/issues_resolver_spec.rb @@ -23,11 +23,11 @@ let_it_be(:weight_issue4) { create(:issue, project: project, weight: nil) } it 'sorts issues ascending' do - expect(resolve_issues(sort: :weight_asc)).to eq [weight_issue3, weight_issue1, weight_issue4, weight_issue2] + expect(resolve_issues(sort: :weight_asc).to_a).to eq [weight_issue3, weight_issue1, weight_issue4, weight_issue2] end it 'sorts issues descending' do - expect(resolve_issues(sort: :weight_desc)).to eq [weight_issue1, weight_issue3, weight_issue4, weight_issue2] + expect(resolve_issues(sort: :weight_desc).to_a).to eq [weight_issue1, weight_issue3, weight_issue4, weight_issue2] end end @@ -36,11 +36,11 @@ let_it_be(:published) { create(:issue, :published, project: project) } it 'sorts issues ascending' do - expect(resolve_issues(sort: :published_asc)).to eq [not_published, published] + expect(resolve_issues(sort: :published_asc).to_a).to eq [not_published, published] end it 'sorts issues descending' do - expect(resolve_issues(sort: :published_desc)).to eq [published, not_published] + expect(resolve_issues(sort: :published_desc).to_a).to eq [published, not_published] end end @@ -54,11 +54,11 @@ end it 'sorts issues ascending' do - expect(resolve_issues(sort: :sla_due_at_asc)).to eq [sla_due_first, sla_due_last] + expect(resolve_issues(sort: :sla_due_at_asc).to_a).to eq [sla_due_first, sla_due_last] end it 'sorts issues descending' do - expect(resolve_issues(sort: :sla_due_at_desc)).to eq [sla_due_last, sla_due_first] + expect(resolve_issues(sort: :sla_due_at_desc).to_a).to eq [sla_due_last, sla_due_first] end end end @@ -69,7 +69,7 @@ let_it_be(:issue_without_iteration) { create(:issue, project: project) } it 'returns issues with iteration' do - expect(resolve_issues(iteration_id: iteration1.id)).to eq [issue_with_iteration] + expect(resolve_issues(iteration_id: [iteration1.id])).to contain_exactly(issue_with_iteration) end end diff --git a/ee/spec/graphql/resolvers/board_groupings/epics_resolvers_spec.rb b/ee/spec/graphql/resolvers/board_groupings/epics_resolvers_spec.rb index ae70409e00ea70..6f85c33f139a19 100644 --- a/ee/spec/graphql/resolvers/board_groupings/epics_resolvers_spec.rb +++ b/ee/spec/graphql/resolvers/board_groupings/epics_resolvers_spec.rb @@ -69,7 +69,7 @@ it 'finds only epics for issues matching issue filters' do result = resolve_board_epics( - group_board, { issue_filters: { label_name: label1.title, not: { label_name: label2.title } } }) + group_board, { issue_filters: { label_name: [label1.title], not: { label_name: [label2.title] } } }) expect(result).to match_array([epic1]) end @@ -82,13 +82,14 @@ end it 'accepts negated issue params' do + filters = { label_name: ['foo'], not: { label_name: %w(foo bar) } } expect(Boards::Issues::ListService).to receive(:new).with( group_board.resource_parent, current_user, - { all_lists: true, board_id: group_board.id, label_name: 'foo', not: { label_name: %w(foo bar) } } + { all_lists: true, board_id: group_board.id, **filters } ).and_call_original - resolve_board_epics(group_board, { issue_filters: { label_name: 'foo', not: { label_name: %w(foo bar) } } }) + resolve_board_epics(group_board, { issue_filters: filters }) end it 'raises an exception if both epic_id and epic_wildcard_id are present' do diff --git a/ee/spec/graphql/resolvers/clusters/agents_resolver_spec.rb b/ee/spec/graphql/resolvers/clusters/agents_resolver_spec.rb index 15fd673a4dd952..00c8924c490f1b 100644 --- a/ee/spec/graphql/resolvers/clusters/agents_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/clusters/agents_resolver_spec.rb @@ -14,32 +14,39 @@ end describe '#resolve' do - let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:maintainer) { create(:user, maintainer_projects: [project]) } + let_it_be(:developer) { create(:user, developer_projects: [project]) } + let_it_be(:agents) { create_list(:cluster_agent, 2, project: project) } - let(:finder) { double(execute: relation) } - let(:relation) { double } - let(:project) { create(:project) } - let(:args) { Hash(key: 'value') } - let(:ctx) { Hash(current_user: user) } + let(:ctx) { { current_user: current_user } } - let(:lookahead) do - double(selects?: true).tap do |selection| - allow(selection).to receive(:selection).and_return(selection) - end + before do + stub_licensed_features(cluster_agents: true) end - subject { resolve(described_class, obj: project, args: args.merge(lookahead: lookahead), ctx: ctx) } + subject { resolve_agents } + + context 'the current user has access to clusters' do + let(:current_user) { maintainer } - it 'calls the agents finder' do - expect(::Clusters::AgentsFinder).to receive(:new) - .with(project, user, params: args).and_return(finder) + it 'finds all agents' do + expect(subject).to match_array(agents) + end + end - expect(relation).to receive(:preload) - .with(:agent_tokens).and_return(relation) + context 'the current user does not have access to clusters' do + let(:current_user) { developer } - expect(subject).to eq(relation) + it 'returns an empty result' do + expect(subject).to be_empty + end end end + + def resolve_agents(args = {}) + resolve(described_class, obj: project, ctx: ctx, lookahead: positive_lookahead, args: args) + end end RSpec.describe Resolvers::Clusters::AgentsResolver.single do diff --git a/ee/spec/graphql/resolvers/epic_issues_resolver_spec.rb b/ee/spec/graphql/resolvers/epic_issues_resolver_spec.rb index 8fda672dd176f8..9e975d079c9699 100644 --- a/ee/spec/graphql/resolvers/epic_issues_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/epic_issues_resolver_spec.rb @@ -21,36 +21,34 @@ let_it_be(:epic_issue3) { create(:epic_issue, epic: epic2, issue: issue3, relative_position: 1) } let_it_be(:epic_issue4) { create(:epic_issue, epic: epic2, issue: issue4, relative_position: nil) } - specify do - expect(described_class).to have_nullable_graphql_type(Types::EpicIssueType.connection_type) - end - before do group.add_developer(current_user) stub_licensed_features(epics: true) end + specify do + expect(described_class).to have_nullable_graphql_type(Types::EpicIssueType.connection_type) + end + describe '#resolve' do + let(:epics) { [epic1, epic2] } + it 'finds all epic issues' do - result = [resolve_epic_issues(epic1), resolve_epic_issues(epic2)] + result = epics.map { |epic| resolve_epic_issues(epic).to_a } - expect(result).to contain_exactly([issue2, issue1], [issue3, issue4]) + expect(result).to eq [[issue2, issue1], [issue3, issue4]] end it 'finds only epic issues that user can read' do guest = create(:user) - result = - [ - resolve_epic_issues(epic1, {}, { current_user: guest }), - resolve_epic_issues(epic2, {}, { current_user: guest }) - ] + result = epics.map { |e| resolve_epic_issues(e, user: guest).to_a } - expect(result).to contain_exactly([], [issue1]) + expect(result).to eq [[issue1], []] end end - def resolve_epic_issues(object, args = {}, context = { current_user: current_user }) - resolve(described_class, obj: object, args: args, ctx: context) + def resolve_epic_issues(object, user: current_user) + resolve(described_class, obj: object, ctx: { current_user: user }) end end diff --git a/ee/spec/graphql/resolvers/epics_resolver_spec.rb b/ee/spec/graphql/resolvers/epics_resolver_spec.rb index 535f2f3e6d16b9..53e7891a3a5e6d 100644 --- a/ee/spec/graphql/resolvers/epics_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/epics_resolver_spec.rb @@ -44,7 +44,7 @@ context 'with iids' do it 'finds a specific epic with iids' do - expect(resolve_epics(iids: epic1.iid)).to contain_exactly(epic1) + expect(resolve_epics(iids: [epic1.iid.to_s])).to contain_exactly(epic1) end it 'finds multiple epics with iids' do diff --git a/ee/spec/graphql/resolvers/requirements_management/requirements_resolver_spec.rb b/ee/spec/graphql/resolvers/requirements_management/requirements_resolver_spec.rb index 180d528b490a0f..de9ac91014912a 100644 --- a/ee/spec/graphql/resolvers/requirements_management/requirements_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/requirements_management/requirements_resolver_spec.rb @@ -45,11 +45,11 @@ describe 'sorting' do context 'when sorting by created_at' do it 'sorts requirements ascending' do - expect(resolve_requirements(sort: 'created_asc')).to eq([requirement1, requirement3, requirement2]) + expect(resolve_requirements(sort: 'created_asc').to_a).to eq([requirement1, requirement3, requirement2]) end it 'sorts requirements descending' do - expect(resolve_requirements(sort: 'created_desc')).to eq([requirement2, requirement3, requirement1]) + expect(resolve_requirements(sort: 'created_desc').to_a).to eq([requirement2, requirement3, requirement1]) end end end @@ -89,7 +89,7 @@ context 'single author exists' do let(:params) do - { author_username: other_user.username } + { author_username: [other_user.username] } end it 'filters requirements by author' do @@ -99,7 +99,7 @@ context 'single nonexistent author' do let(:params) do - { author_username: "nonsense" } + { author_username: ["nonsense"] } end it_behaves_like 'returns no items' @@ -121,14 +121,6 @@ it_behaves_like 'returns unfiltered' end - context 'single author is nil' do - let(:params) do - { author_username: nil } - end - - it_behaves_like 'returns unfiltered' - end - context 'an empty array' do let(:params) do { author_username: [] } diff --git a/ee/spec/graphql/resolvers/security_report_summary_resolver_spec.rb b/ee/spec/graphql/resolvers/security_report_summary_resolver_spec.rb index 751416c8005cf0..c77b5abb42e9e7 100644 --- a/ee/spec/graphql/resolvers/security_report_summary_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/security_report_summary_resolver_spec.rb @@ -29,10 +29,10 @@ pipeline, expected_selection_info ) do |summary_service| - expect(summary_service).to receive(:execute) + expect(summary_service).to receive(:execute).and_return({}) end - resolve(described_class, obj: pipeline, args: { lookahead: lookahead }) + resolve(described_class, obj: pipeline, lookahead: lookahead) end end end diff --git a/ee/spec/graphql/resolvers/vulnerabilities_grade_resolver_spec.rb b/ee/spec/graphql/resolvers/vulnerabilities_grade_resolver_spec.rb index ccca23e26d231f..e0ec16fa991513 100644 --- a/ee/spec/graphql/resolvers/vulnerabilities_grade_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/vulnerabilities_grade_resolver_spec.rb @@ -4,8 +4,11 @@ RSpec.describe Resolvers::VulnerabilitiesGradeResolver do include GraphqlHelpers + include ::Gitlab::Graphql::Laziness - subject { resolve(described_class, obj: group, args: args, ctx: { current_user: user }).execute } + subject do + force(resolve(described_class, obj: group, args: args, ctx: { current_user: user })) + end let_it_be(:group) { create(:group) } let_it_be(:subgroup) { create(:group, parent: group) } diff --git a/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb b/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb index 04dce83ee55367..b7bc802b5ba5bd 100644 --- a/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb +++ b/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb @@ -34,15 +34,15 @@ NODE end - def epic_query(params = {}) + def epic_query(params = {}, epic_fields = epic_node) graphql_query_for("group", { "fullPath" => group.full_path }, - query_graphql_field("epics", params, epic_node) + query_graphql_field("epics", params, epic_fields) ) end - def issue_ids - node_array(epics_data).each_with_object({}) do |node, result| - result[node['iid'].to_i] = node_array(node['issues']['edges'], 'id') + def issue_ids(epics = epics_data) + node_array(epics).to_h do |node| + [node['iid'].to_i, node_array(node['issues']['edges'], 'id')] end end @@ -75,13 +75,12 @@ def first_epic_issues_page_info end context 'pagination' do - let(:after_cursor) { '' } - let(:epic_node) do + def epic_fields(after) <<~NODE edges { node { iid - issues(first: 1, after: "#{after_cursor}") { + issues(#{attributes_to_graphql(first: 1, after: after)}) { pageInfo { hasNextPage hasPreviousPage @@ -99,28 +98,33 @@ def first_epic_issues_page_info NODE end - context 'without a cursor' do - it 'return first page of issues' do - post_graphql(epic_query(iid: epic.iid), current_user: user) + let(:params) { { iid: epic.iid } } - expect(response).to have_gitlab_http_status(:success) - expect(first_epic_issues_page_info['hasNextPage']).to be_truthy - expect(first_epic_issues_page_info['endCursor']).to eq 'MQ' - expect(issue_ids[epic.iid]).to eq [issue.to_global_id.to_s] - end + def get_page(after) + query = epic_query(params, epic_fields(after)) + post_graphql(query, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + + data = ::Gitlab::Json.parse(response.body) + expect(data['errors']).to be_blank + epics = data.dig('data', 'group', 'epics', 'edges') + issues = issue_ids(epics)[epic.iid] + page = epics.dig(0, 'node', 'issues', 'pageInfo') + + [issues, page] end - context 'with an after cursor' do - let(:after_cursor) { 'MQ' } + it 'paginates correctly' do + issues, page_1 = get_page(nil) + + expect(issues).to contain_exactly(global_id_of(issue)) + expect(page_1).to include("hasNextPage" => true, "hasPreviousPage" => false) - it 'return first page after the cursor' do - post_graphql(epic_query(iid: epic.iid), current_user: user) + next_issues, page_2 = get_page(page_1['endCursor']) - expect(response).to have_gitlab_http_status(:success) - expect(first_epic_issues_page_info['hasNextPage']).to be_falsey - expect(first_epic_issues_page_info['endCursor']).to eq 'Mg' - expect(issue_ids[epic.iid]).to eq [confidential_issue.to_global_id.to_s] - end + expect(next_issues).to contain_exactly(global_id_of(confidential_issue)) + expect(page_2).to include("hasNextPage" => false, "hasPreviousPage" => true) end end end @@ -160,7 +164,8 @@ def first_epic_issues_page_info # TODO remove the pending state of this spec when # we have an efficient way of preloading data on GraphQL. # For more information check: https://gitlab.com/gitlab-org/gitlab/-/issues/207898 - xit 'avoids N+1 queries' do + it 'avoids N+1 queries' do + pending 'https://gitlab.com/gitlab-org/gitlab/-/issues/207898' control_count = ActiveRecord::QueryRecorder.new do post_graphql(epic_query(iid: epic.iid), current_user: user) end.count diff --git a/ee/spec/requests/api/graphql/instance_security_dashboard_spec.rb b/ee/spec/requests/api/graphql/instance_security_dashboard_spec.rb index 7c02dc0b7e54ad..390265e9f568d0 100644 --- a/ee/spec/requests/api/graphql/instance_security_dashboard_spec.rb +++ b/ee/spec/requests/api/graphql/instance_security_dashboard_spec.rb @@ -9,8 +9,6 @@ let_it_be(:other_project) { create(:project) } let_it_be(:user) { create(:user, security_dashboard_projects: [project]) } - let(:current_user) { user } - before do project.add_developer(user) other_project.add_developer(user) @@ -18,28 +16,26 @@ stub_licensed_features(security_dashboard: true) end - let(:fields) do - <<~QUERY - nodes { - id - } - QUERY - end - let(:query) do - graphql_query_for('instanceSecurityDashboard', nil, query_graphql_field('projects', {}, fields)) + graphql_query_for(:instance_security_dashboard, dashboard_fields) end - subject(:projects) { graphql_data.dig('instanceSecurityDashboard', 'projects', 'nodes') } - context 'with logged in user' do - it_behaves_like 'a working graphql query' do - before do - post_graphql(query, current_user: current_user) - end + let(:current_user) { user } + + context 'requesting projects in the dashboard' do + let(:dashboard_fields) { query_graphql_path(%i[projects nodes], 'id') } - it 'finds only projects that were added to instance security dashboard' do - expect(projects).to eq([{ "id" => GitlabSchema.id_from_object(project).to_s }]) + subject(:projects) { graphql_data_at(:instance_security_dashboard, :projects, :nodes) } + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + + it 'finds only projects that were added to instance security dashboard' do + expect(projects).to contain_exactly({ "id" => global_id_of(project) }) + end end end @@ -188,6 +184,10 @@ context 'with no user' do let(:current_user) { nil } + let(:dashboard_fields) { nil } + + subject { graphql_data_at(:instance_security_dashboard) } + it_behaves_like 'a working graphql query' do before do post_graphql(query, current_user: current_user) diff --git a/lib/gitlab/graphql/deferred.rb b/lib/gitlab/graphql/deferred.rb new file mode 100644 index 00000000000000..d0b36aabd5f2ca --- /dev/null +++ b/lib/gitlab/graphql/deferred.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# A marker interface that allows use to lazily resolve a wider range of value +module Gitlab + module Graphql + module Deferred + def execute + raise NotImplementedError, 'Deferred classes must provide an execute method' + end + end + end +end diff --git a/lib/gitlab/graphql/externally_paginated_array.rb b/lib/gitlab/graphql/externally_paginated_array.rb index 4797fe15cd3b82..873d7f4efdfe57 100644 --- a/lib/gitlab/graphql/externally_paginated_array.rb +++ b/lib/gitlab/graphql/externally_paginated_array.rb @@ -3,12 +3,12 @@ module Gitlab module Graphql class ExternallyPaginatedArray < Array - attr_reader :previous_cursor, :next_cursor + attr_reader :start_cursor, :end_cursor def initialize(previous_cursor, next_cursor, *args) super(args) - @previous_cursor = previous_cursor - @next_cursor = next_cursor + @start_cursor = previous_cursor + @end_cursor = next_cursor end end end diff --git a/lib/gitlab/graphql/laziness.rb b/lib/gitlab/graphql/laziness.rb new file mode 100644 index 00000000000000..ae63ac233f30be --- /dev/null +++ b/lib/gitlab/graphql/laziness.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + # This module allows your class to easily defer and force values. + # Its methods are just sugar for calls to the Gitlab::Graphql::Lazy class. + # + # example: + # + # class MyAwesomeClass + # def sum_frobbocities(ids) + # ids.map { |n| get_the_thing(n) }.map(&method(:force).sum + # end + # + # def get_the_thing(id) + # thunk = SomeBatchLoader.load(id) + # defer { force(thunk).frobbocity * 2 } + # end + # end + # + # In the example above, we use defer to delay forcing the batch-loaded + # item until we need it, and then we use `force` to consume the lazy values + # + # If `SomeBatchLoader.load(id)` batches correctly, calling + # `sum_frobbocities` will only perform one batched load. + # + module Laziness + def defer(&block) + ::Gitlab::Graphql::Lazy.new(&block) + end + + def force(lazy) + ::Gitlab::Graphql::Lazy.force(lazy) + end + end + end +end diff --git a/lib/gitlab/graphql/lazy.rb b/lib/gitlab/graphql/lazy.rb index 3cc11047387e85..54013cf4790fc6 100644 --- a/lib/gitlab/graphql/lazy.rb +++ b/lib/gitlab/graphql/lazy.rb @@ -24,6 +24,8 @@ def self.force(value) value.force when ::BatchLoader::GraphQL value.sync + when ::Gitlab::Graphql::Deferred + value.execute when ::GraphQL::Execution::Lazy value.value # part of the private api, but we can force this as well when ::Concurrent::Promise diff --git a/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb b/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb index 90a20861b0d290..ce309df65d90eb 100644 --- a/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb +++ b/lib/gitlab/graphql/pagination/externally_paginated_array_connection.rb @@ -8,13 +8,7 @@ class ExternallyPaginatedArrayConnection < GraphQL::Pagination::ArrayConnection include ::Gitlab::Graphql::ConnectionCollectionMethods prepend ::Gitlab::Graphql::ConnectionRedaction - def start_cursor - items.previous_cursor - end - - def end_cursor - items.next_cursor - end + delegate :start_cursor, :end_cursor, to: :items def next_page? end_cursor.present? diff --git a/spec/graphql/resolvers/alert_management/alert_resolver_spec.rb b/spec/graphql/resolvers/alert_management/alert_resolver_spec.rb index 73f878bb7452e0..c042f6dac19bfb 100644 --- a/spec/graphql/resolvers/alert_management/alert_resolver_spec.rb +++ b/spec/graphql/resolvers/alert_management/alert_resolver_spec.rb @@ -33,7 +33,7 @@ end context 'finding by status' do - let(:args) { { status: [Types::AlertManagement::StatusEnum.values['IGNORED'].value] } } + let(:args) { { statuses: [Types::AlertManagement::StatusEnum.values['IGNORED'].value] } } it { is_expected.to contain_exactly(ignored_alert) } end diff --git a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb index 4ccf194522fc37..e7c56a526f412b 100644 --- a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb +++ b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb @@ -29,7 +29,7 @@ end it 'finds only issues matching filters' do - result = resolve_board_list_issues(args: { filters: { label_name: label.title, not: { label_name: label2.title } } }).items + result = resolve_board_list_issues(args: { filters: { label_name: [label.title], not: { label_name: [label2.title] } } }).items expect(result).to match_array([issue1, issue3]) end diff --git a/spec/graphql/resolvers/ci/jobs_resolver_spec.rb b/spec/graphql/resolvers/ci/jobs_resolver_spec.rb index a836c89bd6100c..46ee74a5f7eff5 100644 --- a/spec/graphql/resolvers/ci/jobs_resolver_spec.rb +++ b/spec/graphql/resolvers/ci/jobs_resolver_spec.rb @@ -17,10 +17,14 @@ describe '#resolve' do context 'when security_report_types is empty' do it "returns all of the pipeline's jobs" do - jobs = resolve(described_class, obj: pipeline, args: {}, ctx: {}) - - job_names = jobs.map(&:name) - expect(job_names).to contain_exactly('Normal job', 'DAST job', 'SAST job', 'Container scanning job') + jobs = resolve(described_class, obj: pipeline) + + expect(jobs).to contain_exactly( + have_attributes(name: 'Normal job'), + have_attributes(name: 'DAST job'), + have_attributes(name: 'SAST job'), + have_attributes(name: 'Container scanning job') + ) end end @@ -30,10 +34,12 @@ ::Types::Security::ReportTypeEnum.values['SAST'].value, ::Types::Security::ReportTypeEnum.values['DAST'].value ] - jobs = resolve(described_class, obj: pipeline, args: { security_report_types: report_types }, ctx: {}) + jobs = resolve(described_class, obj: pipeline, args: { security_report_types: report_types }) - job_names = jobs.map(&:name) - expect(job_names).to contain_exactly('DAST job', 'SAST job') + expect(jobs).to contain_exactly( + have_attributes(name: 'DAST job'), + have_attributes(name: 'SAST job') + ) end end end diff --git a/spec/graphql/resolvers/commit_pipelines_resolver_spec.rb b/spec/graphql/resolvers/commit_pipelines_resolver_spec.rb index a408981c08efd8..241a9e58147b25 100644 --- a/spec/graphql/resolvers/commit_pipelines_resolver_spec.rb +++ b/spec/graphql/resolvers/commit_pipelines_resolver_spec.rb @@ -50,6 +50,6 @@ def resolve_pipelines it 'resolves pipelines for commit and ref' do pipelines = resolve_pipelines - expect(pipelines).to eq([pipeline2, pipeline]) + expect(pipelines.to_a).to eq([pipeline2, pipeline]) end end diff --git a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb index b6fe94a23125fc..bfd63cd861ecb2 100644 --- a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb +++ b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb @@ -5,17 +5,23 @@ RSpec.describe ::CachingArrayResolver do include GraphqlHelpers - let_it_be(:non_admins) { create_list(:user, 4, admin: false) } - let(:query_context) { {} } + let_it_be(:admins) { create_list(:user, 4, admin: true) } + let(:query_context) { { current_user: admins.first } } let(:max_page_size) { 10 } let(:field) { double('Field', max_page_size: max_page_size) } - let(:schema) { double('Schema', default_max_page_size: 3) } + let(:schema) do + Class.new(GitlabSchema) do + default_max_page_size 3 + end + end let_it_be(:caching_resolver) do mod = described_class Class.new(::Resolvers::BaseResolver) do include mod + type [::Types::UserType], null: true + argument :is_admin, ::GraphQL::BOOLEAN_TYPE, required: false def query_input(is_admin:) is_admin @@ -44,6 +50,8 @@ def model_class Class.new(::Resolvers::BaseResolver) do include mod + type [::Types::UserType], null: true + argument :username, ::GraphQL::STRING_TYPE, required: false def query_input(username:) username @@ -72,7 +80,7 @@ def model_class expect(User).to receive(:from_union).twice.and_call_original results = users.in_groups_of(2, false).map do |users| - resolve(resolver, args: { username: users.map(&:username) }, field: field, schema: schema) + resolve(resolver, args: { username: users.map(&:username) }, schema: schema) end expect(results.flat_map(&method(:force))).to match_array(users) @@ -80,7 +88,7 @@ def model_class end context 'all queries return results' do - let_it_be(:admins) { create_list(:admin, 3) } + let_it_be(:non_admins) { create_list(:user, 3, admin: false) } it 'batches the queries' do expect do @@ -113,9 +121,9 @@ def model_class found_others = resolve_users(false) found_all = resolve_users(nil) - expect(force(found_admins)).to be_empty - expect(force(found_others)).to match_array(non_admins) - expect(force(found_all)).to match_array(non_admins) + expect(force(found_admins)).to match_array(admins) + expect(force(found_others)).to be_empty + expect(force(found_all)).to match_array(admins) end end @@ -128,13 +136,13 @@ def model_class expect do repeated_find = resolve_users(nil) - expect(force(repeated_find)).to match_array(non_admins) + expect(force(repeated_find)).to match_array(admins) end.not_to exceed_query_limit(0) end end context 'the resolver overrides item_found' do - let_it_be(:admins) { create_list(:admin, 2) } + let_it_be(:non_admins) { create_list(:user, 2, admin: false) } let(:query_context) do { found: { true => [], false => [], nil => [] } @@ -156,8 +164,8 @@ def item_found(key, item) [found_admins, found_all].each(&method(:force)) expect(query_context[:found]).to match({ - false => be_empty, true => match_array(admins), + false => be_empty, nil => match_array(admins + non_admins) }) end @@ -168,10 +176,10 @@ def item_found(key, item) it 'respects the max_page_size, on a per subset basis' do found_all = resolve_users(nil) - found_others = resolve_users(false) + found_admins = resolve_users(true) expect(force(found_all).size).to eq(2) - expect(force(found_others).size).to eq(2) + expect(force(found_admins).size).to eq(2) end end @@ -180,10 +188,10 @@ def item_found(key, item) it 'takes the page size from schema.default_max_page_size' do found_all = resolve_users(nil) - found_others = resolve_users(false) + found_admins = resolve_users(true) expect(force(found_all).size).to eq(schema.default_max_page_size) - expect(force(found_others).size).to eq(schema.default_max_page_size) + expect(force(found_admins).size).to eq(schema.default_max_page_size) end end @@ -199,7 +207,9 @@ def item_found(key, item) def resolve_users(is_admin, resolver = caching_resolver) args = { is_admin: is_admin } - resolve(resolver, args: args, field: field, ctx: query_context, schema: schema) + opts = resolver.field_options + allow(resolver).to receive(:field_options).and_return(opts.merge(max_page_size: max_page_size)) + resolve(resolver, args: args, ctx: query_context, schema: schema, field: field) end def force(lazy) diff --git a/spec/graphql/resolvers/design_management/version/design_at_version_resolver_spec.rb b/spec/graphql/resolvers/design_management/version/design_at_version_resolver_spec.rb index 850b9f8cc872dc..cc7e2f6814a2f6 100644 --- a/spec/graphql/resolvers/design_management/version/design_at_version_resolver_spec.rb +++ b/spec/graphql/resolvers/design_management/version/design_at_version_resolver_spec.rb @@ -15,7 +15,7 @@ let(:all_singular_args) do { - design_at_version_id: global_id_of(dav(design)), + id: global_id_of(dav(design)), design_id: global_id_of(design), filename: design.filename } @@ -50,7 +50,7 @@ end end - %i[design_at_version_id design_id filename].each do |arg| + %i[id design_id filename].each do |arg| describe "passing #{arg}" do let(:args) { all_singular_args.slice(arg) } @@ -71,7 +71,7 @@ describe 'attempting to retrieve an object not visible at this version' do let(:design) { design_d } - %i[design_at_version_id design_id filename].each do |arg| + %i[id design_id filename].each do |arg| describe "passing #{arg}" do let(:args) { all_singular_args.slice(arg) } diff --git a/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb b/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb index 403261fc22a2e6..b0fc78af2af4a1 100644 --- a/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb +++ b/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb @@ -32,7 +32,7 @@ end context 'we pass an id' do - let(:params) { { version_id: global_id_of(first_version) } } + let(:params) { { id: global_id_of(first_version) } } it { is_expected.to eq(first_version) } end @@ -44,13 +44,13 @@ end context 'we pass an inconsistent mixture of sha and version id' do - let(:params) { { sha: first_version.sha, version_id: global_id_of(create(:design_version)) } } + let(:params) { { sha: first_version.sha, id: global_id_of(create(:design_version)) } } it { is_expected.to be_nil } end context 'we pass the id of something that is not a design_version' do - let(:params) { { version_id: global_id_of(project) } } + let(:params) { { id: global_id_of(project) } } let(:appropriate_error) { ::GraphQL::CoercionError } it 'raises an appropriate error' do diff --git a/spec/graphql/resolvers/design_management/versions_resolver_spec.rb b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb index 5bc1c555e9a36d..23d4d86c79a7bf 100644 --- a/spec/graphql/resolvers/design_management/versions_resolver_spec.rb +++ b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb @@ -27,7 +27,7 @@ end shared_examples 'a source of versions' do - subject(:result) { resolve_versions(object) } + subject(:result) { resolve_versions(object)&.to_a } let_it_be(:all_versions) { object.versions.ordered } @@ -39,7 +39,7 @@ context 'without constraints' do it 'returns the ordered versions' do - expect(result).to eq(all_versions) + expect(result.to_a).to eq(all_versions) end end @@ -51,13 +51,13 @@ end context 'by earlier_or_equal_to_id' do - let(:params) { { id: global_id_of(first_version) } } + let(:params) { { earlier_or_equal_to_id: global_id_of(first_version) } } it_behaves_like 'a query for all_versions up to the first_version' end context 'by earlier_or_equal_to_sha' do - let(:params) { { sha: first_version.sha } } + let(:params) { { earlier_or_equal_to_sha: first_version.sha } } it_behaves_like 'a query for all_versions up to the first_version' end @@ -68,8 +68,8 @@ # return successfully let(:params) do { - sha: first_version.sha, - id: global_id_of(first_version) + earlier_or_equal_to_sha: first_version.sha, + earlier_or_equal_to_id: global_id_of(first_version) } end @@ -79,8 +79,8 @@ context 'and they do not match' do let(:params) do { - sha: first_version.sha, - id: global_id_of(other_version) + earlier_or_equal_to_sha: first_version.sha, + earlier_or_equal_to_id: global_id_of(other_version) } end @@ -111,7 +111,7 @@ end def resolve_versions(obj, context = { current_user: current_user }) - eager_resolve(resolver, obj: obj, args: params.merge(parent: parent), ctx: context) + eager_resolve(resolver, obj: obj, parent: parent, args: params, ctx: context) end end end diff --git a/spec/graphql/resolvers/error_tracking/sentry_errors_resolver_spec.rb b/spec/graphql/resolvers/error_tracking/sentry_errors_resolver_spec.rb index edca11f40d7061..170a602fb0dedc 100644 --- a/spec/graphql/resolvers/error_tracking/sentry_errors_resolver_spec.rb +++ b/spec/graphql/resolvers/error_tracking/sentry_errors_resolver_spec.rb @@ -88,8 +88,8 @@ it 'sets the pagination variables' do result = resolve_errors - expect(result.next_cursor).to eq 'next' - expect(result.previous_cursor).to eq 'prev' + expect(result.end_cursor).to eq 'next' + expect(result.start_cursor).to eq 'prev' end it 'returns an externally paginated array' do diff --git a/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb b/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb index 16eb190efc6e0f..decc3569d6cdc2 100644 --- a/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb +++ b/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb @@ -62,22 +62,13 @@ end it 'filters by issue type', :aggregate_failures do - result = resolve_issue_status_counts(issue_types: ['incident']) + result = resolve_issue_status_counts(types: ['incident']) expect(result.all).to eq 1 expect(result.opened).to eq 0 expect(result.closed).to eq 1 end - # The state param is ignored in IssuableFinder#count_by_state - it 'ignores state filter', :aggregate_failures do - result = resolve_issue_status_counts(state: 'closed') - - expect(result.all).to eq 2 - expect(result.opened).to eq 1 - expect(result.closed).to eq 1 - end - private def resolve_issue_status_counts(args = {}, context = { current_user: current_user }) diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 43cbd4d2bdd018..f6f746a8572227 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -5,7 +5,7 @@ RSpec.describe Resolvers::IssuesResolver do include GraphqlHelpers - let(:current_user) { create(:user) } + let_it_be(:current_user) { create(:user) } let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, group: group) } @@ -25,7 +25,7 @@ end context "with a project" do - before do + before_all do project.add_developer(current_user) create(:label_link, label: label1, target: issue1) create(:label_link, label: label1, target: issue2) @@ -43,11 +43,11 @@ end it 'filters by milestone' do - expect(resolve_issues(milestone_title: milestone.title)).to contain_exactly(issue1) + expect(resolve_issues(milestone_title: [milestone.title])).to contain_exactly(issue1) end it 'filters by assignee_username' do - expect(resolve_issues(assignee_username: assignee.username)).to contain_exactly(issue2) + expect(resolve_issues(assignee_username: [assignee.username])).to contain_exactly(issue2) end it 'filters by two assignees' do @@ -112,15 +112,19 @@ describe 'filters by issue_type' do it 'filters by a single type' do - expect(resolve_issues(issue_types: ['incident'])).to contain_exactly(issue1) + expect(resolve_issues(types: %w[incident])).to contain_exactly(issue1) + end + + it 'filters by a single type, negative assertion' do + expect(resolve_issues(types: %w[issue])).not_to include(issue1) end it 'filters by more than one type' do - expect(resolve_issues(issue_types: %w(incident issue))).to contain_exactly(issue1, issue2) + expect(resolve_issues(types: %w[incident issue])).to contain_exactly(issue1, issue2) end it 'ignores the filter if none given' do - expect(resolve_issues(issue_types: [])).to contain_exactly(issue1, issue2) + expect(resolve_issues(types: [])).to contain_exactly(issue1, issue2) end end @@ -143,44 +147,44 @@ describe 'sorting' do context 'when sorting by created' do it 'sorts issues ascending' do - expect(resolve_issues(sort: 'created_asc')).to eq [issue1, issue2] + expect(resolve_issues(sort: 'created_asc').to_a).to eq [issue1, issue2] end it 'sorts issues descending' do - expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue1] + expect(resolve_issues(sort: 'created_desc').to_a).to eq [issue2, issue1] end end context 'when sorting by due date' do - let_it_be(:project) { create(:project) } + let_it_be(:project) { create(:project, :public) } let_it_be(:due_issue1) { create(:issue, project: project, due_date: 3.days.from_now) } let_it_be(:due_issue2) { create(:issue, project: project, due_date: nil) } let_it_be(:due_issue3) { create(:issue, project: project, due_date: 2.days.ago) } let_it_be(:due_issue4) { create(:issue, project: project, due_date: nil) } it 'sorts issues ascending' do - expect(resolve_issues(sort: :due_date_asc)).to eq [due_issue3, due_issue1, due_issue4, due_issue2] + expect(resolve_issues(sort: :due_date_asc).to_a).to eq [due_issue3, due_issue1, due_issue4, due_issue2] end it 'sorts issues descending' do - expect(resolve_issues(sort: :due_date_desc)).to eq [due_issue1, due_issue3, due_issue4, due_issue2] + expect(resolve_issues(sort: :due_date_desc).to_a).to eq [due_issue1, due_issue3, due_issue4, due_issue2] end end context 'when sorting by relative position' do - let_it_be(:project) { create(:project) } + let_it_be(:project) { create(:project, :public) } let_it_be(:relative_issue1) { create(:issue, project: project, relative_position: 2000) } let_it_be(:relative_issue2) { create(:issue, project: project, relative_position: nil) } let_it_be(:relative_issue3) { create(:issue, project: project, relative_position: 1000) } let_it_be(:relative_issue4) { create(:issue, project: project, relative_position: nil) } it 'sorts issues ascending' do - expect(resolve_issues(sort: :relative_position_asc)).to eq [relative_issue3, relative_issue1, relative_issue4, relative_issue2] + expect(resolve_issues(sort: :relative_position_asc).to_a).to eq [relative_issue3, relative_issue1, relative_issue4, relative_issue2] end end context 'when sorting by priority' do - let_it_be(:project) { create(:project) } + let_it_be(:project) { create(:project, :public) } let_it_be(:early_milestone) { create(:milestone, project: project, due_date: 10.days.from_now) } let_it_be(:late_milestone) { create(:milestone, project: project, due_date: 30.days.from_now) } let_it_be(:priority_label1) { create(:label, project: project, priority: 1) } @@ -200,7 +204,7 @@ end context 'when sorting by label priority' do - let_it_be(:project) { create(:project) } + let_it_be(:project) { create(:project, :public) } let_it_be(:label1) { create(:label, project: project, priority: 1) } let_it_be(:label2) { create(:label, project: project, priority: 5) } let_it_be(:label3) { create(:label, project: project, priority: 10) } @@ -219,7 +223,7 @@ end context 'when sorting by milestone due date' do - let_it_be(:project) { create(:project) } + let_it_be(:project) { create(:project, :public) } let_it_be(:early_milestone) { create(:milestone, project: project, due_date: 10.days.from_now) } let_it_be(:late_milestone) { create(:milestone, project: project, due_date: 30.days.from_now) } let_it_be(:milestone_issue1) { create(:issue, project: project) } @@ -236,17 +240,17 @@ end context 'when sorting by severity' do - let_it_be(:project) { create(:project) } + let_it_be(:project) { create(:project, :public) } let_it_be(:issue_high_severity) { create_issue_with_severity(project, severity: :high) } let_it_be(:issue_low_severity) { create_issue_with_severity(project, severity: :low) } let_it_be(:issue_no_severity) { create(:incident, project: project) } it 'sorts issues ascending' do - expect(resolve_issues(sort: :severity_asc)).to eq([issue_no_severity, issue_low_severity, issue_high_severity]) + expect(resolve_issues(sort: :severity_asc).to_a).to eq([issue_no_severity, issue_low_severity, issue_high_severity]) end it 'sorts issues descending' do - expect(resolve_issues(sort: :severity_desc)).to eq([issue_high_severity, issue_low_severity, issue_no_severity]) + expect(resolve_issues(sort: :severity_desc).to_a).to eq([issue_high_severity, issue_low_severity, issue_no_severity]) end end end @@ -267,7 +271,9 @@ it 'batches queries that only include IIDs', :request_store do result = batch_sync(max_queries: 2) do - resolve_issues(iid: issue1.iid) + resolve_issues(iids: issue2.iid) + [issue1, issue2] + .map { |issue| resolve_issues(iid: issue.iid.to_s) } + .flat_map(&:to_a) end expect(result).to contain_exactly(issue1, issue2) diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb index 3a3393a185cf07..63fbd04848d13d 100644 --- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb +++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb @@ -75,9 +75,9 @@ it 'can batch-resolve merge requests from different projects' do # 2 queries for project_authorizations, and 2 for merge_requests result = batch_sync(max_queries: queries_per_project * 2) do - resolve_mr(project, iids: iid_1) + - resolve_mr(project, iids: iid_2) + - resolve_mr(other_project, iids: other_iid) + resolve_mr(project, iids: [iid_1]) + + resolve_mr(project, iids: [iid_2]) + + resolve_mr(other_project, iids: [other_iid]) end expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request) @@ -110,7 +110,7 @@ context 'by source branches' do it 'takes one argument' do - result = resolve_mr(project, source_branch: [merge_request_3.source_branch]) + result = resolve_mr(project, source_branches: [merge_request_3.source_branch]) expect(result).to contain_exactly(merge_request_3) end @@ -118,7 +118,7 @@ it 'takes more than one argument' do mrs = [merge_request_3, merge_request_4] branches = mrs.map(&:source_branch) - result = resolve_mr(project, source_branch: branches ) + result = resolve_mr(project, source_branches: branches ) expect(result).to match_array(mrs) end @@ -126,7 +126,7 @@ context 'by target branches' do it 'takes one argument' do - result = resolve_mr(project, target_branch: [merge_request_3.target_branch]) + result = resolve_mr(project, target_branches: [merge_request_3.target_branch]) expect(result).to contain_exactly(merge_request_3) end @@ -134,7 +134,7 @@ it 'takes more than one argument' do mrs = [merge_request_3, merge_request_4] branches = mrs.map(&:target_branch) - result = resolve_mr(project, target_branch: branches ) + result = resolve_mr(project, target_branches: branches ) expect(result.compact).to match_array(mrs) end @@ -153,13 +153,13 @@ let_it_be(:with_label) { create(:labeled_merge_request, :closed, labels: [label], **common_attrs) } it 'takes one argument' do - result = resolve_mr(project, label_name: [label.title]) + result = resolve_mr(project, labels: [label.title]) expect(result).to contain_exactly(merge_request_6, with_label) end it 'takes multiple arguments, with semantics of ALL MUST MATCH' do - result = resolve_mr(project, label_name: merge_request_6.labels.map(&:title)) + result = resolve_mr(project, labels: merge_request_6.labels.map(&:title)) expect(result).to contain_exactly(merge_request_6) end @@ -201,7 +201,7 @@ it 'requires all filters' do create(:merge_request, :closed, source_project: project, target_project: project, source_branch: merge_request_4.source_branch) - result = resolve_mr(project, source_branch: [merge_request_4.source_branch], state: 'locked') + result = resolve_mr(project, source_branches: [merge_request_4.source_branch], state: 'locked') expect(result.compact).to contain_exactly(merge_request_4) end @@ -236,7 +236,7 @@ end def resolve_mr_single(project, iid) - resolve_mr(project, resolver: described_class.single, iids: iid) + resolve_mr(project, resolver: described_class.single, iid: iid.to_s) end def resolve_mr(project, resolver: described_class, user: current_user, **args) diff --git a/spec/graphql/resolvers/project_merge_requests_resolver_spec.rb b/spec/graphql/resolvers/project_merge_requests_resolver_spec.rb index 32a264469ba0db..45777aa96e10d5 100644 --- a/spec/graphql/resolvers/project_merge_requests_resolver_spec.rb +++ b/spec/graphql/resolvers/project_merge_requests_resolver_spec.rb @@ -28,7 +28,7 @@ it 'filters merge requests by assignee username' do result = resolve_mr(project, assignee_username: other_user.username) - expect(result).to eq([merge_request]) + expect(result).to contain_exactly(merge_request) end it 'does not find anything' do @@ -42,7 +42,7 @@ it 'filters merge requests by author username' do result = resolve_mr(project, author_username: other_user.username) - expect(result).to eq([merge_request]) + expect(result).to contain_exactly(merge_request) end it 'does not find anything' do @@ -56,7 +56,7 @@ it 'filters merge requests by reviewer username' do result = resolve_mr(project, reviewer_username: reviewer.username) - expect(result).to eq([merge_request]) + expect(result).to contain_exactly(merge_request) end it 'does not find anything' do diff --git a/spec/graphql/resolvers/projects/snippets_resolver_spec.rb b/spec/graphql/resolvers/projects/snippets_resolver_spec.rb index 6f7feff8fe54f3..2d8929c0e8faa1 100644 --- a/spec/graphql/resolvers/projects/snippets_resolver_spec.rb +++ b/spec/graphql/resolvers/projects/snippets_resolver_spec.rb @@ -6,16 +6,18 @@ include GraphqlHelpers describe '#resolve' do - let_it_be(:current_user) { create(:user) } + let_it_be(:user) { create(:user) } let_it_be(:other_user) { create(:user) } let_it_be(:project) { create(:project) } - let_it_be(:personal_snippet) { create(:personal_snippet, :private, author: current_user) } - let_it_be(:project_snippet) { create(:project_snippet, :internal, author: current_user, project: project) } + let_it_be(:personal_snippet) { create(:personal_snippet, :private, author: user) } + let_it_be(:project_snippet) { create(:project_snippet, :internal, author: user, project: project) } let_it_be(:other_project_snippet) { create(:project_snippet, :public, author: other_user, project: project) } - before do - project.add_developer(current_user) + let(:current_user) { user } + + before_all do + project.add_developer(user) end it 'calls SnippetsFinder' do @@ -42,20 +44,26 @@ end it 'returns the snippets by gid' do - snippets = resolve_snippets(args: { ids: project_snippet.to_global_id }) + snippets = resolve_snippets(args: { ids: [global_id_of(project_snippet)] }) expect(snippets).to contain_exactly(project_snippet) end it 'returns the snippets by array of gid' do args = { - ids: [project_snippet.to_global_id, other_project_snippet.to_global_id] + ids: [global_id_of(project_snippet), global_id_of(other_project_snippet)] } snippets = resolve_snippets(args: args) expect(snippets).to contain_exactly(project_snippet, other_project_snippet) end + + it 'returns an error if the gid is invalid' do + expect do + resolve_snippets(args: { ids: ['foo'] }) + end.to raise_error(GraphQL::CoercionError) + end end context 'when no project is provided' do @@ -65,8 +73,10 @@ end context 'when provided user is not current user' do + let(:current_user) { other_user } + it 'returns no snippets' do - expect(resolve_snippets(context: { current_user: other_user }, args: { ids: project_snippet.to_global_id })).to be_empty + expect(resolve_snippets(args: { ids: [global_id_of(project_snippet)] })).to be_empty end end diff --git a/spec/graphql/resolvers/releases_resolver_spec.rb b/spec/graphql/resolvers/releases_resolver_spec.rb index b9b90686aa7464..89623be891f2db 100644 --- a/spec/graphql/resolvers/releases_resolver_spec.rb +++ b/spec/graphql/resolvers/releases_resolver_spec.rb @@ -17,6 +17,7 @@ let_it_be(:public_user) { create(:user) } let(:args) { { sort: :released_at_desc } } + let(:all_releases) { [release_v1, release_v2, release_v3] } before do project.add_developer(developer) @@ -27,7 +28,7 @@ let(:current_user) { public_user } it 'returns an empty array' do - expect(resolve_releases).to eq([]) + expect(resolve_releases).to be_empty end end @@ -35,7 +36,7 @@ let(:current_user) { developer } it 'returns all releases associated to the project' do - expect(resolve_releases).to eq([release_v3, release_v2, release_v1]) + expect(resolve_releases).to match_array(all_releases) end describe 'sorting behavior' do @@ -43,7 +44,9 @@ let(:args) { { sort: :released_at_desc } } it 'returns the releases ordered by released_at in descending order' do - expect(resolve_releases).to eq([release_v3, release_v2, release_v1]) + expect(resolve_releases.to_a) + .to match_array(all_releases) + .and be_sorted(:released_at, :desc) end end @@ -51,7 +54,9 @@ let(:args) { { sort: :released_at_asc } } it 'returns the releases ordered by released_at in ascending order' do - expect(resolve_releases).to eq([release_v1, release_v2, release_v3]) + expect(resolve_releases.to_a) + .to match_array(all_releases) + .and be_sorted(:released_at, :asc) end end @@ -59,7 +64,9 @@ let(:args) { { sort: :created_desc } } it 'returns the releases ordered by created_at in descending order' do - expect(resolve_releases).to eq([release_v1, release_v3, release_v2]) + expect(resolve_releases.to_a) + .to match_array(all_releases) + .and be_sorted(:created_at, :desc) end end @@ -67,7 +74,9 @@ let(:args) { { sort: :created_asc } } it 'returns the releases ordered by created_at in ascending order' do - expect(resolve_releases).to eq([release_v2, release_v3, release_v1]) + expect(resolve_releases.to_a) + .to match_array(all_releases) + .and be_sorted(:created_at, :asc) end end end diff --git a/spec/graphql/resolvers/snippets_resolver_spec.rb b/spec/graphql/resolvers/snippets_resolver_spec.rb index a58d9c5ac3a9ea..11cb1c0ec4bb21 100644 --- a/spec/graphql/resolvers/snippets_resolver_spec.rb +++ b/spec/graphql/resolvers/snippets_resolver_spec.rb @@ -84,19 +84,18 @@ end it 'returns the snippets by single gid' do - snippets = resolve_snippets(args: { ids: personal_snippet.to_global_id }) + snippets = resolve_snippets(args: { ids: [global_id_of(personal_snippet)] }) expect(snippets).to contain_exactly(personal_snippet) end it 'returns the snippets by array of gid' do - args = { - ids: [personal_snippet.to_global_id, project_snippet.to_global_id] - } + snippets = [personal_snippet, project_snippet] + args = { ids: snippets.map { |s| global_id_of(s) } } - snippets = resolve_snippets(args: args) + found = resolve_snippets(args: args) - expect(snippets).to contain_exactly(personal_snippet, project_snippet) + expect(found).to match_array(snippets) end it 'returns an error if the id cannot be coerced' do diff --git a/spec/graphql/resolvers/todo_resolver_spec.rb b/spec/graphql/resolvers/todo_resolver_spec.rb index c764f389c16ff8..ac14852b365323 100644 --- a/spec/graphql/resolvers/todo_resolver_spec.rb +++ b/spec/graphql/resolvers/todo_resolver_spec.rb @@ -20,7 +20,7 @@ it 'calls TodosFinder' do expect_next_instance_of(TodosFinder) do |finder| - expect(finder).to receive(:execute) + expect(finder).to receive(:execute).and_call_original end resolve_todos @@ -48,7 +48,7 @@ end it 'returns the todos for single filter' do - todos = resolve_todos(type: 'MergeRequest') + todos = resolve_todos(type: ['MergeRequest']) expect(todos).to contain_exactly(merge_request_todo_pending) end diff --git a/spec/graphql/resolvers/users/snippets_resolver_spec.rb b/spec/graphql/resolvers/users/snippets_resolver_spec.rb index 9ccbebc59e6abc..11a5b7517e019b 100644 --- a/spec/graphql/resolvers/users/snippets_resolver_spec.rb +++ b/spec/graphql/resolvers/users/snippets_resolver_spec.rb @@ -51,24 +51,23 @@ end it 'returns the snippets by single gid' do - snippets = resolve_snippets(args: { ids: private_personal_snippet.to_global_id }) + snippets = resolve_snippets(args: { ids: [global_id_of(private_personal_snippet)] }) expect(snippets).to contain_exactly(private_personal_snippet) end it 'returns the snippets by array of gid' do - args = { - ids: [private_personal_snippet.to_global_id, public_personal_snippet.to_global_id] - } + snippets = [private_personal_snippet, public_personal_snippet] + args = { ids: snippets.map { |s| global_id_of(s) } } - snippets = resolve_snippets(args: args) + found = resolve_snippets(args: args) - expect(snippets).to contain_exactly(private_personal_snippet, public_personal_snippet) + expect(found).to match_array(snippets) end it 'returns an error if the gid is invalid' do args = { - ids: [private_personal_snippet.to_global_id, 'foo'] + ids: [global_id_of(private_personal_snippet), 'foo'] } expect do diff --git a/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb b/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb index 0e9994035d8ce1..b10c2a2ab2acf1 100644 --- a/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb +++ b/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb @@ -11,11 +11,14 @@ describe 'resolvers' do shared_examples_for 'has field with value' do |field_name| it 'correctly renders the field' do - expect(resolve_field(field_name, integration)).to eq(value) + result = resolve_field(field_name, integration, current_user: user) + + expect(result).to eq(value) end end let_it_be_with_reload(:integration) { create(:prometheus_service) } + let_it_be(:user) { create(:user, maintainer_projects: [integration.project]) } it_behaves_like 'has field with value', 'name' do let(:value) { integration.title } diff --git a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb index 4ad35e7f0d16eb..b8cde32877b13c 100644 --- a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb +++ b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb @@ -6,8 +6,9 @@ include GraphqlHelpers let_it_be(:admin) { create(:admin) } + let(:queue) { 'authorized_projects' } - let(:variables) { { user: admin.username, queue_name: 'authorized_projects' } } + let(:variables) { { user: admin.username, queue_name: queue } } let(:mutation) { graphql_mutation(:admin_sidekiq_queues_delete_jobs, variables) } def mutation_response @@ -26,18 +27,19 @@ def mutation_response context 'valid request' do around do |example| - Sidekiq::Queue.new('authorized_projects').clear + Sidekiq::Queue.new(queue).clear Sidekiq::Testing.disable!(&example) - Sidekiq::Queue.new('authorized_projects').clear + Sidekiq::Queue.new(queue).clear end def add_job(user, args) Sidekiq::Client.push( 'class' => 'AuthorizedProjectsWorker', - 'queue' => 'authorized_projects', + 'queue' => queue, 'args' => args, 'meta.user' => user.username ) + raise 'Not enqueued!' if Sidekiq::Queue.new(queue).size.zero? end it 'returns info about the deleted jobs' do @@ -55,7 +57,7 @@ def add_job(user, args) end context 'when no required params are provided' do - let(:variables) { { queue_name: 'authorized_projects' } } + let(:variables) { { queue_name: queue } } it_behaves_like 'a mutation that returns errors in the response', errors: ['No metadata provided'] diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb index d2fa3cfc24f523..fa5daee6381515 100644 --- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb @@ -163,9 +163,15 @@ def mutation_response context 'when there are uploaded files' do shared_examples 'expected files argument' do |file_value, expected_value| let(:uploaded_files) { file_value } + let(:ham) { build(:snippet) } + let(:creation_response) do + ::ServiceResponse.error(message: 'urk', payload: { snippet: ham }) + end it do - expect(::Snippets::CreateService).to receive(:new).with(nil, user, hash_including(files: expected_value)) + expect(::Snippets::CreateService).to receive(:new) + .with(nil, user, hash_including(files: expected_value)) + .and_return(double(execute: creation_response)) subject end diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb index 4bce3c7fe0fc77..f544d78ecbb256 100644 --- a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb +++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb @@ -42,7 +42,6 @@ def query(vq = version_fields) describe 'scalar fields' do let(:path) { path_prefix } - let(:version_fields) { query_graphql_field(:sha) } before do post_query @@ -50,7 +49,7 @@ def query(vq = version_fields) { id: ->(x) { x.to_global_id.to_s }, sha: ->(x) { x.sha } }.each do |field, value| describe ".#{field}" do - let(:version_fields) { query_graphql_field(field) } + let(:version_fields) { field } it "retrieves the #{field}" do expect(data).to match(a_hash_including(field.to_s => value[version])) diff --git a/spec/requests/api/graphql/project/issue_spec.rb b/spec/requests/api/graphql/project/issue_spec.rb index 5f3688331813dd..ddf63a8f2c962e 100644 --- a/spec/requests/api/graphql/project/issue_spec.rb +++ b/spec/requests/api/graphql/project/issue_spec.rb @@ -29,8 +29,8 @@ let(:design_fields) do [ - query_graphql_field(:filename), - query_graphql_field(:project, nil, query_graphql_field(:id)) + :filename, + query_graphql_field(:project, :id) ] end @@ -173,7 +173,7 @@ let(:result_fields) { { 'version' => id_hash(version) } } let(:object_fields) do - design_fields + [query_graphql_field(:version, nil, query_graphql_field(:id))] + design_fields + [query_graphql_field(:version, :id)] end let(:no_argument_error) { missing_required_argument(path, :id) } diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb index 57dbe258ce4cae..99b15ff00b1663 100644 --- a/spec/requests/api/graphql/project/release_spec.rb +++ b/spec/requests/api/graphql/project/release_spec.rb @@ -36,7 +36,7 @@ def query(rq = release_fields) let(:path) { path_prefix } let(:release_fields) do - query_graphql_field(%{ + %{ tagName tagPath description @@ -45,7 +45,7 @@ def query(rq = release_fields) createdAt releasedAt upcomingRelease - }) + } end before do @@ -233,7 +233,7 @@ def query(rq = release_fields) let(:path) { path_prefix } let(:release_fields) do - query_graphql_field('description') + 'description' end before do @@ -394,10 +394,10 @@ def query(rq = release_fields) let(:current_user) { developer } let(:release_fields) do - query_graphql_field(%{ + %{ releasedAt upcomingRelease - }) + } end before do diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb index f1dcd530218efe..1e70e7f53677f8 100644 --- a/spec/requests/api/graphql/project/terraform/states_spec.rb +++ b/spec/requests/api/graphql/project/terraform/states_spec.rb @@ -48,37 +48,38 @@ let(:current_user) { project.creator } let(:data) { graphql_data.dig('project', 'terraformStates') } + let(:download_path) do + expose_path( + api_v4_projects_terraform_state_versions_path( + id: project.id, + name: terraform_state.name, + serial: latest_version.version + ) + ) + end before do post_graphql(query, current_user: current_user) end - it 'returns terraform state data', :aggregate_failures do - state = data.dig('nodes', 0) - version = state['latestVersion'] - - expect(state['id']).to eq(terraform_state.to_global_id.to_s) - expect(state['name']).to eq(terraform_state.name) - expect(state['lockedAt']).to eq(terraform_state.locked_at.iso8601) - expect(state['createdAt']).to eq(terraform_state.created_at.iso8601) - expect(state['updatedAt']).to eq(terraform_state.updated_at.iso8601) - expect(state.dig('lockedByUser', 'id')).to eq(terraform_state.locked_by_user.to_global_id.to_s) - - expect(version['id']).to eq(latest_version.to_global_id.to_s) - expect(version['serial']).to eq(latest_version.version) - expect(version['downloadPath']).to eq( - expose_path( - api_v4_projects_terraform_state_versions_path( - id: project.id, - name: terraform_state.name, - serial: latest_version.version - ) - ) - ) - expect(version['createdAt']).to eq(latest_version.created_at.iso8601) - expect(version['updatedAt']).to eq(latest_version.updated_at.iso8601) - expect(version.dig('createdByUser', 'id')).to eq(latest_version.created_by_user.to_global_id.to_s) - expect(version.dig('job', 'name')).to eq(latest_version.build.name) + it 'returns terraform state data' do + expect(data['nodes']).to contain_exactly({ + 'id' => global_id_of(terraform_state), + 'name' => terraform_state.name, + 'lockedAt' => terraform_state.locked_at.iso8601, + 'createdAt' => terraform_state.created_at.iso8601, + 'updatedAt' => terraform_state.updated_at.iso8601, + 'lockedByUser' => { 'id' => global_id_of(terraform_state.locked_by_user) }, + 'latestVersion' => { + 'id' => eq(latest_version.to_global_id.to_s), + 'serial' => eq(latest_version.version), + 'downloadPath' => eq(download_path), + 'createdAt' => eq(latest_version.created_at.iso8601), + 'updatedAt' => eq(latest_version.updated_at.iso8601), + 'createdByUser' => { 'id' => eq(latest_version.created_by_user.to_global_id.to_s) }, + 'job' => { 'name' => eq(latest_version.build.name) } + } + }) end it 'returns count of terraform states' do diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb index b0bf5bd484488d..261b1d20831484 100644 --- a/spec/requests/api/graphql/project_query_spec.rb +++ b/spec/requests/api/graphql/project_query_spec.rb @@ -5,35 +5,33 @@ RSpec.describe 'getting project information' do include GraphqlHelpers - let(:group) { create(:group) } - let(:project) { create(:project, :repository, group: group) } - let(:current_user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :repository, group: group) } + let_it_be(:current_user) { create(:user) } + let(:fields) { all_graphql_fields_for(Project, max_depth: 2, excluded: %w(jiraImports services)) } let(:query) do - graphql_query_for( - 'project', - { 'fullPath' => project.full_path }, - all_graphql_fields_for('project'.to_s.classify, excluded: %w(jiraImports services)) - ) + graphql_query_for(:project, { full_path: project.full_path }, fields) end context 'when the user has full access to the project' do let(:full_access_query) do - graphql_query_for('project', 'fullPath' => project.full_path) + graphql_query_for(:project, { full_path: project.full_path }, + all_graphql_fields_for('Project', max_depth: 2)) end before do project.add_maintainer(current_user) end - it 'includes the project' do - post_graphql(query, current_user: current_user) + it 'includes the project', :use_clean_rails_memory_store_caching, :request_store do + post_graphql(full_access_query, current_user: current_user) expect(graphql_data['project']).not_to be_nil end end - context 'when the user has access to the project' do + context 'when the user has access to the project', :use_clean_rails_memory_store_caching, :request_store do before do project.add_developer(current_user) end @@ -55,10 +53,12 @@ create(:ci_pipeline, project: project) end + let(:fields) { query_nodes(:pipelines) } + it 'is included in the pipelines connection' do post_graphql(query, current_user: current_user) - expect(graphql_data['project']['pipelines']['edges'].size).to eq(1) + expect(graphql_data_at(:project, :pipelines, :nodes)).to contain_exactly(a_kind_of(Hash)) end end @@ -109,7 +109,7 @@ end describe 'performance' do - before do + before_all do project.add_developer(current_user) mrs = create_list(:merge_request, 10, :closed, :with_head_pipeline, source_project: project, diff --git a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb index ef7086234c4a19..9c2eb3e5a5ce29 100644 --- a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb +++ b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb @@ -26,27 +26,35 @@ end describe '#image' do + let_it_be(:current_user) { create(:user) } let(:schema) { GitlabSchema } let(:query) { GraphQL::Query.new(schema) } - let(:context) { double('Context', schema: schema, query: query, parent: nil) } + let(:context) { query.context } let(:field) { described_class.fields['image'] } let(:args) { GraphQL::Query::Arguments::NO_ARGS } - let(:instance) do + let(:instance) { instantiate(object_id) } + let(:instance_b) { instantiate(object_id_b) } + + def instantiate(object_id) object = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id)) object_type.authorized_new(object, query.context) end - let(:instance_b) do - object_b = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id_b)) - object_type.authorized_new(object_b, query.context) + def resolve_image(instance) + field.resolve_field(instance, args, context) + end + + before do + context[:current_user] = current_user + allow(Ability).to receive(:allowed?).with(current_user, :read_design, anything).and_return(true) + allow(context).to receive(:parent).and_return(nil) end it 'resolves to the design image URL' do - image = field.resolve_field(instance, args, context) sha = design.versions.first.sha url = ::Gitlab::Routing.url_helpers.project_design_management_designs_raw_image_url(design.project, design, sha) - expect(image).to eq(url) + expect(resolve_image(instance)).to eq(url) end it 'has better than O(N) peformance', :request_store do @@ -68,10 +76,10 @@ # = 10 expect(instance).not_to eq(instance_b) # preload designs themselves. expect do - image_a = field.resolve_field(instance, args, context) - image_b = field.resolve_field(instance, args, context) - image_c = field.resolve_field(instance_b, args, context) - image_d = field.resolve_field(instance_b, args, context) + image_a = resolve_image(instance) + image_b = resolve_image(instance) + image_c = resolve_image(instance_b) + image_d = resolve_image(instance_b) expect(image_a).to eq(image_b) expect(image_c).not_to eq(image_b) expect(image_c).to eq(image_d) diff --git a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb index b2047f1d32c43d..d2987b324b19fb 100644 --- a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb +++ b/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb @@ -2,14 +2,12 @@ RSpec.shared_examples 'no Jira import data present' do it 'returns none' do - expect(resolve_imports).to eq JiraImportState.none + expect(resolve_imports).to be_empty end end RSpec.shared_examples 'no Jira import access' do - it 'raises error' do - expect do - resolve_imports - end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + it 'returns nil' do + expect(resolve_imports).to be_nil end end diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb index b67cac94547e82..84ebd4852b974c 100644 --- a/spec/support/shared_examples/graphql/mutation_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb @@ -13,6 +13,8 @@ it do post_graphql_mutation(mutation, current_user: current_user) + expect(graphql_errors).to be_present + error_messages = graphql_errors.map { |e| e['message'] } expect(error_messages).to match_errors diff --git a/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb b/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb index 54b3f84a6e6837..8678b23ad3112a 100644 --- a/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb @@ -13,7 +13,8 @@ RSpec.shared_examples 'can raise spam flag' do it 'spam parameters are passed to the service' do - expect(service).to receive(:new).with(anything, anything, hash_including(api: true, request: instance_of(ActionDispatch::Request))) + args = [anything, anything, hash_including(api: true, request: instance_of(ActionDispatch::Request))] + expect(service).to receive(:new).with(*args).and_call_original subject end @@ -39,7 +40,9 @@ end it 'request parameter is not passed to the service' do - expect(service).to receive(:new).with(anything, anything, hash_not_including(request: instance_of(ActionDispatch::Request))) + expect(service).to receive(:new) + .with(anything, anything, hash_not_including(request: instance_of(ActionDispatch::Request))) + .and_call_original subject end diff --git a/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb index b1de1432a37202..16c2ab07f3aac9 100644 --- a/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb +++ b/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'no project services' do it 'returns empty collection' do - expect(resolve_services).to eq [] + expect(resolve_services).to be_empty end end -- GitLab From 1b99ac214e4da59bb224f91f559456bcc5560b91 Mon Sep 17 00:00:00 2001 From: Alex Kalderimis Date: Fri, 4 Dec 2020 00:14:34 +0000 Subject: [PATCH 2/9] Remove dead code --- .../projects/jira_imports_resolver.rb | 24 ------------------- app/graphql/types/jira_import_type.rb | 3 +-- .../jira_import_resolver_shared_examples.rb | 13 ---------- 3 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 app/graphql/resolvers/projects/jira_imports_resolver.rb delete mode 100644 spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb diff --git a/app/graphql/resolvers/projects/jira_imports_resolver.rb b/app/graphql/resolvers/projects/jira_imports_resolver.rb deleted file mode 100644 index 9ef924f2beea10..00000000000000 --- a/app/graphql/resolvers/projects/jira_imports_resolver.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module Resolvers - module Projects - class JiraImportsResolver < BaseResolver - prepend ::ManualAuthorization - include Gitlab::Graphql::Authorize::AuthorizeResource - - type Types::JiraImportType.connection_type, null: true - - alias_method :project, :object - - def resolve(**args) - authorize!(project) - - project.jira_imports - end - - def authorized_resource?(project) - context[:current_user].present? && Ability.allowed?(context[:current_user], :read_project, project) - end - end - end -end diff --git a/app/graphql/types/jira_import_type.rb b/app/graphql/types/jira_import_type.rb index cf58a53b40d8a7..b3854487cec9a4 100644 --- a/app/graphql/types/jira_import_type.rb +++ b/app/graphql/types/jira_import_type.rb @@ -2,8 +2,7 @@ module Types # rubocop: disable Graphql/AuthorizeTypes - # Authorization is at project level for owners or admins, - # so it is added directly to the Resolvers::JiraImportsResolver + # Authorization is at project level for owners or admins class JiraImportType < BaseObject graphql_name 'JiraImport' diff --git a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb deleted file mode 100644 index d2987b324b19fb..00000000000000 --- a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'no Jira import data present' do - it 'returns none' do - expect(resolve_imports).to be_empty - end -end - -RSpec.shared_examples 'no Jira import access' do - it 'returns nil' do - expect(resolve_imports).to be_nil - end -end -- GitLab From f304ff909a95450d762c97e661ca11c70c2ed276 Mon Sep 17 00:00:00 2001 From: Alex Kalderimis Date: Fri, 4 Dec 2020 09:59:38 +0000 Subject: [PATCH 3/9] Remove custom pagination spec Prefer to use the pagination shared examples --- .../graphql/group/epic/epic_issues_spec.rb | 57 ++++--------------- 1 file changed, 11 insertions(+), 46 deletions(-) diff --git a/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb b/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb index b7bc802b5ba5bd..9d28cb5f16339e 100644 --- a/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb +++ b/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb @@ -75,56 +75,21 @@ def first_epic_issues_page_info end context 'pagination' do - def epic_fields(after) - <<~NODE - edges { - node { - iid - issues(#{attributes_to_graphql(first: 1, after: after)}) { - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - }, - edges { - node { - id - } - } - } - } - } - NODE - end - - let(:params) { { iid: epic.iid } } + let(:data_path) { %i[group epics nodes] + [0] + %i[issues] } - def get_page(after) - query = epic_query(params, epic_fields(after)) - post_graphql(query, current_user: user) - - expect(response).to have_gitlab_http_status(:success) - - data = ::Gitlab::Json.parse(response.body) - expect(data['errors']).to be_blank - epics = data.dig('data', 'group', 'epics', 'edges') - issues = issue_ids(epics)[epic.iid] - page = epics.dig(0, 'node', 'issues', 'pageInfo') - - [issues, page] + def pagination_query(args) + epic_query({ iid: epic.iid }, epic_fields(args)) end - it 'paginates correctly' do - issues, page_1 = get_page(nil) - - expect(issues).to contain_exactly(global_id_of(issue)) - expect(page_1).to include("hasNextPage" => true, "hasPreviousPage" => false) - - next_issues, page_2 = get_page(page_1['endCursor']) + def epic_fields(args) + query_graphql_field(:nodes, query_nodes(:issues, :id, args: args, include_pagination_info: true)) + end - expect(next_issues).to contain_exactly(global_id_of(confidential_issue)) - expect(page_2).to include("hasNextPage" => false, "hasPreviousPage" => true) + it_behaves_like 'sorted paginated query' do + let(:current_user) { user } + let(:sort_param) { } + let(:first_param) { 1 } + let(:expected_results) { [issue, confidential_issue].map { |i| global_id_of(i) } } end end end -- GitLab From dbb0c4de337b2342660033a53140f5f9364c13f4 Mon Sep 17 00:00:00 2001 From: Alex Kalderimis Date: Fri, 4 Dec 2020 10:25:22 +0000 Subject: [PATCH 4/9] Use kwarg for clarity --- .../concerns/caching_array_resolver_spec.rb | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb index bfd63cd861ecb2..c0425759d03a79 100644 --- a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb +++ b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb @@ -92,15 +92,15 @@ def model_class it 'batches the queries' do expect do - [resolve_users(true), resolve_users(false)].each(&method(:force)) - end.to issue_same_number_of_queries_as { force(resolve_users(nil)) } + [resolve_users(admin: true), resolve_users(admin: false)].each(&method(:force)) + end.to issue_same_number_of_queries_as { force(resolve_users(admin: nil)) } end it 'finds the correct values' do - found_admins = resolve_users(true) - found_others = resolve_users(false) - admins_again = resolve_users(true) - found_all = resolve_users(nil) + found_admins = resolve_users(admin: true) + found_others = resolve_users(admin: false) + admins_again = resolve_users(admin: true) + found_all = resolve_users(admin: nil) expect(force(found_admins)).to match_array(admins) expect(force(found_others)).to match_array(non_admins) @@ -112,14 +112,14 @@ def model_class it 'does not perform a union of a query with itself' do expect(User).to receive(:where).once.and_call_original - [resolve_users(false), resolve_users(false)].each(&method(:force)) + [resolve_users(admin: false), resolve_users(admin: false)].each(&method(:force)) end context 'one of the queries returns no results' do it 'finds the correct values' do - found_admins = resolve_users(true) - found_others = resolve_users(false) - found_all = resolve_users(nil) + found_admins = resolve_users(admin: true) + found_others = resolve_users(admin: false) + found_all = resolve_users(admin: nil) expect(force(found_admins)).to match_array(admins) expect(force(found_others)).to be_empty @@ -129,12 +129,12 @@ def model_class context 'one of the queries has already been cached' do before do - force(resolve_users(nil)) + force(resolve_users(admin: nil)) end it 'avoids further queries' do expect do - repeated_find = resolve_users(nil) + repeated_find = resolve_users(admin: nil) expect(force(repeated_find)).to match_array(admins) end.not_to exceed_query_limit(0) @@ -158,8 +158,8 @@ def item_found(key, item) end it 'receives item_found for each key the item mapped to' do - found_admins = resolve_users(true, with_item_found) - found_all = resolve_users(nil, with_item_found) + found_admins = resolve_users(admin: true, resolver: with_item_found) + found_all = resolve_users(admin: nil, resolver: with_item_found) [found_admins, found_all].each(&method(:force)) @@ -175,8 +175,8 @@ def item_found(key, item) let(:max_page_size) { 2 } it 'respects the max_page_size, on a per subset basis' do - found_all = resolve_users(nil) - found_admins = resolve_users(true) + found_all = resolve_users(admin: nil) + found_admins = resolve_users(admin: true) expect(force(found_all).size).to eq(2) expect(force(found_admins).size).to eq(2) @@ -187,8 +187,8 @@ def item_found(key, item) let(:max_page_size) { nil } it 'takes the page size from schema.default_max_page_size' do - found_all = resolve_users(nil) - found_admins = resolve_users(true) + found_all = resolve_users(admin: nil) + found_admins = resolve_users(admin: true) expect(force(found_all).size).to eq(schema.default_max_page_size) expect(force(found_admins).size).to eq(schema.default_max_page_size) @@ -205,8 +205,8 @@ def item_found(key, item) end end - def resolve_users(is_admin, resolver = caching_resolver) - args = { is_admin: is_admin } + def resolve_users(admin:, resolver: caching_resolver) + args = { is_admin: admin } opts = resolver.field_options allow(resolver).to receive(:field_options).and_return(opts.merge(max_page_size: max_page_size)) resolve(resolver, args: args, ctx: query_context, schema: schema, field: field) -- GitLab From cc54581b760566510f6debc99ba98774790d72f6 Mon Sep 17 00:00:00 2001 From: Alex Kalderimis Date: Fri, 4 Dec 2020 16:57:45 +0000 Subject: [PATCH 5/9] Link to new issue in pending spec --- ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb b/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb index 9d28cb5f16339e..52543721812c52 100644 --- a/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb +++ b/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb @@ -126,11 +126,8 @@ def epic_fields(args) expect(result[epic2.iid]).to eq [issue2.to_global_id.to_s] end - # TODO remove the pending state of this spec when - # we have an efficient way of preloading data on GraphQL. - # For more information check: https://gitlab.com/gitlab-org/gitlab/-/issues/207898 it 'avoids N+1 queries' do - pending 'https://gitlab.com/gitlab-org/gitlab/-/issues/207898' + pending 'https://gitlab.com/gitlab-org/gitlab/-/issues/291089' control_count = ActiveRecord::QueryRecorder.new do post_graphql(epic_query(iid: epic.iid), current_user: user) end.count -- GitLab From 4e501c948d2a3285f5cd893ba3d2ec02c8f974e5 Mon Sep 17 00:00:00 2001 From: Alex Kalderimis Date: Thu, 10 Dec 2020 15:22:11 +0000 Subject: [PATCH 6/9] Changes in response to reviewer comments This updates some module doc comments, changes some variable names, uses `global_id_of` in a few more places, and adds a new use of `before_all`. --- lib/gitlab/graphql/laziness.rb | 23 +++++++++++++------ .../concerns/caching_array_resolver_spec.rb | 5 +--- .../graphql/mutations/snippets/create_spec.rb | 4 ++-- .../graphql/project/terraform/states_spec.rb | 4 ++-- .../api/graphql/project_query_spec.rb | 2 +- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/gitlab/graphql/laziness.rb b/lib/gitlab/graphql/laziness.rb index ae63ac233f30be..749d832919d11f 100644 --- a/lib/gitlab/graphql/laziness.rb +++ b/lib/gitlab/graphql/laziness.rb @@ -8,21 +8,30 @@ module Graphql # example: # # class MyAwesomeClass - # def sum_frobbocities(ids) - # ids.map { |n| get_the_thing(n) }.map(&method(:force).sum + # include ::Gitlab::Graphql::Laziness + # + # # takes a list of id and list of factors, and computes + # # sum of [SomeObject[i]#value * factor[i]] + # def resolve(ids:, factors:) + # ids.zip(factors) + # .map { |id, factor| promise_an_int(id, factor) } + # .map(&method(:force)) + # .sum # end # - # def get_the_thing(id) - # thunk = SomeBatchLoader.load(id) - # defer { force(thunk).frobbocity * 2 } + # # returns a promise for an Integer + # def (id, factor) + # thunk = SomeObject.lazy_find(id) + # defer { force(thunk).value * factor } # end # end # # In the example above, we use defer to delay forcing the batch-loaded # item until we need it, and then we use `force` to consume the lazy values # - # If `SomeBatchLoader.load(id)` batches correctly, calling - # `sum_frobbocities` will only perform one batched load. + # If `SomeObject.lazy_find(id)` batches correctly, calling + # `resolve` will only perform one batched load for all objects, rather than + # loading them individually before combining the results. # module Laziness def defer(&block) diff --git a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb index c0425759d03a79..5370f7a743350b 100644 --- a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb +++ b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb @@ -4,6 +4,7 @@ RSpec.describe ::CachingArrayResolver do include GraphqlHelpers + include Gitlab::Graphql::Laziness let_it_be(:admins) { create_list(:user, 4, admin: true) } let(:query_context) { { current_user: admins.first } } @@ -211,8 +212,4 @@ def resolve_users(admin:, resolver: caching_resolver) allow(resolver).to receive(:field_options).and_return(opts.merge(max_page_size: max_page_size)) resolve(resolver, args: args, ctx: query_context, schema: schema, field: field) end - - def force(lazy) - ::Gitlab::Graphql::Lazy.force(lazy) - end end diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb index fa5daee6381515..fd0dc98a8d3246 100644 --- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb @@ -163,9 +163,9 @@ def mutation_response context 'when there are uploaded files' do shared_examples 'expected files argument' do |file_value, expected_value| let(:uploaded_files) { file_value } - let(:ham) { build(:snippet) } + let(:snippet) { build(:snippet) } let(:creation_response) do - ::ServiceResponse.error(message: 'urk', payload: { snippet: ham }) + ::ServiceResponse.error(message: 'urk', payload: { snippet: snippet }) end it do diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb index 1e70e7f53677f8..aa44295d0bba7d 100644 --- a/spec/requests/api/graphql/project/terraform/states_spec.rb +++ b/spec/requests/api/graphql/project/terraform/states_spec.rb @@ -71,12 +71,12 @@ 'updatedAt' => terraform_state.updated_at.iso8601, 'lockedByUser' => { 'id' => global_id_of(terraform_state.locked_by_user) }, 'latestVersion' => { - 'id' => eq(latest_version.to_global_id.to_s), + 'id' => eq(global_id_of(latest_version)), 'serial' => eq(latest_version.version), 'downloadPath' => eq(download_path), 'createdAt' => eq(latest_version.created_at.iso8601), 'updatedAt' => eq(latest_version.updated_at.iso8601), - 'createdByUser' => { 'id' => eq(latest_version.created_by_user.to_global_id.to_s) }, + 'createdByUser' => { 'id' => eq(global_id_of(latest_version.created_by_user)) }, 'job' => { 'name' => eq(latest_version.build.name) } } }) diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb index 261b1d20831484..b29f9ae913fb29 100644 --- a/spec/requests/api/graphql/project_query_spec.rb +++ b/spec/requests/api/graphql/project_query_spec.rb @@ -32,7 +32,7 @@ end context 'when the user has access to the project', :use_clean_rails_memory_store_caching, :request_store do - before do + before_all do project.add_developer(current_user) end -- GitLab From 7370f60f34f9031c8d9982ceb89ae70381fbc440 Mon Sep 17 00:00:00 2001 From: Alex Kalderimis Date: Fri, 11 Dec 2020 12:55:09 +0000 Subject: [PATCH 7/9] Review: Apply suggested changes --- .../board_groupings/epics_resolvers_spec.rb | 1 + .../api/graphql/project/terraform/states_spec.rb | 15 +++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ee/spec/graphql/resolvers/board_groupings/epics_resolvers_spec.rb b/ee/spec/graphql/resolvers/board_groupings/epics_resolvers_spec.rb index 6f85c33f139a19..78119d21d70a7b 100644 --- a/ee/spec/graphql/resolvers/board_groupings/epics_resolvers_spec.rb +++ b/ee/spec/graphql/resolvers/board_groupings/epics_resolvers_spec.rb @@ -83,6 +83,7 @@ it 'accepts negated issue params' do filters = { label_name: ['foo'], not: { label_name: %w(foo bar) } } + expect(Boards::Issues::ListService).to receive(:new).with( group_board.resource_parent, current_user, diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb index aa44295d0bba7d..2879530acc5d01 100644 --- a/spec/requests/api/graphql/project/terraform/states_spec.rb +++ b/spec/requests/api/graphql/project/terraform/states_spec.rb @@ -48,21 +48,20 @@ let(:current_user) { project.creator } let(:data) { graphql_data.dig('project', 'terraformStates') } - let(:download_path) do - expose_path( + + before do + post_graphql(query, current_user: current_user) + end + + it 'returns terraform state data' do + download_path = expose_path( api_v4_projects_terraform_state_versions_path( id: project.id, name: terraform_state.name, serial: latest_version.version ) ) - end - - before do - post_graphql(query, current_user: current_user) - end - it 'returns terraform state data' do expect(data['nodes']).to contain_exactly({ 'id' => global_id_of(terraform_state), 'name' => terraform_state.name, -- GitLab From f1f9654046e97d945d2cdcf3402d399bdee74bcf Mon Sep 17 00:00:00 2001 From: Alex Kalderimis Date: Fri, 11 Dec 2020 14:46:42 +0000 Subject: [PATCH 8/9] Fix N+1 in Epic.issues This uses the caching_array_resolver to eliminate the N+1 for Epic.issues. The filtering by accessibility is removed since we have authorization to do that for us, which is tested in the request specs. --- .../concerns/caching_array_resolver.rb | 5 +++++ app/models/issue.rb | 1 + .../graphql/resolvers/epic_issues_resolver.rb | 18 ++++++++++++++++-- .../resolvers/epic_issues_resolver_spec.rb | 11 ++--------- .../graphql/group/epic/epic_issues_spec.rb | 19 +++++++++---------- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/app/graphql/resolvers/concerns/caching_array_resolver.rb b/app/graphql/resolvers/concerns/caching_array_resolver.rb index 4f2c8b98928056..f62b9932a4da82 100644 --- a/app/graphql/resolvers/concerns/caching_array_resolver.rb +++ b/app/graphql/resolvers/concerns/caching_array_resolver.rb @@ -62,6 +62,7 @@ def resolve(**args) queries.in_groups_of(max_union_size, false).each do |group| by_id = model_class .from_union(tag(group), remove_duplicates: false) + .preload(preload) # rubocop: disable CodeReuse/ActiveRecord .group_by { |r| r[primary_key] } by_id.values.each do |item_group| @@ -75,6 +76,10 @@ def resolve(**args) end end + def preload + nil + end + # Override this to intercept the items once they are found def item_found(query_input, item) end diff --git a/app/models/issue.rb b/app/models/issue.rb index b4071307e06c45..253f4465cd9127 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -22,6 +22,7 @@ class Issue < ApplicationRecord include Presentable include IssueAvailableFeatures include Todoable + include FromUnion DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze diff --git a/ee/app/graphql/resolvers/epic_issues_resolver.rb b/ee/app/graphql/resolvers/epic_issues_resolver.rb index 40e0236eaacb5e..042feafac4e94a 100644 --- a/ee/app/graphql/resolvers/epic_issues_resolver.rb +++ b/ee/app/graphql/resolvers/epic_issues_resolver.rb @@ -2,12 +2,26 @@ module Resolvers class EpicIssuesResolver < BaseResolver + include CachingArrayResolver + type Types::EpicIssueType.connection_type, null: true alias_method :epic, :object - def resolve(**args) - epic.issues_readable_by(context[:current_user], preload: { project: [:namespace, :project_feature] }) + def model_class + ::Issue + end + + def query_input(**args) + epic.id + end + + def query_for(id) + ::Epic.related_issues(ids: id) + end + + def preload + { project: [:namespace, :project_feature] } end end end diff --git a/ee/spec/graphql/resolvers/epic_issues_resolver_spec.rb b/ee/spec/graphql/resolvers/epic_issues_resolver_spec.rb index 9e975d079c9699..704ad08dcd65a7 100644 --- a/ee/spec/graphql/resolvers/epic_issues_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/epic_issues_resolver_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe Resolvers::EpicIssuesResolver do + include ::Gitlab::Graphql::Laziness include GraphqlHelpers let_it_be(:current_user) { create(:user) } @@ -38,17 +39,9 @@ expect(result).to eq [[issue2, issue1], [issue3, issue4]] end - - it 'finds only epic issues that user can read' do - guest = create(:user) - - result = epics.map { |e| resolve_epic_issues(e, user: guest).to_a } - - expect(result).to eq [[issue1], []] - end end def resolve_epic_issues(object, user: current_user) - resolve(described_class, obj: object, ctx: { current_user: user }) + force(resolve(described_class, obj: object, ctx: { current_user: user })) end end diff --git a/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb b/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb index 52543721812c52..3747622f6ee594 100644 --- a/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb +++ b/ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb @@ -113,11 +113,8 @@ def epic_fields(args) let_it_be(:epic_issue3) { create(:epic_issue, epic: epic2, issue: issue2, relative_position: 3) } let(:params) { { iids: [epic.iid, epic2.iid] } } - before do - project.add_developer(user) - end - it 'returns issues for each epic' do + project.add_developer(user) post_graphql(epic_query(params), current_user: user) expect(response).to have_gitlab_http_status(:success) @@ -127,14 +124,16 @@ def epic_fields(args) end it 'avoids N+1 queries' do - pending 'https://gitlab.com/gitlab-org/gitlab/-/issues/291089' - control_count = ActiveRecord::QueryRecorder.new do - post_graphql(epic_query(iid: epic.iid), current_user: user) - end.count + user_1 = create(:user, developer_projects: [project]) + user_2 = create(:user, developer_projects: [project]) + + control_count = ActiveRecord::QueryRecorder.new(query_recorder_debug: true) do + post_graphql(epic_query(iid: epic.iid), current_user: user_1) + end expect do - post_graphql(epic_query(params), current_user: user) - end.not_to exceed_query_limit(control_count) + post_graphql(epic_query(params), current_user: user_2) + end.not_to exceed_query_limit(control_count).ignoring(/FROM "namespaces"/) expect(graphql_errors).to be_nil end -- GitLab From 2ef928ca575b396706d7ce5ebf5cfddd00307d48 Mon Sep 17 00:00:00 2001 From: Alex Kalderimis Date: Sun, 13 Dec 2020 23:49:28 +0000 Subject: [PATCH 9/9] Add filtering to epic issues resolver --- .../resolvers/concerns/caching_array_resolver.rb | 12 +++++++++++- ee/app/graphql/resolvers/epic_issues_resolver.rb | 4 ++++ .../graphql/resolvers/epic_issues_resolver_spec.rb | 12 ++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/graphql/resolvers/concerns/caching_array_resolver.rb b/app/graphql/resolvers/concerns/caching_array_resolver.rb index f62b9932a4da82..e7555dcf42c5ed 100644 --- a/app/graphql/resolvers/concerns/caching_array_resolver.rb +++ b/app/graphql/resolvers/concerns/caching_array_resolver.rb @@ -43,8 +43,10 @@ # (i.e. `resolve(**args).sync == query_for(query_input(**args)).to_a`). # # Classes may implement: -# - #item_found(A, R) (return value is ignored) # - max_union_size Integer (the maximum number of queries to run in any one union) +# - preload -> Preloads|NilClass (a set of preloads to apply to each query) +# - #item_found(A, R) (return value is ignored) +# - allowed?(R) -> Boolean (if this method returns false, the value is not resolved) module CachingArrayResolver MAX_UNION_SIZE = 50 @@ -76,6 +78,12 @@ def resolve(**args) end end + # Override to apply filters on a per-item basis + def allowed?(item) + true + end + + # Override to specify preloads for each query def preload nil end @@ -99,6 +107,8 @@ def batch end def found(loader, key, value) + return unless allowed?(value) + loader.call(key) do |vs| item_found(key, value) vs << value diff --git a/ee/app/graphql/resolvers/epic_issues_resolver.rb b/ee/app/graphql/resolvers/epic_issues_resolver.rb index 042feafac4e94a..da5684a8e47898 100644 --- a/ee/app/graphql/resolvers/epic_issues_resolver.rb +++ b/ee/app/graphql/resolvers/epic_issues_resolver.rb @@ -12,6 +12,10 @@ def model_class ::Issue end + def allowed?(issue) + DeclarativePolicy.user_scope { issue.visible_to_user?(current_user) } + end + def query_input(**args) epic.id end diff --git a/ee/spec/graphql/resolvers/epic_issues_resolver_spec.rb b/ee/spec/graphql/resolvers/epic_issues_resolver_spec.rb index 704ad08dcd65a7..88f24449223d96 100644 --- a/ee/spec/graphql/resolvers/epic_issues_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/epic_issues_resolver_spec.rb @@ -39,6 +39,18 @@ expect(result).to eq [[issue2, issue1], [issue3, issue4]] end + + it 'finds only epic issues that user can read' do + guest = create(:user) + + result = + [ + resolve_epic_issues(epic1, user: guest).to_a, + resolve_epic_issues(epic2, user: guest).to_a + ] + + expect(result).to eq([[issue1], []]) + end end def resolve_epic_issues(object, user: current_user) -- GitLab