diff options
Diffstat (limited to 'spec/requests/api/graphql')
17 files changed, 523 insertions, 88 deletions
diff --git a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb index 241c658441b..6324db0be4a 100644 --- a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb @@ -16,6 +16,7 @@ RSpec.describe 'get board lists' do let(:params) { '' } let(:board) { } + let(:confidential) { false } let(:board_parent_type) { board_parent.class.to_s.downcase } let(:board_data) { graphql_data[board_parent_type]['boards']['nodes'][0] } let(:lists_data) { board_data['lists']['nodes'][0] } @@ -30,7 +31,7 @@ RSpec.describe 'get board lists' do nodes { lists { nodes { - issues(filters: {labelName: "#{label2.title}"}, first: 3) { + issues(filters: {labelName: "#{label2.title}", confidential: #{confidential}}, first: 3) { count nodes { #{all_graphql_fields_for('issues'.classify)} @@ -57,14 +58,15 @@ RSpec.describe 'get board lists' do end shared_examples 'group and project board list issues query' do - let!(:board) { create(:board, resource_parent: board_parent) } - let!(:label_list) { create(:list, board: board, label: label, position: 10) } - let!(:issue1) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 9) } - let!(:issue2) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 2) } - let!(:issue3) { create(:issue, project: issue_project, labels: [label, label2], relative_position: nil) } - let!(:issue4) { create(:issue, project: issue_project, labels: [label], relative_position: 9) } - let!(:issue5) { create(:issue, project: issue_project, labels: [label2], relative_position: 432) } - let!(:issue6) { create(:issue, project: issue_project, labels: [label, label2], relative_position: nil) } + let_it_be(:board) { create(:board, resource_parent: board_parent) } + let_it_be(:label_list) { create(:list, board: board, label: label, position: 10) } + let_it_be(:issue1) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 9) } + let_it_be(:issue2) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 2) } + let_it_be(:issue3) { create(:issue, project: issue_project, labels: [label, label2], relative_position: nil) } + let_it_be(:issue4) { create(:issue, project: issue_project, labels: [label], relative_position: 9) } + let_it_be(:issue5) { create(:issue, project: issue_project, labels: [label2], relative_position: 432) } + let_it_be(:issue6) { create(:issue, project: issue_project, labels: [label, label2], relative_position: nil) } + let_it_be(:issue7) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 5, confidential: true) } context 'when the user does not have access to the board' do it 'returns nil' do @@ -90,23 +92,33 @@ RSpec.describe 'get board lists' do expect(issue_id).not_to include(issue6.id) expect(issue3.relative_position).to be_nil end + + context 'when filtering by confidential' do + let(:confidential) { true } + + it 'returns matching issue' do + expect(issue_titles).to match_array([issue7.title]) + expect(issue_relative_positions).not_to include(nil) + end + end end end describe 'for a project' do - let(:board_parent) { project } - let(:label) { project_label } - let(:label2) { project_label2 } - let(:issue_project) { project } + let_it_be(:board_parent) { project } + let_it_be(:label) { project_label } + let_it_be(:label2) { project_label2 } + let_it_be(:issue_project) { project } it_behaves_like 'group and project board list issues query' end describe 'for a group' do - let(:board_parent) { group } - let(:label) { group_label } - let(:label2) { group_label2 } - let(:issue_project) { create(:project, :private, group: group) } + let_it_be(:board_parent) { group } + let_it_be(:label) { group_label } + let_it_be(:label2) { group_label2 } + + let_it_be(:issue_project) { create(:project, :private, group: group) } before do allow(board_parent).to receive(:multiple_issue_boards_available?).and_return(false) diff --git a/spec/requests/api/graphql/boards/board_list_query_spec.rb b/spec/requests/api/graphql/boards/board_list_query_spec.rb index dec7ca715f2..f01f7e87f10 100644 --- a/spec/requests/api/graphql/boards/board_list_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_list_query_spec.rb @@ -12,6 +12,7 @@ RSpec.describe 'Querying a Board list' do let_it_be(:list) { create(:list, board: board, label: label) } let_it_be(:issue1) { create(:issue, project: project, labels: [label]) } let_it_be(:issue2) { create(:issue, project: project, labels: [label], assignees: [current_user]) } + let_it_be(:issue3) { create(:issue, project: project, labels: [label], confidential: true) } let(:filters) { {} } let(:query) do @@ -37,19 +38,33 @@ RSpec.describe 'Querying a Board list' do it { is_expected.to include({ 'issuesCount' => 2, 'title' => list.title }) } - context 'with matching issue filters' do - let(:filters) { { assigneeUsername: current_user.username } } + describe 'issue filters' do + context 'with matching assignee username issue filters' do + let(:filters) { { assigneeUsername: current_user.username } } - it 'filters issues metadata' do - is_expected.to include({ 'issuesCount' => 1, 'title' => list.title }) + it 'filters issues metadata' do + is_expected.to include({ 'issuesCount' => 1, 'title' => list.title }) + end end - end - context 'with unmatching issue filters' do - let(:filters) { { assigneeUsername: 'foo' } } + context 'with unmatching assignee username issue filters' do + let(:filters) { { assigneeUsername: 'foo' } } + + it 'filters issues metadata' do + is_expected.to include({ 'issuesCount' => 0, 'title' => list.title }) + end + end + + context 'when filtering by confidential' do + let(:filters) { { confidential: true } } + + before_all do + project.add_developer(current_user) + end - it 'filters issues metadata' do - is_expected.to include({ 'issuesCount' => 0, 'title' => list.title }) + it 'filters issues metadata' do + is_expected.to include({ 'issuesCount' => 1, 'title' => list.title }) + end end end end diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb index ace8c59e82d..e8fb9daa43b 100644 --- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb @@ -109,9 +109,15 @@ RSpec.describe 'get board lists' do it 'returns the correct list with issue count for matching issue filters' do label_list = create(:list, board: board, label: label, position: 10) create(:issue, project: project, labels: [label, label2]) + create(:issue, project: project, labels: [label, label2], confidential: true) create(:issue, project: project, labels: [label]) - post_graphql(query(id: global_id_of(label_list), issueFilters: { labelName: label2.title }), current_user: user) + post_graphql( + query( + id: global_id_of(label_list), + issueFilters: { labelName: label2.title, confidential: false } + ), current_user: user + ) aggregate_failures do list_node = lists_data[0]['node'] diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb index b2f4801a083..3a1df3525ef 100644 --- a/spec/requests/api/graphql/ci/jobs_spec.rb +++ b/spec/requests/api/graphql/ci/jobs_spec.rb @@ -14,8 +14,8 @@ RSpec.describe 'Query.project.pipeline' do describe '.stages.groups.jobs' do let(:pipeline) do pipeline = create(:ci_pipeline, project: project, user: user) - stage = create(:ci_stage_entity, project: project, pipeline: pipeline, name: 'first') - create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'my test job') + stage = create(:ci_stage_entity, project: project, pipeline: pipeline, name: 'first', position: 1) + create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'my test job', scheduling_type: :stage) pipeline end @@ -44,13 +44,23 @@ RSpec.describe 'Query.project.pipeline' do name jobs { nodes { - detailedStatus { - id - } name needs { nodes { #{all_graphql_fields_for('CiBuildNeed')} } } + previousStageJobsOrNeeds { + nodes { + ... on CiBuildNeed { + #{all_graphql_fields_for('CiBuildNeed')} + } + ... on CiJob { + #{all_graphql_fields_for('CiJob')} + } + } + } + detailedStatus { + id + } pipeline { id } @@ -62,58 +72,61 @@ RSpec.describe 'Query.project.pipeline' do FIELDS end - context 'when there are build needs' do - before do - pipeline.statuses.each do |build| - create_list(:ci_build_need, 2, build: build) - end - end - - it 'reports the build needs' do - post_graphql(query, current_user: user) - - expect(jobs_graphql_data).to contain_exactly a_hash_including( - 'needs' => a_hash_including( - 'nodes' => contain_exactly( - a_hash_including('name' => String), - a_hash_including('name' => String) - ) - ) - ) - end - end - it 'returns the jobs of a pipeline stage' do post_graphql(query, current_user: user) expect(jobs_graphql_data).to contain_exactly(a_hash_including('name' => 'my test job')) end - describe 'performance' do + context 'when there is more than one stage and job needs' do before do build_stage = create(:ci_stage_entity, position: 2, name: 'build', project: project, pipeline: pipeline) test_stage = create(:ci_stage_entity, position: 3, name: 'test', project: project, pipeline: pipeline) - create(:commit_status, pipeline: pipeline, stage_id: build_stage.id, name: 'docker 1 2') - create(:commit_status, pipeline: pipeline, stage_id: build_stage.id, name: 'docker 2 2') - create(:commit_status, pipeline: pipeline, stage_id: test_stage.id, name: 'rspec 1 2') - create(:commit_status, pipeline: pipeline, stage_id: test_stage.id, name: 'rspec 2 2') - end - it 'can find the first stage' do - post_graphql(query, current_user: user, variables: first_n.with(1)) + create(:ci_build, pipeline: pipeline, name: 'docker 1 2', scheduling_type: :stage, stage: build_stage, stage_idx: build_stage.position) + create(:ci_build, pipeline: pipeline, name: 'docker 2 2', stage: build_stage, stage_idx: build_stage.position, scheduling_type: :dag) + create(:ci_build, pipeline: pipeline, name: 'rspec 1 2', scheduling_type: :stage, stage: test_stage, stage_idx: test_stage.position) + test_job = create(:ci_build, pipeline: pipeline, name: 'rspec 2 2', scheduling_type: :dag, stage: test_stage, stage_idx: test_stage.position) - expect(jobs_graphql_data).to contain_exactly(a_hash_including('name' => 'my test job')) + create(:ci_build_need, build: test_job, name: 'my test job') end - it 'reports the build needs and previous stages with no duplicates', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/346433' do + it 'reports the build needs and execution requirements', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347290' do post_graphql(query, current_user: user) expect(jobs_graphql_data).to contain_exactly( - a_hash_including('name' => 'my test job'), - a_hash_including('name' => 'docker 1 2'), - a_hash_including('name' => 'docker 2 2'), - a_hash_including('name' => 'rspec 1 2'), - a_hash_including('name' => 'rspec 2 2') + a_hash_including( + 'name' => 'my test job', + 'needs' => { 'nodes' => [] }, + 'previousStageJobsOrNeeds' => { 'nodes' => [] } + ), + a_hash_including( + 'name' => 'docker 1 2', + 'needs' => { 'nodes' => [] }, + 'previousStageJobsOrNeeds' => { 'nodes' => [ + a_hash_including( 'name' => 'my test job' ) + ] } + ), + a_hash_including( + 'name' => 'docker 2 2', + 'needs' => { 'nodes' => [] }, + 'previousStageJobsOrNeeds' => { 'nodes' => [] } + ), + a_hash_including( + 'name' => 'rspec 1 2', + 'needs' => { 'nodes' => [] }, + 'previousStageJobsOrNeeds' => { 'nodes' => [ + a_hash_including('name' => 'docker 1 2'), + a_hash_including('name' => 'docker 2 2') + ] } + ), + a_hash_including( + 'name' => 'rspec 2 2', + 'needs' => { 'nodes' => [a_hash_including('name' => 'my test job')] }, + 'previousStageJobsOrNeeds' => { 'nodes' => [ + a_hash_including('name' => 'my test job' ) + ] } + ) ) end diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb index 1f47f678898..95ddd0250e7 100644 --- a/spec/requests/api/graphql/ci/pipelines_spec.rb +++ b/spec/requests/api/graphql/ci/pipelines_spec.rb @@ -79,12 +79,13 @@ RSpec.describe 'Query.project(fullPath).pipelines' do create(:ci_build, pipeline: pipeline, stage_id: other_stage.id, name: 'linux: [baz]') end - it 'is null if the user is a guest' do + it 'is present if the user has guest access' do project.add_guest(user) - post_graphql(query, current_user: user, variables: first_n.with(1)) + post_graphql(query, current_user: user) - expect(graphql_data_at(:project, :pipelines, :nodes)).to contain_exactly a_hash_including('stages' => be_nil) + expect(graphql_data_at(:project, :pipelines, :nodes, :stages, :nodes, :name)) + .to contain_exactly(eq(stage.name), eq(other_stage.name)) end it 'is present if the user has reporter access' do @@ -113,12 +114,13 @@ RSpec.describe 'Query.project(fullPath).pipelines' do wrap_fields(query_graphql_path(query_path, :name)) end - it 'is empty if the user is a guest' do + it 'is present if the user has guest access' do project.add_guest(user) post_graphql(query, current_user: user) - expect(graphql_data_at(:project, :pipelines, :nodes, :stages, :nodes, :groups)).to be_empty + expect(graphql_data_at(:project, :pipelines, :nodes, :stages, :nodes, :groups, :nodes, :name)) + .to contain_exactly('linux', 'linux') end it 'is present if the user has reporter access' do diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index ab53ff654e9..98d3a3b1c51 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -63,7 +63,7 @@ RSpec.describe 'Query.runner(id)' do 'revision' => runner.revision, 'locked' => false, 'active' => runner.active, - 'status' => runner.status.to_s.upcase, + 'status' => runner.status('14.5').to_s.upcase, 'maximumTimeout' => runner.maximum_timeout, 'accessLevel' => runner.access_level.to_s.upcase, 'runUntagged' => runner.run_untagged, @@ -221,6 +221,54 @@ RSpec.describe 'Query.runner(id)' do end end + describe 'for runner with status' do + let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) } + let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) } + + let(:status_fragment) do + %( + status + legacyStatusWithExplicitVersion: status(legacyMode: "14.5") + newStatus: status(legacyMode: null) + ) + end + + let(:query) do + %( + query { + staleRunner: runner(id: "#{stale_runner.to_global_id}") { #{status_fragment} } + pausedRunner: runner(id: "#{inactive_instance_runner.to_global_id}") { #{status_fragment} } + neverContactedInstanceRunner: runner(id: "#{never_contacted_instance_runner.to_global_id}") { #{status_fragment} } + } + ) + end + + it 'retrieves status fields with expected values' do + post_graphql(query, current_user: user) + + stale_runner_data = graphql_data_at(:stale_runner) + expect(stale_runner_data).to match a_hash_including( + 'status' => 'NOT_CONNECTED', + 'legacyStatusWithExplicitVersion' => 'NOT_CONNECTED', + 'newStatus' => 'STALE' + ) + + paused_runner_data = graphql_data_at(:paused_runner) + expect(paused_runner_data).to match a_hash_including( + 'status' => 'PAUSED', + 'legacyStatusWithExplicitVersion' => 'PAUSED', + 'newStatus' => 'OFFLINE' + ) + + never_contacted_instance_runner_data = graphql_data_at(:never_contacted_instance_runner) + expect(never_contacted_instance_runner_data).to match a_hash_including( + 'status' => 'NOT_CONNECTED', + 'legacyStatusWithExplicitVersion' => 'NOT_CONNECTED', + 'newStatus' => 'NEVER_CONTACTED' + ) + end + end + describe 'for multiple runners' do let_it_be(:project1) { create(:project, :test_repo) } let_it_be(:project2) { create(:project, :test_repo) } diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb index 51a07e60e15..267dd1b5e6f 100644 --- a/spec/requests/api/graphql/ci/runners_spec.rb +++ b/spec/requests/api/graphql/ci/runners_spec.rb @@ -62,6 +62,15 @@ RSpec.describe 'Query.runners' do it_behaves_like 'a working graphql query returning expected runner' end + + context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do + let(:runner_type) { 'PROJECT_TYPE' } + let(:status) { 'NEVER_CONTACTED' } + + let!(:expected_runner) { project_runner } + + it_behaves_like 'a working graphql query returning expected runner' + end end describe 'pagination' do diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb index d93afcc0f33..802ab847b3d 100644 --- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb +++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb @@ -30,6 +30,14 @@ RSpec.describe 'container repository details' do subject { post_graphql(query, current_user: user, variables: variables) } + shared_examples 'returning an invalid value error' do + it 'returns an error' do + subject + + expect(graphql_errors.first.dig('message')).to match(/invalid value/) + end + end + it_behaves_like 'a working graphql query' do before do subject @@ -138,6 +146,80 @@ RSpec.describe 'container repository details' do end end + context 'sorting the tags' do + let(:sort) { 'NAME_DESC' } + let(:tags_response) { container_repository_details_response.dig('tags', 'edges') } + let(:variables) do + { id: container_repository_global_id, n: sort } + end + + let(:query) do + <<~GQL + query($id: ID!, $n: ContainerRepositoryTagSort) { + containerRepository(id: $id) { + tags(sort: $n) { + edges { + node { + #{all_graphql_fields_for('ContainerRepositoryTag')} + } + } + } + } + } + GQL + end + + it 'sorts the tags', :aggregate_failures do + subject + + expect(tags_response.first.dig('node', 'name')).to eq('tag5') + expect(tags_response.last.dig('node', 'name')).to eq('latest') + end + + context 'invalid sort' do + let(:sort) { 'FOO_ASC' } + + it_behaves_like 'returning an invalid value error' + end + end + + context 'filtering by name' do + let(:name) { 'l' } + let(:tags_response) { container_repository_details_response.dig('tags', 'edges') } + let(:variables) do + { id: container_repository_global_id, n: name } + end + + let(:query) do + <<~GQL + query($id: ID!, $n: String) { + containerRepository(id: $id) { + tags(name: $n) { + edges { + node { + #{all_graphql_fields_for('ContainerRepositoryTag')} + } + } + } + } + } + GQL + end + + it 'sorts the tags', :aggregate_failures do + subject + + expect(tags_response.size).to eq(1) + expect(tags_response.first.dig('node', 'name')).to eq('latest') + end + + context 'invalid filter' do + let(:name) { 1 } + + it_behaves_like 'returning an invalid value error' + end + end + context 'with tags with a manifest containing nil fields' do let(:tags_response) { container_repository_details_response.dig('tags', 'nodes') } let(:errors) { container_repository_details_response.dig('errors') } diff --git a/spec/requests/api/graphql/current_user/todos_query_spec.rb b/spec/requests/api/graphql/current_user/todos_query_spec.rb index 981b10a7467..5a45f0db518 100644 --- a/spec/requests/api/graphql/current_user/todos_query_spec.rb +++ b/spec/requests/api/graphql/current_user/todos_query_spec.rb @@ -69,7 +69,7 @@ RSpec.describe 'Query current user todos' do QUERY end - it 'avoids N+1 queries', :request_store do + it 'avoids N+1 queries', :request_store, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338671' do control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) } project2 = create(:project) diff --git a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb index 1dffb86b344..1f43f113e65 100644 --- a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb @@ -53,7 +53,7 @@ RSpec.describe "deleting designs" do context 'the designs list contains filenames we cannot find' do it_behaves_like 'a failed request' do - let(:designs) { %w/foo bar baz/.map { |fn| instance_double('file', filename: fn) } } + let(:designs) { %w/foo bar baz/.map { |fn| double('file', filename: fn) } } let(:the_error) { a_string_matching %r/filenames were not found/ } end end diff --git a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb index 3da702c55d7..2da69509ad6 100644 --- a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb @@ -12,7 +12,7 @@ RSpec.describe 'Setting issues crm contacts' do let(:issue) { create(:issue, project: project) } let(:operation_mode) { Types::MutationOperationModeEnum.default_mode } - let(:crm_contact_ids) { [global_id_of(contacts[1]), global_id_of(contacts[2])] } + let(:contact_ids) { [global_id_of(contacts[1]), global_id_of(contacts[2])] } let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" } let(:mutation) do @@ -20,7 +20,7 @@ RSpec.describe 'Setting issues crm contacts' do project_path: issue.project.full_path, iid: issue.iid.to_s, operation_mode: operation_mode, - crm_contact_ids: crm_contact_ids + contact_ids: contact_ids } graphql_mutation(:issue_set_crm_contacts, variables, @@ -83,7 +83,7 @@ RSpec.describe 'Setting issues crm contacts' do end context 'append' do - let(:crm_contact_ids) { [global_id_of(contacts[3])] } + let(:contact_ids) { [global_id_of(contacts[3])] } let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] } it 'updates the issue with correct contacts' do @@ -95,7 +95,7 @@ RSpec.describe 'Setting issues crm contacts' do end context 'remove' do - let(:crm_contact_ids) { [global_id_of(contacts[0])] } + let(:contact_ids) { [global_id_of(contacts[0])] } let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] } it 'updates the issue with correct contacts' do @@ -107,7 +107,7 @@ RSpec.describe 'Setting issues crm contacts' do end context 'when the contact does not exist' do - let(:crm_contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] } + let(:contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] } it 'returns expected error' do post_graphql_mutation(mutation, current_user: user) @@ -120,7 +120,7 @@ RSpec.describe 'Setting issues crm contacts' do context 'when the contact belongs to a different group' do let(:group2) { create(:group) } let(:contact) { create(:contact, group: group2) } - let(:crm_contact_ids) { [global_id_of(contact)] } + let(:contact_ids) { [global_id_of(contact)] } before do group2.add_reporter(user) @@ -137,7 +137,7 @@ RSpec.describe 'Setting issues crm contacts' do context 'when attempting to add more than 6' do let(:operation_mode) { Types::MutationOperationModeEnum.enum[:append] } let(:gid) { global_id_of(contacts[0]) } - let(:crm_contact_ids) { [gid, gid, gid, gid, gid, gid, gid] } + let(:contact_ids) { [gid, gid, gid, gid, gid, gid, gid] } it 'returns expected error' do post_graphql_mutation(mutation, current_user: user) @@ -149,7 +149,7 @@ RSpec.describe 'Setting issues crm contacts' do context 'when trying to remove non-existent contact' do let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] } - let(:crm_contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] } + let(:contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] } it 'raises expected error' do post_graphql_mutation(mutation, current_user: user) diff --git a/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb b/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb index 716983f01d2..28a46583d2a 100644 --- a/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb +++ b/spec/requests/api/graphql/mutations/user_callouts/create_spec.rb @@ -7,7 +7,7 @@ RSpec.describe 'Create a user callout' do let_it_be(:current_user) { create(:user) } - let(:feature_name) { ::UserCallout.feature_names.each_key.first } + let(:feature_name) { ::Users::Callout.feature_names.each_key.first } let(:input) do { diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb index 83ea9ff4dc8..a9019a7611a 100644 --- a/spec/requests/api/graphql/packages/package_spec.rb +++ b/spec/requests/api/graphql/packages/package_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe 'package details' do include GraphqlHelpers - let_it_be(:project) { create(:project) } + let_it_be_with_reload(:project) { create(:project) } let_it_be(:composer_package) { create(:composer_package, project: project) } let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } } let_it_be(:composer_metadatum) do @@ -68,7 +68,7 @@ RSpec.describe 'package details' do subject expect(graphql_data_at(:package, :versions, :nodes, :version)).to be_present - expect(graphql_data_at(:package, :versions, :nodes, :versions, :nodes)).to be_empty + expect(graphql_data_at(:package, :versions, :nodes, :versions, :nodes)).to eq [nil, nil] end end end @@ -96,4 +96,87 @@ RSpec.describe 'package details' do expect(graphql_data_at(:b)).to be(nil) end end + + context 'with unauthorized user' do + let_it_be(:user) { create(:user) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it 'returns no packages' do + subject + + expect(graphql_data_at(:package)).to be_nil + end + end + + context 'pipelines field', :aggregate_failures do + let(:pipelines) { create_list(:ci_pipeline, 6, project: project) } + let(:pipeline_gids) { pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse } + + before do + composer_package.pipelines = pipelines + composer_package.save! + end + + def run_query(args) + pipelines_nodes = <<~QUERY + nodes { + id + } + pageInfo { + startCursor + endCursor + } + QUERY + + query = graphql_query_for(:package, { id: package_global_id }, query_graphql_field("pipelines", args, pipelines_nodes)) + post_graphql(query, current_user: user) + end + + it 'loads the second page with pagination first correctly' do + run_query(first: 2) + pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id') + + expect(pipeline_ids).to eq(pipeline_gids[0..1]) + + cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'endCursor') + + run_query(first: 2, after: cursor) + + pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id') + + expect(pipeline_ids).to eq(pipeline_gids[2..3]) + end + + it 'loads the second page with pagination last correctly' do + run_query(last: 2) + pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id') + + expect(pipeline_ids).to eq(pipeline_gids[4..5]) + + cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'startCursor') + + run_query(last: 2, before: cursor) + + pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id') + + expect(pipeline_ids).to eq(pipeline_gids[2..3]) + end + + context 'with unauthorized user' do + let_it_be(:user) { create(:user) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it 'returns no packages' do + run_query(first: 2) + + expect(graphql_data_at(:package)).to be_nil + end + end + end end diff --git a/spec/requests/api/graphql/project/cluster_agents_spec.rb b/spec/requests/api/graphql/project/cluster_agents_spec.rb index dc7254dd552..585126f3849 100644 --- a/spec/requests/api/graphql/project/cluster_agents_spec.rb +++ b/spec/requests/api/graphql/project/cluster_agents_spec.rb @@ -7,7 +7,7 @@ RSpec.describe 'Project.cluster_agents' do let_it_be(:project) { create(:project, :public) } let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } - let_it_be(:agents) { create_list(:cluster_agent, 5, project: project) } + let_it_be(:agents) { create_list(:cluster_agent, 3, project: project) } let(:first) { var('Int') } let(:cluster_agents_fields) { nil } @@ -105,4 +105,37 @@ RSpec.describe 'Project.cluster_agents' do }) end end + + context 'selecting activity events' do + let_it_be(:token) { create(:cluster_agent_token, agent: agents.first) } + let_it_be(:event) { create(:agent_activity_event, agent: agents.first, agent_token: token, user: current_user) } + + let(:cluster_agents_fields) { [:id, query_nodes(:activity_events, of: 'ClusterAgentActivityEvent', max_depth: 2)] } + + it 'retrieves activity event details' do + post_graphql(query, current_user: current_user) + + response = graphql_data_at(:project, :cluster_agents, :nodes, :activity_events, :nodes).first + + expect(response).to include({ + 'kind' => event.kind, + 'level' => event.level, + 'recordedAt' => event.recorded_at.iso8601, + 'agentToken' => hash_including('name' => token.name), + 'user' => hash_including('name' => current_user.name) + }) + end + + it 'preloads associations to prevent N+1 queries' do + user = create(:user) + token = create(:cluster_agent_token, agent: agents.second) + create(:agent_activity_event, agent: agents.second, agent_token: token, user: user) + + post_graphql(query, current_user: current_user) + + expect do + post_graphql(query, current_user: current_user) + end.to issue_same_number_of_queries_as { post_graphql(query, current_user: current_user, variables: [first.with(1)]) } + end + end end diff --git a/spec/requests/api/graphql/project/jobs_spec.rb b/spec/requests/api/graphql/project/jobs_spec.rb new file mode 100644 index 00000000000..1a823ede9ac --- /dev/null +++ b/spec/requests/api/graphql/project/jobs_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'Query.project.jobs' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:user) { create(:user) } + + let(:pipeline) do + create(:ci_pipeline, project: project, user: user) + end + + let(:query) do + <<~QUERY + { + project(fullPath: "#{project.full_path}") { + jobs { + nodes { + name + previousStageJobsAndNeeds { + nodes { + name + } + } + } + } + } + } + QUERY + end + + it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do + build_stage = create(:ci_stage_entity, position: 1, name: 'build', project: project, pipeline: pipeline) + test_stage = create(:ci_stage_entity, position: 2, name: 'test', project: project, pipeline: pipeline) + create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 1 2', stage: build_stage) + create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 2 2', stage: build_stage) + create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 1 2', stage: test_stage) + test_job = create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 2 2', stage: test_stage) + create(:ci_build_need, build: test_job, name: 'docker 1 2') + + post_graphql(query, current_user: user) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: user) + end + + create(:ci_build, name: 'test-a', stage: test_stage, stage_idx: test_stage.position, pipeline: pipeline) + test_b_job = create(:ci_build, name: 'test-b', stage: test_stage, stage_idx: test_stage.position, pipeline: pipeline) + create(:ci_build_need, build: test_b_job, name: 'docker 2 2') + + expect do + post_graphql(query, current_user: user) + end.not_to exceed_all_query_limit(control) + end +end diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index d46ef313563..73e02e2a4b1 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -273,6 +273,48 @@ RSpec.describe 'getting pipeline information nested in a project' do end end + context 'N+1 queries on pipeline jobs' do + let(:pipeline) { create(:ci_pipeline, project: project) } + + let(:fields) do + <<~FIELDS + jobs { + nodes { + previousStageJobsAndNeeds { + nodes { + name + } + } + } + } + FIELDS + end + + it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do + build_stage = create(:ci_stage_entity, position: 1, name: 'build', project: project, pipeline: pipeline) + test_stage = create(:ci_stage_entity, position: 2, name: 'test', project: project, pipeline: pipeline) + create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 1 2', stage: build_stage) + create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 2 2', stage: build_stage) + create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 1 2', stage: test_stage) + test_job = create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 2 2', stage: test_stage) + create(:ci_build_need, build: test_job, name: 'docker 1 2') + + post_graphql(query, current_user: current_user) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: current_user) + end + + create(:ci_build, name: 'test-a', stage: test_stage, stage_idx: test_stage.position, pipeline: pipeline) + test_b_job = create(:ci_build, name: 'test-b', stage: test_stage, stage_idx: test_stage.position, pipeline: pipeline) + create(:ci_build_need, build: test_b_job, name: 'docker 2 2') + + expect do + post_graphql(query, current_user: current_user) + end.not_to exceed_all_query_limit(control) + end + end + context 'N+1 queries on stages jobs' do let(:depth) { 5 } let(:fields) do diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb index e44a7efb354..310a8e9fa33 100644 --- a/spec/requests/api/graphql/project_query_spec.rb +++ b/spec/requests/api/graphql/project_query_spec.rb @@ -143,6 +143,40 @@ RSpec.describe 'getting project information' do end end + context 'when the user has guest access' do + context 'when the project has public pipelines' do + before do + pipeline = create(:ci_pipeline, project: project) + create(:ci_build, project: project, pipeline: pipeline, name: 'a test job') + project.add_guest(current_user) + end + + it 'shows all jobs' do + query = <<~GQL + query { + project(fullPath: "#{project.full_path}") { + jobs { + nodes { + name + stage { + name + } + } + } + } + } + GQL + + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(:project, :jobs, :nodes)).to contain_exactly({ + 'name' => 'a test job', + 'stage' => { 'name' => 'test' } + }) + end + end + end + context 'when the user does not have access to the project' do it 'returns an empty field' do post_graphql(query, current_user: current_user) |