diff --git a/app/graphql/mutations/achievements/delete.rb b/app/graphql/mutations/achievements/delete.rb new file mode 100644 index 0000000000000000000000000000000000000000..0b510b44b4ef37d89ce9fb79e5bf9b1ccd69592b --- /dev/null +++ b/app/graphql/mutations/achievements/delete.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Mutations + module Achievements + class Delete < BaseMutation + graphql_name 'AchievementsDelete' + + include Gitlab::Graphql::Authorize::AuthorizeResource + + field :achievement, + ::Types::Achievements::AchievementType, + null: true, + description: 'Achievement.' + + argument :achievement_id, ::Types::GlobalIDType[::Achievements::Achievement], + required: true, + description: 'Global ID of the achievement being deleted.' + + authorize :admin_achievement + + def resolve(args) + achievement = authorized_find!(id: args[:achievement_id]) + + result = ::Achievements::DestroyService.new(current_user, achievement).execute + { achievement: result.payload, errors: result.errors } + end + + def find_object(id:) + GitlabSchema.object_from_id(id, expected_type: ::Achievements::Achievement) + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 9bdbdad43861d0046894dab218ba88ecb164f7a6..43ca4f9643d2d5d856a2c132a5b319afb8975463 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -8,6 +8,7 @@ class MutationType < BaseObject mount_mutation Mutations::Achievements::Award, alpha: { milestone: '15.10' } mount_mutation Mutations::Achievements::Create, alpha: { milestone: '15.8' } + mount_mutation Mutations::Achievements::Delete, alpha: { milestone: '15.11' } mount_mutation Mutations::Achievements::Revoke, alpha: { milestone: '15.10' } mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs mount_mutation Mutations::AlertManagement::CreateAlertIssue diff --git a/app/services/achievements/destroy_service.rb b/app/services/achievements/destroy_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..3204adb8e891596c052a9a7b7f10d9080378925b --- /dev/null +++ b/app/services/achievements/destroy_service.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Achievements + class DestroyService + attr_reader :current_user, :achievement + + def initialize(current_user, achievement) + @current_user = current_user + @achievement = achievement + end + + def execute + return error_no_permissions unless allowed? + + achievement.delete + ServiceResponse.success(payload: achievement) + end + + private + + def allowed? + current_user&.can?(:admin_achievement, achievement) + end + + def error_no_permissions + error('You have insufficient permissions to delete this achievement') + end + + def error(message) + ServiceResponse.error(message: Array(message)) + end + end +end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 27da9f2b653493120833d7b02047d54c6e1d09b1..312e8894071c6380c6bf985b2b433d3665da5f75 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -795,6 +795,29 @@ Input type: `AchievementsCreateInput` | <a id="mutationachievementscreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | <a id="mutationachievementscreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +### `Mutation.achievementsDelete` + +WARNING: +**Introduced** in 15.11. +This feature is in Alpha. It can be changed or removed at any time. + +Input type: `AchievementsDeleteInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationachievementsdeleteachievementid"></a>`achievementId` | [`AchievementsAchievementID!`](#achievementsachievementid) | Global ID of the achievement being deleted. | +| <a id="mutationachievementsdeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationachievementsdeleteachievement"></a>`achievement` | [`Achievement`](#achievement) | Achievement. | +| <a id="mutationachievementsdeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationachievementsdeleteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | + ### `Mutation.achievementsRevoke` WARNING: diff --git a/spec/graphql/mutations/achievements/delete_spec.rb b/spec/graphql/mutations/achievements/delete_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0eb6f5a2e6f9b29952473432f4b9dd5cdab925ef --- /dev/null +++ b/spec/graphql/mutations/achievements/delete_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Achievements::Delete, feature_category: :user_profile do + include GraphqlHelpers + + let_it_be(:developer) { create(:user) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:recipient) { create(:user) } + let_it_be(:group) { create(:group) } + + let(:achievement) { create(:achievement, namespace: group) } + + describe '#resolve' do + subject(:resolve_mutation) do + described_class.new(object: nil, context: { current_user: current_user }, field: nil).resolve( + achievement_id: achievement&.to_global_id + ) + end + + before_all do + group.add_developer(developer) + group.add_maintainer(maintainer) + end + + context 'when the user does not have permission' do + let(:current_user) { developer } + + it 'raises an error' do + expect { resolve_mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + .with_message(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR) + end + end + + context 'when the user has permission' do + let(:current_user) { maintainer } + + context 'when the params are invalid' do + let(:achievement) { nil } + + it 'returns the validation error' do + expect { resolve_mutation }.to raise_error { Gitlab::Graphql::Errors::ArgumentError } + end + end + + it 'deletes the achievement' do + resolve_mutation + + expect(Achievements::Achievement.find_by(id: achievement.id)).to be_nil + end + end + end + + specify { expect(described_class).to require_graphql_authorizations(:admin_achievement) } +end diff --git a/spec/requests/api/graphql/mutations/achievements/delete_spec.rb b/spec/requests/api/graphql/mutations/achievements/delete_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..276da4f46a8158bdf00519a32ee51b17aaa6072c --- /dev/null +++ b/spec/requests/api/graphql/mutations/achievements/delete_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Achievements::Delete, feature_category: :user_profile do + include GraphqlHelpers + + let_it_be(:developer) { create(:user) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:group) { create(:group) } + + let!(:achievement) { create(:achievement, namespace: group) } + let(:mutation) { graphql_mutation(:achievements_delete, params) } + let(:achievement_id) { achievement&.to_global_id } + let(:params) { { achievement_id: achievement_id } } + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + def mutation_response + graphql_mutation_response(:achievements_delete) + end + + before_all do + group.add_developer(developer) + group.add_maintainer(maintainer) + end + + context 'when the user does not have permission' do + let(:current_user) { developer } + + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not revoke any achievements' do + expect { subject }.not_to change { Achievements::Achievement.count } + end + end + + context 'when the user has permission' do + let(:current_user) { maintainer } + + context 'when the params are invalid' do + let(:achievement) { nil } + + it 'returns the validation error' do + subject + + expect(graphql_errors.to_s).to include('invalid value for achievementId (Expected value to not be null)') + end + end + + context 'when the achievement_id is invalid' do + let(:achievement_id) { "gid://gitlab/Achievements::Achievement/#{non_existing_record_id}" } + + it 'returns the validation error' do + subject + + expect(graphql_errors.to_s) + .to include("The resource that you are attempting to access does not exist or you don't have permission") + end + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(achievements: false) + end + + it 'returns the relevant error' do + subject + + expect(graphql_errors.to_s) + .to include("The resource that you are attempting to access does not exist or you don't have permission") + end + end + + it 'deletes the achievement' do + expect { subject }.to change { Achievements::Achievement.count }.by(-1) + end + end +end diff --git a/spec/services/achievements/destroy_service_spec.rb b/spec/services/achievements/destroy_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7af10ceec6af97827a9fe0beccc6a0bcec72bed2 --- /dev/null +++ b/spec/services/achievements/destroy_service_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Achievements::DestroyService, feature_category: :user_profile do + describe '#execute' do + let_it_be(:developer) { create(:user) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:group) { create(:group) } + + let(:achievement) { create(:achievement, namespace: group) } + + subject(:response) { described_class.new(current_user, achievement).execute } + + before_all do + group.add_developer(developer) + group.add_maintainer(maintainer) + end + + context 'when user does not have permission' do + let(:current_user) { developer } + + it 'returns an error' do + expect(response).to be_error + expect(response.message).to match_array( + ['You have insufficient permissions to delete this achievement']) + end + end + + context 'when user has permission' do + let(:current_user) { maintainer } + + it 'deletes the achievement' do + expect(response).to be_success + expect(Achievements::Achievement.find_by(id: achievement.id)).to be_nil + end + end + end +end