diff options
Diffstat (limited to 'spec/requests/api/graphql/mutations')
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 |