summaryrefslogtreecommitdiff
path: root/spec/requests/api/graphql/mutations
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/mutations
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/mutations')
-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
20 files changed, 1255 insertions, 45 deletions
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