Commit 1042cfed authored by Nathan Friend's avatar Nathan Friend Committed by Stan Hu

Add GraphQL mutation to delete release

This commit adds a new GraphQL mutation - `releaseDelete` - that deletes
a release.
parent fe6f4c1b
# frozen_string_literal: true
module Mutations
module Releases
class Delete < Base
graphql_name 'ReleaseDelete'
field :release,
Types::ReleaseType,
null: true,
description: 'The deleted release.'
argument :tag_name, GraphQL::STRING_TYPE,
required: true, as: :tag,
description: 'Name of the tag associated with the release to delete.'
authorize :destroy_release
def resolve(project_path:, tag:)
project = authorized_find!(full_path: project_path)
params = { tag: tag }.with_indifferent_access
result = ::Releases::DestroyService.new(project, current_user, params).execute
if result[:status] == :success
{
release: result[:release],
errors: []
}
else
{
release: nil,
errors: [result[:message]]
}
end
end
end
end
end
......@@ -66,6 +66,7 @@ module Types
mount_mutation Mutations::Notes::Destroy
mount_mutation Mutations::Releases::Create
mount_mutation Mutations::Releases::Update
mount_mutation Mutations::Releases::Delete
mount_mutation Mutations::Terraform::State::Delete
mount_mutation Mutations::Terraform::State::Lock
mount_mutation Mutations::Terraform::State::Unlock
......
---
title: Add GraphQL mutation to delete a release
merge_request: 48364
author:
type: added
......@@ -14066,6 +14066,7 @@ type Mutation {
prometheusIntegrationUpdate(input: PrometheusIntegrationUpdateInput!): PrometheusIntegrationUpdatePayload
promoteToEpic(input: PromoteToEpicInput!): PromoteToEpicPayload
releaseCreate(input: ReleaseCreateInput!): ReleaseCreatePayload
releaseDelete(input: ReleaseDeleteInput!): ReleaseDeletePayload
releaseUpdate(input: ReleaseUpdateInput!): ReleaseUpdatePayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload @deprecated(reason: "Use awardEmojiRemove. Deprecated in 13.2")
removeProjectFromSecurityDashboard(input: RemoveProjectFromSecurityDashboardInput!): RemoveProjectFromSecurityDashboardPayload
......@@ -18664,6 +18665,46 @@ type ReleaseCreatePayload {
release: Release
}
"""
Autogenerated input type of ReleaseDelete
"""
input ReleaseDeleteInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Full path of the project the release is associated with
"""
projectPath: ID!
"""
Name of the tag associated with the release to delete.
"""
tagName: String!
}
"""
Autogenerated return type of ReleaseDelete
"""
type ReleaseDeletePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The deleted release.
"""
release: Release
}
"""
An edge in a connection.
"""
......
......@@ -41110,6 +41110,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "releaseDelete",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "ReleaseDeleteInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "ReleaseDeletePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "releaseUpdate",
"description": null,
......@@ -54087,6 +54114,122 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "ReleaseDeleteInput",
"description": "Autogenerated input type of ReleaseDelete",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "Full path of the project the release is associated with",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "tagName",
"description": "Name of the tag associated with the release to delete.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ReleaseDeletePayload",
"description": "Autogenerated return type of ReleaseDelete",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "release",
"description": "The deleted release.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Release",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ReleaseEdge",
......@@ -2638,6 +2638,16 @@ Autogenerated return type of ReleaseCreate.
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `release` | Release | The release after mutation |
### ReleaseDeletePayload
Autogenerated return type of ReleaseDelete.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `release` | Release | The deleted release. |
### ReleaseEvidence
Evidence for a release.
......
# 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
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Deleting a release' do
include GraphqlHelpers
include Presentable
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_it_be(:maintainer) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:tag_name) { 'v1.1.0' }
let_it_be(:release) { create(:release, project: project, tag: tag_name) }
let(:mutation_name) { :release_delete }
let(:project_path) { project.full_path }
let(:mutation_arguments) do
{
projectPath: project_path,
tagName: tag_name
}
end
let(:mutation) do
graphql_mutation(mutation_name, mutation_arguments, <<~FIELDS)
release {
tagName
}
errors
FIELDS
end
let(:delete_release) { post_graphql_mutation(mutation, current_user: current_user) }
let(:mutation_response) { graphql_mutation_response(mutation_name)&.with_indifferent_access }
before do
project.add_guest(guest)
project.add_reporter(reporter)
project.add_developer(developer)
project.add_maintainer(maintainer)
end
shared_examples 'unauthorized or not found error' do
it 'returns a top-level error with message' do
delete_release
expect(mutation_response).to be_nil
expect(graphql_errors.count).to eq(1)
expect(graphql_errors.first['message']).to eq("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
end
end
context 'when the current user has access to update releases' do
let(:current_user) { maintainer }
it 'deletes the release' do
expect { delete_release }.to change { Release.count }.by(-1)
end
it 'returns the deleted release' do
delete_release
expected_release = { tagName: tag_name }.with_indifferent_access
expect(mutation_response[:release]).to eq(expected_release)
end
it 'does not remove the Git tag associated with the deleted release' do
expect { delete_release }.not_to change { Project.find_by_id(project.id).repository.tag_count }
end
it 'returns no errors' do
delete_release
expect(mutation_response[:errors]).to eq([])
end
context 'validation' do
context 'when the release does not exist' do
let_it_be(:tag_name) { 'not-a-real-release' }
it 'returns the release as null' do
delete_release
expect(mutation_response[:release]).to be_nil
end
it 'returns an errors-at-data message' do
delete_release
expect(mutation_response[:errors]).to eq(['Release does not exist'])
end
end
context 'when the project does not exist' do
let(: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 current user is a Developer' do
let(:current_user) { developer }
it_behaves_like 'unauthorized or not found error'
end
context 'when the current user is a Reporter' do
let(:current_user) { reporter }
it_behaves_like 'unauthorized or not found error'
end
context 'when the current user is a Guest' do
let(:current_user) { guest }
it_behaves_like 'unauthorized or not found error'
end
context 'when the current user is a public user' do
let(:current_user) { public_user }
it_behaves_like 'unauthorized or not found error'
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment