diff options
Diffstat (limited to 'spec/requests/api/graphql')
33 files changed, 1251 insertions, 138 deletions
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb index 2d1bb45390b..d1737fc22ae 100644 --- a/spec/requests/api/graphql/ci/jobs_spec.rb +++ b/spec/requests/api/graphql/ci/jobs_spec.rb @@ -258,4 +258,81 @@ RSpec.describe 'Query.project.pipeline' do end end end + + describe '.jobs.count' do + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:successful_job) { create(:ci_build, :success, pipeline: pipeline) } + let_it_be(:pending_job) { create(:ci_build, :pending, pipeline: pipeline) } + let_it_be(:failed_job) { create(:ci_build, :failed, pipeline: pipeline) } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipeline(iid: "#{pipeline.iid}") { + jobs { + count + } + } + } + } + ) + end + + before do + post_graphql(query, current_user: user) + end + + it 'returns the number of jobs' do + expect(graphql_data_at(:project, :pipeline, :jobs, :count)).to eq(3) + end + + context 'with limit value' do + let(:limit) { 1 } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipeline(iid: "#{pipeline.iid}") { + jobs { + count(limit: #{limit}) + } + } + } + } + ) + end + + it 'returns a limited number of jobs' do + expect(graphql_data_at(:project, :pipeline, :jobs, :count)).to eq(2) + end + + context 'with invalid value' do + let(:limit) { 1500 } + + it 'returns a validation error' do + expect(graphql_errors).to include(a_hash_including('message' => 'limit must be less than or equal to 1000')) + end + end + end + + context 'with jobs filter' do + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + jobs(statuses: FAILED) { + count + } + } + } + ) + end + + it 'returns the number of failed jobs' do + expect(graphql_data_at(:project, :jobs, :count)).to eq(1) + end + end + end end diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 6fa455cbfca..446d1fb1bdb 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -12,7 +12,7 @@ RSpec.describe 'Query.runner(id)' do create(:ci_runner, :instance, description: 'Runner 1', contacted_at: 2.hours.ago, active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600, access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true, executor_type: :custom, - maintenance_note: 'Test maintenance note') + maintenance_note: '**Test maintenance note**') end let_it_be(:inactive_instance_runner) do @@ -66,6 +66,8 @@ RSpec.describe 'Query.runner(id)' do 'architectureName' => runner.architecture, 'platformName' => runner.platform, 'maintenanceNote' => runner.maintenance_note, + 'maintenanceNoteHtml' => + runner.maintainer_note.present? ? a_string_including('<strong>Test maintenance note</strong>') : '', 'jobCount' => 0, 'jobs' => a_hash_including("count" => 0, "nodes" => [], "pageInfo" => anything), 'projectCount' => nil, @@ -150,34 +152,72 @@ RSpec.describe 'Query.runner(id)' do end describe 'for project runner' do - using RSpec::Parameterized::TableSyntax + describe 'locked' do + using RSpec::Parameterized::TableSyntax - where(is_locked: [true, false]) + where(is_locked: [true, false]) - with_them do - let(:project_runner) do - create(:ci_runner, :project, description: 'Runner 3', contacted_at: 1.day.ago, active: false, locked: is_locked, - version: 'adfe157', revision: 'b', ip_address: '10.10.10.10', access_level: 1, run_untagged: true) - end + with_them do + let(:project_runner) do + create(:ci_runner, :project, description: 'Runner 3', contacted_at: 1.day.ago, active: false, locked: is_locked, + version: 'adfe157', revision: 'b', ip_address: '10.10.10.10', access_level: 1, run_untagged: true) + end - let(:query) do - wrap_fields(query_graphql_path(query_path, all_graphql_fields_for('CiRunner'))) + let(:query) do + wrap_fields(query_graphql_path(query_path, 'id locked')) + end + + let(:query_path) do + [ + [:runner, { id: project_runner.to_global_id.to_s }] + ] + end + + it 'retrieves correct locked value' do + post_graphql(query, current_user: user) + + runner_data = graphql_data_at(:runner) + + expect(runner_data).to match a_hash_including( + 'id' => project_runner.to_global_id.to_s, + 'locked' => is_locked + ) + end end + end - let(:query_path) do - [ - [:runner, { id: project_runner.to_global_id.to_s }] - ] + describe 'ownerProject' do + let_it_be(:project1) { create(:project) } + let_it_be(:project2) { create(:project) } + let_it_be(:runner1) { create(:ci_runner, :project, projects: [project2, project1]) } + let_it_be(:runner2) { create(:ci_runner, :project, projects: [project1, project2]) } + + let(:runner_query_fragment) { 'id ownerProject { id }' } + let(:query) do + %( + query { + runner1: runner(id: "#{runner1.to_global_id}") { #{runner_query_fragment} } + runner2: runner(id: "#{runner2.to_global_id}") { #{runner_query_fragment} } + } + ) end - it 'retrieves correct locked value' do + it 'retrieves correct ownerProject.id values' do post_graphql(query, current_user: user) - runner_data = graphql_data_at(:runner) - - expect(runner_data).to match a_hash_including( - 'id' => project_runner.to_global_id.to_s, - 'locked' => is_locked + expect(graphql_data).to match a_hash_including( + 'runner1' => { + 'id' => runner1.to_global_id.to_s, + 'ownerProject' => { + 'id' => project2.to_global_id.to_s + } + }, + 'runner2' => { + 'id' => runner2.to_global_id.to_s, + 'ownerProject' => { + 'id' => project1.to_global_id.to_s + } + } ) end end @@ -405,17 +445,35 @@ RSpec.describe 'Query.runner(id)' do <<~SINGLE runner(id: "#{runner.to_global_id}") { #{all_graphql_fields_for('CiRunner', excluded: excluded_fields)} + groups { + nodes { + id + } + } + projects { + nodes { + id + } + } + ownerProject { + id + } } SINGLE end - # Currently excluding a known N+1 issue, see https://gitlab.com/gitlab-org/gitlab/-/issues/334759 - let(:excluded_fields) { %w[jobCount] } + let(:active_project_runner2) { create(:ci_runner, :project) } + let(:active_group_runner2) { create(:ci_runner, :group) } + + # Currently excluding known N+1 issues, see https://gitlab.com/gitlab-org/gitlab/-/issues/334759 + let(:excluded_fields) { %w[jobCount groups projects ownerProject] } let(:single_query) do <<~QUERY { - active: #{runner_query(active_instance_runner)} + instance_runner1: #{runner_query(active_instance_runner)} + project_runner1: #{runner_query(active_project_runner)} + group_runner1: #{runner_query(active_group_runner)} } QUERY end @@ -423,22 +481,51 @@ RSpec.describe 'Query.runner(id)' do let(:double_query) do <<~QUERY { - active: #{runner_query(active_instance_runner)} - inactive: #{runner_query(inactive_instance_runner)} + instance_runner1: #{runner_query(active_instance_runner)} + instance_runner2: #{runner_query(inactive_instance_runner)} + group_runner1: #{runner_query(active_group_runner)} + group_runner2: #{runner_query(active_group_runner2)} + project_runner1: #{runner_query(active_project_runner)} + project_runner2: #{runner_query(active_project_runner2)} } QUERY end it 'does not execute more queries per runner', :aggregate_failures do # warm-up license cache and so on: - post_graphql(single_query, current_user: user) + post_graphql(double_query, current_user: user) control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, current_user: user) } expect { post_graphql(double_query, current_user: user) } .not_to exceed_query_limit(control) - expect(graphql_data_at(:active)).not_to be_nil - expect(graphql_data_at(:inactive)).not_to be_nil + + expect(graphql_data.count).to eq 6 + expect(graphql_data).to match( + a_hash_including( + 'instance_runner1' => a_hash_including('id' => active_instance_runner.to_global_id.to_s), + 'instance_runner2' => a_hash_including('id' => inactive_instance_runner.to_global_id.to_s), + 'group_runner1' => a_hash_including( + 'id' => active_group_runner.to_global_id.to_s, + 'groups' => { 'nodes' => [a_hash_including('id' => group.to_global_id.to_s)] } + ), + 'group_runner2' => a_hash_including( + 'id' => active_group_runner2.to_global_id.to_s, + 'groups' => { 'nodes' => [a_hash_including('id' => active_group_runner2.groups[0].to_global_id.to_s)] } + ), + 'project_runner1' => a_hash_including( + 'id' => active_project_runner.to_global_id.to_s, + 'projects' => { 'nodes' => [a_hash_including('id' => active_project_runner.projects[0].to_global_id.to_s)] }, + 'ownerProject' => a_hash_including('id' => active_project_runner.projects[0].to_global_id.to_s) + ), + 'project_runner2' => a_hash_including( + 'id' => active_project_runner2.to_global_id.to_s, + 'projects' => { + 'nodes' => [a_hash_including('id' => active_project_runner2.projects[0].to_global_id.to_s)] + }, + 'ownerProject' => a_hash_including('id' => active_project_runner2.projects[0].to_global_id.to_s) + ) + )) end end end diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb index d3e94671724..a5b8115286e 100644 --- a/spec/requests/api/graphql/ci/runners_spec.rb +++ b/spec/requests/api/graphql/ci/runners_spec.rb @@ -18,7 +18,10 @@ RSpec.describe 'Query.runners' do let(:fields) do <<~QUERY nodes { - #{all_graphql_fields_for('CiRunner')} + #{all_graphql_fields_for('CiRunner', excluded: %w[ownerProject])} + ownerProject { + id + } } QUERY end @@ -123,3 +126,47 @@ RSpec.describe 'Query.runners' do end end end + +RSpec.describe 'Group.runners' do + include GraphqlHelpers + + let_it_be(:group) { create(:group) } + let_it_be(:group_owner) { create_default(:user) } + + before do + group.add_owner(group_owner) + end + + describe 'edges' do + let_it_be(:runner) do + create(:ci_runner, :group, active: false, version: 'def', revision: '456', + description: 'Project runner', groups: [group], ip_address: '127.0.0.1') + end + + let(:query) do + %( + query($path: ID!) { + group(fullPath: $path) { + runners { + edges { + webUrl + editUrl + node { #{all_graphql_fields_for('CiRunner')} } + } + } + } + } + ) + end + + it 'contains custom edge information' do + r = GitlabSchema.execute(query, + context: { current_user: group_owner }, + variables: { path: group.full_path }) + + edges = graphql_dig_at(r.to_h, :data, :group, :runners, :edges) + + expect(edges).to contain_exactly(a_graphql_entity_for(web_url: be_present, edit_url: be_present)) + end + end +end diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb index e80f5e0e0ff..c1beadb6c45 100644 --- a/spec/requests/api/graphql/gitlab_schema_spec.rb +++ b/spec/requests/api/graphql/gitlab_schema_spec.rb @@ -190,7 +190,7 @@ RSpec.describe 'GitlabSchema configurations' do let(:query) { File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql')) } it 'logs the query complexity and depth' do - expect_any_instance_of(Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer).to receive(:duration).and_return(7) + expect_any_instance_of(Gitlab::Graphql::QueryAnalyzers::AST::LoggerAnalyzer).to receive(:duration).and_return(7) expect(Gitlab::GraphqlLogger).to receive(:info).with( hash_including( diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb index 05fd6bf3022..6e2d736f244 100644 --- a/spec/requests/api/graphql/issue/issue_spec.rb +++ b/spec/requests/api/graphql/issue/issue_spec.rb @@ -129,6 +129,29 @@ RSpec.describe 'Query.issue(id)' do expect(graphql_errors.first['message']).to eq("\"#{gid}\" does not represent an instance of Issue") end end + + context 'when selecting `closed_as_duplicate_of`' do + let(:issue_fields) { ['closedAsDuplicateOf { id }'] } + let(:duplicate_issue) { create(:issue, project: project) } + + before do + issue.update!(duplicated_to_id: duplicate_issue.id) + + post_graphql(query, current_user: current_user) + end + + it 'returns the related issue' do + expect(issue_data['closedAsDuplicateOf']['id']).to eq(duplicate_issue.to_global_id.to_s) + end + + context 'no permission to related issue' do + let(:duplicate_issue) { create(:issue) } + + it 'does not return the related issue' do + expect(issue_data['closedAsDuplicateOf']).to eq(nil) + end + end + end end context 'when there is a confidential issue' do diff --git a/spec/requests/api/graphql/milestone_spec.rb b/spec/requests/api/graphql/milestone_spec.rb index 59de116fa2b..f6835936418 100644 --- a/spec/requests/api/graphql/milestone_spec.rb +++ b/spec/requests/api/graphql/milestone_spec.rb @@ -5,43 +5,125 @@ require 'spec_helper' RSpec.describe 'Querying a Milestone' do include GraphqlHelpers - let_it_be(:current_user) { create(:user) } + let_it_be(:guest) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:release_a) { create(:release, project: project) } + let_it_be(:release_b) { create(:release, project: project) } - let(:query) do - graphql_query_for('milestone', { id: milestone.to_global_id.to_s }, 'title') + before_all do + milestone.releases << [release_a, release_b] + project.add_guest(guest) end - subject { graphql_data['milestone'] } - - before do - post_graphql(query, current_user: current_user) + let(:expected_release_nodes) do + contain_exactly(a_graphql_entity_for(release_a), a_graphql_entity_for(release_b)) end - context 'when the user has access to the milestone' do - before_all do - project.add_guest(current_user) + context 'when we post the query' do + let(:current_user) { nil } + let(:query) do + graphql_query_for('milestone', { id: milestone.to_global_id.to_s }, all_graphql_fields_for('Milestone')) end - it_behaves_like 'a working graphql query' + subject { graphql_data['milestone'] } - it { is_expected.to include('title' => milestone.name) } - end + before do + post_graphql(query, current_user: current_user) + end - context 'when the user does not have access to the milestone' do - it_behaves_like 'a working graphql query' + context 'when the user has access to the milestone' do + let(:current_user) { guest } - it { is_expected.to be_nil } + it_behaves_like 'a working graphql query' + + it { is_expected.to include('title' => milestone.name) } + + it 'contains release information' do + is_expected.to include('releases' => include('nodes' => expected_release_nodes)) + end + end + + context 'when the user does not have access to the milestone' do + it_behaves_like 'a working graphql query' + + it { is_expected.to be_nil } + end + + context 'when ID argument is missing' do + let(:query) do + graphql_query_for('milestone', {}, 'title') + end + + it 'raises an exception' do + expect(graphql_errors).to include(a_hash_including('message' => "Field 'milestone' is missing required arguments: id")) + end + end end - context 'when ID argument is missing' do - let(:query) do - graphql_query_for('milestone', {}, 'title') + context 'when there are two milestones' do + let_it_be(:milestone_b) { create(:milestone, project: project) } + + let(:current_user) { guest } + let(:milestone_fields) do + <<~GQL + fragment milestoneFields on Milestone { + #{all_graphql_fields_for('Milestone', max_depth: 1)} + releases { nodes { #{all_graphql_fields_for('Release', max_depth: 1)} } } + } + GQL + end + + let(:single_query) do + <<~GQL + query ($id_a: MilestoneID!) { + a: milestone(id: $id_a) { ...milestoneFields } + } + + #{milestone_fields} + GQL + end + + let(:multi_query) do + <<~GQL + query ($id_a: MilestoneID!, $id_b: MilestoneID!) { + a: milestone(id: $id_a) { ...milestoneFields } + b: milestone(id: $id_b) { ...milestoneFields } + } + #{milestone_fields} + GQL + end + + it 'produces correct results' do + r = run_with_clean_state(multi_query, + context: { current_user: current_user }, + variables: { + id_a: global_id_of(milestone).to_s, + id_b: milestone_b.to_global_id.to_s + }) + + expect(r.to_h['errors']).to be_blank + expect(graphql_dig_at(r.to_h, :data, :a, :releases, :nodes)).to match expected_release_nodes + expect(graphql_dig_at(r.to_h, :data, :b, :releases, :nodes)).to be_empty end - it 'raises an exception' do - expect(graphql_errors).to include(a_hash_including('message' => "Field 'milestone' is missing required arguments: id")) + it 'does not suffer from N+1 performance issues' do + baseline = ActiveRecord::QueryRecorder.new do + run_with_clean_state(single_query, + context: { current_user: current_user }, + variables: { id_a: milestone.to_global_id.to_s }) + end + + multi = ActiveRecord::QueryRecorder.new do + run_with_clean_state(multi_query, + context: { current_user: current_user }, + variables: { + id_a: milestone.to_global_id.to_s, + id_b: milestone_b.to_global_id.to_s + }) + end + + expect(multi).not_to exceed_query_limit(baseline) end end end diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb index 37656ab4eea..7abd5ca8772 100644 --- a/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb @@ -28,4 +28,21 @@ RSpec.describe 'PipelineDestroy' do expect(response).to have_gitlab_http_status(:success) expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound) end + + context 'when project is undergoing stats refresh' do + before do + create(:project_build_artifacts_size_refresh, :pending, project: pipeline.project) + end + + it 'returns an error and does not destroy the pipeline' do + expect(Gitlab::ProjectStatsRefreshConflictsLogger) + .to receive(:warn_request_rejected_during_stats_refresh) + .with(pipeline.project.id) + + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_mutation_response(:pipeline_destroy)['errors']).not_to be_empty + expect(pipeline.reload).to be_persisted + end + end end 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 index decb2e7bccc..ef00f45ef18 100644 --- a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb +++ b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb @@ -91,7 +91,7 @@ RSpec.describe 'Destroying a container repository tags' do it 'returns too many tags error' do expect { subject }.not_to change { ::Packages::Event.count } - explanation = graphql_errors.dig(0, 'extensions', 'problems', 0, 'explanation') + explanation = graphql_errors.dig(0, 'message') expect(explanation).to eq(Mutations::ContainerRepositories::DestroyTags::TOO_MANY_TAGS_ERROR_MESSAGE) end end 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 1f43f113e65..e2ab08b301b 100644 --- a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb @@ -47,7 +47,7 @@ RSpec.describe "deleting designs" do context 'the designs list is empty' do it_behaves_like 'a failed request' do let(:designs) { [] } - let(:the_error) { a_string_matching %r/was provided invalid value/ } + let(:the_error) { a_string_matching %r/no filenames/ } end end diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb index 3ea8b38e20f..923e12a3c06 100644 --- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb +++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb @@ -53,7 +53,7 @@ RSpec.describe 'Creating an incident timeline event' do }, 'note' => note, 'action' => 'comment', - 'editable' => false, + 'editable' => true, 'occurredAt' => event_occurred_at.iso8601 ) end diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb index faff3bfe23a..85208869ad9 100644 --- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb @@ -56,7 +56,7 @@ RSpec.describe 'Removing an incident timeline event' do }, 'note' => timeline_event.note, 'noteHtml' => timeline_event.note_html, - 'editable' => false, + 'editable' => true, 'action' => timeline_event.action, 'occurredAt' => timeline_event.occurred_at.iso8601, 'createdAt' => timeline_event.created_at.iso8601, diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb index b92f6af1d3d..9272e218172 100644 --- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb +++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb @@ -55,7 +55,7 @@ RSpec.describe 'Promote an incident timeline event from a comment' do }, 'note' => comment.note, 'action' => 'comment', - 'editable' => false, + 'editable' => true, 'occurredAt' => comment.created_at.iso8601 ) 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 715507c3cc5..395a490bfc3 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 @@ -102,18 +102,6 @@ RSpec.describe 'Setting issues crm contacts' do group.add_reporter(user) end - context 'when the feature is disabled' do - before do - stub_feature_flags(customer_relations: false) - end - - it 'raises expected error' do - post_graphql_mutation(mutation, current_user: user) - - expect(graphql_errors).to include(a_hash_including('message' => 'Feature disabled')) - end - end - it_behaves_like 'successful mutation' context 'when the contact does not exist' do diff --git a/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb b/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb index 0166871502b..a81364d37b2 100644 --- a/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb @@ -49,14 +49,6 @@ RSpec.describe 'Setting the escalation status of an incident' do it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue'] end - context 'with feature disabled' do - before do - stub_feature_flags(incident_escalations: false) - end - - it_behaves_like 'a mutation that returns top-level errors', errors: ['Feature unavailable for provided issue'] - end - it 'sets given escalation_policy to the escalation status for the issue' do post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb b/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb index 45cc70f09fd..b438e1ba881 100644 --- a/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb +++ b/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'Importing Jira Users' do - include JiraServiceHelper + include JiraIntegrationHelpers include GraphqlHelpers let_it_be(:user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb index b14305281af..1508ba31e37 100644 --- a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb +++ b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'Starting a Jira Import' do - include JiraServiceHelper + include JiraIntegrationHelpers include GraphqlHelpers let_it_be(:user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb index 5bc3c68cf26..9ef443af76a 100644 --- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb +++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb @@ -76,7 +76,6 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do context 'when environment_id is missing' do let(:mutation) do variables = { - environment_id: nil, starting_at: starting_at, ending_at: ending_at, dashboard_path: dashboard_path, @@ -147,7 +146,6 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do context 'when cluster_id is missing' do let(:mutation) do variables = { - cluster_id: nil, starting_at: starting_at, ending_at: ending_at, dashboard_path: dashboard_path, diff --git a/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb index 0f7ccac3179..c4674155aa0 100644 --- a/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb @@ -68,15 +68,7 @@ RSpec.describe 'Repositioning an ImageDiffNote' do let(:new_position) { { x: nil } } it_behaves_like 'a mutation that returns top-level errors' do - let(:match_errors) { include(/RepositionImageDiffNoteInput! was provided invalid value/) } - end - - it 'contains an explanation for the error' do - post_graphql_mutation(mutation, current_user: current_user) - - explanation = graphql_errors.first['extensions']['problems'].first['explanation'] - - expect(explanation).to eq('At least one property of `UpdateDiffImagePositionInput` must be set') + let(:match_errors) { include(/At least one property of `UpdateDiffImagePositionInput` must be set/) } end end end diff --git a/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb b/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb new file mode 100644 index 00000000000..7e00f3ca53a --- /dev/null +++ b/spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Updating the packages cleanup policy' do + include GraphqlHelpers + using RSpec::Parameterized::TableSyntax + + let_it_be(:project, reload: true) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:params) do + { + project_path: project.full_path, + keep_n_duplicated_package_files: 'TWENTY_PACKAGE_FILES' + } + end + + let(:mutation) do + graphql_mutation(:update_packages_cleanup_policy, params, + <<~QUERY + packagesCleanupPolicy { + keepNDuplicatedPackageFiles + nextRunAt + } + errors + QUERY + ) + end + + let(:mutation_response) { graphql_mutation_response(:update_packages_cleanup_policy) } + let(:packages_cleanup_policy_response) { mutation_response['packagesCleanupPolicy'] } + + shared_examples 'accepting the mutation request and updates the existing policy' do + it 'returns the updated packages cleanup policy' do + expect { subject }.not_to change { ::Packages::Cleanup::Policy.count } + + expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('20') + expect_graphql_errors_to_be_empty + expect(packages_cleanup_policy_response['keepNDuplicatedPackageFiles']) + .to eq(params[:keep_n_duplicated_package_files]) + expect(packages_cleanup_policy_response['nextRunAt']).not_to eq(nil) + end + end + + shared_examples 'accepting the mutation request and creates a policy' do + it 'returns the created packages cleanup policy' do + expect { subject }.to change { ::Packages::Cleanup::Policy.count }.by(1) + + expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('20') + expect_graphql_errors_to_be_empty + expect(packages_cleanup_policy_response['keepNDuplicatedPackageFiles']) + .to eq(params[:keep_n_duplicated_package_files]) + expect(packages_cleanup_policy_response['nextRunAt']).not_to eq(nil) + end + end + + shared_examples 'denying the mutation request' do + it 'returns an error' do + expect { subject }.not_to change { ::Packages::Cleanup::Policy.count } + + expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).not_to eq('20') + expect(mutation_response).to be_nil + expect_graphql_errors_to_include(/you don't have permission to perform this action/) + end + end + + describe 'post graphql mutation' do + subject { post_graphql_mutation(mutation, current_user: user) } + + context 'with existing packages cleanup policy' do + let_it_be(:project_packages_cleanup_policy) { create(:packages_cleanup_policy, project: project) } + + where(:user_role, :shared_examples_name) do + :maintainer | 'accepting the mutation request and updates the existing policy' + :developer | 'denying the mutation request' + :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 'without existing packages cleanup policy' do + where(:user_role, :shared_examples_name) do + :maintainer | 'accepting the mutation request and creates a policy' + :developer | 'denying the mutation request' + :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 + end +end diff --git a/spec/requests/api/graphql/mutations/packages/destroy_files_spec.rb b/spec/requests/api/graphql/mutations/packages/destroy_files_spec.rb new file mode 100644 index 00000000000..002cd634ebd --- /dev/null +++ b/spec/requests/api/graphql/mutations/packages/destroy_files_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Destroying multiple package files' do + using RSpec::Parameterized::TableSyntax + + include GraphqlHelpers + + let_it_be_with_reload(:package) { create(:maven_package) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { package.project } + + let(:ids) { package.package_files.first(2).map { |pf| pf.to_global_id.to_s } } + + let(:query) do + <<~GQL + errors + GQL + end + + let(:params) do + { + project_path: project.full_path, + ids: ids + } + end + + let(:mutation) { graphql_mutation(:destroy_package_files, params, query) } + + describe 'post graphql mutation' do + subject(:mutation_request) { post_graphql_mutation(mutation, current_user: user) } + + shared_examples 'destroying the package files' do + it 'marks the package file as pending destruction' do + expect { mutation_request }.to change { ::Packages::PackageFile.pending_destruction.count }.by(2) + end + + it_behaves_like 'returning response status', :success + end + + shared_examples 'denying the mutation request' do |response = "you don't have permission to perform this action"| + it 'does not mark the package file as pending destruction' do + expect { mutation_request }.not_to change { ::Packages::PackageFile.pending_destruction.count } + + expect_graphql_errors_to_include(response) + end + + it_behaves_like 'returning response status', :success + end + + context 'with valid params' do + where(:user_role, :shared_examples_name) do + :maintainer | 'destroying the package files' + :developer | 'denying the mutation request' + :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 + + context 'with more than 100 files' do + let(:ids) { package.package_files.map { |pf| pf.to_global_id.to_s } } + + before do + project.add_maintainer(user) + create_list(:package_file, 99, package: package) + end + + it_behaves_like 'denying the mutation request', 'Cannot delete more than 100 files' + end + + context 'with files outside of the project' do + let_it_be(:package2) { create(:maven_package) } + + let(:ids) { super().push(package2.package_files.first.to_global_id.to_s) } + + before do + project.add_maintainer(user) + end + + it_behaves_like 'denying the mutation request', 'All files must be in the requested project' + end + end + + context 'with invalid params' do + let(:params) { { id: 'foo' } } + + before do + project.add_maintainer(user) + end + + it_behaves_like 'denying the mutation request', 'invalid value for id' + end + end +end diff --git a/spec/requests/api/graphql/mutations/releases/create_spec.rb b/spec/requests/api/graphql/mutations/releases/create_spec.rb index 86995c10f10..1e62942c29d 100644 --- a/spec/requests/api/graphql/mutations/releases/create_spec.rb +++ b/spec/requests/api/graphql/mutations/releases/create_spec.rb @@ -17,6 +17,7 @@ RSpec.describe 'Creation of a new release' do let(:mutation_name) { :release_create } let(:tag_name) { 'v7.12.5'} + let(:tag_message) { nil } let(:ref) { 'master'} let(:name) { 'Version 7.12.5'} let(:description) { 'Release 7.12.5 :rocket:' } @@ -29,6 +30,7 @@ RSpec.describe 'Creation of a new release' do { projectPath: project.full_path, tagName: tag_name, + tagMessage: tag_message, ref: ref, name: name, description: description, @@ -191,10 +193,26 @@ RSpec.describe 'Creation of a new release' do context 'when the provided tag does not already exist' do let(:tag_name) { 'v7.12.5-alpha' } + after do + project.repository.rm_tag(developer, tag_name) + end + it_behaves_like 'no errors' - it 'creates a new tag' do + it 'creates a new lightweight tag' do expect { create_release }.to change { Project.find_by_id(project.id).repository.tag_count }.by(1) + expect(project.repository.find_tag(tag_name).message).to be_blank + end + + context 'and tag_message is provided' do + let(:tag_message) { 'Annotated tag message' } + + it_behaves_like 'no errors' + + it 'creates a new annotated tag with the message' do + expect { create_release }.to change { Project.find_by_id(project.id).repository.tag_count }.by(1) + expect(project.repository.find_tag(tag_name).message).to eq(tag_message) + end end end diff --git a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb index 85194e6eb20..e1c7fd9d60d 100644 --- a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb +++ b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb @@ -28,17 +28,6 @@ RSpec.describe Mutations::UserPreferences::Update do expect(current_user.user_preference.persisted?).to eq(true) expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s) end - - context 'when incident_escalations feature flag is disabled' do - let(:sort_value) { 'ESCALATION_STATUS_ASC' } - - before do - stub_feature_flags(incident_escalations: false) - end - - it_behaves_like 'a mutation that returns top-level errors', - errors: ['Feature flag `incident_escalations` must be enabled to use this sort order.'] - end end context 'when user has existing preference' do @@ -56,16 +45,5 @@ RSpec.describe Mutations::UserPreferences::Update do expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s) end - - context 'when incident_escalations feature flag is disabled' do - let(:sort_value) { 'ESCALATION_STATUS_DESC' } - - before do - stub_feature_flags(incident_escalations: false) - end - - it_behaves_like 'a mutation that returns top-level errors', - errors: ['Feature flag `incident_escalations` must be enabled to use this sort order.'] - end end end diff --git a/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb new file mode 100644 index 00000000000..32468a46ace --- /dev/null +++ b/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Update a work item task' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } + let_it_be(:unauthorized_work_item) { create(:work_item) } + let_it_be(:referenced_work_item, refind: true) { create(:work_item, project: project, title: 'REFERENCED') } + let_it_be(:parent_work_item) do + create(:work_item, project: project, description: "- [ ] #{referenced_work_item.to_reference}+") + end + + let(:task) { referenced_work_item } + let(:work_item) { parent_work_item } + let(:task_params) { { 'title' => 'UPDATED' } } + let(:task_input) { { 'id' => task.to_global_id.to_s }.merge(task_params) } + let(:input) { { 'id' => work_item.to_global_id.to_s, 'taskData' => task_input } } + let(:mutation) { graphql_mutation(:workItemUpdateTask, input) } + let(:mutation_response) { graphql_mutation_response(:work_item_update_task) } + + context 'the user is not allowed to read a work item' do + let(:current_user) { create(:user) } + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user has permissions to update a work item' do + let(:current_user) { developer } + + it 'updates the work item and invalidates markdown cache on the original work item' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + referenced_work_item.reload + end.to change(referenced_work_item, :title).from(referenced_work_item.title).to('UPDATED') + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response).to include( + 'workItem' => hash_including( + 'title' => work_item.title, + 'descriptionHtml' => a_string_including('UPDATED') + ), + 'task' => hash_including( + 'title' => 'UPDATED' + ) + ) + end + + context 'when providing invalid task params' do + let(:task_params) { { 'title' => '' } } + + it 'makes no changes to the DB and returns an error message' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + task.reload + end.to not_change(task, :title).and( + not_change(work_item, :description_html) + ) + + expect(mutation_response['errors']).to contain_exactly("Title can't be blank") + end + end + + context 'when user cannot update the task' do + let(:task) { unauthorized_work_item } + + it_behaves_like 'a mutation that returns a top-level access error' + end + + it_behaves_like 'has spam protection' do + let(:mutation_class) { ::Mutations::WorkItems::UpdateTask } + end + + context 'when the work_items feature flag is disabled' do + before do + stub_feature_flags(work_items: false) + end + + it 'does not update the task item and returns and error' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + task.reload + end.to not_change(task, :title) + + expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project') + end + end + end + + context 'when user does not have permissions to update a work item' do + let(:current_user) { developer } + let(:work_item) { unauthorized_work_item } + + it_behaves_like 'a mutation that returns a top-level access error' + end +end diff --git a/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb new file mode 100644 index 00000000000..595d8fe97ed --- /dev/null +++ b/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Update work item widgets' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } + let_it_be(:work_item, refind: true) { create(:work_item, project: project) } + + let(:input) do + { + 'descriptionWidget' => { 'description' => 'updated description' } + } + end + + let(:mutation) { graphql_mutation(:workItemUpdateWidgets, input.merge('id' => work_item.to_global_id.to_s)) } + + let(:mutation_response) { graphql_mutation_response(:work_item_update_widgets) } + + context 'the user is not allowed to update a work item' do + let(:current_user) { create(:user) } + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user has permissions to update a work item', :aggregate_failures do + let(:current_user) { developer } + + context 'when the updated work item is not valid' do + it 'returns validation errors without the work item' do + errors = ActiveModel::Errors.new(work_item).tap { |e| e.add(:description, 'error message') } + + allow_next_found_instance_of(::WorkItem) do |instance| + allow(instance).to receive(:valid?).and_return(false) + allow(instance).to receive(:errors).and_return(errors) + end + + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['workItem']).to be_nil + expect(mutation_response['errors']).to match_array(['Description error message']) + end + end + + it 'updates the work item widgets' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :description).from(nil).to('updated description') + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']).to include( + 'title' => work_item.title + ) + end + + it_behaves_like 'has spam protection' do + let(:mutation_class) { ::Mutations::WorkItems::UpdateWidgets } + end + + context 'when the work_items feature flag is disabled' do + before do + stub_feature_flags(work_items: false) + end + + it 'does not update the work item and returns and error' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to not_change(work_item, :title) + + expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project') + end + end + end +end diff --git a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb index 708fa96986c..31fef75f679 100644 --- a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb +++ b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb @@ -94,7 +94,7 @@ RSpec.describe 'getting incident timeline events' do 'id' => promoted_from_note.to_global_id.to_s, 'body' => promoted_from_note.note }, - 'editable' => false, + 'editable' => true, 'action' => timeline_event.action, 'occurredAt' => timeline_event.occurred_at.iso8601, 'createdAt' => timeline_event.created_at.iso8601, diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index f358ec3e53f..69e14eace66 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -635,6 +635,18 @@ RSpec.describe 'getting an issue list for a project' do include_examples 'N+1 query check' end + context 'when requesting `closed_as_duplicate_of`' do + let(:requested_fields) { 'closedAsDuplicateOf { id }' } + let(:issue_a_dup) { create(:issue, project: project) } + let(:issue_b_dup) { create(:issue, project: project) } + + before do + issue_a.update!(duplicated_to_id: issue_a_dup) + issue_b.update!(duplicated_to_id: issue_a_dup) + end + + include_examples 'N+1 query check' + end end def issues_ids diff --git a/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb b/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb index 820a5d818c7..4dc272b5c2e 100644 --- a/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb +++ b/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb @@ -7,6 +7,7 @@ RSpec.describe 'Query.project.mergeRequests.pipelines' do let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:author) { create(:user) } + let_it_be(:mr_nodes_path) { [:data, :project, :merge_requests, :nodes] } let_it_be(:merge_requests) do [ create(:merge_request, author: author, source_project: project), @@ -33,8 +34,49 @@ RSpec.describe 'Query.project.mergeRequests.pipelines' do GQL end - def run_query(first = nil) - post_graphql(query, current_user: author, variables: { path: project.full_path, first: first }) + before do + merge_requests.first(2).each do |mr| + shas = mr.recent_diff_head_shas + + shas.each do |sha| + create(:ci_pipeline, :success, project: project, ref: mr.source_branch, sha: sha) + end + end + end + + it 'produces correct results' do + r = run_query(3) + + nodes = graphql_dig_at(r, *mr_nodes_path) + + expect(nodes).to all(match('iid' => be_present, 'pipelines' => include('count' => be_a(Integer)))) + expect(graphql_dig_at(r, *mr_nodes_path, :pipelines, :count)).to contain_exactly(1, 1, 0) + end + + it 'is scalable', :request_store, :use_clean_rails_memory_store_caching do + baseline = ActiveRecord::QueryRecorder.new { run_query(1) } + + expect { run_query(2) }.not_to exceed_query_limit(baseline) + end + end + + describe '.nodes' do + let(:query) do + <<~GQL + query($path: ID!, $first: Int) { + project(fullPath: $path) { + mergeRequests(first: $first) { + nodes { + iid + pipelines { + count + nodes { id } + } + } + } + } + } + GQL end before do @@ -48,18 +90,27 @@ RSpec.describe 'Query.project.mergeRequests.pipelines' do end it 'produces correct results' do - run_query(2) - - p_nodes = graphql_data_at(:project, :merge_requests, :nodes) + r = run_query - expect(p_nodes).to all(match('iid' => be_present, 'pipelines' => match('count' => 1))) + expect(graphql_dig_at(r, *mr_nodes_path, :pipelines, :nodes, :id).uniq.size).to eq 3 end it 'is scalable', :request_store, :use_clean_rails_memory_store_caching do - # warm up - run_query + baseline = ActiveRecord::QueryRecorder.new { run_query(1) } - expect { run_query(2) }.to(issue_same_number_of_queries_as { run_query(1) }.ignoring_cached_queries) + expect { run_query(2) }.not_to exceed_query_limit(baseline) end + + it 'requests merge_request_diffs at most once' do + r = ActiveRecord::QueryRecorder.new { run_query(2) } + + expect(r.log.grep(/merge_request_diffs/)).to contain_exactly(a_string_including('SELECT')) + end + end + + def run_query(first = nil) + run_with_clean_state(query, + context: { current_user: author }, + variables: { path: project.full_path, first: first }) end end diff --git a/spec/requests/api/graphql/project/milestones_spec.rb b/spec/requests/api/graphql/project/milestones_spec.rb index 3e8948d83b1..d1ee157fc74 100644 --- a/spec/requests/api/graphql/project/milestones_spec.rb +++ b/spec/requests/api/graphql/project/milestones_spec.rb @@ -59,6 +59,27 @@ RSpec.describe 'getting milestone listings nested in a project' do end end + context 'the user does not have access' do + let_it_be(:project) { create(:project) } + let_it_be(:milestones) { create_list(:milestone, 2, project: project) } + + it 'is nil' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(:project)).to be_nil + end + + context 'the user has access' do + let(:expected) { milestones } + + before do + project.add_guest(current_user) + end + + it_behaves_like 'searching with parameters' + end + end + context 'there are no search params' do let(:search_params) { nil } let(:expected) { all_milestones } diff --git a/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb b/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb new file mode 100644 index 00000000000..a025c57d4b8 --- /dev/null +++ b/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'getting the packages cleanup policy linked to a project' do + using RSpec::Parameterized::TableSyntax + include GraphqlHelpers + + let_it_be_with_reload(:project) { create(:project) } + let_it_be(:current_user) { project.first_owner } + + let(:fields) do + <<~QUERY + #{all_graphql_fields_for('packages_cleanup_policy'.classify)} + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('packagesCleanupPolicy', {}, fields) + ) + end + + subject { post_graphql(query, current_user: current_user) } + + it_behaves_like 'a working graphql query' do + before do + subject + end + end + + context 'with an existing policy' do + let_it_be(:policy) { create(:packages_cleanup_policy, project: project) } + + it_behaves_like 'a working graphql query' do + before do + subject + end + end + end + + context 'with different permissions' do + let_it_be(:current_user) { create(:user) } + + let(:packages_cleanup_policy_response) { graphql_data_at('project', 'packagesCleanupPolicy') } + + where(:visibility, :role, :policy_visible) do + :private | :maintainer | true + :private | :developer | false + :private | :reporter | false + :private | :guest | false + :private | :anonymous | false + :public | :maintainer | true + :public | :developer | false + :public | :reporter | false + :public | :guest | false + :public | :anonymous | false + end + + with_them do + before do + project.update!(visibility: visibility.to_s) + project.add_user(current_user, role) unless role == :anonymous + end + + it 'return the proper response' do + subject + + if policy_visible + expect(packages_cleanup_policy_response) + .to eq('keepNDuplicatedPackageFiles' => 'ALL_PACKAGE_FILES', 'nextRunAt' => nil) + else + expect(packages_cleanup_policy_response).to be_blank + end + end + end + end +end diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb new file mode 100644 index 00000000000..66742fcbeb6 --- /dev/null +++ b/spec/requests/api/graphql/project/work_items_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting an work item list for a project' do + include GraphqlHelpers + + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :repository, :public, group: group) } + let_it_be(:current_user) { create(:user) } + + let_it_be(:item1) { create(:work_item, project: project, discussion_locked: true, title: 'item1') } + let_it_be(:item2) { create(:work_item, project: project, title: 'item2') } + let_it_be(:confidential_item) { create(:work_item, confidential: true, project: project, title: 'item3') } + let_it_be(:other_item) { create(:work_item) } + + let(:items_data) { graphql_data['project']['workItems']['edges'] } + let(:item_filter_params) { {} } + + let(:fields) do + <<~QUERY + edges { + node { + #{all_graphql_fields_for('workItems'.classify)} + } + } + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('workItems', item_filter_params, fields) + ) + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + + context 'when the user does not have access to the item' do + before do + project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) + end + + it 'returns an empty list' do + post_graphql(query) + + expect(items_data).to eq([]) + end + end + + context 'when work_items flag is disabled' do + before do + stub_feature_flags(work_items: false) + end + + it 'returns an empty list' do + post_graphql(query) + + expect(items_data).to eq([]) + end + end + + it 'returns only items visible to user' do + post_graphql(query, current_user: current_user) + + expect(item_ids).to eq([item2.to_global_id.to_s, item1.to_global_id.to_s]) + end + + context 'when the user can see confidential items' do + before do + project.add_developer(current_user) + end + + it 'returns also confidential items' do + post_graphql(query, current_user: current_user) + + expect(item_ids).to eq([confidential_item.to_global_id.to_s, item2.to_global_id.to_s, item1.to_global_id.to_s]) + end + end + + describe 'sorting and pagination' do + let(:data_path) { [:project, :work_items] } + + def pagination_query(params) + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('workItems', params, "#{page_info} nodes { id }") + ) + end + + before do + project.add_developer(current_user) + end + + context 'when sorting by title ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :TITLE_ASC } + let(:first_param) { 2 } + let(:all_records) { [item1, item2, confidential_item].map { |item| item.to_global_id.to_s } } + end + end + + context 'when sorting by title descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :TITLE_DESC } + let(:first_param) { 2 } + let(:all_records) { [confidential_item, item2, item1].map { |item| item.to_global_id.to_s } } + end + end + end + + def item_ids + graphql_dig_at(items_data, :node, :id) + end +end diff --git a/spec/requests/api/graphql/terraform/state/delete_spec.rb b/spec/requests/api/graphql/terraform/state/delete_spec.rb index 35927d03b49..ba0619ea611 100644 --- a/spec/requests/api/graphql/terraform/state/delete_spec.rb +++ b/spec/requests/api/graphql/terraform/state/delete_spec.rb @@ -12,12 +12,12 @@ RSpec.describe 'delete a terraform state' do let(:mutation) { graphql_mutation(:terraform_state_delete, id: state.to_global_id.to_s) } before do + expect_next_instance_of(Terraform::States::TriggerDestroyService, state, current_user: user) do |service| + expect(service).to receive(:execute).once.and_return(ServiceResponse.success) + end + post_graphql_mutation(mutation, current_user: user) end include_examples 'a working graphql query' - - it 'deletes the state' do - expect { state.reload }.to raise_error(ActiveRecord::RecordNotFound) - end end diff --git a/spec/requests/api/graphql/user/starred_projects_query_spec.rb b/spec/requests/api/graphql/user/starred_projects_query_spec.rb index 37a85b98e5f..75a17ed34c4 100644 --- a/spec/requests/api/graphql/user/starred_projects_query_spec.rb +++ b/spec/requests/api/graphql/user/starred_projects_query_spec.rb @@ -17,7 +17,6 @@ RSpec.describe 'Getting starredProjects of the user' do let_it_be(:user, reload: true) { create(:user) } let(:user_fields) { 'starredProjects { nodes { id } }' } - let(:current_user) { nil } let(:starred_projects) do post_graphql(query, current_user: current_user) @@ -34,21 +33,23 @@ RSpec.describe 'Getting starredProjects of the user' do user.toggle_star(project_c) end - it_behaves_like 'a working graphql query' do - before do - post_graphql(query) - end - end + context 'anonymous access' do + let(:current_user) { nil } - it 'found only public project' do - expect(starred_projects).to contain_exactly( - a_graphql_entity_for(project_a) - ) + it 'returns nothing' do + expect(starred_projects).to be_nil + end end context 'the current user is the user' do let(:current_user) { user } + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + it 'found all projects' do expect(starred_projects).to contain_exactly( a_graphql_entity_for(project_a), diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index 5b34c21989a..09bda8ee0d5 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -6,8 +6,13 @@ RSpec.describe 'Query.work_item(id)' do include GraphqlHelpers let_it_be(:developer) { create(:user) } - let_it_be(:project) { create(:project, :private).tap { |project| project.add_developer(developer) } } - let_it_be(:work_item) { create(:work_item, project: project) } + let_it_be(:guest) { create(:user) } + let_it_be(:project) { create(:project, :private) } + let_it_be(:work_item) { create(:work_item, project: project, description: '- List item') } + let_it_be(:child_item1) { create(:work_item, :task, project: project) } + let_it_be(:child_item2) { create(:work_item, :task, confidential: true, project: project) } + let_it_be(:child_link1) { create(:parent_link, work_item_parent: work_item, work_item: child_item1) } + let_it_be(:child_link2) { create(:parent_link, work_item_parent: work_item, work_item: child_item2) } let(:current_user) { developer } let(:work_item_data) { graphql_data['workItem'] } @@ -20,6 +25,9 @@ RSpec.describe 'Query.work_item(id)' do context 'when the user can read the work item' do before do + project.add_developer(developer) + project.add_guest(guest) + post_graphql(query, current_user: current_user) end @@ -38,6 +46,136 @@ RSpec.describe 'Query.work_item(id)' do ) end + context 'when querying widgets' do + describe 'description widget' do + let(:work_item_fields) do + <<~GRAPHQL + id + widgets { + type + ... on WorkItemWidgetDescription { + description + descriptionHtml + } + } + GRAPHQL + end + + it 'returns widget information' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'widgets' => match_array([ + hash_including( + 'type' => 'DESCRIPTION', + 'description' => work_item.description, + 'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {}) + ), + hash_including( + 'type' => 'HIERARCHY' + ) + ]) + ) + end + end + + describe 'hierarchy widget' do + let(:work_item_fields) do + <<~GRAPHQL + id + widgets { + type + ... on WorkItemWidgetHierarchy { + parent { + id + } + children { + nodes { + id + } + } + } + } + GRAPHQL + end + + it 'returns widget information' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'widgets' => match_array([ + hash_including( + 'type' => 'DESCRIPTION' + ), + hash_including( + 'type' => 'HIERARCHY', + 'parent' => nil, + 'children' => { 'nodes' => match_array([ + hash_including('id' => child_link1.work_item.to_gid.to_s), + hash_including('id' => child_link2.work_item.to_gid.to_s) + ]) } + ) + ]) + ) + end + + it 'avoids N+1 queries' do + post_graphql(query, current_user: current_user) # warm up + + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: current_user) + end + + create_list(:parent_link, 3, work_item_parent: work_item) + + expect do + post_graphql(query, current_user: current_user) + end.not_to exceed_all_query_limit(control_count) + end + + context 'when user is guest' do + let(:current_user) { guest } + + it 'filters out not accessible children or parent' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'widgets' => match_array([ + hash_including( + 'type' => 'DESCRIPTION' + ), + hash_including( + 'type' => 'HIERARCHY', + 'parent' => nil, + 'children' => { 'nodes' => match_array([ + hash_including('id' => child_link1.work_item.to_gid.to_s) + ]) } + ) + ]) + ) + end + end + + context 'when requesting child item' do + let_it_be(:work_item) { create(:work_item, :task, project: project, description: '- List item') } + let_it_be(:parent_link) { create(:parent_link, work_item: work_item) } + + it 'returns parent information' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'widgets' => match_array([ + hash_including( + 'type' => 'DESCRIPTION' + ), + hash_including( + 'type' => 'HIERARCHY', + 'parent' => hash_including('id' => parent_link.work_item_parent.to_gid.to_s), + 'children' => { 'nodes' => match_array([]) } + ) + ]) + ) + end + end + end + end + context 'when an Issue Global ID is provided' do let(:global_id) { Issue.find(work_item.id).to_gid.to_s } |