summaryrefslogtreecommitdiff
path: root/spec/graphql/mutations
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 11:59:07 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 11:59:07 +0000
commit8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca (patch)
tree544930fb309b30317ae9797a9683768705d664c4 /spec/graphql/mutations
parent4b1de649d0168371549608993deac953eb692019 (diff)
downloadgitlab-ce-8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca.tar.gz
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc42
Diffstat (limited to 'spec/graphql/mutations')
-rw-r--r--spec/graphql/mutations/alert_management/create_alert_issue_spec.rb1
-rw-r--r--spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb2
-rw-r--r--spec/graphql/mutations/alert_management/http_integration/reset_token_spec.rb2
-rw-r--r--spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb2
-rw-r--r--spec/graphql/mutations/alert_management/update_alert_status_spec.rb2
-rw-r--r--spec/graphql/mutations/boards/issues/issue_move_list_spec.rb18
-rw-r--r--spec/graphql/mutations/boards/lists/create_spec.rb9
-rw-r--r--spec/graphql/mutations/concerns/mutations/finds_by_gid_spec.rb26
-rw-r--r--spec/graphql/mutations/container_expiration_policies/update_spec.rb2
-rw-r--r--spec/graphql/mutations/container_repositories/destroy_tags_spec.rb92
-rw-r--r--spec/graphql/mutations/discussions/toggle_resolve_spec.rb2
-rw-r--r--spec/graphql/mutations/environments/canary_ingress/update_spec.rb66
-rw-r--r--spec/graphql/mutations/issues/create_spec.rb8
-rw-r--r--spec/graphql/mutations/issues/update_spec.rb2
-rw-r--r--spec/graphql/mutations/labels/create_spec.rb2
-rw-r--r--spec/graphql/mutations/notes/reposition_image_diff_note_spec.rb2
-rw-r--r--spec/graphql/mutations/releases/delete_spec.rb92
-rw-r--r--spec/graphql/mutations/releases/update_spec.rb232
18 files changed, 529 insertions, 33 deletions
diff --git a/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb b/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb
index e6a7434d579..47ee338ad34 100644
--- a/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb
+++ b/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb
@@ -28,6 +28,7 @@ RSpec.describe Mutations::AlertManagement::CreateAlertIssue do
end
it_behaves_like 'an incident management tracked event', :incident_management_incident_created
+ it_behaves_like 'an incident management tracked event', :incident_management_alert_create_incident
end
context 'when CreateAlertIssue responds with an error' do
diff --git a/spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb b/spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb
index f74f9186743..acd7070d0d3 100644
--- a/spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb
+++ b/spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Mutations::AlertManagement::HttpIntegration::Destroy do
specify { expect(described_class).to require_graphql_authorizations(:admin_operations) }
describe '#resolve' do
- subject(:resolve) { mutation_for(project, current_user).resolve(args) }
+ subject(:resolve) { mutation_for(project, current_user).resolve(**args) }
context 'user has access to project' do
before do
diff --git a/spec/graphql/mutations/alert_management/http_integration/reset_token_spec.rb b/spec/graphql/mutations/alert_management/http_integration/reset_token_spec.rb
index d3ffb2abb47..96974c2aa6f 100644
--- a/spec/graphql/mutations/alert_management/http_integration/reset_token_spec.rb
+++ b/spec/graphql/mutations/alert_management/http_integration/reset_token_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Mutations::AlertManagement::HttpIntegration::ResetToken do
specify { expect(described_class).to require_graphql_authorizations(:admin_operations) }
describe '#resolve' do
- subject(:resolve) { mutation_for(project, current_user).resolve(args) }
+ subject(:resolve) { mutation_for(project, current_user).resolve(**args) }
context 'user has sufficient access to project' do
before do
diff --git a/spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb b/spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb
index 45d92695e06..ddf23909035 100644
--- a/spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb
+++ b/spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::ResetToken do
specify { expect(described_class).to require_graphql_authorizations(:admin_project) }
describe '#resolve' do
- subject(:resolve) { mutation_for(project, current_user).resolve(args) }
+ subject(:resolve) { mutation_for(project, current_user).resolve(**args) }
context 'user has sufficient access to project' do
before do
diff --git a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb
index 08761ce64c2..8465393f299 100644
--- a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb
+++ b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Mutations::AlertManagement::UpdateAlertStatus do
specify { expect(described_class).to require_graphql_authorizations(:update_alert_management_alert) }
describe '#resolve' do
- subject(:resolve) { mutation_for(project, current_user).resolve(args) }
+ subject(:resolve) { mutation_for(project, current_user).resolve(**args) }
context 'user has access to project' do
before do
diff --git a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb
index 71c43ed826c..24104a20465 100644
--- a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb
+++ b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb
@@ -34,18 +34,18 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do
end
subject do
- mutation.resolve(params.merge(move_params))
+ mutation.resolve(**params.merge(move_params))
end
describe '#ready?' do
it 'raises an error if required arguments are missing' do
- expect { mutation.ready?(params) }
+ expect { mutation.ready?(**params) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "At least one of the arguments " \
"fromListId, toListId, afterId or beforeId is required")
end
it 'raises an error if only one of fromListId and toListId is present' do
- expect { mutation.ready?(params.merge(from_list_id: list1.id)) }
+ expect { mutation.ready?(**params.merge(from_list_id: list1.id)) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError,
'Both fromListId and toListId must be present'
)
@@ -73,18 +73,6 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do
it_behaves_like 'raises a resource not available error'
end
-
- context 'when user cannot access board' do
- let(:board) { create(:board, group: create(:group, :private)) }
-
- it_behaves_like 'raises a resource not available error'
- end
-
- context 'when passing board_id as nil' do
- let(:board) { nil }
-
- it_behaves_like 'raises a resource not available error'
- end
end
end
end
diff --git a/spec/graphql/mutations/boards/lists/create_spec.rb b/spec/graphql/mutations/boards/lists/create_spec.rb
index b1fe9911c7b..894dd1f34b4 100644
--- a/spec/graphql/mutations/boards/lists/create_spec.rb
+++ b/spec/graphql/mutations/boards/lists/create_spec.rb
@@ -23,12 +23,12 @@ RSpec.describe Mutations::Boards::Lists::Create do
describe '#ready?' do
it 'raises an error if required arguments are missing' do
- expect { mutation.ready?({ board_id: 'some id' }) }
+ expect { mutation.ready?(board_id: 'some id') }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
end
it 'raises an error if too many required arguments are specified' do
- expect { mutation.ready?({ board_id: 'some id', backlog: true, label_id: 'some label' }) }
+ expect { mutation.ready?(board_id: 'some id', backlog: true, label_id: 'some label') }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
end
end
@@ -68,9 +68,8 @@ RSpec.describe Mutations::Boards::Lists::Create do
context 'when label not found' do
let(:list_create_params) { { label_id: "gid://gitlab/Label/#{non_existing_record_id}" } }
- it 'raises an error' do
- expect { subject }
- .to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'Label not found!')
+ it 'returns an error' do
+ expect(subject[:errors]).to include 'Label not found'
end
end
end
diff --git a/spec/graphql/mutations/concerns/mutations/finds_by_gid_spec.rb b/spec/graphql/mutations/concerns/mutations/finds_by_gid_spec.rb
new file mode 100644
index 00000000000..37e0fd611e4
--- /dev/null
+++ b/spec/graphql/mutations/concerns/mutations/finds_by_gid_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::FindsByGid do
+ include GraphqlHelpers
+
+ let(:mutation_class) do
+ Class.new(Mutations::BaseMutation) do
+ authorize :read_user
+
+ include Mutations::FindsByGid
+ end
+ end
+
+ let(:query) { double('Query', schema: GitlabSchema) }
+ let(:context) { GraphQL::Query::Context.new(query: query, object: nil, values: { current_user: user }) }
+ let(:user) { create(:user) }
+ let(:gid) { user.to_global_id }
+
+ subject(:mutation) { mutation_class.new(object: nil, context: context, field: nil) }
+
+ it 'calls GitlabSchema.find_by_gid to find objects during authorized_find!' do
+ expect(mutation.authorized_find!(id: gid)).to eq(user)
+ end
+end
diff --git a/spec/graphql/mutations/container_expiration_policies/update_spec.rb b/spec/graphql/mutations/container_expiration_policies/update_spec.rb
index 9c6016e0af4..e22fb951172 100644
--- a/spec/graphql/mutations/container_expiration_policies/update_spec.rb
+++ b/spec/graphql/mutations/container_expiration_policies/update_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Mutations::ContainerExpirationPolicies::Update do
specify { expect(described_class).to require_graphql_authorizations(:destroy_container_image) }
describe '#resolve' do
- subject { described_class.new(object: project, context: { current_user: user }, field: nil).resolve(params) }
+ subject { described_class.new(object: project, context: { current_user: user }, field: nil).resolve(**params) }
RSpec.shared_examples 'returning a success' do
it 'returns the container expiration policy with no errors' do
diff --git a/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb b/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb
new file mode 100644
index 00000000000..f22d9ffe753
--- /dev/null
+++ b/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::ContainerRepositories::DestroyTags do
+ include_context 'container repository delete tags service shared context'
+ using RSpec::Parameterized::TableSyntax
+
+ let(:id) { repository.to_global_id.to_s }
+
+ specify { expect(described_class).to require_graphql_authorizations(:destroy_container_image) }
+
+ describe '#resolve' do
+ let(:tags) { %w[A C D E] }
+
+ subject do
+ described_class.new(object: nil, context: { current_user: user }, field: nil)
+ .resolve(id: id, tag_names: tags)
+ end
+
+ shared_examples 'destroying container repository tags' do
+ before do
+ stub_delete_reference_requests(tags)
+ expect_delete_tag_by_names(tags)
+ allow_next_instance_of(ContainerRegistry::Client) do |client|
+ allow(client).to receive(:supports_tag_delete?).and_return(true)
+ end
+ end
+
+ it 'destroys the container repository tags' do
+ expect(Projects::ContainerRepository::DeleteTagsService)
+ .to receive(:new).and_call_original
+
+ expect(subject).to eq(errors: [], deleted_tag_names: tags)
+ end
+
+ it 'creates a package event' do
+ expect(::Packages::CreateEventService)
+ .to receive(:new).with(nil, user, event_name: :delete_tag_bulk, scope: :tag).and_call_original
+ expect { subject }.to change { ::Packages::Event.count }.by(1)
+ end
+ end
+
+ shared_examples 'denying access to container respository' do
+ it 'raises an error' do
+ expect(::Projects::ContainerRepository::DeleteTagsService).not_to receive(:new)
+
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'with valid id' do
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'destroying container repository tags'
+ :developer | 'destroying container repository tags'
+ :reporter | 'denying access to container respository'
+ :guest | 'denying access to container respository'
+ :anonymous | 'denying access to container respository'
+ 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(:id) { 'gid://gitlab/ContainerRepository/5555' }
+
+ it_behaves_like 'denying access to container respository'
+ end
+
+ context 'with service error' do
+ before do
+ project.add_maintainer(user)
+ allow_next_instance_of(Projects::ContainerRepository::DeleteTagsService) do |service|
+ allow(service).to receive(:execute).and_return(message: 'could not delete tags', status: :error)
+ end
+ end
+
+ it { is_expected.to eq(errors: ['could not delete tags'], deleted_tag_names: []) }
+
+ it 'does not create a package event' do
+ expect(::Packages::CreateEventService).not_to receive(:new)
+ expect { subject }.not_to change { ::Packages::Event.count }
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/discussions/toggle_resolve_spec.rb b/spec/graphql/mutations/discussions/toggle_resolve_spec.rb
index 2e5d41a8f1e..162b1249ab5 100644
--- a/spec/graphql/mutations/discussions/toggle_resolve_spec.rb
+++ b/spec/graphql/mutations/discussions/toggle_resolve_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Mutations::Discussions::ToggleResolve do
describe '#resolve' do
subject do
- mutation.resolve({ id: id_arg, resolve: resolve_arg })
+ mutation.resolve(id: id_arg, resolve: resolve_arg)
end
let(:id_arg) { discussion.to_global_id.to_s }
diff --git a/spec/graphql/mutations/environments/canary_ingress/update_spec.rb b/spec/graphql/mutations/environments/canary_ingress/update_spec.rb
new file mode 100644
index 00000000000..c022828cf09
--- /dev/null
+++ b/spec/graphql/mutations/environments/canary_ingress/update_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Environments::CanaryIngress::Update do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let(:user) { maintainer }
+
+ subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+
+ before_all do
+ project.add_maintainer(maintainer)
+ project.add_reporter(reporter)
+ end
+
+ describe '#resolve' do
+ subject { mutation.resolve(id: environment_id, weight: weight) }
+
+ let(:environment_id) { environment.to_global_id.to_s }
+ let(:weight) { 50 }
+ let(:update_service) { double('update_service') }
+
+ before do
+ allow(Environments::CanaryIngress::UpdateService).to receive(:new) { update_service }
+ end
+
+ context 'when service execution succeeded' do
+ before do
+ allow(update_service).to receive(:execute_async) { { status: :success } }
+ end
+
+ it 'returns no errors' do
+ expect(subject[:errors]).to be_empty
+ end
+ end
+
+ context 'when service encounters a problem' do
+ before do
+ allow(update_service).to receive(:execute_async) { { status: :error, message: 'something went wrong' } }
+ end
+
+ it 'returns an error' do
+ expect(subject[:errors]).to eq(['something went wrong'])
+ end
+ end
+
+ context 'when environment is not found' do
+ let(:environment_id) { non_existing_record_id.to_s }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(GraphQL::CoercionError)
+ end
+ end
+
+ context 'when user is reporter who does not have permission to access the environment' do
+ let(:user) { reporter }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/issues/create_spec.rb b/spec/graphql/mutations/issues/create_spec.rb
index 57658f6b358..422ad40a9cb 100644
--- a/spec/graphql/mutations/issues/create_spec.rb
+++ b/spec/graphql/mutations/issues/create_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe Mutations::Issues::Create do
project.add_guest(assignee2)
end
- subject { mutation.resolve(mutation_params) }
+ subject { mutation.resolve(**mutation_params) }
context 'when the user does not have permission to create an issue' do
it 'raises an error' do
@@ -117,7 +117,7 @@ RSpec.describe Mutations::Issues::Create do
end
it 'raises exception when mutually exclusive params are given' do
- expect { mutation.ready?(mutation_params) }
+ expect { mutation.ready?(**mutation_params) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
end
end
@@ -128,7 +128,7 @@ RSpec.describe Mutations::Issues::Create do
end
it 'raises exception when mutually exclusive params are given' do
- expect { mutation.ready?(mutation_params) }
+ expect { mutation.ready?(**mutation_params) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /to resolve a discussion please also provide `merge_request_to_resolve_discussions_of` parameter/)
end
end
@@ -139,7 +139,7 @@ RSpec.describe Mutations::Issues::Create do
end
it 'raises exception when mutually exclusive params are given' do
- expect { mutation.ready?(mutation_params) }.not_to raise_error
+ expect { mutation.ready?(**mutation_params) }.not_to raise_error
end
end
end
diff --git a/spec/graphql/mutations/issues/update_spec.rb b/spec/graphql/mutations/issues/update_spec.rb
index ce1eb874bcf..f10e257e153 100644
--- a/spec/graphql/mutations/issues/update_spec.rb
+++ b/spec/graphql/mutations/issues/update_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Mutations::Issues::Update do
}.merge(expected_attributes)
end
- subject { mutation.resolve(mutation_params) }
+ subject { mutation.resolve(**mutation_params) }
it_behaves_like 'permission level for issue mutation is correctly verified'
diff --git a/spec/graphql/mutations/labels/create_spec.rb b/spec/graphql/mutations/labels/create_spec.rb
index 8b284816d63..b2dd94f31bb 100644
--- a/spec/graphql/mutations/labels/create_spec.rb
+++ b/spec/graphql/mutations/labels/create_spec.rb
@@ -58,7 +58,7 @@ RSpec.describe Mutations::Labels::Create do
end
describe '#ready?' do
- subject { mutation.ready?(attributes.merge(extra_params)) }
+ subject { mutation.ready?(**attributes.merge(extra_params)) }
context 'when passing both project_path and group_path' do
let(:extra_params) { { project_path: 'foo', group_path: 'bar' } }
diff --git a/spec/graphql/mutations/notes/reposition_image_diff_note_spec.rb b/spec/graphql/mutations/notes/reposition_image_diff_note_spec.rb
index 8c22e1a1cb6..d88b196cbff 100644
--- a/spec/graphql/mutations/notes/reposition_image_diff_note_spec.rb
+++ b/spec/graphql/mutations/notes/reposition_image_diff_note_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Mutations::Notes::RepositionImageDiffNote do
describe '#resolve' do
subject do
- mutation.resolve({ note: note, position: new_position })
+ mutation.resolve(note: note, position: new_position)
end
let_it_be(:noteable) { create(:merge_request) }
diff --git a/spec/graphql/mutations/releases/delete_spec.rb b/spec/graphql/mutations/releases/delete_spec.rb
new file mode 100644
index 00000000000..bedb72b002c
--- /dev/null
+++ b/spec/graphql/mutations/releases/delete_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Releases::Delete do
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:non_project_member) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:tag) { 'v1.1.0'}
+ let_it_be(:release) { create(:release, project: project, tag: tag) }
+
+ let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
+
+ let(:mutation_arguments) do
+ {
+ project_path: project.full_path,
+ tag: tag
+ }
+ end
+
+ before do
+ project.add_developer(developer)
+ project.add_maintainer(maintainer)
+ end
+
+ shared_examples 'unauthorized or not found error' do
+ it 'raises a Gitlab::Graphql::Errors::ResourceNotAvailable error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, "The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ end
+ end
+
+ describe '#resolve' do
+ subject(:resolve) do
+ mutation.resolve(**mutation_arguments)
+ end
+
+ context 'when the current user has access to create releases' do
+ let(:current_user) { maintainer }
+
+ it 'deletes the release' do
+ expect { subject }.to change { Release.count }.by(-1)
+ end
+
+ it 'returns the deleted release' do
+ expect(subject[:release].tag).to eq(tag)
+ end
+
+ it 'does not remove the Git tag associated with the deleted release' do
+ expect { subject }.not_to change { Project.find_by_id(project.id).repository.tag_count }
+ end
+
+ it 'returns no errors' do
+ expect(subject[:errors]).to eq([])
+ end
+
+ context 'validation' do
+ context 'when the release does not exist' do
+ let(:mutation_arguments) { super().merge(tag: 'not-a-real-release') }
+
+ it 'returns the release as nil' do
+ expect(subject[:release]).to be_nil
+ end
+
+ it 'returns an errors-at-data message' do
+ expect(subject[:errors]).to eq(['Release does not exist'])
+ end
+ end
+
+ context 'when the project does not exist' do
+ let(:mutation_arguments) { super().merge(project_path: 'not/a/real/path') }
+
+ it_behaves_like 'unauthorized or not found error'
+ end
+ end
+ end
+
+ context "when the current user doesn't have access to update releases" do
+ context 'when the user is a developer' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'unauthorized or not found error'
+ end
+
+ context 'when the user is a non-project member' do
+ let(:current_user) { non_project_member }
+
+ it_behaves_like 'unauthorized or not found error'
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/releases/update_spec.rb b/spec/graphql/mutations/releases/update_spec.rb
new file mode 100644
index 00000000000..0406e9c96f3
--- /dev/null
+++ b/spec/graphql/mutations/releases/update_spec.rb
@@ -0,0 +1,232 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Releases::Update do
+ 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(:reporter) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+
+ let_it_be(:tag) { 'v1.1.0'}
+ let_it_be(:name) { 'Version 1.0'}
+ let_it_be(:description) { 'The first release :rocket:' }
+ let_it_be(:released_at) { Time.parse('2018-12-10').utc }
+ let_it_be(:created_at) { Time.parse('2018-11-05').utc }
+ let_it_be(:milestones) { [milestone_12_3.title, milestone_12_4.title] }
+
+ let_it_be(:release) do
+ create(:release, project: project, tag: tag, name: name,
+ description: description, released_at: released_at,
+ created_at: created_at, milestones: [milestone_12_3, milestone_12_4])
+ end
+
+ let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
+
+ let(:mutation_arguments) do
+ {
+ project_path: project.full_path,
+ tag: tag
+ }
+ end
+
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ before do
+ project.add_reporter(reporter)
+ project.add_developer(developer)
+ end
+
+ shared_examples 'no changes to the release except for the' do |except_for|
+ it 'does not change other release properties' do
+ expect(updated_release.project).to eq(project)
+ expect(updated_release.tag).to eq(tag)
+
+ expect(updated_release.name).to eq(name) unless except_for == :name
+ expect(updated_release.description).to eq(description) unless except_for == :description
+ expect(updated_release.released_at).to eq(released_at) unless except_for == :released_at
+
+ # Right now the milestones are returned in a non-deterministic order.
+ # Because of this, we need to allow for milestones to be returned in any order.
+ # Once https://gitlab.com/gitlab-org/gitlab/-/issues/259012 has been
+ # fixed, this can be updated to expect a specific order.
+ expect(updated_release.milestones).to match_array([milestone_12_3, milestone_12_4]) unless except_for == :milestones
+ end
+ end
+
+ shared_examples 'validation error with message' do |message|
+ it 'returns the updated release as nil' do
+ expect(updated_release).to be_nil
+ end
+
+ it 'returns a validation error' do
+ expect(subject[:errors]).to eq([message])
+ end
+ end
+
+ describe '#ready?' do
+ let(:current_user) { developer }
+
+ subject(:ready) do
+ mutation.ready?(**mutation_arguments)
+ end
+
+ context 'when released_at is included as an argument but is passed nil' do
+ let(:mutation_arguments) { super().merge(released_at: nil) }
+
+ it 'raises a validation error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'if the releasedAt argument is provided, it cannot be null')
+ end
+ end
+
+ context 'when milestones is included as an argument but is passed nil' do
+ let(:mutation_arguments) { super().merge(milestones: nil) }
+
+ it 'raises a validation error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'if the milestones argument is provided, it cannot be null')
+ end
+ end
+ end
+
+ describe '#resolve' do
+ subject(:resolve) do
+ mutation.resolve(**mutation_arguments)
+ end
+
+ let(:updated_release) { subject[:release] }
+
+ context 'when the current user has access to create releases' do
+ let(:current_user) { developer }
+
+ context 'name' do
+ let(:mutation_arguments) { super().merge(name: updated_name) }
+
+ context 'when a new name is provided' do
+ let(:updated_name) { 'Updated name' }
+
+ it 'updates the name' do
+ expect(updated_release.name).to eq(updated_name)
+ end
+
+ it_behaves_like 'no changes to the release except for the', :name
+ end
+
+ context 'when nil is provided' do
+ let(:updated_name) { nil }
+
+ it 'updates the name to be the tag name' do
+ expect(updated_release.name).to eq(tag)
+ end
+
+ it_behaves_like 'no changes to the release except for the', :name
+ end
+ end
+
+ context 'description' do
+ let(:mutation_arguments) { super().merge(description: updated_description) }
+
+ context 'when a new description is provided' do
+ let(:updated_description) { 'Updated description' }
+
+ it 'updates the description' do
+ expect(updated_release.description).to eq(updated_description)
+ end
+
+ it_behaves_like 'no changes to the release except for the', :description
+ end
+
+ context 'when nil is provided' do
+ let(:updated_description) { nil }
+
+ it 'updates the description to nil' do
+ expect(updated_release.description).to eq(nil)
+ end
+
+ it_behaves_like 'no changes to the release except for the', :description
+ end
+ end
+
+ context 'released_at' do
+ let(:mutation_arguments) { super().merge(released_at: updated_released_at) }
+
+ context 'when a new released_at is provided' do
+ let(:updated_released_at) { Time.parse('2020-12-10').utc }
+
+ it 'updates the released_at' do
+ expect(updated_release.released_at).to eq(updated_released_at)
+ end
+
+ it_behaves_like 'no changes to the release except for the', :released_at
+ end
+ end
+
+ context 'milestones' do
+ let(:mutation_arguments) { super().merge(milestones: updated_milestones) }
+
+ context 'when a new set of milestones is provided provided' do
+ let(:updated_milestones) { [milestone_12_3.title] }
+
+ it 'updates the milestone associations' do
+ expect(updated_release.milestones).to eq([milestone_12_3])
+ end
+
+ it_behaves_like 'no changes to the release except for the', :milestones
+ end
+
+ context 'when an empty array is provided' do
+ let(:updated_milestones) { [] }
+
+ it 'removes all milestone associations' do
+ expect(updated_release.milestones).to eq([])
+ end
+
+ it_behaves_like 'no changes to the release except for the', :milestones
+ end
+
+ context 'when a non-existent milestone title is provided' do
+ let(:updated_milestones) { ['not real'] }
+
+ it_behaves_like 'validation error with message', 'Milestone(s) not found: not real'
+ end
+
+ context 'when a milestone title from a different project is provided' do
+ let(:milestone_in_different_project) { create(:milestone, title: 'milestone in different project') }
+ let(:updated_milestones) { [milestone_in_different_project.title] }
+
+ it_behaves_like 'validation error with message', 'Milestone(s) not found: milestone in different project'
+ end
+ end
+
+ context 'validation' do
+ context 'when no updated fields are provided' do
+ it_behaves_like 'validation error with message', 'params is empty'
+ end
+
+ context 'when the tag does not exist' do
+ let(:mutation_arguments) { super().merge(tag: 'not-a-real-tag') }
+
+ it_behaves_like 'validation error with message', 'Tag does not exist'
+ end
+
+ context 'when the project does not exist' do
+ let(:mutation_arguments) { super().merge(project_path: 'not/a/real/path') }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, "The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ end
+ end
+ end
+ end
+
+ context "when the current user doesn't have access to update releases" do
+ let(:current_user) { reporter }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, "The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ end
+ end
+ end
+end