Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/requests/api/graphql')
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb77
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb143
-rw-r--r--spec/requests/api/graphql/ci/runners_spec.rb49
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb2
-rw-r--r--spec/requests/api/graphql/issue/issue_spec.rb23
-rw-r--r--spec/requests/api/graphql/milestone_spec.rb124
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb17
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/design_management/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_escalation_status_spec.rb8
-rw-r--r--spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/jira_import/start_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb10
-rw-r--r--spec/requests/api/graphql/mutations/packages/cleanup/policy/update_spec.rb109
-rw-r--r--spec/requests/api/graphql/mutations/packages/destroy_files_spec.rb103
-rw-r--r--spec/requests/api/graphql/mutations/releases/create_spec.rb20
-rw-r--r--spec/requests/api/graphql/mutations/user_preferences/update_spec.rb22
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_task_spec.rb101
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb78
-rw-r--r--spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb12
-rw-r--r--spec/requests/api/graphql/project/merge_request/pipelines_spec.rb69
-rw-r--r--spec/requests/api/graphql/project/milestones_spec.rb21
-rw-r--r--spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb79
-rw-r--r--spec/requests/api/graphql/project/work_items_spec.rb121
-rw-r--r--spec/requests/api/graphql/terraform/state/delete_spec.rb8
-rw-r--r--spec/requests/api/graphql/user/starred_projects_query_spec.rb21
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb142
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 }