summaryrefslogtreecommitdiff
path: root/spec/requests/api/graphql
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-19 08:27:35 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-19 08:27:35 +0000
commit7e9c479f7de77702622631cff2628a9c8dcbc627 (patch)
treec8f718a08e110ad7e1894510980d2155a6549197 /spec/requests/api/graphql
parente852b0ae16db4052c1c567d9efa4facc81146e88 (diff)
downloadgitlab-ce-7e9c479f7de77702622631cff2628a9c8dcbc627.tar.gz
Add latest changes from gitlab-org/gitlab@13-6-stable-eev13.6.0-rc42
Diffstat (limited to 'spec/requests/api/graphql')
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb28
-rw-r--r--spec/requests/api/graphql/ci/pipelines_spec.rb221
-rw-r--r--spec/requests/api/graphql/container_repository/container_repository_details_spec.rb108
-rw-r--r--spec/requests/api/graphql/custom_emoji_query_spec.rb37
-rw-r--r--spec/requests/api/graphql/group/container_repositories_spec.rb146
-rw-r--r--spec/requests/api/graphql/instance_statistics_measurements_spec.rb13
-rw-r--r--spec/requests/api/graphql/issue/issue_spec.rb24
-rw-r--r--spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb1
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb67
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb55
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb45
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb49
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb67
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb59
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb47
-rw-r--r--spec/requests/api/graphql/mutations/commits/create_spec.rb38
-rw-r--r--spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb25
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb91
-rw-r--r--spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb43
-rw-r--r--spec/requests/api/graphql/mutations/labels/create_spec.rb86
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb49
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb81
-rw-r--r--spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb8
-rw-r--r--spec/requests/api/graphql/mutations/releases/create_spec.rb375
-rw-r--r--spec/requests/api/graphql/mutations/snippets/destroy_spec.rb5
-rw-r--r--spec/requests/api/graphql/mutations/todos/create_spec.rb38
-rw-r--r--spec/requests/api/graphql/mutations/todos/restore_many_spec.rb70
-rw-r--r--spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/alert_management/integrations_spec.rb83
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb145
-rw-r--r--spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb12
-rw-r--r--spec/requests/api/graphql/project/grafana_integration_spec.rb1
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/version_spec.rb1
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb3
-rw-r--r--spec/requests/api/graphql/project/issue/designs/notes_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb45
-rw-r--r--spec/requests/api/graphql/project/jira_import_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb11
-rw-r--r--spec/requests/api/graphql/project/project_statistics_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/release_spec.rb22
-rw-r--r--spec/requests/api/graphql/project/releases_spec.rb92
-rw-r--r--spec/requests/api/graphql/project/terraform/states_spec.rb81
-rw-r--r--spec/requests/api/graphql/read_only_spec.rb50
-rw-r--r--spec/requests/api/graphql/terraform/state/delete_spec.rb23
-rw-r--r--spec/requests/api/graphql/terraform/state/lock_spec.rb25
-rw-r--r--spec/requests/api/graphql/terraform/state/unlock_spec.rb24
-rw-r--r--spec/requests/api/graphql/user/group_member_query_spec.rb1
-rw-r--r--spec/requests/api/graphql/user/project_member_query_spec.rb1
-rw-r--r--spec/requests/api/graphql/user_query_spec.rb14
50 files changed, 2411 insertions, 123 deletions
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index 7d416f4720b..618705e5f94 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -34,6 +34,9 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
jobs {
nodes {
name
+ pipeline {
+ id
+ }
}
}
}
@@ -53,7 +56,7 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
end
context 'when fetching jobs from the pipeline' do
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :aggregate_failures do
control_count = ActiveRecord::QueryRecorder.new do
post_graphql(query, current_user: user)
end
@@ -86,8 +89,27 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
docker_jobs = docker_group.dig('jobs', 'nodes')
rspec_jobs = rspec_group.dig('jobs', 'nodes')
- expect(docker_jobs).to eq([{ 'name' => 'docker 1 2' }, { 'name' => 'docker 2 2' }])
- expect(rspec_jobs).to eq([{ 'name' => 'rspec 1 2' }, { 'name' => 'rspec 2 2' }])
+ expect(docker_jobs).to eq([
+ {
+ 'name' => 'docker 1 2',
+ 'pipeline' => { 'id' => pipeline.to_global_id.to_s }
+ },
+ {
+ 'name' => 'docker 2 2',
+ 'pipeline' => { 'id' => pipeline.to_global_id.to_s }
+ }
+ ])
+
+ expect(rspec_jobs).to eq([
+ {
+ 'name' => 'rspec 1 2',
+ 'pipeline' => { 'id' => pipeline.to_global_id.to_s }
+ },
+ {
+ 'name' => 'rspec 2 2',
+ 'pipeline' => { 'id' => pipeline.to_global_id.to_s }
+ }
+ ])
end
end
end
diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb
new file mode 100644
index 00000000000..414ddabbac9
--- /dev/null
+++ b/spec/requests/api/graphql/ci/pipelines_spec.rb
@@ -0,0 +1,221 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project(fullPath).pipelines' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:first_user) { create(:user) }
+ let_it_be(:second_user) { create(:user) }
+
+ describe '.jobs' do
+ let_it_be(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ jobs {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'fetches the jobs without an N+1' do
+ pipeline = create(:ci_pipeline, project: project)
+ create(:ci_build, pipeline: pipeline, name: 'Job 1')
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: first_user)
+ end
+
+ pipeline = create(:ci_pipeline, project: project)
+ create(:ci_build, pipeline: pipeline, name: 'Job 2')
+
+ expect do
+ post_graphql(query, current_user: second_user)
+ end.not_to exceed_query_limit(control_count)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ pipelines_data = graphql_data.dig('project', 'pipelines', 'nodes')
+
+ job_names = pipelines_data.map do |pipeline_data|
+ jobs_data = pipeline_data.dig('jobs', 'nodes')
+ jobs_data.map { |job_data| job_data['name'] }
+ end.flatten
+
+ expect(job_names).to contain_exactly('Job 1', 'Job 2')
+ end
+ end
+
+ describe '.jobs(securityReportTypes)' do
+ let_it_be(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ jobs(securityReportTypes: [SAST]) {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'fetches the jobs matching the report type filter' do
+ pipeline = create(:ci_pipeline, project: project)
+ create(:ci_build, :dast, name: 'DAST Job 1', pipeline: pipeline)
+ create(:ci_build, :sast, name: 'SAST Job 1', pipeline: pipeline)
+
+ post_graphql(query, current_user: first_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ pipelines_data = graphql_data.dig('project', 'pipelines', 'nodes')
+
+ job_names = pipelines_data.map do |pipeline_data|
+ jobs_data = pipeline_data.dig('jobs', 'nodes')
+ jobs_data.map { |job_data| job_data['name'] }
+ end.flatten
+
+ expect(job_names).to contain_exactly('SAST Job 1')
+ end
+ end
+
+ describe 'upstream' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: first_user) }
+ let_it_be(:upstream_project) { create(:project, :repository, :public) }
+ let_it_be(:upstream_pipeline) { create(:ci_pipeline, project: upstream_project, user: first_user) }
+ let(:upstream_pipelines_graphql_data) { graphql_data.dig(*%w[project pipelines nodes]).first['upstream'] }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ upstream {
+ iid
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ create(:ci_sources_pipeline, source_pipeline: upstream_pipeline, pipeline: pipeline )
+
+ post_graphql(query, current_user: first_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns the upstream pipeline of a pipeline' do
+ expect(upstream_pipelines_graphql_data['iid'].to_i).to eq(upstream_pipeline.iid)
+ end
+
+ context 'when fetching the upstream pipeline from the pipeline' do
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: first_user)
+ end
+
+ pipeline_2 = create(:ci_pipeline, project: project, user: first_user)
+ upstream_pipeline_2 = create(:ci_pipeline, project: upstream_project, user: first_user)
+ create(:ci_sources_pipeline, source_pipeline: upstream_pipeline_2, pipeline: pipeline_2 )
+ pipeline_3 = create(:ci_pipeline, project: project, user: first_user)
+ upstream_pipeline_3 = create(:ci_pipeline, project: upstream_project, user: first_user)
+ create(:ci_sources_pipeline, source_pipeline: upstream_pipeline_3, pipeline: pipeline_3 )
+
+ expect do
+ post_graphql(query, current_user: second_user)
+ end.not_to exceed_query_limit(control_count)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ describe 'downstream' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: first_user) }
+ let(:pipeline_2) { create(:ci_pipeline, project: project, user: first_user) }
+
+ let_it_be(:downstream_project) { create(:project, :repository, :public) }
+ let_it_be(:downstream_pipeline_a) { create(:ci_pipeline, project: downstream_project, user: first_user) }
+ let_it_be(:downstream_pipeline_b) { create(:ci_pipeline, project: downstream_project, user: first_user) }
+
+ let(:pipelines_graphql_data) { graphql_data.dig(*%w[project pipelines nodes]) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ downstream {
+ nodes {
+ iid
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ create(:ci_sources_pipeline, source_pipeline: pipeline, pipeline: downstream_pipeline_a)
+ create(:ci_sources_pipeline, source_pipeline: pipeline_2, pipeline: downstream_pipeline_b)
+
+ post_graphql(query, current_user: first_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns the downstream pipelines of a pipeline' do
+ downstream_pipelines_graphql_data = pipelines_graphql_data.map { |pip| pip['downstream']['nodes'] }.flatten
+
+ expect(
+ downstream_pipelines_graphql_data.map { |pip| pip['iid'].to_i }
+ ).to contain_exactly(downstream_pipeline_a.iid, downstream_pipeline_b.iid)
+ end
+
+ context 'when fetching the downstream pipelines from the pipeline' do
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: first_user)
+ end
+
+ downstream_pipeline_2a = create(:ci_pipeline, project: downstream_project, user: first_user)
+ create(:ci_sources_pipeline, source_pipeline: pipeline, pipeline: downstream_pipeline_2a)
+ downsteam_pipeline_3a = create(:ci_pipeline, project: downstream_project, user: first_user)
+ create(:ci_sources_pipeline, source_pipeline: pipeline, pipeline: downsteam_pipeline_3a)
+
+ downstream_pipeline_2b = create(:ci_pipeline, project: downstream_project, user: first_user)
+ create(:ci_sources_pipeline, source_pipeline: pipeline_2, pipeline: downstream_pipeline_2b)
+ downsteam_pipeline_3b = create(:ci_pipeline, project: downstream_project, user: first_user)
+ create(:ci_sources_pipeline, source_pipeline: pipeline_2, pipeline: downsteam_pipeline_3b)
+
+ expect do
+ post_graphql(query, current_user: second_user)
+ end.not_to exceed_query_limit(control_count)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
new file mode 100644
index 00000000000..3c1c63c1670
--- /dev/null
+++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'container repository details' do
+ using RSpec::Parameterized::TableSyntax
+ include GraphqlHelpers
+
+ let_it_be_with_reload(:project) { create(:project) }
+ let_it_be(:container_repository) { create(:container_repository, project: project) }
+
+ let(:query) do
+ graphql_query_for(
+ 'containerRepository',
+ { id: container_repository_global_id },
+ all_graphql_fields_for('ContainerRepositoryDetails')
+ )
+ end
+
+ let(:user) { project.owner }
+ let(:variables) { {} }
+ let(:tags) { %w(latest tag1 tag2 tag3 tag4 tag5) }
+ let(:container_repository_global_id) { container_repository.to_global_id.to_s }
+ let(:container_repository_details_response) { graphql_data.dig('containerRepository') }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags(repository: container_repository.path, tags: tags, with_manifest: true)
+ end
+
+ subject { post_graphql(query, current_user: user, variables: variables) }
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ subject
+ end
+
+ it 'matches the JSON schema' do
+ expect(container_repository_details_response).to match_schema('graphql/container_repository_details')
+ end
+ end
+
+ context 'with different permissions' do
+ let_it_be(:user) { create(:user) }
+
+ let(:tags_response) { container_repository_details_response.dig('tags', 'nodes') }
+
+ where(:project_visibility, :role, :access_granted, :can_delete) do
+ :private | :maintainer | true | true
+ :private | :developer | true | true
+ :private | :reporter | true | false
+ :private | :guest | false | false
+ :private | :anonymous | false | false
+ :public | :maintainer | true | true
+ :public | :developer | true | true
+ :public | :reporter | true | false
+ :public | :guest | true | false
+ :public | :anonymous | true | false
+ end
+
+ with_them do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility.to_s.upcase, false))
+ project.add_user(user, role) unless role == :anonymous
+ end
+
+ it 'return the proper response' do
+ subject
+
+ if access_granted
+ expect(tags_response.size).to eq(tags.size)
+ expect(container_repository_details_response.dig('canDelete')).to eq(can_delete)
+ else
+ expect(container_repository_details_response).to eq(nil)
+ end
+ end
+ end
+ end
+
+ context 'limiting the number of tags' do
+ let(:limit) { 2 }
+ let(:tags_response) { container_repository_details_response.dig('tags', 'edges') }
+ let(:variables) do
+ { id: container_repository_global_id, n: limit }
+ end
+
+ let(:query) do
+ <<~GQL
+ query($id: ID!, $n: Int) {
+ containerRepository(id: $id) {
+ tags(first: $n) {
+ edges {
+ node {
+ #{all_graphql_fields_for('ContainerRepositoryTag')}
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'only returns n tags' do
+ subject
+
+ expect(tags_response.size).to eq(limit)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb
new file mode 100644
index 00000000000..d5a423d0eba
--- /dev/null
+++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting custom emoji within namespace' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:custom_emoji) { create(:custom_emoji, group: group) }
+
+ before do
+ stub_feature_flags(custom_emoji: true)
+ group.add_developer(current_user)
+ end
+
+ describe "Query CustomEmoji on Group" do
+ def custom_emoji_query(group)
+ graphql_query_for('group', 'fullPath' => group.full_path)
+ end
+
+ it 'returns emojis when authorised' do
+ post_graphql(custom_emoji_query(group), current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(graphql_data['group']['customEmoji']['nodes'].count). to eq(1)
+ expect(graphql_data['group']['customEmoji']['nodes'].first['name']). to eq(custom_emoji.name)
+ end
+
+ it 'returns nil when unauthorised' do
+ user = create(:user)
+ post_graphql(custom_emoji_query(group), current_user: user)
+
+ expect(graphql_data['group']).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/group/container_repositories_spec.rb b/spec/requests/api/graphql/group/container_repositories_spec.rb
new file mode 100644
index 00000000000..bcf689a5e8f
--- /dev/null
+++ b/spec/requests/api/graphql/group/container_repositories_spec.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'getting container repositories in a group' do
+ using RSpec::Parameterized::TableSyntax
+ include GraphqlHelpers
+
+ let_it_be(:owner) { create(:user) }
+ let_it_be_with_reload(:group) { create(:group) }
+ let_it_be_with_reload(:project) { create(:project, group: group) }
+ let_it_be(:container_repository) { create(:container_repository, project: project) }
+ let_it_be(:container_repositories_delete_scheduled) { create_list(:container_repository, 2, :status_delete_scheduled, project: project) }
+ let_it_be(:container_repositories_delete_failed) { create_list(:container_repository, 2, :status_delete_failed, project: project) }
+ let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten }
+ let_it_be(:container_expiration_policy) { project.container_expiration_policy }
+
+ let(:fields) do
+ <<~GQL
+ edges {
+ node {
+ #{all_graphql_fields_for('container_repositories'.classify)}
+ }
+ }
+ GQL
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'group',
+ { 'fullPath' => group.full_path },
+ query_graphql_field('container_repositories', {}, fields)
+ )
+ end
+
+ let(:user) { owner }
+ let(:variables) { {} }
+ let(:container_repositories_response) { graphql_data.dig('group', 'containerRepositories', 'edges') }
+
+ before do
+ group.add_owner(owner)
+ stub_container_registry_config(enabled: true)
+ container_repositories.each do |repository|
+ stub_container_registry_tags(repository: repository.path, tags: %w(tag1 tag2 tag3), with_manifest: false)
+ end
+ end
+
+ subject { post_graphql(query, current_user: user, variables: variables) }
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ subject
+ end
+ end
+
+ context 'with different permissions' do
+ let_it_be(:user) { create(:user) }
+
+ where(:group_visibility, :role, :access_granted, :can_delete) do
+ :private | :maintainer | true | true
+ :private | :developer | true | true
+ :private | :reporter | true | false
+ :private | :guest | false | false
+ :private | :anonymous | false | false
+ :public | :maintainer | true | true
+ :public | :developer | true | true
+ :public | :reporter | true | false
+ :public | :guest | false | false
+ :public | :anonymous | false | false
+ end
+
+ with_them do
+ before do
+ group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false))
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false))
+
+ group.add_user(user, role) unless role == :anonymous
+ end
+
+ it 'return the proper response' do
+ subject
+
+ if access_granted
+ expect(container_repositories_response.size).to eq(container_repositories.size)
+ container_repositories_response.each do |repository_response|
+ expect(repository_response.dig('node', 'canDelete')).to eq(can_delete)
+ end
+ else
+ expect(container_repositories_response).to eq(nil)
+ end
+ end
+ end
+ end
+
+ context 'limiting the number of repositories' do
+ let(:limit) { 1 }
+ let(:variables) do
+ { path: group.full_path, n: limit }
+ end
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $n: Int) {
+ group(fullPath: $path) {
+ containerRepositories(first: $n) { #{fields} }
+ }
+ }
+ GQL
+ end
+
+ it 'only returns N repositories' do
+ subject
+
+ expect(container_repositories_response.size).to eq(limit)
+ end
+ end
+
+ context 'filter by name' do
+ let_it_be(:container_repository) { create(:container_repository, name: 'fooBar', project: project) }
+
+ let(:name) { 'ooba' }
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $name: String) {
+ group(fullPath: $path) {
+ containerRepositories(name: $name) { #{fields} }
+ }
+ }
+ GQL
+ end
+
+ let(:variables) do
+ { path: group.full_path, name: name }
+ end
+
+ before do
+ stub_container_registry_tags(repository: container_repository.path, tags: %w(tag4 tag5 tag6), with_manifest: false)
+ end
+
+ it 'returns the searched container repository' do
+ subject
+
+ expect(container_repositories_response.size).to eq(1)
+ expect(container_repositories_response.first.dig('node', 'id')).to eq(container_repository.to_global_id.to_s)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/instance_statistics_measurements_spec.rb b/spec/requests/api/graphql/instance_statistics_measurements_spec.rb
index 5d7dbcf2e3c..eb73dc59253 100644
--- a/spec/requests/api/graphql/instance_statistics_measurements_spec.rb
+++ b/spec/requests/api/graphql/instance_statistics_measurements_spec.rb
@@ -9,7 +9,8 @@ RSpec.describe 'InstanceStatisticsMeasurements' do
let!(:instance_statistics_measurement_1) { create(:instance_statistics_measurement, :project_count, recorded_at: 20.days.ago, count: 5) }
let!(:instance_statistics_measurement_2) { create(:instance_statistics_measurement, :project_count, recorded_at: 10.days.ago, count: 10) }
- let(:query) { graphql_query_for(:instanceStatisticsMeasurements, 'identifier: PROJECTS', 'nodes { count identifier }') }
+ let(:arguments) { 'identifier: PROJECTS' }
+ let(:query) { graphql_query_for(:instanceStatisticsMeasurements, arguments, 'nodes { count identifier }') }
before do
post_graphql(query, current_user: current_user)
@@ -21,4 +22,14 @@ RSpec.describe 'InstanceStatisticsMeasurements' do
{ "count" => 5, 'identifier' => 'PROJECTS' }
])
end
+
+ context 'with recorded_at filters' do
+ let(:arguments) { %(identifier: PROJECTS, recordedAfter: "#{15.days.ago.to_date}", recordedBefore: "#{5.days.ago.to_date}") }
+
+ it 'returns filtered measurement objects' do
+ expect(graphql_data.dig('instanceStatisticsMeasurements', 'nodes')).to eq([
+ { "count" => 10, 'identifier' => 'PROJECTS' }
+ ])
+ end
+ end
end
diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb
index 1c9d6b25856..d7fa680d29b 100644
--- a/spec/requests/api/graphql/issue/issue_spec.rb
+++ b/spec/requests/api/graphql/issue/issue_spec.rb
@@ -71,14 +71,34 @@ RSpec.describe 'Query.issue(id)' do
end
context 'selecting multiple fields' do
- let(:issue_fields) { %w(title description) }
+ let(:issue_fields) { ['title', 'description', 'updatedBy { username }'] }
it 'returns the Issue with the specified fields' do
post_graphql(query, current_user: current_user)
- expect(issue_data.keys).to eq( %w(title description) )
+ expect(issue_data.keys).to eq( %w(title description updatedBy) )
expect(issue_data['title']).to eq(issue.title)
expect(issue_data['description']).to eq(issue.description)
+ expect(issue_data['updatedBy']['username']).to eq(issue.author.username)
+ end
+ end
+
+ context 'when issue got moved' do
+ let_it_be(:issue_fields) { ['moved', 'movedTo { title }'] }
+ let_it_be(:new_issue) { create(:issue) }
+ let_it_be(:issue) { create(:issue, project: project, moved_to: new_issue) }
+ let_it_be(:issue_params) { { 'id' => issue.to_global_id.to_s } }
+
+ before_all do
+ new_issue.project.add_developer(current_user)
+ end
+
+ it 'returns correct attributes' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issue_data.keys).to eq( %w(moved movedTo) )
+ expect(issue_data['moved']).to eq(true)
+ expect(issue_data['movedTo']['title']).to eq(new_issue.title)
end
end
diff --git a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
index ca5a9165760..72ec2b8e070 100644
--- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
+++ b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe 'Getting Metrics Dashboard Annotations' do
let_it_be(:to_old_annotation) do
create(:metrics_dashboard_annotation, environment: environment, starting_at: Time.parse(from).advance(minutes: -5), dashboard_path: path)
end
+
let_it_be(:to_new_annotation) do
create(:metrics_dashboard_annotation, environment: environment, starting_at: to.advance(minutes: 5), dashboard_path: path)
end
diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb
new file mode 100644
index 00000000000..a285cebc805
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Creating a new HTTP Integration' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let(:variables) do
+ {
+ project_path: project.full_path,
+ active: false,
+ name: 'New HTTP Integration'
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(:http_integration_create, variables) do
+ <<~QL
+ clientMutationId
+ errors
+ integration {
+ id
+ type
+ name
+ active
+ token
+ url
+ apiUrl
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:http_integration_create) }
+
+ before do
+ project.add_maintainer(current_user)
+ end
+
+ it 'creates a new integration' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ new_integration = ::AlertManagement::HttpIntegration.last!
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(new_integration).to_s)
+ expect(integration_response['type']).to eq('HTTP')
+ expect(integration_response['name']).to eq(new_integration.name)
+ expect(integration_response['active']).to eq(new_integration.active)
+ expect(integration_response['token']).to eq(new_integration.token)
+ expect(integration_response['url']).to eq(new_integration.url)
+ expect(integration_response['apiUrl']).to eq(nil)
+ end
+
+ [:project_path, :active, :name].each do |argument|
+ context "without required argument #{argument}" do
+ before do
+ variables.delete(argument)
+ end
+
+ it_behaves_like 'an invalid argument to the mutation', argument_name: argument
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb
new file mode 100644
index 00000000000..1ecb5c76b57
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Removing an HTTP Integration' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:integration) { create(:alert_management_http_integration, project: project) }
+
+ let(:mutation) do
+ variables = {
+ id: GitlabSchema.id_from_object(integration).to_s
+ }
+ graphql_mutation(:http_integration_destroy, variables) do
+ <<~QL
+ clientMutationId
+ errors
+ integration {
+ id
+ type
+ name
+ active
+ token
+ url
+ apiUrl
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:http_integration_destroy) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'removes the integration' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s)
+ expect(integration_response['type']).to eq('HTTP')
+ expect(integration_response['name']).to eq(integration.name)
+ expect(integration_response['active']).to eq(integration.active)
+ expect(integration_response['token']).to eq(integration.token)
+ expect(integration_response['url']).to eq(integration.url)
+ expect(integration_response['apiUrl']).to eq(nil)
+
+ expect { integration.reload }.to raise_error ActiveRecord::RecordNotFound
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb
new file mode 100644
index 00000000000..badd9412589
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Resetting a token on an existing HTTP Integration' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:integration) { create(:alert_management_http_integration, project: project) }
+
+ let(:mutation) do
+ variables = {
+ id: GitlabSchema.id_from_object(integration).to_s
+ }
+ graphql_mutation(:http_integration_reset_token, variables) do
+ <<~QL
+ clientMutationId
+ errors
+ integration {
+ id
+ token
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:http_integration_reset_token) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'updates the integration' do
+ previous_token = integration.token
+
+ post_graphql_mutation(mutation, current_user: user)
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s)
+ expect(integration_response['token']).not_to eq(previous_token)
+ expect(integration_response['token']).to eq(integration.reload.token)
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb
new file mode 100644
index 00000000000..bf7eb3d980c
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Updating an existing HTTP Integration' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:integration) { create(:alert_management_http_integration, project: project) }
+
+ let(:mutation) do
+ variables = {
+ id: GitlabSchema.id_from_object(integration).to_s,
+ name: 'Modified Name',
+ active: false
+ }
+ graphql_mutation(:http_integration_update, variables) do
+ <<~QL
+ clientMutationId
+ errors
+ integration {
+ id
+ name
+ active
+ url
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:http_integration_update) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'updates the integration' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s)
+ expect(integration_response['name']).to eq('Modified Name')
+ expect(integration_response['active']).to be_falsey
+ expect(integration_response['url']).to include('modified-name')
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb
new file mode 100644
index 00000000000..0ef61ae0d5b
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Creating a new Prometheus Integration' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let(:variables) do
+ {
+ project_path: project.full_path,
+ active: false,
+ api_url: 'https://prometheus-url.com'
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(:prometheus_integration_create, variables) do
+ <<~QL
+ clientMutationId
+ errors
+ integration {
+ id
+ type
+ name
+ active
+ token
+ url
+ apiUrl
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:prometheus_integration_create) }
+
+ before do
+ project.add_maintainer(current_user)
+ end
+
+ it 'creates a new integration' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ new_integration = ::PrometheusService.last!
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(new_integration).to_s)
+ expect(integration_response['type']).to eq('PROMETHEUS')
+ expect(integration_response['name']).to eq(new_integration.title)
+ expect(integration_response['active']).to eq(new_integration.manual_configuration?)
+ expect(integration_response['token']).to eq(new_integration.project.alerting_setting.token)
+ expect(integration_response['url']).to eq("http://localhost/#{project.full_path}/prometheus/alerts/notify.json")
+ expect(integration_response['apiUrl']).to eq(new_integration.api_url)
+ end
+
+ [:project_path, :active, :api_url].each do |argument|
+ context "without required argument #{argument}" do
+ before do
+ variables.delete(argument)
+ end
+
+ it_behaves_like 'an invalid argument to the mutation', argument_name: argument
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb
new file mode 100644
index 00000000000..d8d0ace5981
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Resetting a token on an existing Prometheus Integration' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:integration) { create(:prometheus_service, project: project) }
+
+ let(:mutation) do
+ variables = {
+ id: GitlabSchema.id_from_object(integration).to_s
+ }
+ graphql_mutation(:prometheus_integration_reset_token, variables) do
+ <<~QL
+ clientMutationId
+ errors
+ integration {
+ id
+ token
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:prometheus_integration_reset_token) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'creates a token' do
+ post_graphql_mutation(mutation, current_user: user)
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s)
+ expect(integration_response['token']).not_to be_nil
+ expect(integration_response['token']).to eq(project.alerting_setting.token)
+ end
+
+ context 'with an existing alerting setting' do
+ let_it_be(:alerting_setting) { create(:project_alerting_setting, project: project) }
+
+ it 'updates the token' do
+ previous_token = alerting_setting.token
+
+ post_graphql_mutation(mutation, current_user: user)
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s)
+ expect(integration_response['token']).not_to eq(previous_token)
+ expect(integration_response['token']).to eq(alerting_setting.reload.token)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb
new file mode 100644
index 00000000000..6c4a647a353
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Updating an existing Prometheus Integration' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:integration) { create(:prometheus_service, project: project) }
+
+ let(:mutation) do
+ variables = {
+ id: GitlabSchema.id_from_object(integration).to_s,
+ api_url: 'http://modified-url.com',
+ active: true
+ }
+ graphql_mutation(:prometheus_integration_update, variables) do
+ <<~QL
+ clientMutationId
+ errors
+ integration {
+ id
+ active
+ apiUrl
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:prometheus_integration_update) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'updates the integration' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s)
+ expect(integration_response['apiUrl']).to eq('http://modified-url.com')
+ expect(integration_response['active']).to be_truthy
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/commits/create_spec.rb b/spec/requests/api/graphql/mutations/commits/create_spec.rb
index ac4fa7cfe83..375d4f10b40 100644
--- a/spec/requests/api/graphql/mutations/commits/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/commits/create_spec.rb
@@ -23,6 +23,18 @@ RSpec.describe 'Creation of a new commit' do
let(:mutation) { graphql_mutation(:commit_create, input) }
let(:mutation_response) { graphql_mutation_response(:commit_create) }
+ shared_examples 'a commit is successful' do
+ it 'creates a new commit' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+
+ expect(mutation_response['commit']).to include(
+ 'title' => message
+ )
+ end
+ end
+
context 'the user is not allowed to create a commit' do
it_behaves_like 'a mutation that returns a top-level access error'
end
@@ -32,14 +44,7 @@ RSpec.describe 'Creation of a new commit' do
project.add_developer(current_user)
end
- it 'creates a new commit' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['commit']).to include(
- 'title' => message
- )
- end
+ it_behaves_like 'a commit is successful'
context 'when branch is not correct' do
let(:branch) { 'unknown' }
@@ -47,5 +52,22 @@ RSpec.describe 'Creation of a new commit' do
it_behaves_like 'a mutation that returns errors in the response',
errors: ['You can only create or edit files when you are on a branch']
end
+
+ context 'when branch is new, and a start_branch is defined' do
+ let(:input) { { project_path: project.full_path, branch: branch, start_branch: start_branch, message: message, actions: actions } }
+ let(:branch) { 'new-branch' }
+ let(:start_branch) { 'master' }
+ let(:actions) do
+ [
+ {
+ action: 'CREATE',
+ filePath: 'ANOTHER_FILE.md',
+ content: 'Bye'
+ }
+ ]
+ end
+
+ it_behaves_like 'a commit is successful'
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb
index 7bef812bfec..23e8e366483 100644
--- a/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb
@@ -73,6 +73,29 @@ RSpec.describe 'Updating the container expiration policy' do
end
end
+ RSpec.shared_examples 'rejecting blank name_regex when enabled' do
+ context "for blank name_regex" do
+ let(:params) do
+ {
+ project_path: project.full_path,
+ name_regex: '',
+ enabled: true
+ }
+ end
+
+ it_behaves_like 'returning response status', :success
+
+ it_behaves_like 'not creating the container expiration policy'
+
+ it 'returns an error' do
+ subject
+
+ expect(graphql_data['updateContainerExpirationPolicy']['errors'].size).to eq(1)
+ expect(graphql_data['updateContainerExpirationPolicy']['errors']).to include("Name regex can't be blank")
+ end
+ end
+ end
+
RSpec.shared_examples 'accepting the mutation request updating the container expiration policy' do
it_behaves_like 'updating the container expiration policy attributes', mode: :update, from: { cadence: '1d', keep_n: 10, older_than: '90d' }, to: { cadence: '3month', keep_n: 100, older_than: '14d' }
@@ -80,6 +103,7 @@ RSpec.describe 'Updating the container expiration policy' do
it_behaves_like 'rejecting invalid regex for', :name_regex
it_behaves_like 'rejecting invalid regex for', :name_regex_keep
+ it_behaves_like 'rejecting blank name_regex when enabled'
end
RSpec.shared_examples 'accepting the mutation request creating the container expiration policy' do
@@ -89,6 +113,7 @@ RSpec.describe 'Updating the container expiration policy' do
it_behaves_like 'rejecting invalid regex for', :name_regex
it_behaves_like 'rejecting invalid regex for', :name_regex_keep
+ it_behaves_like 'rejecting blank name_regex when enabled'
end
RSpec.shared_examples 'denying the mutation request' do
diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
new file mode 100644
index 00000000000..645edfc2e43
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Destroying a container repository' do
+ using RSpec::Parameterized::TableSyntax
+
+ include GraphqlHelpers
+
+ let_it_be_with_reload(:container_repository) { create(:container_repository) }
+ let_it_be(:user) { create(:user) }
+
+ let(:project) { container_repository.project }
+ let(:id) { container_repository.to_global_id.to_s }
+
+ let(:query) do
+ <<~GQL
+ containerRepository {
+ #{all_graphql_fields_for('ContainerRepository')}
+ }
+ errors
+ GQL
+ end
+
+ let(:params) { { id: container_repository.to_global_id.to_s } }
+ let(:mutation) { graphql_mutation(:destroy_container_repository, params, query) }
+ let(:mutation_response) { graphql_mutation_response(:destroyContainerRepository) }
+ let(:container_repository_mutation_response) { mutation_response['containerRepository'] }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags(tags: %w[a b c])
+ end
+
+ shared_examples 'destroying the container repository' do
+ it 'destroy the container repository' do
+ expect(::Packages::CreateEventService)
+ .to receive(:new).with(nil, user, event_name: :delete_repository, scope: :container).and_call_original
+ expect(DeleteContainerRepositoryWorker)
+ .to receive(:perform_async).with(user.id, container_repository.id)
+
+ expect { subject }.to change { ::Packages::Event.count }.by(1)
+
+ expect(container_repository_mutation_response).to match_schema('graphql/container_repository')
+ expect(container_repository_mutation_response['status']).to eq('DELETE_SCHEDULED')
+ end
+
+ it_behaves_like 'returning response status', :success
+ end
+
+ shared_examples 'denying the mutation request' do
+ it 'does not destroy the container repository' do
+ expect(DeleteContainerRepositoryWorker)
+ .not_to receive(:perform_async).with(user.id, container_repository.id)
+
+ expect { subject }.not_to change { ::Packages::Event.count }
+
+ expect(mutation_response).to be_nil
+ end
+
+ it_behaves_like 'returning response status', :success
+ end
+
+ describe 'post graphql mutation' do
+ subject { post_graphql_mutation(mutation, current_user: user) }
+
+ context 'with valid id' do
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'destroying the container repository'
+ :developer | 'destroying the container repository'
+ :reporter | 'denying the mutation request'
+ :guest | 'denying the mutation request'
+ :anonymous | 'denying the mutation request'
+ end
+
+ with_them do
+ before do
+ project.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+
+ context 'with invalid id' do
+ let(:params) { { id: 'gid://gitlab/ContainerRepository/5555' } }
+
+ it_behaves_like 'denying the mutation request'
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
new file mode 100644
index 00000000000..c91437fa355
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Creation of a new Custom Emoji' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ let(:attributes) do
+ {
+ name: 'my_new_emoji',
+ url: 'https://example.com/image.png',
+ group_path: group.full_path
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(:create_custom_emoji, attributes)
+ end
+
+ context 'when the user has no permission' do
+ it 'does not create custom emoji' do
+ expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(CustomEmoji, :count)
+ end
+ end
+
+ context 'when user has permission' do
+ before do
+ group.add_developer(current_user)
+ end
+
+ it 'creates custom emoji' do
+ expect { post_graphql_mutation(mutation, current_user: current_user) }.to change(CustomEmoji, :count).by(1)
+
+ gql_response = graphql_mutation_response(:create_custom_emoji)
+ expect(gql_response['errors']).to eq([])
+ expect(gql_response['customEmoji']['name']).to eq(attributes[:name])
+ expect(gql_response['customEmoji']['url']).to eq(attributes[:url])
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/labels/create_spec.rb b/spec/requests/api/graphql/mutations/labels/create_spec.rb
new file mode 100644
index 00000000000..28284408306
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/labels/create_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Labels::Create do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+
+ let(:params) do
+ {
+ 'title' => 'foo',
+ 'description' => 'some description',
+ 'color' => '#FF0000'
+ }
+ end
+
+ let(:mutation) { graphql_mutation(:label_create, params.merge(extra_params)) }
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:label_create)
+ end
+
+ shared_examples_for 'labels create mutation' do
+ context 'when the user does not have permission to create a label' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not create the label' do
+ expect { subject }.not_to change { Label.count }
+ end
+ end
+
+ context 'when the user has permission to create a label' do
+ before do
+ parent.add_developer(current_user)
+ end
+
+ context 'when the parent (project_path or group_path) param is given' do
+ it 'creates the label' do
+ expect { subject }.to change { Label.count }.to(1)
+
+ expect(mutation_response).to include(
+ 'label' => a_hash_including(params))
+ end
+
+ it 'does not create a label when there are errors' do
+ label_factory = parent.is_a?(Group) ? :group_label : :label
+ create(label_factory, title: 'foo', parent.class.name.underscore.to_sym => parent)
+
+ expect { subject }.not_to change { Label.count }
+
+ expect(mutation_response).to have_key('label')
+ expect(mutation_response['label']).to be_nil
+ expect(mutation_response['errors'].first).to eq('Title has already been taken')
+ end
+ end
+ end
+ end
+
+ context 'when creating a project label' do
+ let_it_be(:parent) { create(:project) }
+ let(:extra_params) { { project_path: parent.full_path } }
+
+ it_behaves_like 'labels create mutation'
+ end
+
+ context 'when creating a group label' do
+ let_it_be(:parent) { create(:group) }
+ let(:extra_params) { { group_path: parent.full_path } }
+
+ it_behaves_like 'labels create mutation'
+ end
+
+ context 'when neither project_path nor group_path param is given' do
+ let(:mutation) { graphql_mutation(:label_create, params) }
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ['Exactly one of group_path or project_path arguments is required']
+
+ it 'does not create the label' do
+ expect { subject }.not_to change { Label.count }
+ end
+ end
+end
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 81d13b29dde..2a39757e103 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
@@ -101,9 +101,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do
graphql_mutation(:create_annotation, variables)
end
- it_behaves_like 'a mutation that returns top-level errors' do
- let(:match_errors) { include(/is not a valid Global ID/) }
- end
+ it_behaves_like 'an invalid argument to the mutation', argument_name: :environment_id
end
end
end
@@ -190,9 +188,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do
graphql_mutation(:create_annotation, variables)
end
- it_behaves_like 'a mutation that returns top-level errors' do
- let(:match_errors) { include(/is not a valid Global ID/) }
- end
+ it_behaves_like 'an invalid argument to the mutation', argument_name: :cluster_id
end
end
@@ -213,35 +209,26 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do
it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::ANNOTATION_SOURCE_ARGUMENT_ERROR]
end
- context 'when a non-cluster or environment id is provided' do
- let(:gid) { { environment_id: project.to_global_id.to_s } }
- let(:mutation) do
- variables = {
- starting_at: starting_at,
- ending_at: ending_at,
- dashboard_path: dashboard_path,
- description: description
- }.merge!(gid)
-
- graphql_mutation(:create_annotation, variables)
- end
-
- before do
- project.add_developer(current_user)
- end
+ [:environment_id, :cluster_id].each do |arg_name|
+ context "when #{arg_name} is given an ID of the wrong type" do
+ let(:gid) { global_id_of(project) }
+ let(:mutation) do
+ variables = {
+ starting_at: starting_at,
+ ending_at: ending_at,
+ dashboard_path: dashboard_path,
+ description: description,
+ arg_name => gid
+ }
- describe 'non-environment id' do
- it_behaves_like 'a mutation that returns top-level errors' do
- let(:match_errors) { include(/does not represent an instance of Environment/) }
+ graphql_mutation(:create_annotation, variables)
end
- end
-
- describe 'non-cluster id' do
- let(:gid) { { cluster_id: project.to_global_id.to_s } }
- it_behaves_like 'a mutation that returns top-level errors' do
- let(:match_errors) { include(/does not represent an instance of Clusters::Cluster/) }
+ before do
+ project.add_developer(current_user)
end
+
+ it_behaves_like 'an invalid argument to the mutation', argument_name: arg_name
end
end
end
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
index 9a612c841a2..b956734068c 100644
--- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do
let(:variables) { { id: GitlabSchema.id_from_object(project).to_s } }
it_behaves_like 'a mutation that returns top-level errors' do
- let(:match_errors) { eq(["#{variables[:id]} is not a valid ID for #{annotation.class}."]) }
+ let(:match_errors) { contain_exactly(include('invalid value for id')) }
end
end
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
new file mode 100644
index 00000000000..4efa7f9d509
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Repositioning an ImageDiffNote' do
+ include GraphqlHelpers
+
+ let_it_be(:noteable) { create(:merge_request) }
+ let_it_be(:project) { noteable.project }
+ let(:note) { create(:image_diff_note_on_merge_request, noteable: noteable, project: project) }
+ let(:new_position) { { x: 10 } }
+ let(:current_user) { project.creator }
+
+ let(:mutation_variables) do
+ {
+ id: global_id_of(note),
+ position: new_position
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(:reposition_image_diff_note, mutation_variables) do
+ <<~QL
+ note {
+ id
+ }
+ errors
+ QL
+ end
+ end
+
+ def mutation_response
+ graphql_mutation_response(:reposition_image_diff_note)
+ end
+
+ it 'updates the note', :aggregate_failures do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { note.reset.position.x }.to(10)
+
+ expect(mutation_response['note']).to eq('id' => global_id_of(note))
+ expect(mutation_response['errors']).to be_empty
+ end
+
+ context 'when the note is not a DiffNote' do
+ let(:note) { project }
+
+ it_behaves_like 'a mutation that returns top-level errors' do
+ let(:match_errors) { include(/does not represent an instance of DiffNote/) }
+ end
+ end
+
+ context 'when a position arg is nil' do
+ let(:new_position) { { x: nil, y: 10 } }
+
+ it 'does not set the property to nil', :aggregate_failures do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.not_to change { note.reset.position.x }
+
+ expect(mutation_response['note']).to eq('id' => global_id_of(note))
+ expect(mutation_response['errors']).to be_empty
+ end
+ end
+
+ context 'when all position args are nil' 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')
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb
index efa2ceb65c2..713b26a6a9b 100644
--- a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe 'Updating an image DiffNote' do
position_type: 'image'
)
end
+
let_it_be(:updated_body) { 'Updated body' }
let_it_be(:updated_width) { 50 }
let_it_be(:updated_height) { 100 }
@@ -31,7 +32,7 @@ RSpec.describe 'Updating an image DiffNote' do
height: updated_height,
x: updated_x,
y: updated_y
- }
+ }.compact.presence
end
let!(:diff_note) do
@@ -45,10 +46,11 @@ RSpec.describe 'Updating an image DiffNote' do
let(:mutation) do
variables = {
id: GitlabSchema.id_from_object(diff_note).to_s,
- body: updated_body,
- position: updated_position
+ body: updated_body
}
+ variables[:position] = updated_position if updated_position
+
graphql_mutation(:update_image_diff_note, variables)
end
diff --git a/spec/requests/api/graphql/mutations/releases/create_spec.rb b/spec/requests/api/graphql/mutations/releases/create_spec.rb
new file mode 100644
index 00000000000..d745eb3083d
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/releases/create_spec.rb
@@ -0,0 +1,375 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Creation of a new release' do
+ include GraphqlHelpers
+ include Presentable
+
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:milestone_12_3) { create(:milestone, project: project, title: '12.3') }
+ let_it_be(:milestone_12_4) { create(:milestone, project: project, title: '12.4') }
+ let_it_be(:public_user) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+
+ let(:mutation_name) { :release_create }
+
+ let(:tag_name) { 'v7.12.5'}
+ let(:ref) { 'master'}
+ let(:name) { 'Version 7.12.5'}
+ let(:description) { 'Release 7.12.5 :rocket:' }
+ let(:released_at) { '2018-12-10' }
+ let(:milestones) { [milestone_12_3.title, milestone_12_4.title] }
+ let(:asset_link) { { name: 'An asset link', url: 'https://gitlab.example.com/link', directAssetPath: '/permanent/link', linkType: 'OTHER' } }
+ let(:assets) { { links: [asset_link] } }
+
+ let(:mutation_arguments) do
+ {
+ projectPath: project.full_path,
+ tagName: tag_name,
+ ref: ref,
+ name: name,
+ description: description,
+ releasedAt: released_at,
+ milestones: milestones,
+ assets: assets
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(mutation_name, mutation_arguments, <<~FIELDS)
+ release {
+ tagName
+ name
+ description
+ releasedAt
+ createdAt
+ milestones {
+ nodes {
+ title
+ }
+ }
+ assets {
+ links {
+ nodes {
+ name
+ url
+ linkType
+ external
+ directAssetUrl
+ }
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ let(:create_release) { post_graphql_mutation(mutation, current_user: current_user) }
+ let(:mutation_response) { graphql_mutation_response(mutation_name)&.with_indifferent_access }
+
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ before do
+ project.add_guest(guest)
+ project.add_reporter(reporter)
+ project.add_developer(developer)
+
+ stub_default_url_options(host: 'www.example.com')
+ end
+
+ shared_examples 'no errors' do
+ it 'returns no errors' do
+ create_release
+
+ expect(graphql_errors).not_to be_present
+ end
+ end
+
+ shared_examples 'top-level error with message' do |error_message|
+ it 'returns a top-level error with message' do
+ create_release
+
+ expect(mutation_response).to be_nil
+ expect(graphql_errors.count).to eq(1)
+ expect(graphql_errors.first['message']).to eq(error_message)
+ end
+ end
+
+ shared_examples 'errors-as-data with message' do |error_message|
+ it 'returns an error-as-data with message' do
+ create_release
+
+ expect(mutation_response[:release]).to be_nil
+ expect(mutation_response[:errors].count).to eq(1)
+ expect(mutation_response[:errors].first).to match(error_message)
+ end
+ end
+
+ context 'when the current user has access to create releases' do
+ let(:current_user) { developer }
+
+ context 'when all available mutation arguments are provided' do
+ it_behaves_like 'no errors'
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ it 'returns the new release data' do
+ create_release
+
+ release = mutation_response[:release]
+ expected_direct_asset_url = Gitlab::Routing.url_helpers.project_release_url(project, Release.find_by(tag: tag_name)) << "/downloads#{asset_link[:directAssetPath]}"
+
+ expected_attributes = {
+ tagName: tag_name,
+ name: name,
+ description: description,
+ releasedAt: Time.parse(released_at).utc.iso8601,
+ createdAt: Time.current.utc.iso8601,
+ assets: {
+ links: {
+ nodes: [{
+ name: asset_link[:name],
+ url: asset_link[:url],
+ linkType: asset_link[:linkType],
+ external: true,
+ directAssetUrl: expected_direct_asset_url
+ }]
+ }
+ }
+ }
+
+ expect(release).to include(expected_attributes)
+
+ # Right now the milestones are returned in a non-deterministic order.
+ # This `milestones` test should be moved up into the expect(release)
+ # above (and `.to include` updated to `.to eq`) once
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/259012 is addressed.
+ expect(release['milestones']['nodes']).to match_array([
+ { 'title' => '12.4' },
+ { 'title' => '12.3' }
+ ])
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+
+ context 'when only the required mutation arguments are provided' do
+ let(:mutation_arguments) { super().slice(:projectPath, :tagName, :ref) }
+
+ it_behaves_like 'no errors'
+
+ it 'returns the new release data' do
+ create_release
+
+ expected_response = {
+ tagName: tag_name,
+ name: tag_name,
+ description: nil,
+ releasedAt: Time.current.utc.iso8601,
+ createdAt: Time.current.utc.iso8601,
+ milestones: {
+ nodes: []
+ },
+ assets: {
+ links: {
+ nodes: []
+ }
+ }
+ }.with_indifferent_access
+
+ expect(mutation_response[:release]).to eq(expected_response)
+ end
+ end
+
+ context 'when the provided tag already exists' do
+ let(:tag_name) { 'v1.1.0' }
+
+ it_behaves_like 'no errors'
+
+ it 'does not create a new tag' do
+ expect { create_release }.not_to change { Project.find_by_id(project.id).repository.tag_count }
+ end
+ end
+
+ context 'when the provided tag does not already exist' do
+ let(:tag_name) { 'v7.12.5-alpha' }
+
+ it_behaves_like 'no errors'
+
+ it 'creates a new tag' do
+ expect { create_release }.to change { Project.find_by_id(project.id).repository.tag_count }.by(1)
+ end
+ end
+
+ context 'when a local timezone is provided for releasedAt' do
+ let(:released_at) { Time.parse(super()).in_time_zone('Hawaii').iso8601 }
+
+ it_behaves_like 'no errors'
+
+ it 'returns the correct releasedAt date in UTC' do
+ create_release
+
+ expect(mutation_response[:release]).to include({ releasedAt: Time.parse(released_at).utc.iso8601 })
+ end
+ end
+
+ context 'when no releasedAt is provided' do
+ let(:mutation_arguments) { super().except(:releasedAt) }
+
+ it_behaves_like 'no errors'
+
+ it 'sets releasedAt to the current time' do
+ create_release
+
+ expect(mutation_response[:release]).to include({ releasedAt: Time.current.utc.iso8601 })
+ end
+ end
+
+ context "when a release asset doesn't include an explicit linkType" do
+ let(:asset_link) { super().except(:linkType) }
+
+ it_behaves_like 'no errors'
+
+ it 'defaults the linkType to OTHER' do
+ create_release
+
+ returned_asset_link_type = mutation_response.dig(:release, :assets, :links, :nodes, 0, :linkType)
+
+ expect(returned_asset_link_type).to eq('OTHER')
+ end
+ end
+
+ context "when a release asset doesn't include a directAssetPath" do
+ let(:asset_link) { super().except(:directAssetPath) }
+
+ it_behaves_like 'no errors'
+
+ it 'returns the provided url as the directAssetUrl' do
+ create_release
+
+ returned_asset_link_type = mutation_response.dig(:release, :assets, :links, :nodes, 0, :directAssetUrl)
+
+ expect(returned_asset_link_type).to eq(asset_link[:url])
+ end
+ end
+
+ context 'empty milestones' do
+ shared_examples 'no associated milestones' do
+ it_behaves_like 'no errors'
+
+ it 'creates a release with no associated milestones' do
+ create_release
+
+ returned_milestones = mutation_response.dig(:release, :milestones, :nodes)
+
+ expect(returned_milestones.count).to eq(0)
+ end
+ end
+
+ context 'when the milestones parameter is not provided' do
+ let(:mutation_arguments) { super().except(:milestones) }
+
+ it_behaves_like 'no associated milestones'
+ end
+
+ context 'when the milestones parameter is null' do
+ let(:milestones) { nil }
+
+ it_behaves_like 'no associated milestones'
+ end
+
+ context 'when the milestones parameter is an empty array' do
+ let(:milestones) { [] }
+
+ it_behaves_like 'no associated milestones'
+ end
+ end
+
+ context 'validation' do
+ context 'when a release is already associated to the specified tag' do
+ before do
+ create(:release, project: project, tag: tag_name)
+ end
+
+ it_behaves_like 'errors-as-data with message', 'Release already exists'
+ end
+
+ context "when a provided milestone doesn\'t exist" do
+ let(:milestones) { ['a fake milestone'] }
+
+ it_behaves_like 'errors-as-data with message', 'Milestone(s) not found: a fake milestone'
+ end
+
+ context "when a provided milestone belongs to a different project than the release" do
+ let(:milestone_in_different_project) { create(:milestone, title: 'different milestone') }
+ let(:milestones) { [milestone_in_different_project.title] }
+
+ it_behaves_like 'errors-as-data with message', "Milestone(s) not found: different milestone"
+ end
+
+ context 'when two release assets share the same name' do
+ let(:asset_link_1) { { name: 'My link', url: 'https://example.com/1' } }
+ let(:asset_link_2) { { name: 'My link', url: 'https://example.com/2' } }
+ let(:assets) { { links: [asset_link_1, asset_link_2] } }
+
+ # Right now the raw Postgres error message is sent to the user as the validation message.
+ # We should catch this validation error and return a nicer message:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/277087
+ it_behaves_like 'errors-as-data with message', 'PG::UniqueViolation'
+ end
+
+ context 'when two release assets share the same URL' do
+ let(:asset_link_1) { { name: 'My first link', url: 'https://example.com' } }
+ let(:asset_link_2) { { name: 'My second link', url: 'https://example.com' } }
+ let(:assets) { { links: [asset_link_1, asset_link_2] } }
+
+ # Same note as above about the ugly error message
+ it_behaves_like 'errors-as-data with message', 'PG::UniqueViolation'
+ end
+
+ context 'when the provided tag name is HEAD' do
+ let(:tag_name) { 'HEAD' }
+
+ it_behaves_like 'errors-as-data with message', 'Tag name invalid'
+ end
+
+ context 'when the provided tag name is empty' do
+ let(:tag_name) { '' }
+
+ it_behaves_like 'errors-as-data with message', 'Tag name invalid'
+ end
+
+ context "when the provided tag doesn't already exist, and no ref parameter was provided" do
+ let(:ref) { nil }
+ let(:tag_name) { 'v7.12.5-beta' }
+
+ it_behaves_like 'errors-as-data with message', 'Ref is not specified'
+ end
+ end
+ end
+
+ context "when the current user doesn't have access to create releases" do
+ expected_error_message = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+
+ context 'when the current user is a Reporter' do
+ let(:current_user) { reporter }
+
+ it_behaves_like 'top-level error with message', expected_error_message
+ end
+
+ context 'when the current user is a Guest' do
+ let(:current_user) { guest }
+
+ it_behaves_like 'top-level error with message', expected_error_message
+ end
+
+ context 'when the current user is a public user' do
+ let(:current_user) { public_user }
+
+ it_behaves_like 'top-level error with message', expected_error_message
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
index b71f87d2702..1be8ce142ac 100644
--- a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
@@ -53,10 +53,11 @@ RSpec.describe 'Destroying a Snippet' do
let!(:snippet_gid) { project.to_gid.to_s }
it 'returns an error' do
+ err_message = %Q["#{snippet_gid}" does not represent an instance of Snippet]
+
post_graphql_mutation(mutation, current_user: current_user)
- expect(graphql_errors)
- .to include(a_hash_including('message' => "#{snippet_gid} is not a valid ID for Snippet."))
+ expect(graphql_errors).to include(a_hash_including('message' => a_string_including(err_message)))
end
it 'does not destroy the Snippet' do
diff --git a/spec/requests/api/graphql/mutations/todos/create_spec.rb b/spec/requests/api/graphql/mutations/todos/create_spec.rb
new file mode 100644
index 00000000000..aca00519682
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/todos/create_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Create a todo' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:target) { create(:issue) }
+
+ let(:input) do
+ {
+ 'targetId' => target.to_global_id.to_s
+ }
+ end
+
+ let(:mutation) { graphql_mutation(:todoCreate, input) }
+
+ let(:mutation_response) { graphql_mutation_response(:todoCreate) }
+
+ context 'the user is not allowed to create todo' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user has permissions to create todo' do
+ before do
+ target.project.add_guest(current_user)
+ end
+
+ it 'creates todo' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['todo']['body']).to eq(target.title)
+ expect(mutation_response['todo']['state']).to eq('pending')
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
new file mode 100644
index 00000000000..3e96d5c5058
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Restoring many Todos' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:other_user) { create(:user) }
+
+ let_it_be(:todo1) { create(:todo, user: current_user, author: author, state: :done) }
+ let_it_be(:todo2) { create(:todo, user: current_user, author: author, state: :done) }
+
+ let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :done) }
+
+ let(:input_ids) { [todo1, todo2].map { |obj| global_id_of(obj) } }
+ let(:input) { { ids: input_ids } }
+
+ let(:mutation) do
+ graphql_mutation(:todo_restore_many, input,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ updatedIds
+ todos {
+ id
+ state
+ }
+ QL
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:todo_restore_many)
+ end
+
+ it 'restores many todos' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(todo1.reload.state).to eq('pending')
+ expect(todo2.reload.state).to eq('pending')
+ expect(other_user_todo.reload.state).to eq('done')
+
+ expect(mutation_response).to include(
+ 'errors' => be_empty,
+ 'updatedIds' => match_array(input_ids),
+ 'todos' => contain_exactly(
+ { 'id' => global_id_of(todo1), 'state' => 'pending' },
+ { 'id' => global_id_of(todo2), 'state' => 'pending' }
+ )
+ )
+ end
+
+ context 'when using an invalid gid' do
+ let(:input_ids) { [global_id_of(author)] }
+ let(:invalid_gid_error) { /does not represent an instance of #{todo1.class}/ }
+
+ it 'contains the expected error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ errors = json_response['errors']
+ expect(errors).not_to be_blank
+ expect(errors.first['message']).to match(invalid_gid_error)
+
+ expect(todo1.reload.state).to eq('done')
+ expect(todo2.reload.state).to eq('done')
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
index 44e68c59248..37cc502103d 100644
--- a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
+++ b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'rendering namespace statistics' do
include GraphqlHelpers
let(:namespace) { user.namespace }
- let!(:statistics) { create(:namespace_root_storage_statistics, namespace: namespace, packages_size: 5.gigabytes) }
+ let!(:statistics) { create(:namespace_root_storage_statistics, namespace: namespace, packages_size: 5.gigabytes, uploads_size: 3.gigabytes) }
let(:user) { create(:user) }
let(:query) do
@@ -28,6 +28,12 @@ RSpec.describe 'rendering namespace statistics' do
expect(graphql_data['namespace']['rootStorageStatistics']).not_to be_blank
expect(graphql_data['namespace']['rootStorageStatistics']['packagesSize']).to eq(5.gigabytes)
end
+
+ it 'includes uploads size if the user can read the statistics' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:namespace, :root_storage_statistics, :uploads_size)).to eq(3.gigabytes)
+ end
end
it_behaves_like 'a working namespace with storage statistics query'
diff --git a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb
new file mode 100644
index 00000000000..b13805a61ce
--- /dev/null
+++ b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'getting Alert Management Integrations' do
+ include ::Gitlab::Routing
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:prometheus_service) { create(:prometheus_service, project: project) }
+ let_it_be(:project_alerting_setting) { create(:project_alerting_setting, project: project) }
+ let_it_be(:active_http_integration) { create(:alert_management_http_integration, project: project) }
+ let_it_be(:inactive_http_integration) { create(:alert_management_http_integration, :inactive, project: project) }
+ let_it_be(:other_project_http_integration) { create(:alert_management_http_integration) }
+
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ #{all_graphql_fields_for('AlertManagementIntegration')}
+ }
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('alertManagementIntegrations', {}, fields)
+ )
+ end
+
+ context 'with integrations' do
+ let(:integrations) { graphql_data.dig('project', 'alertManagementIntegrations', 'nodes') }
+
+ context 'without project permissions' do
+ let(:user) { create(:user) }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(integrations).to be_nil }
+ end
+
+ context 'with project permissions' do
+ before do
+ project.add_maintainer(current_user)
+ post_graphql(query, current_user: current_user)
+ end
+
+ let(:http_integration) { integrations.first }
+ let(:prometheus_integration) { integrations.second }
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(integrations.size).to eq(2) }
+
+ it 'returns the correct properties of the integrations' do
+ expect(http_integration).to include(
+ 'id' => GitlabSchema.id_from_object(active_http_integration).to_s,
+ 'type' => 'HTTP',
+ 'name' => active_http_integration.name,
+ 'active' => active_http_integration.active,
+ 'token' => active_http_integration.token,
+ 'url' => active_http_integration.url,
+ 'apiUrl' => nil
+ )
+
+ expect(prometheus_integration).to include(
+ 'id' => GitlabSchema.id_from_object(prometheus_service).to_s,
+ 'type' => 'PROMETHEUS',
+ 'name' => 'Prometheus',
+ 'active' => prometheus_service.manual_configuration?,
+ 'token' => project_alerting_setting.token,
+ 'url' => "http://localhost/#{project.full_path}/prometheus/alerts/notify.json",
+ 'apiUrl' => prometheus_service.api_url
+ )
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
new file mode 100644
index 00000000000..7e32f54bf1d
--- /dev/null
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'getting container repositories in a project' do
+ using RSpec::Parameterized::TableSyntax
+ include GraphqlHelpers
+
+ let_it_be_with_reload(:project) { create(:project, :private) }
+ let_it_be(:container_repository) { create(:container_repository, project: project) }
+ let_it_be(:container_repositories_delete_scheduled) { create_list(:container_repository, 2, :status_delete_scheduled, project: project) }
+ let_it_be(:container_repositories_delete_failed) { create_list(:container_repository, 2, :status_delete_failed, project: project) }
+ let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten }
+ let_it_be(:container_expiration_policy) { project.container_expiration_policy }
+
+ let(:fields) do
+ <<~GQL
+ edges {
+ node {
+ #{all_graphql_fields_for('container_repositories'.classify)}
+ }
+ }
+ GQL
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('container_repositories', {}, fields)
+ )
+ end
+
+ let(:user) { project.owner }
+ let(:variables) { {} }
+ let(:container_repositories_response) { graphql_data.dig('project', 'containerRepositories', 'edges') }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ container_repositories.each do |repository|
+ stub_container_registry_tags(repository: repository.path, tags: %w(tag1 tag2 tag3), with_manifest: false)
+ end
+ end
+
+ subject { post_graphql(query, current_user: user, variables: variables) }
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ subject
+ end
+
+ it 'matches the JSON schema' do
+ expect(container_repositories_response).to match_schema('graphql/container_repositories')
+ end
+ end
+
+ context 'with different permissions' do
+ let_it_be(:user) { create(:user) }
+
+ where(:project_visibility, :role, :access_granted, :can_delete) do
+ :private | :maintainer | true | true
+ :private | :developer | true | true
+ :private | :reporter | true | false
+ :private | :guest | false | false
+ :private | :anonymous | false | false
+ :public | :maintainer | true | true
+ :public | :developer | true | true
+ :public | :reporter | true | false
+ :public | :guest | true | false
+ :public | :anonymous | true | false
+ end
+
+ with_them do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility.to_s.upcase, false))
+ project.add_user(user, role) unless role == :anonymous
+ end
+
+ it 'return the proper response' do
+ subject
+
+ if access_granted
+ expect(container_repositories_response.size).to eq(container_repositories.size)
+ container_repositories_response.each do |repository_response|
+ expect(repository_response.dig('node', 'canDelete')).to eq(can_delete)
+ end
+ else
+ expect(container_repositories_response).to eq(nil)
+ end
+ end
+ end
+ end
+
+ context 'limiting the number of repositories' do
+ let(:limit) { 1 }
+ let(:variables) do
+ { path: project.full_path, n: limit }
+ end
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $n: Int) {
+ project(fullPath: $path) {
+ containerRepositories(first: $n) { #{fields} }
+ }
+ }
+ GQL
+ end
+
+ it 'only returns N repositories' do
+ subject
+
+ expect(container_repositories_response.size).to eq(limit)
+ end
+ end
+
+ context 'filter by name' do
+ let_it_be(:container_repository) { create(:container_repository, name: 'fooBar', project: project) }
+
+ let(:name) { 'ooba' }
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $name: String) {
+ project(fullPath: $path) {
+ containerRepositories(name: $name) { #{fields} }
+ }
+ }
+ GQL
+ end
+
+ let(:variables) do
+ { path: project.full_path, name: name }
+ end
+
+ before do
+ stub_container_registry_tags(repository: container_repository.path, tags: %w(tag4 tag5 tag6), with_manifest: false)
+ end
+
+ it 'returns the searched container repository' do
+ subject
+
+ expect(container_repositories_response.size).to eq(1)
+ expect(container_repositories_response.first.dig('node', 'id')).to eq(container_repository.to_global_id.to_s)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
index cd84ce9cb96..c7d327a62af 100644
--- a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
+++ b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb
@@ -29,10 +29,12 @@ RSpec.describe 'sentry errors requests' do
let(:error_data) { graphql_data.dig('project', 'sentryErrors', 'detailedError') }
- it_behaves_like 'a working graphql query' do
- before do
- post_graphql(query, current_user: current_user)
- end
+ it 'returns a successful response', :aggregate_failures, :quarantine do
+ post_graphql(query, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_errors).to be_nil
+ expect(json_response.keys).to include('data')
end
context 'when data is loading via reactive cache' do
@@ -191,7 +193,7 @@ RSpec.describe 'sentry errors requests' do
describe 'getting a stack trace' do
let_it_be(:sentry_stack_trace) { build(:error_tracking_error_event) }
- let(:sentry_gid) { Gitlab::ErrorTracking::DetailedError.new(id: 1).to_global_id.to_s }
+ let(:sentry_gid) { global_id_of(Gitlab::ErrorTracking::DetailedError.new(id: 1)) }
let(:stack_trace_fields) do
all_graphql_fields_for('SentryErrorStackTrace'.classify)
diff --git a/spec/requests/api/graphql/project/grafana_integration_spec.rb b/spec/requests/api/graphql/project/grafana_integration_spec.rb
index 688959e622d..9b24698f40c 100644
--- a/spec/requests/api/graphql/project/grafana_integration_spec.rb
+++ b/spec/requests/api/graphql/project/grafana_integration_spec.rb
@@ -45,7 +45,6 @@ RSpec.describe 'Getting Grafana Integration' do
it_behaves_like 'a working graphql query'
- specify { expect(integration_data['token']).to eql grafana_integration.masked_token }
specify { expect(integration_data['grafanaUrl']).to eql grafana_integration.grafana_url }
specify do
diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
index 1b654e660e3..4bce3c7fe0f 100644
--- a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
+++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
create(:design_version, issue: issue,
created_designs: create_list(:design, 3, issue: issue))
end
+
let_it_be(:version) do
create(:design_version, issue: issue,
modified_designs: old_version.designs,
diff --git a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
index 640ac95cd86..ee0085718b3 100644
--- a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
+++ b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
@@ -11,12 +11,15 @@ RSpec.describe 'Getting versions related to an issue' do
let_it_be(:version_a) do
create(:design_version, issue: issue)
end
+
let_it_be(:version_b) do
create(:design_version, issue: issue)
end
+
let_it_be(:version_c) do
create(:design_version, issue: issue)
end
+
let_it_be(:version_d) do
create(:design_version, issue: issue)
end
diff --git a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
index e25453510d5..a671ddc7ab1 100644
--- a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
+++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'Getting designs related to an issue' do
post_graphql(query(note_fields), current_user: nil)
- designs_data = graphql_data['project']['issue']['designs']['designs']
+ designs_data = graphql_data['project']['issue']['designCollection']['designs']
design_data = designs_data['nodes'].first
note_data = design_data['notes']['nodes'].first
@@ -56,7 +56,7 @@ RSpec.describe 'Getting designs related to an issue' do
'issue',
{ iid: design.issue.iid.to_s },
query_graphql_field(
- 'designs', {}, design_node
+ 'designCollection', {}, design_node
)
)
)
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 40fec6ba068..4f27f08bf98 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -9,10 +9,9 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:current_user) { create(:user) }
- let_it_be(:issues, reload: true) do
- [create(:issue, project: project, discussion_locked: true),
- create(:issue, :with_alert, project: project)]
- end
+ let_it_be(:issue_a, reload: true) { create(:issue, project: project, discussion_locked: true) }
+ let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) }
+ let_it_be(:issues, reload: true) { [issue_a, issue_b] }
let(:fields) do
<<~QUERY
@@ -414,4 +413,42 @@ RSpec.describe 'getting an issue list for a project' do
expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(new_issues))
end
end
+
+ describe 'N+1 query checks' do
+ let(:extra_iid_for_second_query) { issue_b.iid.to_s }
+ let(:search_params) { { iids: [issue_a.iid.to_s] } }
+
+ def execute_query
+ query = graphql_query_for(
+ :project,
+ { full_path: project.full_path },
+ query_graphql_field(:issues, search_params, [
+ query_graphql_field(:nodes, nil, requested_fields)
+ ])
+ )
+ post_graphql(query, current_user: current_user)
+ end
+
+ context 'when requesting `user_notes_count`' do
+ let(:requested_fields) { [:user_notes_count] }
+
+ before do
+ create_list(:note_on_issue, 2, noteable: issue_a, project: project)
+ create(:note_on_issue, noteable: issue_b, project: project)
+ end
+
+ include_examples 'N+1 query check'
+ end
+
+ context 'when requesting `user_discussions_count`' do
+ let(:requested_fields) { [:user_discussions_count] }
+
+ before do
+ create_list(:note_on_issue, 2, noteable: issue_a, project: project)
+ create(:note_on_issue, noteable: issue_b, project: project)
+ end
+
+ include_examples 'N+1 query check'
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb
index 1cc30b95162..98a3f08baa6 100644
--- a/spec/requests/api/graphql/project/jira_import_spec.rb
+++ b/spec/requests/api/graphql/project/jira_import_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe 'query Jira import data' do
total_issue_count: 4
)
end
+
let_it_be(:jira_import2) do
create(
:jira_import_state, :finished,
@@ -31,6 +32,7 @@ RSpec.describe 'query Jira import data' do
total_issue_count: 3
)
end
+
let(:query) do
%(
query {
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index c737e0b8caf..2b8d537f9fc 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -243,6 +243,17 @@ RSpec.describe 'getting merge request listings nested in a project' do
include_examples 'N+1 query check'
end
+
+ context 'when requesting `user_discussions_count`' do
+ let(:requested_fields) { [:user_discussions_count] }
+
+ before do
+ create_list(:note_on_merge_request, 2, noteable: merge_request_a, project: project)
+ create(:note_on_merge_request, noteable: merge_request_c, project: project)
+ end
+
+ include_examples 'N+1 query check'
+ end
end
describe 'sorting and pagination' do
diff --git a/spec/requests/api/graphql/project/project_statistics_spec.rb b/spec/requests/api/graphql/project/project_statistics_spec.rb
index c226b10ab51..b57c594c64f 100644
--- a/spec/requests/api/graphql/project/project_statistics_spec.rb
+++ b/spec/requests/api/graphql/project/project_statistics_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'rendering project statistics' do
include GraphqlHelpers
let(:project) { create(:project) }
- let!(:project_statistics) { create(:project_statistics, project: project, packages_size: 5.gigabytes) }
+ let!(:project_statistics) { create(:project_statistics, project: project, packages_size: 5.gigabytes, uploads_size: 3.gigabytes) }
let(:user) { create(:user) }
let(:query) do
@@ -31,6 +31,12 @@ RSpec.describe 'rendering project statistics' do
expect(graphql_data['project']['statistics']['packagesSize']).to eq(5.gigabytes)
end
+ it 'includes uploads size if the user can read the statistics' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :statistics, :uploadsSize)).to eq(3.gigabytes)
+ end
+
context 'when the project is public' do
let(:project) { create(:project, :public) }
diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb
index 8fce29d0dc6..57dbe258ce4 100644
--- a/spec/requests/api/graphql/project/release_spec.rb
+++ b/spec/requests/api/graphql/project/release_spec.rb
@@ -13,7 +13,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let_it_be(:link_filepath) { '/direct/asset/link/path' }
let_it_be(:released_at) { Time.now - 1.day }
- let(:params_for_issues_and_mrs) { { scope: 'all', state: 'opened', release_tag: release.tag } }
+ let(:base_url_params) { { scope: 'all', release_tag: release.tag } }
+ let(:opened_url_params) { { state: 'opened', **base_url_params } }
+ let(:merged_url_params) { { state: 'merged', **base_url_params } }
+ let(:closed_url_params) { { state: 'closed', **base_url_params } }
+
let(:post_query) { post_graphql(query, current_user: current_user) }
let(:path_prefix) { %w[project release] }
let(:data) { graphql_data.dig(*path) }
@@ -143,7 +147,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
'name' => link.name,
'url' => link.url,
'external' => link.external?,
- 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << link.filepath : link.url
+ 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url
}
end
@@ -180,8 +184,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let(:release_fields) do
query_graphql_field(:links, nil, %{
selfUrl
- mergeRequestsUrl
- issuesUrl
+ openedMergeRequestsUrl
+ mergedMergeRequestsUrl
+ closedMergeRequestsUrl
+ openedIssuesUrl
+ closedIssuesUrl
})
end
@@ -190,8 +197,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
expect(data).to eq(
'selfUrl' => project_release_url(project, release),
- 'mergeRequestsUrl' => project_merge_requests_url(project, params_for_issues_and_mrs),
- 'issuesUrl' => project_issues_url(project, params_for_issues_and_mrs)
+ 'openedMergeRequestsUrl' => project_merge_requests_url(project, opened_url_params),
+ 'mergedMergeRequestsUrl' => project_merge_requests_url(project, merged_url_params),
+ 'closedMergeRequestsUrl' => project_merge_requests_url(project, closed_url_params),
+ 'openedIssuesUrl' => project_issues_url(project, opened_url_params),
+ 'closedIssuesUrl' => project_issues_url(project, closed_url_params)
)
end
end
diff --git a/spec/requests/api/graphql/project/releases_spec.rb b/spec/requests/api/graphql/project/releases_spec.rb
index 7c57c0e9177..6e364c7d7b5 100644
--- a/spec/requests/api/graphql/project/releases_spec.rb
+++ b/spec/requests/api/graphql/project/releases_spec.rb
@@ -10,6 +10,11 @@ RSpec.describe 'Query.project(fullPath).releases()' do
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
+ let(:base_url_params) { { scope: 'all', release_tag: release.tag } }
+ let(:opened_url_params) { { state: 'opened', **base_url_params } }
+ let(:merged_url_params) { { state: 'merged', **base_url_params } }
+ let(:closed_url_params) { { state: 'closed', **base_url_params } }
+
let(:query) do
graphql_query_for(:project, { fullPath: project.full_path },
%{
@@ -37,8 +42,11 @@ RSpec.describe 'Query.project(fullPath).releases()' do
}
links {
selfUrl
- mergeRequestsUrl
- issuesUrl
+ openedMergeRequestsUrl
+ mergedMergeRequestsUrl
+ closedMergeRequestsUrl
+ openedIssuesUrl
+ closedIssuesUrl
}
}
}
@@ -101,8 +109,11 @@ RSpec.describe 'Query.project(fullPath).releases()' do
},
'links' => {
'selfUrl' => project_release_url(project, release),
- 'mergeRequestsUrl' => project_merge_requests_url(project, params_for_issues_and_mrs),
- 'issuesUrl' => project_issues_url(project, params_for_issues_and_mrs)
+ 'openedMergeRequestsUrl' => project_merge_requests_url(project, opened_url_params),
+ 'mergedMergeRequestsUrl' => project_merge_requests_url(project, merged_url_params),
+ 'closedMergeRequestsUrl' => project_merge_requests_url(project, closed_url_params),
+ 'openedIssuesUrl' => project_issues_url(project, opened_url_params),
+ 'closedIssuesUrl' => project_issues_url(project, closed_url_params)
}
)
end
@@ -300,4 +311,77 @@ RSpec.describe 'Query.project(fullPath).releases()' do
it_behaves_like 'no access to any release data'
end
end
+
+ describe 'sorting behavior' do
+ let_it_be(:today) { Time.now }
+ let_it_be(:yesterday) { today - 1.day }
+ let_it_be(:tomorrow) { today + 1.day }
+
+ let_it_be(:project) { create(:project, :repository, :public) }
+
+ let_it_be(:release_v1) { create(:release, project: project, tag: 'v1', released_at: yesterday, created_at: tomorrow) }
+ let_it_be(:release_v2) { create(:release, project: project, tag: 'v2', released_at: today, created_at: yesterday) }
+ let_it_be(:release_v3) { create(:release, project: project, tag: 'v3', released_at: tomorrow, created_at: today) }
+
+ let(:current_user) { developer }
+
+ let(:params) { nil }
+
+ let(:sorted_tags) do
+ graphql_data.dig('project', 'releases', 'nodes').map { |release| release['tagName'] }
+ end
+
+ let(:query) do
+ graphql_query_for(:project, { fullPath: project.full_path },
+ %{
+ releases#{params ? "(#{params})" : ""} {
+ nodes {
+ tagName
+ }
+ }
+ })
+ end
+
+ before do
+ post_query
+ end
+
+ context 'when no sort: parameter is provided' do
+ it 'returns the results with the default sort applied (sort: RELEASED_AT_DESC)' do
+ expect(sorted_tags).to eq(%w(v3 v2 v1))
+ end
+ end
+
+ context 'with sort: RELEASED_AT_DESC' do
+ let(:params) { 'sort: RELEASED_AT_DESC' }
+
+ it 'returns the releases ordered by released_at in descending order' do
+ expect(sorted_tags).to eq(%w(v3 v2 v1))
+ end
+ end
+
+ context 'with sort: RELEASED_AT_ASC' do
+ let(:params) { 'sort: RELEASED_AT_ASC' }
+
+ it 'returns the releases ordered by released_at in ascending order' do
+ expect(sorted_tags).to eq(%w(v1 v2 v3))
+ end
+ end
+
+ context 'with sort: CREATED_DESC' do
+ let(:params) { 'sort: CREATED_DESC' }
+
+ it 'returns the releases ordered by created_at in descending order' do
+ expect(sorted_tags).to eq(%w(v1 v3 v2))
+ end
+ end
+
+ context 'with sort: CREATED_ASC' do
+ let(:params) { 'sort: CREATED_ASC' }
+
+ it 'returns the releases ordered by created_at in ascending order' do
+ expect(sorted_tags).to eq(%w(v2 v3 v1))
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb
new file mode 100644
index 00000000000..8b67b549efa
--- /dev/null
+++ b/spec/requests/api/graphql/project/terraform/states_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'query terraform states' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:terraform_state) { create(:terraform_state, :with_version, :locked, project: project) }
+ let_it_be(:latest_version) { terraform_state.latest_version }
+
+ let(:query) do
+ graphql_query_for(:project, { fullPath: project.full_path },
+ %{
+ terraformStates {
+ count
+ nodes {
+ id
+ name
+ lockedAt
+ createdAt
+ updatedAt
+
+ latestVersion {
+ id
+ createdAt
+ updatedAt
+
+ createdByUser {
+ id
+ }
+
+ job {
+ name
+ }
+ }
+
+ lockedByUser {
+ id
+ }
+ }
+ }
+ })
+ end
+
+ let(:current_user) { project.creator }
+ let(:data) { graphql_data.dig('project', 'terraformStates') }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'returns terraform state data', :aggregate_failures do
+ state = data.dig('nodes', 0)
+ version = state['latestVersion']
+
+ expect(state['id']).to eq(terraform_state.to_global_id.to_s)
+ expect(state['name']).to eq(terraform_state.name)
+ expect(state['lockedAt']).to eq(terraform_state.locked_at.iso8601)
+ expect(state['createdAt']).to eq(terraform_state.created_at.iso8601)
+ expect(state['updatedAt']).to eq(terraform_state.updated_at.iso8601)
+ expect(state.dig('lockedByUser', 'id')).to eq(terraform_state.locked_by_user.to_global_id.to_s)
+
+ expect(version['id']).to eq(latest_version.to_global_id.to_s)
+ expect(version['createdAt']).to eq(latest_version.created_at.iso8601)
+ expect(version['updatedAt']).to eq(latest_version.updated_at.iso8601)
+ expect(version.dig('createdByUser', 'id')).to eq(latest_version.created_by_user.to_global_id.to_s)
+ expect(version.dig('job', 'name')).to eq(latest_version.build.name)
+ end
+
+ it 'returns count of terraform states' do
+ count = data.dig('count')
+ expect(count).to be(project.terraform_states.size)
+ end
+
+ context 'unauthorized users' do
+ let(:current_user) { nil }
+
+ it { expect(data).to be_nil }
+ end
+end
diff --git a/spec/requests/api/graphql/read_only_spec.rb b/spec/requests/api/graphql/read_only_spec.rb
index ce8a3f6ef5c..d2a45603886 100644
--- a/spec/requests/api/graphql/read_only_spec.rb
+++ b/spec/requests/api/graphql/read_only_spec.rb
@@ -3,55 +3,11 @@
require 'spec_helper'
RSpec.describe 'Requests on a read-only node' do
- include GraphqlHelpers
-
- before do
- allow(Gitlab::Database).to receive(:read_only?) { true }
- end
-
- context 'mutations' do
- let(:current_user) { note.author }
- let!(:note) { create(:note) }
-
- let(:mutation) do
- variables = {
- id: GitlabSchema.id_from_object(note).to_s
- }
-
- graphql_mutation(:destroy_note, variables)
- end
-
- def mutation_response
- graphql_mutation_response(:destroy_note)
- end
-
- it 'disallows the query' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(json_response['errors'].first['message']).to eq(Mutations::BaseMutation::ERROR_MESSAGE)
- end
-
- it 'does not destroy the Note' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- end.not_to change { Note.count }
- end
- end
-
- context 'read-only queries' do
- let(:current_user) { create(:user) }
- let(:project) { create(:project, :repository) }
-
+ context 'when db is read-only' do
before do
- project.add_developer(current_user)
+ allow(Gitlab::Database).to receive(:read_only?) { true }
end
- it 'allows the query' do
- query = graphql_query_for('project', 'fullPath' => project.full_path)
-
- post_graphql(query, current_user: current_user)
-
- expect(graphql_data['project']).not_to be_nil
- end
+ it_behaves_like 'graphql on a read-only GitLab instance'
end
end
diff --git a/spec/requests/api/graphql/terraform/state/delete_spec.rb b/spec/requests/api/graphql/terraform/state/delete_spec.rb
new file mode 100644
index 00000000000..35927d03b49
--- /dev/null
+++ b/spec/requests/api/graphql/terraform/state/delete_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'delete a terraform state' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, maintainer_projects: [project]) }
+
+ let(:state) { create(:terraform_state, project: project) }
+ let(:mutation) { graphql_mutation(:terraform_state_delete, id: state.to_global_id.to_s) }
+
+ before do
+ 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/terraform/state/lock_spec.rb b/spec/requests/api/graphql/terraform/state/lock_spec.rb
new file mode 100644
index 00000000000..e4d3b6336ab
--- /dev/null
+++ b/spec/requests/api/graphql/terraform/state/lock_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'lock a terraform state' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, maintainer_projects: [project]) }
+
+ let(:state) { create(:terraform_state, project: project) }
+ let(:mutation) { graphql_mutation(:terraform_state_lock, id: state.to_global_id.to_s) }
+
+ before do
+ expect(state).not_to be_locked
+ post_graphql_mutation(mutation, current_user: user)
+ end
+
+ include_examples 'a working graphql query'
+
+ it 'locks the state' do
+ expect(state.reload).to be_locked
+ expect(state.locked_by_user).to eq(user)
+ end
+end
diff --git a/spec/requests/api/graphql/terraform/state/unlock_spec.rb b/spec/requests/api/graphql/terraform/state/unlock_spec.rb
new file mode 100644
index 00000000000..e90730f2d8f
--- /dev/null
+++ b/spec/requests/api/graphql/terraform/state/unlock_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'unlock a terraform state' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, maintainer_projects: [project]) }
+
+ let(:state) { create(:terraform_state, :locked, project: project) }
+ let(:mutation) { graphql_mutation(:terraform_state_unlock, id: state.to_global_id.to_s) }
+
+ before do
+ expect(state).to be_locked
+ post_graphql_mutation(mutation, current_user: user)
+ end
+
+ include_examples 'a working graphql query'
+
+ it 'unlocks the state' do
+ expect(state.reload).not_to be_locked
+ end
+end
diff --git a/spec/requests/api/graphql/user/group_member_query_spec.rb b/spec/requests/api/graphql/user/group_member_query_spec.rb
index 3a16d962214..e47cef8cc37 100644
--- a/spec/requests/api/graphql/user/group_member_query_spec.rb
+++ b/spec/requests/api/graphql/user/group_member_query_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe 'GroupMember' do
}
HEREDOC
end
+
let_it_be(:query) do
graphql_query_for('user', { id: member.user.to_global_id.to_s }, query_graphql_field("groupMemberships", {}, fields))
end
diff --git a/spec/requests/api/graphql/user/project_member_query_spec.rb b/spec/requests/api/graphql/user/project_member_query_spec.rb
index 0790e148caf..01827e94d5d 100644
--- a/spec/requests/api/graphql/user/project_member_query_spec.rb
+++ b/spec/requests/api/graphql/user/project_member_query_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe 'ProjectMember' do
}
HEREDOC
end
+
let_it_be(:query) do
graphql_query_for('user', { id: member.user.to_global_id.to_s }, query_graphql_field("projectMemberships", {}, fields))
end
diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb
index 79debd0b7ef..738e120549e 100644
--- a/spec/requests/api/graphql/user_query_spec.rb
+++ b/spec/requests/api/graphql/user_query_spec.rb
@@ -32,22 +32,27 @@ RSpec.describe 'getting user information' do
create(:merge_request, :unique_branches, :unique_author,
source_project: project_a, assignees: [user])
end
+
let_it_be(:assigned_mr_b) do
create(:merge_request, :unique_branches, :unique_author,
source_project: project_b, assignees: [user])
end
+
let_it_be(:assigned_mr_c) do
create(:merge_request, :unique_branches, :unique_author,
source_project: project_b, assignees: [user])
end
+
let_it_be(:authored_mr) do
create(:merge_request, :unique_branches,
source_project: project_a, author: user)
end
+
let_it_be(:authored_mr_b) do
create(:merge_request, :unique_branches,
source_project: project_b, author: user)
end
+
let_it_be(:authored_mr_c) do
create(:merge_request, :unique_branches,
source_project: project_b, author: user)
@@ -59,6 +64,7 @@ RSpec.describe 'getting user information' do
let(:user_params) { { username: user.username } }
before do
+ create(:user_status, user: user)
post_graphql(query, current_user: current_user)
end
@@ -76,9 +82,15 @@ RSpec.describe 'getting user information' do
'username' => presenter.username,
'webUrl' => presenter.web_url,
'avatarUrl' => presenter.avatar_url,
- 'status' => presenter.status,
'email' => presenter.email
))
+
+ expect(graphql_data['user']['status']).to match(
+ a_hash_including(
+ 'emoji' => presenter.status.emoji,
+ 'message' => presenter.status.message,
+ 'availability' => presenter.status.availability.upcase
+ ))
end
describe 'assignedMergeRequests' do