diff options
Diffstat (limited to 'spec/requests/api/graphql')
34 files changed, 1488 insertions, 223 deletions
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 5d5b963fed5..cd94ce91071 100644 --- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb @@ -66,28 +66,22 @@ RSpec.describe 'get board lists' do describe 'sorting and pagination' do let_it_be(:current_user) { user } - let(:data_path) { [board_parent_type, :boards, :edges, 0, :node, :lists] } + let(:data_path) { [board_parent_type, :boards, :nodes, 0, :lists] } - def pagination_query(params, page_info) + def pagination_query(params) graphql_query_for( board_parent_type, { 'fullPath' => board_parent.full_path }, <<~BOARDS boards(first: 1) { - edges { - node { - #{query_graphql_field('lists', params, "#{page_info} edges { node { id } }")} - } + nodes { + #{query_graphql_field(:lists, params, "#{page_info} nodes { id }")} } } BOARDS ) end - def pagination_results_data(data) - data.map { |list| list.dig('node', 'id') } - end - context 'when using default sorting' do let!(:label_list) { create(:list, board: board, label: label, position: 10) } let!(:label_list2) { create(:list, board: board, label: label2, position: 2) } @@ -99,7 +93,7 @@ RSpec.describe 'get board lists' do it_behaves_like 'sorted paginated query' do let(:sort_param) { } let(:first_param) { 2 } - let(:expected_results) { lists.map { |list| list.to_global_id.to_s } } + let(:expected_results) { lists.map { |list| global_id_of(list) } } end end end diff --git a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb new file mode 100644 index 00000000000..e086ce02942 --- /dev/null +++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'Getting Ci Cd Setting' do + include GraphqlHelpers + + let_it_be_with_reload(:project) { create(:project, :repository) } + let_it_be(:current_user) { project.owner } + + let(:fields) do + <<~QUERY + #{all_graphql_fields_for('ProjectCiCdSetting')} + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('ciCdSettings', {}, fields) + ) + end + + let(:settings_data) { graphql_data['project']['ciCdSettings'] } + + context 'without permissions' do + let(:user) { create(:user) } + + before do + project.add_reporter(user) + post_graphql(query, current_user: user) + end + + it_behaves_like 'a working graphql query' + + specify { expect(settings_data).to be nil } + end + + context 'with project permissions' do + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + specify { expect(settings_data['mergePipelinesEnabled']).to eql project.ci_cd_settings.merge_pipelines_enabled? } + specify { expect(settings_data['mergeTrainsEnabled']).to eql project.ci_cd_settings.merge_trains_enabled? } + specify { expect(settings_data['project']['id']).to eql "gid://gitlab/Project/#{project.id}" } + end +end diff --git a/spec/requests/api/graphql/ci/config_spec.rb b/spec/requests/api/graphql/ci/config_spec.rb new file mode 100644 index 00000000000..b682470e0a1 --- /dev/null +++ b/spec/requests/api/graphql/ci/config_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.ciConfig' do + include GraphqlHelpers + + subject(:post_graphql_query) { post_graphql(query, current_user: user) } + + let(:user) { create(:user) } + + let_it_be(:content) do + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_includes.yml')) + end + + let(:query) do + %( + query { + ciConfig(content: "#{content}") { + status + errors + stages { + name + groups { + name + size + jobs { + name + groupName + stage + needs { + name + } + } + } + } + } + } + ) + end + + before do + post_graphql_query + end + + it_behaves_like 'a working graphql query' + + it 'returns the correct structure' do + expect(graphql_data['ciConfig']).to eq( + "status" => "VALID", + "errors" => [], + "stages" => + [ + { + "name" => "build", + "groups" => + [ + { + "name" => "rspec", + "size" => 2, + "jobs" => + [ + { "name" => "rspec 0 1", "groupName" => "rspec", "stage" => "build", "needs" => [] }, + { "name" => "rspec 0 2", "groupName" => "rspec", "stage" => "build", "needs" => [] } + ] + }, + { + "name" => "spinach", "size" => 1, "jobs" => + [ + { "name" => "spinach", "groupName" => "spinach", "stage" => "build", "needs" => [] } + ] + } + ] + }, + { + "name" => "test", + "groups" => + [ + { + "name" => "docker", + "size" => 1, + "jobs" => [ + { "name" => "docker", "groupName" => "docker", "stage" => "test", "needs" => [{ "name" => "spinach" }, { "name" => "rspec 0 1" }] } + ] + } + ] + } + ] + ) + end +end diff --git a/spec/requests/api/graphql/ci/job_artifacts_spec.rb b/spec/requests/api/graphql/ci/job_artifacts_spec.rb new file mode 100644 index 00000000000..df6e398fbe5 --- /dev/null +++ b/spec/requests/api/graphql/ci/job_artifacts_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project(fullPath).pipelines.jobs.artifacts' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:user) { create(:user) } + + let_it_be(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipelines { + nodes { + jobs { + nodes { + artifacts { + nodes { + downloadPath + fileType + } + } + } + } + } + } + } + } + ) + end + + it 'returns the fields for the artifacts' do + job = create(:ci_build, pipeline: pipeline) + create(:ci_job_artifact, :junit, job: job) + + post_graphql(query, current_user: user) + + expect(response).to have_gitlab_http_status(:ok) + + pipelines_data = graphql_data.dig('project', 'pipelines', 'nodes') + jobs_data = pipelines_data.first.dig('jobs', 'nodes') + artifact_data = jobs_data.first.dig('artifacts', 'nodes').first + + expect(artifact_data['downloadPath']).to eq( + "/#{project.full_path}/-/jobs/#{job.id}/artifacts/download?file_type=junit" + ) + expect(artifact_data['fileType']).to eq('JUNIT') + end +end diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb index 618705e5f94..19954c4e52f 100644 --- a/spec/requests/api/graphql/ci/jobs_spec.rb +++ b/spec/requests/api/graphql/ci/jobs_spec.rb @@ -1,41 +1,44 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do +RSpec.describe 'Query.project.pipeline' do include GraphqlHelpers - let(:project) { create(:project, :repository, :public) } - let(:user) { create(:user) } - let(:pipeline) do - pipeline = create(:ci_pipeline, project: project, user: user) - stage = create(:ci_stage_entity, pipeline: pipeline, name: 'first') - create(:commit_status, stage_id: stage.id, pipeline: pipeline, name: 'my test job') - - pipeline - end + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:user) { create(:user) } def first(field) [field.pluralize, 'nodes', 0] end - let(:jobs_graphql_data) { graphql_data.dig(*%w[project pipeline], *first('stage'), *first('group'), 'jobs', 'nodes') } - - let(:query) do - %( - query { - project(fullPath: "#{project.full_path}") { - pipeline(iid: "#{pipeline.iid}") { - stages { - nodes { - name - groups { - nodes { - name - jobs { - nodes { - name - pipeline { - id + describe '.stages.groups.jobs' do + let(:pipeline) do + pipeline = create(:ci_pipeline, project: project, user: user) + stage = create(:ci_stage_entity, pipeline: pipeline, name: 'first') + create(:commit_status, stage_id: stage.id, pipeline: pipeline, name: 'my test job') + + pipeline + end + + let(:jobs_graphql_data) { graphql_data.dig(*%w[project pipeline], *first('stage'), *first('group'), 'jobs', 'nodes') } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipeline(iid: "#{pipeline.iid}") { + stages { + nodes { + name + groups { + nodes { + name + jobs { + nodes { + name + pipeline { + id + } } } } @@ -45,17 +48,15 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do } } } - } - ) - end + ) + end - it 'returns the jobs of a pipeline stage' do - post_graphql(query, current_user: user) + 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 + expect(jobs_graphql_data).to contain_exactly(a_hash_including('name' => 'my test job')) + end - context 'when fetching jobs from the pipeline' do it 'avoids N+1 queries', :aggregate_failures do control_count = ActiveRecord::QueryRecorder.new do post_graphql(query, current_user: user) @@ -112,4 +113,50 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do ]) end end + + describe '.jobs.artifacts' do + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipeline(iid: "#{pipeline.iid}") { + jobs { + nodes { + artifacts { + nodes { + downloadPath + } + } + } + } + } + } + } + ) + end + + context 'when the job is a build' do + it "returns the build's artifacts" do + create(:ci_build, :artifacts, pipeline: pipeline) + + post_graphql(query, current_user: user) + + job_data = graphql_data.dig('project', 'pipeline', 'jobs', 'nodes').first + expect(job_data.dig('artifacts', 'nodes').count).to be(2) + end + end + + context 'when the job is not a build' do + it 'returns nil' do + create(:ci_bridge, pipeline: pipeline) + + post_graphql(query, current_user: user) + + job_data = graphql_data.dig('project', 'pipeline', 'jobs', 'nodes').first + expect(job_data['artifacts']).to be_nil + end + end + end end diff --git a/spec/requests/api/graphql/group/container_repositories_spec.rb b/spec/requests/api/graphql/group/container_repositories_spec.rb index bcf689a5e8f..4aa775eba0f 100644 --- a/spec/requests/api/graphql/group/container_repositories_spec.rb +++ b/spec/requests/api/graphql/group/container_repositories_spec.rb @@ -14,7 +14,7 @@ RSpec.describe 'getting container repositories in a group' do let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten } let_it_be(:container_expiration_policy) { project.container_expiration_policy } - let(:fields) do + let(:container_repositories_fields) do <<~GQL edges { node { @@ -24,17 +24,25 @@ RSpec.describe 'getting container repositories in a group' do GQL end + let(:fields) do + <<~GQL + #{query_graphql_field('container_repositories', {}, container_repositories_fields)} + containerRepositoriesCount + GQL + end + let(:query) do graphql_query_for( 'group', { 'fullPath' => group.full_path }, - query_graphql_field('container_repositories', {}, fields) + fields ) end let(:user) { owner } let(:variables) { {} } let(:container_repositories_response) { graphql_data.dig('group', 'containerRepositories', 'edges') } + let(:container_repositories_count_response) { graphql_data.dig('group', 'containerRepositoriesCount') } before do group.add_owner(owner) @@ -101,7 +109,7 @@ RSpec.describe 'getting container repositories in a group' do <<~GQL query($path: ID!, $n: Int) { group(fullPath: $path) { - containerRepositories(first: $n) { #{fields} } + containerRepositories(first: $n) { #{container_repositories_fields} } } } GQL @@ -122,7 +130,7 @@ RSpec.describe 'getting container repositories in a group' do <<~GQL query($path: ID!, $name: String) { group(fullPath: $path) { - containerRepositories(name: $name) { #{fields} } + containerRepositories(name: $name) { #{container_repositories_fields} } } } GQL @@ -143,4 +151,10 @@ RSpec.describe 'getting container repositories in a group' do expect(container_repositories_response.first.dig('node', 'id')).to eq(container_repository.to_global_id.to_s) end end + + it 'returns the total count of container repositories' do + subject + + expect(container_repositories_count_response).to eq(container_repositories.size) + end end diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb index 84b2fd63d46..3554e22cdf2 100644 --- a/spec/requests/api/graphql/group/group_members_spec.rb +++ b/spec/requests/api/graphql/group/group_members_spec.rb @@ -5,44 +5,95 @@ require 'spec_helper' RSpec.describe 'getting group members information' do include GraphqlHelpers - let_it_be(:group) { create(:group, :public) } + let_it_be(:parent_group) { create(:group, :public) } let_it_be(:user) { create(:user) } let_it_be(:user_1) { create(:user, username: 'user') } let_it_be(:user_2) { create(:user, username: 'test') } let(:member_data) { graphql_data['group']['groupMembers']['edges'] } - before do - [user_1, user_2].each { |user| group.add_guest(user) } + before_all do + [user_1, user_2].each { |user| parent_group.add_guest(user) } end context 'when the request is correct' do it_behaves_like 'a working graphql query' do - before do - fetch_members(user) + before_all do + fetch_members end end it 'returns group members successfully' do - fetch_members(user) + fetch_members expect(graphql_errors).to be_nil - expect_array_response(user_1.to_global_id.to_s, user_2.to_global_id.to_s) + expect_array_response(user_1, user_2) end it 'returns members that match the search query' do - fetch_members(user, { search: 'test' }) + fetch_members(args: { search: 'test' }) expect(graphql_errors).to be_nil - expect_array_response(user_2.to_global_id.to_s) + expect_array_response(user_2) end end - def fetch_members(user = nil, args = {}) - post_graphql(members_query(args), current_user: user) + context 'member relations' do + let_it_be(:child_group) { create(:group, :public, parent: parent_group) } + let_it_be(:grandchild_group) { create(:group, :public, parent: child_group) } + let_it_be(:child_user) { create(:user) } + let_it_be(:grandchild_user) { create(:user) } + + before_all do + child_group.add_guest(child_user) + grandchild_group.add_guest(grandchild_user) + end + + it 'returns direct members' do + fetch_members(group: child_group, args: { relations: [:DIRECT] }) + + expect(graphql_errors).to be_nil + expect_array_response(child_user) + end + + it 'returns direct and inherited members' do + fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED] }) + + expect(graphql_errors).to be_nil + expect_array_response(child_user, user_1, user_2) + end + + it 'returns direct, inherited, and descendant members' do + fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED, :DESCENDANTS] }) + + expect(graphql_errors).to be_nil + expect_array_response(child_user, user_1, user_2, grandchild_user) + end + + it 'returns an error for an invalid member relation' do + fetch_members(group: child_group, args: { relations: [:OBLIQUE] }) + + expect(graphql_errors.first) + .to include('path' => %w[query group groupMembers relations], + 'message' => a_string_including('invalid value ([OBLIQUE])')) + end + end + + context 'when unauthenticated' do + it 'returns nothing' do + fetch_members(current_user: nil) + + expect(graphql_errors).to be_nil + expect(response).to have_gitlab_http_status(:success) + expect(member_data).to be_empty + end + end + + def fetch_members(group: parent_group, current_user: user, args: {}) + post_graphql(members_query(group.full_path, args), current_user: current_user) end - def members_query(args = {}) + def members_query(group_path, args = {}) members_node = <<~NODE edges { node { @@ -54,7 +105,7 @@ RSpec.describe 'getting group members information' do NODE graphql_query_for("group", - { full_path: group.full_path }, + { full_path: group_path }, [query_graphql_field("groupMembers", args, members_node)] ) end @@ -62,6 +113,7 @@ RSpec.describe 'getting group members information' do def expect_array_response(*items) expect(response).to have_gitlab_http_status(:success) expect(member_data).to be_an Array - expect(member_data.map { |node| node["node"]["user"]["id"] }).to match_array(items) + expect(member_data.map { |node| node["node"]["user"]["id"] }) + .to match_array(items.map { |u| global_id_of(u) }) end end diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb index 83180c7d7a5..391bae4cfcf 100644 --- a/spec/requests/api/graphql/group_query_spec.rb +++ b/spec/requests/api/graphql/group_query_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' # Based on spec/requests/api/groups_spec.rb # Should follow closely in order to ensure all situations are covered -RSpec.describe 'getting group information', :do_not_mock_admin_mode do +RSpec.describe 'getting group information' do include GraphqlHelpers include UploadHelpers diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb index d7fa680d29b..42c8e0cc9c0 100644 --- a/spec/requests/api/graphql/issue/issue_spec.rb +++ b/spec/requests/api/graphql/issue/issue_spec.rb @@ -125,7 +125,7 @@ RSpec.describe 'Query.issue(id)' do let(:issue_params) { { 'id' => confidential_issue.to_global_id.to_s } } context 'when the user cannot see confidential issues' do - it 'returns nil ' do + it 'returns nil' do post_graphql(query, current_user: current_user) expect(issue_data).to be nil 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 4ad35e7f0d1..b8cde32877b 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 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues do 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 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues do 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 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues do 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/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb index 3aaebb5095a..b39062f2e71 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb @@ -56,10 +56,10 @@ RSpec.describe 'Adding an AwardEmoji' do it_behaves_like 'a mutation that does not create an AwardEmoji' it_behaves_like 'a mutation that returns top-level errors', - errors: ['Cannot award emoji to this resource'] + errors: ['You cannot award emoji to this resource.'] end - context 'when the given awardable an Awardable' do + context 'when the given awardable is an Awardable' do it 'creates an emoji' do expect do post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb index 6910ad80a11..170e7ff3b44 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb @@ -55,7 +55,7 @@ RSpec.describe 'Toggling an AwardEmoji' do it_behaves_like 'a mutation that does not create or destroy an AwardEmoji' it_behaves_like 'a mutation that returns top-level errors', - errors: ['Cannot award emoji to this resource'] + errors: ['You cannot award emoji to this resource.'] end context 'when the given awardable is an Awardable' do diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb index 645edfc2e43..c4121cfed42 100644 --- a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb @@ -22,7 +22,7 @@ RSpec.describe 'Destroying a container repository' do GQL end - let(:params) { { id: container_repository.to_global_id.to_s } } + let(:params) { { id: id } } let(:mutation) { graphql_mutation(:destroy_container_repository, params, query) } let(:mutation_response) { graphql_mutation_response(:destroyContainerRepository) } let(:container_repository_mutation_response) { mutation_response['containerRepository'] } diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb new file mode 100644 index 00000000000..decb2e7bccc --- /dev/null +++ b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Destroying a container repository tags' do + include_context 'container repository delete tags service shared context' + using RSpec::Parameterized::TableSyntax + + include GraphqlHelpers + + let(:id) { repository.to_global_id.to_s } + let(:tags) { %w[A C D E] } + + let(:query) do + <<~GQL + deletedTagNames + errors + GQL + end + + let(:params) { { id: id, tag_names: tags } } + let(:mutation) { graphql_mutation(:destroy_container_repository_tags, params, query) } + let(:mutation_response) { graphql_mutation_response(:destroyContainerRepositoryTags) } + let(:tag_names_response) { mutation_response['deletedTagNames'] } + let(:errors_response) { mutation_response['errors'] } + + shared_examples 'destroying the container repository tags' do + before do + stub_delete_reference_requests(tags) + expect_delete_tag_by_names(tags) + allow_next_instance_of(ContainerRegistry::Client) do |client| + allow(client).to receive(:supports_tag_delete?).and_return(true) + end + end + + it 'destroys the container repository tags' do + expect(Projects::ContainerRepository::DeleteTagsService) + .to receive(:new).and_call_original + expect { subject }.to change { ::Packages::Event.count }.by(1) + + expect(tag_names_response).to eq(tags) + expect(errors_response).to eq([]) + end + + it_behaves_like 'returning response status', :success + end + + shared_examples 'denying the mutation request' do + it 'does not destroy the container repository tags' do + expect(Projects::ContainerRepository::DeleteTagsService) + .not_to receive(:new) + + expect { subject }.not_to change { ::Packages::Event.count } + + expect(mutation_response).to be_nil + end + + it_behaves_like 'returning response status', :success + end + + describe 'post graphql mutation' do + subject { post_graphql_mutation(mutation, current_user: user) } + + context 'with valid id' do + where(:user_role, :shared_examples_name) do + :maintainer | 'destroying the container repository tags' + :developer | 'destroying the container repository tags' + :reporter | 'denying the mutation request' + :guest | 'denying the mutation request' + :anonymous | 'denying the mutation request' + end + + with_them do + before do + project.send("add_#{user_role}", user) unless user_role == :anonymous + end + + it_behaves_like params[:shared_examples_name] + end + end + + context 'with invalid id' do + let(:id) { 'gid://gitlab/ContainerRepository/5555' } + + it_behaves_like 'denying the mutation request' + end + + context 'with too many tags' do + let(:tags) { Array.new(Mutations::ContainerRepositories::DestroyTags::LIMIT + 1, 'x') } + + it 'returns too many tags error' do + expect { subject }.not_to change { ::Packages::Event.count } + + explanation = graphql_errors.dig(0, 'extensions', 'problems', 0, 'explanation') + expect(explanation).to eq(Mutations::ContainerRepositories::DestroyTags::TOO_MANY_TAGS_ERROR_MESSAGE) + end + end + + context 'with service error' do + before do + project.add_maintainer(user) + allow_next_instance_of(Projects::ContainerRepository::DeleteTagsService) do |service| + allow(service).to receive(:execute).and_return(message: 'could not delete tags', status: :error) + end + end + + it 'returns an error' do + subject + + expect(tag_names_response).to eq([]) + expect(errors_response).to eq(['could not delete tags']) + end + + it 'does not create a package event' do + expect(::Packages::CreateEventService).not_to receive(:new) + expect { subject }.not_to change { ::Packages::Event.count } + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb b/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb new file mode 100644 index 00000000000..f25a49291a6 --- /dev/null +++ b/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Update Environment Canary Ingress', :clean_gitlab_redis_cache do + include GraphqlHelpers + include KubernetesHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:cluster) { create(:cluster, :project, projects: [project]) } + let_it_be(:service) { create(:cluster_platform_kubernetes, :configured, cluster: cluster) } + let_it_be(:environment) { create(:environment, project: project) } + let_it_be(:deployment) { create(:deployment, :success, environment: environment, project: project) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:developer) { create(:user) } + let(:environment_id) { environment.to_global_id.to_s } + let(:weight) { 25 } + let(:actor) { developer } + + let(:mutation) do + graphql_mutation(:environments_canary_ingress_update, id: environment_id, weight: weight) + end + + before_all do + project.add_maintainer(maintainer) + project.add_developer(developer) + end + + before do + stub_kubeclient_ingresses(environment.deployment_namespace, response: kube_ingresses_response(with_canary: true)) + end + + context 'when kubernetes accepted the patch request' do + before do + stub_kubeclient_ingresses(environment.deployment_namespace, method: :patch, resource_path: "/production-auto-deploy") + end + + it 'updates successfully' do + post_graphql_mutation(mutation, current_user: actor) + + expect(graphql_mutation_response(:environments_canary_ingress_update)['errors']) + .to be_empty + end + end +end diff --git a/spec/requests/api/graphql/mutations/releases/delete_spec.rb b/spec/requests/api/graphql/mutations/releases/delete_spec.rb new file mode 100644 index 00000000000..3710f118bf4 --- /dev/null +++ b/spec/requests/api/graphql/mutations/releases/delete_spec.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Deleting a release' do + include GraphqlHelpers + include Presentable + + let_it_be(:public_user) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:developer) { create(:user) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:tag_name) { 'v1.1.0' } + let_it_be(:release) { create(:release, project: project, tag: tag_name) } + + let(:mutation_name) { :release_delete } + + let(:project_path) { project.full_path } + let(:mutation_arguments) do + { + projectPath: project_path, + tagName: tag_name + } + end + + let(:mutation) do + graphql_mutation(mutation_name, mutation_arguments, <<~FIELDS) + release { + tagName + } + errors + FIELDS + end + + let(:delete_release) { post_graphql_mutation(mutation, current_user: current_user) } + let(:mutation_response) { graphql_mutation_response(mutation_name)&.with_indifferent_access } + + before do + project.add_guest(guest) + project.add_reporter(reporter) + project.add_developer(developer) + project.add_maintainer(maintainer) + end + + shared_examples 'unauthorized or not found error' do + it 'returns a top-level error with message' do + delete_release + + expect(mutation_response).to be_nil + expect(graphql_errors.count).to eq(1) + expect(graphql_errors.first['message']).to eq("The resource that you are attempting to access does not exist or you don't have permission to perform this action") + end + end + + context 'when the current user has access to update releases' do + let(:current_user) { maintainer } + + it 'deletes the release' do + expect { delete_release }.to change { Release.count }.by(-1) + end + + it 'returns the deleted release' do + delete_release + + expected_release = { tagName: tag_name }.with_indifferent_access + + expect(mutation_response[:release]).to eq(expected_release) + end + + it 'does not remove the Git tag associated with the deleted release' do + expect { delete_release }.not_to change { Project.find_by_id(project.id).repository.tag_count } + end + + it 'returns no errors' do + delete_release + + expect(mutation_response[:errors]).to eq([]) + end + + context 'validation' do + context 'when the release does not exist' do + let_it_be(:tag_name) { 'not-a-real-release' } + + it 'returns the release as null' do + delete_release + + expect(mutation_response[:release]).to be_nil + end + + it 'returns an errors-at-data message' do + delete_release + + expect(mutation_response[:errors]).to eq(['Release does not exist']) + end + end + + context 'when the project does not exist' do + let(:project_path) { 'not/a/real/path' } + + it_behaves_like 'unauthorized or not found error' + end + end + end + + context "when the current user doesn't have access to update releases" do + context 'when the current user is a Developer' do + let(:current_user) { developer } + + it_behaves_like 'unauthorized or not found error' + end + + context 'when the current user is a Reporter' do + let(:current_user) { reporter } + + it_behaves_like 'unauthorized or not found error' + end + + context 'when the current user is a Guest' do + let(:current_user) { guest } + + it_behaves_like 'unauthorized or not found error' + end + + context 'when the current user is a public user' do + let(:current_user) { public_user } + + it_behaves_like 'unauthorized or not found error' + end + end +end diff --git a/spec/requests/api/graphql/mutations/releases/update_spec.rb b/spec/requests/api/graphql/mutations/releases/update_spec.rb new file mode 100644 index 00000000000..19320c3393c --- /dev/null +++ b/spec/requests/api/graphql/mutations/releases/update_spec.rb @@ -0,0 +1,255 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Updating an existing release' do + include GraphqlHelpers + include Presentable + + let_it_be(:public_user) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:developer) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:milestone_12_3) { create(:milestone, project: project, title: '12.3') } + let_it_be(:milestone_12_4) { create(:milestone, project: project, title: '12.4') } + + let_it_be(:tag_name) { 'v1.1.0' } + let_it_be(:name) { 'Version 7.12.5'} + let_it_be(:description) { 'Release 7.12.5 :rocket:' } + let_it_be(:released_at) { '2018-12-10' } + let_it_be(:created_at) { '2018-11-05' } + let_it_be(:milestones) { [milestone_12_3, milestone_12_4] } + + let_it_be(:release) do + create(:release, project: project, tag: tag_name, name: name, + description: description, released_at: Time.parse(released_at).utc, + created_at: Time.parse(created_at).utc, milestones: milestones) + end + + let(:mutation_name) { :release_update } + + let(:mutation_arguments) do + { + projectPath: project.full_path, + tagName: tag_name + } + end + + let(:mutation) do + graphql_mutation(mutation_name, mutation_arguments, <<~FIELDS) + release { + tagName + name + description + releasedAt + createdAt + milestones { + nodes { + title + } + } + } + errors + FIELDS + end + + let(:update_release) { post_graphql_mutation(mutation, current_user: current_user) } + let(:mutation_response) { graphql_mutation_response(mutation_name)&.with_indifferent_access } + + let(:expected_attributes) do + { + tagName: tag_name, + name: name, + description: description, + releasedAt: Time.parse(released_at).utc.iso8601, + createdAt: Time.parse(created_at).utc.iso8601, + milestones: { + nodes: milestones.map { |m| { title: m.title } } + } + }.with_indifferent_access + end + + around do |example| + freeze_time { example.run } + end + + before do + project.add_guest(guest) + project.add_reporter(reporter) + project.add_developer(developer) + + stub_default_url_options(host: 'www.example.com') + end + + shared_examples 'no errors' do + it 'returns no errors' do + update_release + + expect(graphql_errors).not_to be_present + end + end + + shared_examples 'top-level error with message' do |error_message| + it 'returns a top-level error with message' do + update_release + + expect(mutation_response).to be_nil + expect(graphql_errors.count).to eq(1) + expect(graphql_errors.first['message']).to eq(error_message) + end + end + + shared_examples 'errors-as-data with message' do |error_message| + it 'returns an error-as-data with message' do + update_release + + expect(mutation_response[:release]).to be_nil + expect(mutation_response[:errors].count).to eq(1) + expect(mutation_response[:errors].first).to match(error_message) + end + end + + shared_examples 'updates release fields' do |updates| + it_behaves_like 'no errors' + + it 'updates the correct field and returns the release' do + update_release + + expect(mutation_response[:release]).to include(expected_attributes.merge(updates).except(:milestones)) + + # Right now the milestones are returned in a non-deterministic order. + # Because of this, we need to test milestones separately to allow + # for them to be returned in any order. + # Once https://gitlab.com/gitlab-org/gitlab/-/issues/259012 has been + # fixed, this special milestone handling can be removed. + expected_milestones = expected_attributes.merge(updates)[:milestones] + expect(mutation_response[:release][:milestones][:nodes]).to match_array(expected_milestones[:nodes]) + end + end + + context 'when the current user has access to update releases' do + let(:current_user) { developer } + + context 'name' do + context 'when a new name is provided' do + let(:mutation_arguments) { super().merge(name: 'Updated name') } + + it_behaves_like 'updates release fields', name: 'Updated name' + end + + context 'when null is provided' do + let(:mutation_arguments) { super().merge(name: nil) } + + it_behaves_like 'updates release fields', name: 'v1.1.0' + end + end + + context 'description' do + context 'when a new description is provided' do + let(:mutation_arguments) { super().merge(description: 'Updated description') } + + it_behaves_like 'updates release fields', description: 'Updated description' + end + + context 'when null is provided' do + let(:mutation_arguments) { super().merge(description: nil) } + + it_behaves_like 'updates release fields', description: nil + end + end + + context 'releasedAt' do + context 'when no time zone is provided' do + let(:mutation_arguments) { super().merge(releasedAt: '2015-05-05') } + + it_behaves_like 'updates release fields', releasedAt: Time.parse('2015-05-05').utc.iso8601 + end + + context 'when a local time zone is provided' do + let(:mutation_arguments) { super().merge(releasedAt: Time.parse('2015-05-05').in_time_zone('Hawaii').iso8601) } + + it_behaves_like 'updates release fields', releasedAt: Time.parse('2015-05-05').utc.iso8601 + end + + context 'when null is provided' do + let(:mutation_arguments) { super().merge(releasedAt: nil) } + + it_behaves_like 'top-level error with message', 'if the releasedAt argument is provided, it cannot be null' + end + end + + context 'milestones' do + context 'when a new set of milestones is provided provided' do + let(:mutation_arguments) { super().merge(milestones: ['12.3']) } + + it_behaves_like 'updates release fields', milestones: { nodes: [{ title: '12.3' }] } + end + + context 'when an empty array is provided' do + let(:mutation_arguments) { super().merge(milestones: []) } + + it_behaves_like 'updates release fields', milestones: { nodes: [] } + end + + context 'when null is provided' do + let(:mutation_arguments) { super().merge(milestones: nil) } + + it_behaves_like 'top-level error with message', 'if the milestones argument is provided, it cannot be null' + end + + context 'when a non-existent milestone title is provided' do + let(:mutation_arguments) { super().merge(milestones: ['not real']) } + + it_behaves_like 'errors-as-data with message', 'Milestone(s) not found: not real' + end + + context 'when a milestone title from a different project is provided' do + let(:milestone_in_different_project) { create(:milestone, title: 'milestone in different project') } + let(:mutation_arguments) { super().merge(milestones: [milestone_in_different_project.title]) } + + it_behaves_like 'errors-as-data with message', 'Milestone(s) not found: milestone in different project' + end + end + + context 'validation' do + context 'when no updated fields are provided' do + it_behaves_like 'errors-as-data with message', 'params is empty' + end + + context 'when the tag does not exist' do + let(:mutation_arguments) { super().merge(tagName: 'not-a-real-tag') } + + it_behaves_like 'errors-as-data with message', 'Tag does not exist' + end + + context 'when the project does not exist' do + let(:mutation_arguments) { super().merge(projectPath: 'not/a/real/path') } + + it_behaves_like 'top-level error with message', "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + end + end + end + + context "when the current user doesn't have access to update releases" do + expected_error_message = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + + context 'when the current user is a Reporter' do + let(:current_user) { reporter } + + it_behaves_like 'top-level error with message', expected_error_message + end + + context 'when the current user is a Guest' do + let(:current_user) { guest } + + it_behaves_like 'top-level error with message', expected_error_message + end + + context 'when the current user is a public user' do + let(:current_user) { public_user } + + it_behaves_like 'top-level error with message', expected_error_message + end + 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 d2fa3cfc24f..fd0dc98a8d3 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 @@ RSpec.describe 'Creating a Snippet' do context 'when there are uploaded files' do shared_examples 'expected files argument' do |file_value, expected_value| let(:uploaded_files) { file_value } + let(:snippet) { build(:snippet) } + let(:creation_response) do + ::ServiceResponse.error(message: 'urk', payload: { snippet: snippet }) + 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/mutations/snippets/mark_as_spam_spec.rb b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb index 97e6ae8fda8..4d499310591 100644 --- a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb @@ -2,8 +2,9 @@ require 'spec_helper' -RSpec.describe 'Mark snippet as spam', :do_not_mock_admin_mode do +RSpec.describe 'Mark snippet as spam' do include GraphqlHelpers + include AfterNextHelpers let_it_be(:admin) { create(:admin) } let_it_be(:other_user) { create(:user) } @@ -56,11 +57,12 @@ RSpec.describe 'Mark snippet as spam', :do_not_mock_admin_mode do end it 'marks snippet as spam' do - expect_next_instance_of(Spam::MarkAsSpamService) do |instance| - expect(instance).to receive(:execute) - end + expect_next(Spam::MarkAsSpamService, target: snippet) + .to receive(:execute).and_return(true) post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors).to be_blank end end end diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb index 03160719389..3e68503b7fb 100644 --- a/spec/requests/api/graphql/namespace/projects_spec.rb +++ b/spec/requests/api/graphql/namespace/projects_spec.rb @@ -80,38 +80,34 @@ RSpec.describe 'getting projects' do end describe 'sorting and pagination' do + let_it_be(:ns) { create(:group) } + let_it_be(:current_user) { create(:user) } + let_it_be(:project_1) { create(:project, name: 'Project', path: 'project', namespace: ns) } + let_it_be(:project_2) { create(:project, name: 'Test Project', path: 'test-project', namespace: ns) } + let_it_be(:project_3) { create(:project, name: 'Test', path: 'test', namespace: ns) } + let_it_be(:project_4) { create(:project, name: 'Test Project Other', path: 'other-test-project', namespace: ns) } + let(:data_path) { [:namespace, :projects] } - def pagination_query(params, page_info) - graphql_query_for( - 'namespace', - { 'fullPath' => subject.full_path }, - <<~QUERY - projects(includeSubgroups: #{include_subgroups}, search: "#{search}", #{params}) { - #{page_info} edges { - node { - #{all_graphql_fields_for('Project')} - } - } - } - QUERY - ) + let(:ns_args) { { full_path: ns.full_path } } + let(:search) { 'test' } + + before do + ns.add_owner(current_user) end - def pagination_results_data(data) - data.map { |project| project.dig('node', 'name') } + def pagination_query(params) + arguments = params.merge(include_subgroups: include_subgroups, search: search) + graphql_query_for(:namespace, ns_args, query_graphql_field(:projects, arguments, <<~GQL)) + #{page_info} + nodes { name } + GQL end context 'when sorting by similarity' do - let!(:project_1) { create(:project, name: 'Project', path: 'project', namespace: subject) } - let!(:project_2) { create(:project, name: 'Test Project', path: 'test-project', namespace: subject) } - let!(:project_3) { create(:project, name: 'Test', path: 'test', namespace: subject) } - let!(:project_4) { create(:project, name: 'Test Project Other', path: 'other-test-project', namespace: subject) } - let(:search) { 'test' } - let(:current_user) { user } - it_behaves_like 'sorted paginated query' do - let(:sort_param) { 'SIMILARITY' } + let(:node_path) { %w[name] } + let(:sort_param) { :SIMILARITY } let(:first_param) { 2 } let(:expected_results) { [project_3.name, project_2.name, project_4.name] } end diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb index 7e32f54bf1d..6b1c8689515 100644 --- a/spec/requests/api/graphql/project/container_repositories_spec.rb +++ b/spec/requests/api/graphql/project/container_repositories_spec.rb @@ -12,7 +12,7 @@ RSpec.describe 'getting container repositories in a project' do let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten } let_it_be(:container_expiration_policy) { project.container_expiration_policy } - let(:fields) do + let(:container_repositories_fields) do <<~GQL edges { node { @@ -22,17 +22,25 @@ RSpec.describe 'getting container repositories in a project' do GQL end + let(:fields) do + <<~GQL + #{query_graphql_field('container_repositories', {}, container_repositories_fields)} + containerRepositoriesCount + GQL + end + let(:query) do graphql_query_for( 'project', { 'fullPath' => project.full_path }, - query_graphql_field('container_repositories', {}, fields) + fields ) end let(:user) { project.owner } let(:variables) { {} } let(:container_repositories_response) { graphql_data.dig('project', 'containerRepositories', 'edges') } + let(:container_repositories_count_response) { graphql_data.dig('project', 'containerRepositoriesCount') } before do stub_container_registry_config(enabled: true) @@ -100,7 +108,7 @@ RSpec.describe 'getting container repositories in a project' do <<~GQL query($path: ID!, $n: Int) { project(fullPath: $path) { - containerRepositories(first: $n) { #{fields} } + containerRepositories(first: $n) { #{container_repositories_fields} } } } GQL @@ -121,7 +129,7 @@ RSpec.describe 'getting container repositories in a project' do <<~GQL query($path: ID!, $name: String) { project(fullPath: $path) { - containerRepositories(name: $name) { #{fields} } + containerRepositories(name: $name) { #{container_repositories_fields} } } } GQL @@ -142,4 +150,10 @@ RSpec.describe 'getting container repositories in a project' do expect(container_repositories_response.first.dig('node', 'id')).to eq(container_repository.to_global_id.to_s) end end + + it 'returns the total count of container repositories' do + subject + + expect(container_repositories_count_response).to eq(container_repositories.size) + end 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 4bce3c7fe0f..f544d78ecbb 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 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha) describe 'scalar fields' do let(:path) { path_prefix } - let(:version_fields) { query_graphql_field(:sha) } before do post_query @@ -50,7 +49,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha) { 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 5f368833181..ddf63a8f2c9 100644 --- a/spec/requests/api/graphql/project/issue_spec.rb +++ b/spec/requests/api/graphql/project/issue_spec.rb @@ -29,8 +29,8 @@ RSpec.describe 'Query.project(fullPath).issue(iid)' do 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 @@ RSpec.describe 'Query.project(fullPath).issue(iid)' do 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/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 4f27f08bf98..9c915075c42 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -142,16 +142,14 @@ RSpec.describe 'getting an issue list for a project' do describe 'sorting and pagination' do let_it_be(:data_path) { [:project, :issues] } - def pagination_query(params, page_info) - graphql_query_for( - 'project', - { 'fullPath' => sort_project.full_path }, - query_graphql_field('issues', params, "#{page_info} edges { node { iid dueDate} }") + def pagination_query(params) + graphql_query_for(:project, { full_path: sort_project.full_path }, + query_graphql_field(:issues, params, "#{page_info} nodes { iid }") ) end def pagination_results_data(data) - data.map { |issue| issue.dig('node', 'iid').to_i } + data.map { |issue| issue.dig('iid').to_i } end context 'when sorting by due date' do @@ -164,7 +162,7 @@ RSpec.describe 'getting an issue list for a project' do context 'when ascending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { 'DUE_DATE_ASC' } + let(:sort_param) { :DUE_DATE_ASC } let(:first_param) { 2 } let(:expected_results) { [due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid] } end @@ -172,7 +170,7 @@ RSpec.describe 'getting an issue list for a project' do context 'when descending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { 'DUE_DATE_DESC' } + let(:sort_param) { :DUE_DATE_DESC } let(:first_param) { 2 } let(:expected_results) { [due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid] } end @@ -189,7 +187,7 @@ RSpec.describe 'getting an issue list for a project' do context 'when ascending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { 'RELATIVE_POSITION_ASC' } + let(:sort_param) { :RELATIVE_POSITION_ASC } let(:first_param) { 2 } let(:expected_results) { [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] } end @@ -209,7 +207,7 @@ RSpec.describe 'getting an issue list for a project' do context 'when ascending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { 'PRIORITY_ASC' } + let(:sort_param) { :PRIORITY_ASC } let(:first_param) { 2 } let(:expected_results) { [priority_issue3.iid, priority_issue1.iid, priority_issue2.iid, priority_issue4.iid] } end @@ -217,7 +215,7 @@ RSpec.describe 'getting an issue list for a project' do context 'when descending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { 'PRIORITY_DESC' } + let(:sort_param) { :PRIORITY_DESC } let(:first_param) { 2 } let(:expected_results) { [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid] } end @@ -236,7 +234,7 @@ RSpec.describe 'getting an issue list for a project' do context 'when ascending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { 'LABEL_PRIORITY_ASC' } + let(:sort_param) { :LABEL_PRIORITY_ASC } let(:first_param) { 2 } let(:expected_results) { [label_issue3.iid, label_issue1.iid, label_issue2.iid, label_issue4.iid] } end @@ -244,7 +242,7 @@ RSpec.describe 'getting an issue list for a project' do context 'when descending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { 'LABEL_PRIORITY_DESC' } + let(:sort_param) { :LABEL_PRIORITY_DESC } let(:first_param) { 2 } let(:expected_results) { [label_issue2.iid, label_issue3.iid, label_issue1.iid, label_issue4.iid] } end @@ -261,7 +259,7 @@ RSpec.describe 'getting an issue list for a project' do context 'when ascending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { 'MILESTONE_DUE_ASC' } + let(:sort_param) { :MILESTONE_DUE_ASC } let(:first_param) { 2 } let(:expected_results) { [milestone_issue2.iid, milestone_issue3.iid, milestone_issue1.iid] } end @@ -269,7 +267,7 @@ RSpec.describe 'getting an issue list for a project' do context 'when descending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { 'MILESTONE_DUE_DESC' } + let(:sort_param) { :MILESTONE_DUE_DESC } let(:first_param) { 2 } let(:expected_results) { [milestone_issue3.iid, milestone_issue2.iid, milestone_issue1.iid] } end diff --git a/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb b/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb new file mode 100644 index 00000000000..ac0b18a37d6 --- /dev/null +++ b/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project.mergeRequests.pipelines' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:author) { create(:user) } + let_it_be(:merge_requests) do + %i[with_diffs with_image_diffs conflict].map do |trait| + create(:merge_request, trait, author: author, source_project: project) + end + end + + describe '.count' do + let(:query) do + <<~GQL + query($path: ID!, $first: Int) { + project(fullPath: $path) { + mergeRequests(first: $first) { + nodes { + iid + pipelines { + count + } + } + } + } + } + GQL + end + + def run_query(first = nil) + post_graphql(query, current_user: author, variables: { path: project.full_path, first: first }) + end + + before do + merge_requests.each do |mr| + shas = mr.all_commits.limit(2).pluck(:sha) + + shas.each do |sha| + create(:ci_pipeline, :success, project: project, ref: mr.source_branch, sha: sha) + end + end + end + + it 'produces correct results' do + run_query(2) + + p_nodes = graphql_data_at(:project, :merge_requests, :nodes) + + expect(p_nodes).to all(match('iid' => be_present, 'pipelines' => match('count' => 2))) + end + + it 'is scalable', :request_store, :use_clean_rails_memory_store_caching do + # warm up + run_query + + expect { run_query(2) }.to(issue_same_number_of_queries_as { run_query(1) }.ignoring_cached_queries) + end + end +end diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index 2b8d537f9fc..c05a620bb62 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -259,29 +259,19 @@ RSpec.describe 'getting merge request listings nested in a project' do describe 'sorting and pagination' do let(:data_path) { [:project, :mergeRequests] } - def pagination_query(params, page_info) - graphql_query_for( - :project, - { full_path: project.full_path }, + def pagination_query(params) + graphql_query_for(:project, { full_path: project.full_path }, <<~QUERY mergeRequests(#{params}) { - #{page_info} edges { - node { - id - } - } + #{page_info} nodes { id } } QUERY ) end - def pagination_results_data(data) - data.map { |project| project.dig('node', 'id') } - end - context 'when sorting by merged_at DESC' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { 'MERGED_AT_DESC' } + let(:sort_param) { :MERGED_AT_DESC } let(:first_param) { 2 } let(:expected_results) do @@ -291,7 +281,7 @@ RSpec.describe 'getting merge request listings nested in a project' do merge_request_c, merge_request_e, merge_request_a - ].map(&:to_gid).map(&:to_s) + ].map { |mr| global_id_of(mr) } end before do @@ -304,33 +294,6 @@ RSpec.describe 'getting merge request listings nested in a project' do merge_request_b.metrics.update!(merged_at: 1.day.ago) end - - context 'when paginating backwards' do - let(:params) { 'first: 2, sort: MERGED_AT_DESC' } - let(:page_info) { 'pageInfo { startCursor endCursor }' } - - before do - post_graphql(pagination_query(params, page_info), current_user: current_user) - end - - it 'paginates backwards correctly' do - # first page - first_page_response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges) - end_cursor = graphql_dig_at(Gitlab::Json.parse(response.body), :data, :project, :mergeRequests, :pageInfo, :endCursor) - - # second page - params = "first: 2, after: \"#{end_cursor}\", sort: MERGED_AT_DESC" - post_graphql(pagination_query(params, page_info), current_user: current_user) - start_cursor = graphql_dig_at(Gitlab::Json.parse(response.body), :data, :project, :mergeRequests, :pageInfo, :start_cursor) - - # going back to the first page - - params = "last: 2, before: \"#{start_cursor}\", sort: MERGED_AT_DESC" - post_graphql(pagination_query(params, page_info), current_user: current_user) - backward_paginated_response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges) - expect(first_page_response_data).to eq(backward_paginated_response_data) - end - end end end end diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index fef0e7e160c..6179b43629b 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -5,12 +5,12 @@ require 'spec_helper' RSpec.describe 'getting pipeline information nested in a project' do include GraphqlHelpers - let(:project) { create(:project, :repository, :public) } - let(:pipeline) { create(:ci_pipeline, project: project) } - let(:current_user) { create(:user) } + let!(:project) { create(:project, :repository, :public) } + let!(:pipeline) { create(:ci_pipeline, project: project) } + let!(:current_user) { create(:user) } let(:pipeline_graphql_data) { graphql_data['project']['pipeline'] } - let(:query) do + let!(:query) do graphql_query_for( 'project', { 'fullPath' => project.full_path }, @@ -35,4 +35,45 @@ RSpec.describe 'getting pipeline information nested in a project' do expect(pipeline_graphql_data.dig('configSource')).to eq('UNKNOWN_SOURCE') end + + context 'batching' do + let!(:pipeline2) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) } + let!(:pipeline3) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) } + let!(:query) { build_query_to_find_pipeline_shas(pipeline, pipeline2, pipeline3) } + + it 'executes the finder once' do + mock = double(Ci::PipelinesFinder) + opts = { iids: [pipeline.iid, pipeline2.iid, pipeline3.iid].map(&:to_s) } + + expect(Ci::PipelinesFinder).to receive(:new).once.with(project, current_user, opts).and_return(mock) + expect(mock).to receive(:execute).once.and_return(Ci::Pipeline.none) + + post_graphql(query, current_user: current_user) + end + + it 'keeps the queries under the threshold' do + control = ActiveRecord::QueryRecorder.new do + single_pipeline_query = build_query_to_find_pipeline_shas(pipeline) + + post_graphql(single_pipeline_query, current_user: current_user) + end + + aggregate_failures do + expect(response).to have_gitlab_http_status(:success) + expect do + post_graphql(query, current_user: current_user) + end.not_to exceed_query_limit(control) + end + end + end + + private + + def build_query_to_find_pipeline_shas(*pipelines) + pipeline_fields = pipelines.map.each_with_index do |pipeline, idx| + "pipeline#{idx}: pipeline(iid: \"#{pipeline.iid}\") { sha }" + end.join(' ') + + graphql_query_for('project', { 'fullPath' => project.full_path }, pipeline_fields) + end end diff --git a/spec/requests/api/graphql/project/project_members_spec.rb b/spec/requests/api/graphql/project/project_members_spec.rb new file mode 100644 index 00000000000..cb937432ef7 --- /dev/null +++ b/spec/requests/api/graphql/project/project_members_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting project members information' do + include GraphqlHelpers + + let_it_be(:parent_group) { create(:group, :public) } + let_it_be(:parent_project) { create(:project, :public, group: parent_group) } + let_it_be(:user) { create(:user) } + let_it_be(:user_1) { create(:user, username: 'user') } + let_it_be(:user_2) { create(:user, username: 'test') } + + let(:member_data) { graphql_data['project']['projectMembers']['edges'] } + + before_all do + [user_1, user_2].each { |user| parent_group.add_guest(user) } + end + + context 'when the request is correct' do + it_behaves_like 'a working graphql query' do + before_all do + fetch_members(project: parent_project) + end + end + + it 'returns project members successfully' do + fetch_members(project: parent_project) + + expect(graphql_errors).to be_nil + expect_array_response(user_1, user_2) + end + + it 'returns members that match the search query' do + fetch_members(project: parent_project, args: { search: 'test' }) + + expect(graphql_errors).to be_nil + expect_array_response(user_2) + end + end + + context 'member relations' do + let_it_be(:child_group) { create(:group, :public, parent: parent_group) } + let_it_be(:child_project) { create(:project, :public, group: child_group) } + let_it_be(:invited_group) { create(:group, :public) } + let_it_be(:child_user) { create(:user) } + let_it_be(:invited_user) { create(:user) } + let_it_be(:group_link) { create(:project_group_link, project: child_project, group: invited_group) } + + before_all do + child_project.add_guest(child_user) + invited_group.add_guest(invited_user) + end + + it 'returns direct members' do + fetch_members(project: child_project, args: { relations: [:DIRECT] }) + + expect(graphql_errors).to be_nil + expect_array_response(child_user) + end + + it 'returns invited members plus inherited members' do + fetch_members(project: child_project, args: { relations: [:INVITED_GROUPS] }) + + expect(graphql_errors).to be_nil + expect_array_response(invited_user, user_1, user_2) + end + + it 'returns direct, inherited, descendant, and invited members' do + fetch_members(project: child_project, args: { relations: [:DIRECT, :INHERITED, :DESCENDANTS, :INVITED_GROUPS] }) + + expect(graphql_errors).to be_nil + expect_array_response(child_user, user_1, user_2, invited_user) + end + + it 'returns an error for an invalid member relation' do + fetch_members(project: child_project, args: { relations: [:OBLIQUE] }) + + expect(graphql_errors.first) + .to include('path' => %w[query project projectMembers relations], + 'message' => a_string_including('invalid value ([OBLIQUE])')) + end + end + + context 'when unauthenticated' do + it 'returns members' do + fetch_members(current_user: nil, project: parent_project) + + expect(graphql_errors).to be_nil + expect_array_response(user_1, user_2) + end + end + + def fetch_members(project:, current_user: user, args: {}) + post_graphql(members_query(project.full_path, args), current_user: current_user) + end + + def members_query(group_path, args = {}) + members_node = <<~NODE + edges { + node { + user { + id + } + } + } + NODE + + graphql_query_for('project', + { full_path: group_path }, + [query_graphql_field('projectMembers', args, members_node)] + ) + end + + def expect_array_response(*items) + expect(response).to have_gitlab_http_status(:success) + expect(member_data).to be_an Array + expect(member_data.map { |node| node['node']['user']['id'] }) + .to match_array(items.map { |u| global_id_of(u) }) + end +end diff --git a/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb b/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb new file mode 100644 index 00000000000..0f495f3e671 --- /dev/null +++ b/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'rendering project pipeline statistics' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let(:user) { create(:user) } + + let(:fields) do + <<~QUERY + weekPipelinesTotals + weekPipelinesLabels + monthPipelinesLabels + monthPipelinesTotals + yearPipelinesLabels + yearPipelinesTotals + QUERY + end + + let(:query) do + graphql_query_for('project', + { 'fullPath' => project.full_path }, + query_graphql_field('pipelineAnalytics', {}, fields)) + end + + before do + project.add_maintainer(user) + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: user) + end + end + + it "contains two arrays of 8 elements each for the week pipelines" do + post_graphql(query, current_user: user) + + expect(graphql_data_at(:project, :pipelineAnalytics, :weekPipelinesTotals).length).to eq(8) + expect(graphql_data_at(:project, :pipelineAnalytics, :weekPipelinesLabels).length).to eq(8) + end + + it "contains two arrays of 31 elements each for the month pipelines" do + post_graphql(query, current_user: user) + + expect(graphql_data_at(:project, :pipelineAnalytics, :monthPipelinesTotals).length).to eq(31) + expect(graphql_data_at(:project, :pipelineAnalytics, :monthPipelinesLabels).length).to eq(31) + end + + it "contains two arrays of 13 elements each for the year pipelines" do + post_graphql(query, current_user: user) + + expect(graphql_data_at(:project, :pipelineAnalytics, :yearPipelinesTotals).length).to eq(13) + expect(graphql_data_at(:project, :pipelineAnalytics, :yearPipelinesLabels).length).to eq(13) + end +end diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb index 57dbe258ce4..99b15ff00b1 100644 --- a/spec/requests/api/graphql/project/release_spec.rb +++ b/spec/requests/api/graphql/project/release_spec.rb @@ -36,7 +36,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do let(:path) { path_prefix } let(:release_fields) do - query_graphql_field(%{ + %{ tagName tagPath description @@ -45,7 +45,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do createdAt releasedAt upcomingRelease - }) + } end before do @@ -233,7 +233,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do let(:path) { path_prefix } let(:release_fields) do - query_graphql_field('description') + 'description' end before do @@ -394,10 +394,10 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do 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 8b67b549efa..2879530acc5 100644 --- a/spec/requests/api/graphql/project/terraform/states_spec.rb +++ b/spec/requests/api/graphql/project/terraform/states_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe 'query terraform states' do include GraphqlHelpers + include ::API::Helpers::RelatedResourcesHelpers let_it_be(:project) { create(:project) } let_it_be(:terraform_state) { create(:terraform_state, :with_version, :locked, project: project) } @@ -23,6 +24,8 @@ RSpec.describe 'query terraform states' do latestVersion { id + downloadPath + serial createdAt updatedAt @@ -50,22 +53,32 @@ RSpec.describe 'query terraform states' 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['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 + download_path = expose_path( + api_v4_projects_terraform_state_versions_path( + id: project.id, + name: terraform_state.name, + serial: latest_version.version + ) + ) + + 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(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(global_id_of(latest_version.created_by_user)) }, + '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 4b8ffb0675c..b29f9ae913f 100644 --- a/spec/requests/api/graphql/project_query_spec.rb +++ b/spec/requests/api/graphql/project_query_spec.rb @@ -5,36 +5,34 @@ require 'spec_helper' 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 - before do + context 'when the user has access to the project', :use_clean_rails_memory_store_caching, :request_store do + before_all do project.add_developer(current_user) end @@ -55,10 +53,12 @@ RSpec.describe 'getting project information' do 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 @@ RSpec.describe 'getting project information' do 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, @@ -151,8 +151,9 @@ RSpec.describe 'getting project information' do ))) end - it 'can lookahead to eliminate N+1 queries', :use_clean_rails_memory_store_caching, :request_store do - expect { run_query(10) }.to issue_same_number_of_queries_as { run_query(1) }.or_fewer.ignoring_cached_queries + it 'can lookahead to eliminate N+1 queries' do + baseline = ActiveRecord::QueryRecorder.new { run_query(1) } + expect { run_query(10) }.not_to exceed_query_limit(baseline) end end diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb index 8c45a67cb0f..60520906e87 100644 --- a/spec/requests/api/graphql/user_query_spec.rb +++ b/spec/requests/api/graphql/user_query_spec.rb @@ -58,9 +58,25 @@ RSpec.describe 'getting user information' do source_project: project_b, author: user) end + let_it_be(:reviewed_mr) do + create(:merge_request, :unique_branches, :unique_author, + source_project: project_a, reviewers: [user]) + end + + let_it_be(:reviewed_mr_b) do + create(:merge_request, :unique_branches, :unique_author, + source_project: project_b, reviewers: [user]) + end + + let_it_be(:reviewed_mr_c) do + create(:merge_request, :unique_branches, :unique_author, + source_project: project_b, reviewers: [user]) + end + let(:current_user) { authorised_user } let(:authored_mrs) { graphql_data_at(:user, :authored_merge_requests, :nodes) } let(:assigned_mrs) { graphql_data_at(:user, :assigned_merge_requests, :nodes) } + let(:reviewed_mrs) { graphql_data_at(:user, :review_requested_merge_requests, :nodes) } let(:user_params) { { username: user.username } } before do @@ -82,7 +98,8 @@ RSpec.describe 'getting user information' do 'username' => presenter.username, 'webUrl' => presenter.web_url, 'avatarUrl' => presenter.avatar_url, - 'email' => presenter.public_email + 'email' => presenter.public_email, + 'publicEmail' => presenter.public_email )) expect(graphql_data['user']['status']).to match( @@ -156,6 +173,23 @@ RSpec.describe 'getting user information' do ) end end + + context 'filtering by reviewer' do + let(:reviewer) { create(:user) } + let(:mr_args) { { reviewer_username: reviewer.username } } + + it 'finds the assigned mrs' do + assigned_mr_b.reviewers << reviewer + assigned_mr_c.reviewers << reviewer + + post_graphql(query, current_user: current_user) + + expect(assigned_mrs).to contain_exactly( + a_hash_including('id' => global_id_of(assigned_mr_b)), + a_hash_including('id' => global_id_of(assigned_mr_c)) + ) + end + end end context 'the current user does not have access' do @@ -167,6 +201,95 @@ RSpec.describe 'getting user information' do end end + describe 'reviewRequestedMergeRequests' do + let(:user_fields) do + query_graphql_field(:review_requested_merge_requests, mr_args, 'nodes { id }') + end + + let(:mr_args) { nil } + + it_behaves_like 'a working graphql query' + + it 'can be found' do + expect(reviewed_mrs).to contain_exactly( + a_hash_including('id' => global_id_of(reviewed_mr)), + a_hash_including('id' => global_id_of(reviewed_mr_b)), + a_hash_including('id' => global_id_of(reviewed_mr_c)) + ) + end + + context 'applying filters' do + context 'filtering by IID without specifying a project' do + let(:mr_args) do + { iids: [reviewed_mr_b.iid.to_s] } + end + + it 'return an argument error that mentions the missing fields' do + expect_graphql_errors_to_include(/projectPath/) + end + end + + context 'filtering by project path and IID' do + let(:mr_args) do + { project_path: project_b.full_path, iids: [reviewed_mr_b.iid.to_s] } + end + + it 'selects the correct MRs' do + expect(reviewed_mrs).to contain_exactly( + a_hash_including('id' => global_id_of(reviewed_mr_b)) + ) + end + end + + context 'filtering by project path' do + let(:mr_args) do + { project_path: project_b.full_path } + end + + it 'selects the correct MRs' do + expect(reviewed_mrs).to contain_exactly( + a_hash_including('id' => global_id_of(reviewed_mr_b)), + a_hash_including('id' => global_id_of(reviewed_mr_c)) + ) + end + end + + context 'filtering by author' do + let(:author) { reviewed_mr_b.author } + let(:mr_args) { { author_username: author.username } } + + it 'finds the authored mrs' do + expect(reviewed_mrs).to contain_exactly( + a_hash_including('id' => global_id_of(reviewed_mr_b)) + ) + end + end + + context 'filtering by assignee' do + let(:assignee) { create(:user) } + let(:mr_args) { { assignee_username: assignee.username } } + + it 'finds the authored mrs' do + reviewed_mr_c.assignees << assignee + + post_graphql(query, current_user: current_user) + + expect(reviewed_mrs).to contain_exactly( + a_hash_including('id' => global_id_of(reviewed_mr_c)) + ) + end + end + end + + context 'the current user does not have access' do + let(:current_user) { unauthorized_user } + + it 'cannot be found' do + expect(reviewed_mrs).to be_empty + end + end + end + describe 'authoredMergeRequests' do let(:user_fields) do query_graphql_field(:authored_merge_requests, mr_args, 'nodes { id }') @@ -212,6 +335,23 @@ RSpec.describe 'getting user information' do end end + context 'filtering by reviewer' do + let(:reviewer) { create(:user) } + let(:mr_args) { { reviewer_username: reviewer.username } } + + it 'finds the assigned mrs' do + authored_mr_b.reviewers << reviewer + authored_mr_c.reviewers << reviewer + + post_graphql(query, current_user: current_user) + + expect(authored_mrs).to contain_exactly( + a_hash_including('id' => global_id_of(authored_mr_b)), + a_hash_including('id' => global_id_of(authored_mr_c)) + ) + end + end + context 'filtering by project path and IID' do let(:mr_args) do { project_path: project_b.full_path, iids: [authored_mr_b.iid.to_s] } diff --git a/spec/requests/api/graphql/users_spec.rb b/spec/requests/api/graphql/users_spec.rb index 91ac206676b..72d86c10df1 100644 --- a/spec/requests/api/graphql/users_spec.rb +++ b/spec/requests/api/graphql/users_spec.rb @@ -59,20 +59,16 @@ RSpec.describe 'Users' do describe 'sorting and pagination' do let_it_be(:data_path) { [:users] } - def pagination_query(params, page_info) - graphql_query_for("users", params, "#{page_info} edges { node { id } }") - end - - def pagination_results_data(data) - data.map { |user| user.dig('node', 'id') } + def pagination_query(params) + graphql_query_for(:users, params, "#{page_info} nodes { id }") end context 'when sorting by created_at' do - let_it_be(:ascending_users) { [user3, user2, user1, current_user].map(&:to_global_id).map(&:to_s) } + let_it_be(:ascending_users) { [user3, user2, user1, current_user].map { |u| global_id_of(u) } } context 'when ascending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { 'created_asc' } + let(:sort_param) { :CREATED_ASC } let(:first_param) { 1 } let(:expected_results) { ascending_users } end @@ -80,7 +76,7 @@ RSpec.describe 'Users' do context 'when descending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { 'created_desc' } + let(:sort_param) { :CREATED_DESC } let(:first_param) { 1 } let(:expected_results) { ascending_users.reverse } end |