Commit 39422b7b authored by Doug Stull's avatar Doug Stull

Merge branch '344061-delete-timeline-event-graphql-mutation' into 'master'

Add GraphQL mutation to destroy timeline events

See merge request gitlab-org/gitlab!78192
parents e2c953d9 71a1be80
...@@ -4336,6 +4336,25 @@ Input type: `TerraformStateUnlockInput` ...@@ -4336,6 +4336,25 @@ Input type: `TerraformStateUnlockInput`
| <a id="mutationterraformstateunlockclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | <a id="mutationterraformstateunlockclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationterraformstateunlockerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationterraformstateunlockerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.timelineEventDestroy`
Input type: `TimelineEventDestroyInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationtimelineeventdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelineeventdestroyid"></a>`id` | [`IncidentManagementTimelineEventID!`](#incidentmanagementtimelineeventid) | Timeline event ID to remove. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationtimelineeventdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelineeventdestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationtimelineeventdestroytimelineevent"></a>`timelineEvent` | [`TimelineEventType`](#timelineeventtype) | Timeline event. |
### `Mutation.todoCreate` ### `Mutation.todoCreate`
Input type: `TodoCreateInput` Input type: `TodoCreateInput`
...@@ -79,6 +79,7 @@ module EE ...@@ -79,6 +79,7 @@ module EE
mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Create mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Create
mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Update mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Update
mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Destroy mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Destroy
mount_mutation ::Mutations::IncidentManagement::TimelineEvent::Destroy
mount_mutation ::Mutations::AppSec::Fuzzing::API::CiConfiguration::Create mount_mutation ::Mutations::AppSec::Fuzzing::API::CiConfiguration::Create
mount_mutation ::Mutations::AppSec::Fuzzing::Coverage::Corpus::Create, feature_flag: :corpus_management mount_mutation ::Mutations::AppSec::Fuzzing::Coverage::Corpus::Create, feature_flag: :corpus_management
mount_mutation ::Mutations::Projects::SetComplianceFramework mount_mutation ::Mutations::Projects::SetComplianceFramework
......
# frozen_string_literal: true
module Mutations
module IncidentManagement
module TimelineEvent
class Base < BaseMutation
field :timeline_event,
::Types::IncidentManagement::TimelineEventType,
null: true,
description: 'Timeline event.'
authorize :admin_incident_management_timeline_event
private
def response(result)
{
timeline_event: result.payload[:timeline_event],
errors: result.errors
}
end
def find_object(id:)
GitlabSchema.object_from_id(id, expected_type: ::IncidentManagement::TimelineEvent).sync
end
def authorize!(object)
raise_feature_not_available! if object && !timeline_events_available?(object)
super
end
def raise_feature_not_available!
raise_resource_not_available_error! 'Timeline events are not supported for this project'
end
def timeline_events_available?(timeline_event)
::Gitlab::IncidentManagement.timeline_events_available?(timeline_event.project)
end
end
end
end
end
# frozen_string_literal: true
module Mutations
module IncidentManagement
module TimelineEvent
class Destroy < Base
graphql_name 'TimelineEventDestroy'
argument :id, Types::GlobalIDType[::IncidentManagement::TimelineEvent],
required: true,
description: 'Timeline event ID to remove.'
def resolve(id:)
timeline_event = authorized_find!(id: id)
response ::IncidentManagement::TimelineEvents::DestroyService.new(
timeline_event,
current_user
).execute
end
end
end
end
end
...@@ -22,6 +22,10 @@ module EE ...@@ -22,6 +22,10 @@ module EE
enable :read_incident_management_timeline_event enable :read_incident_management_timeline_event
end end
rule { can?(:read_issue) & can?(:developer_access) & timeline_events_available }.policy do
enable :admin_incident_management_timeline_event
end
rule { can?(:create_issue) & can?(:update_issue) }.policy do rule { can?(:create_issue) & can?(:update_issue) }.policy do
enable :upload_issuable_metric_image enable :upload_issuable_metric_image
end end
......
# frozen_string_literal: true
module IncidentManagement
module TimelineEvents
class BaseService
def allowed?
user&.can?(:admin_incident_management_timeline_event, incident)
end
def success(timeline_event)
ServiceResponse.success(payload: { timeline_event: timeline_event })
end
def error(message)
ServiceResponse.error(message: message)
end
def error_no_permissions
error(_('You have insufficient permissions to manage timeline events for this incident'))
end
def error_in_save(timeline_event)
error(timeline_event.errors.full_messages.to_sentence)
end
end
end
end
# frozen_string_literal: true
module IncidentManagement
module TimelineEvents
class DestroyService < TimelineEvents::BaseService
# @param timeline_event [IncidentManagement::TimelineEvent]
# @param user [User]
def initialize(timeline_event, user)
@timeline_event = timeline_event
@user = user
@incident = timeline_event.incident
end
def execute
return error_no_permissions unless allowed?
if timeline_event.destroy
success(timeline_event)
else
error_in_save(timeline_event)
end
end
private
attr_reader :timeline_event, :user, :incident
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::IncidentManagement::TimelineEvent::Destroy do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:incident) { create(:incident, project: project) }
let(:timeline_event) { create(:incident_management_timeline_event, incident: incident, project: project) }
let(:args) { { id: timeline_event.to_global_id } }
specify { expect(described_class).to require_graphql_authorizations(:admin_incident_management_timeline_event) }
before do
stub_licensed_features(incident_timeline_events: true)
end
describe '#resolve' do
subject(:resolve) { mutation_for(project, current_user).resolve(**args) }
context 'when a user has permissions to delete timeline event' do
before do
project.add_developer(current_user)
end
context 'when TimelineEvents::DestroyService responds with success' do
it 'returns the timeline event with no errors' do
expect(resolve).to eq(
timeline_event: timeline_event,
errors: []
)
end
end
context 'when TimelineEvents::DestroyService responds with an error' do
before do
allow_next_instance_of(::IncidentManagement::TimelineEvents::DestroyService) do |service|
allow(service)
.to receive(:execute)
.and_return(ServiceResponse.error(payload: { timeline_event: nil }, message: 'An error has occurred'))
end
end
it 'returns errors' do
expect(resolve).to eq(
timeline_event: nil,
errors: ['An error has occurred']
)
end
end
end
context 'when a user has no permissions to delete timeline event' do
before do
project.add_guest(current_user)
end
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when timeline events feature is not available' do
before do
stub_licensed_features(incident_timeline_events: false)
end
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
private
def mutation_for(project, user)
described_class.new(object: project, context: { current_user: user }, field: nil)
end
end
...@@ -3,9 +3,10 @@ ...@@ -3,9 +3,10 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe IssuablePolicy, models: true do RSpec.describe IssuablePolicy, models: true do
let(:non_member) { create(:user) } let_it_be(:non_member) { create(:user) }
let(:guest) { create(:user) } let_it_be(:guest) { create(:user) }
let(:reporter) { create(:user) } let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let(:guest_issue) { create(:issue, project: project, author: guest) } let(:guest_issue) { create(:issue, project: project, author: guest) }
let(:reporter_issue) { create(:issue, project: project, author: reporter) } let(:reporter_issue) { create(:issue, project: project, author: reporter) }
...@@ -13,6 +14,7 @@ RSpec.describe IssuablePolicy, models: true do ...@@ -13,6 +14,7 @@ RSpec.describe IssuablePolicy, models: true do
before do before do
project.add_guest(guest) project.add_guest(guest)
project.add_reporter(reporter) project.add_reporter(reporter)
project.add_developer(developer)
allow(::Gitlab::IncidentManagement).to receive(:timeline_events_available?).with(project).and_return(true) allow(::Gitlab::IncidentManagement).to receive(:timeline_events_available?).with(project).and_return(true)
end end
...@@ -48,6 +50,14 @@ RSpec.describe IssuablePolicy, models: true do ...@@ -48,6 +50,14 @@ RSpec.describe IssuablePolicy, models: true do
expect(permissions(guest, issue)).to be_allowed(:read_incident_management_timeline_event) expect(permissions(guest, issue)).to be_allowed(:read_incident_management_timeline_event)
end end
it 'disallows reporters from managing timeline events' do
expect(permissions(reporter, issue)).to be_disallowed(:admin_incident_management_timeline_event)
end
it 'allows developers to manage timeline events' do
expect(permissions(developer, issue)).to be_allowed(:admin_incident_management_timeline_event)
end
context 'when timeline events are not available' do context 'when timeline events are not available' do
before do before do
allow(::Gitlab::IncidentManagement).to receive(:timeline_events_available?).with(project).and_return(false) allow(::Gitlab::IncidentManagement).to receive(:timeline_events_available?).with(project).and_return(false)
...@@ -56,6 +66,10 @@ RSpec.describe IssuablePolicy, models: true do ...@@ -56,6 +66,10 @@ RSpec.describe IssuablePolicy, models: true do
it 'disallows guests from reading timeline events' do it 'disallows guests from reading timeline events' do
expect(permissions(guest, issue)).to be_disallowed(:read_incident_management_timeline_event) expect(permissions(guest, issue)).to be_disallowed(:read_incident_management_timeline_event)
end end
it 'disallows developers from managing timeline events' do
expect(permissions(developer, issue)).to be_disallowed(:admin_incident_management_timeline_event)
end
end end
end end
end end
...@@ -89,6 +103,14 @@ RSpec.describe IssuablePolicy, models: true do ...@@ -89,6 +103,14 @@ RSpec.describe IssuablePolicy, models: true do
expect(permissions(guest, issue)).to be_allowed(:read_incident_management_timeline_event) expect(permissions(guest, issue)).to be_allowed(:read_incident_management_timeline_event)
end end
it 'disallows reporters from managing timeline events' do
expect(permissions(reporter, issue)).to be_disallowed(:admin_incident_management_timeline_event)
end
it 'allows developers to manage timeline events' do
expect(permissions(developer, issue)).to be_allowed(:admin_incident_management_timeline_event)
end
context 'when timeline events are not available' do context 'when timeline events are not available' do
before do before do
allow(::Gitlab::IncidentManagement).to receive(:timeline_events_available?).with(project).and_return(false) allow(::Gitlab::IncidentManagement).to receive(:timeline_events_available?).with(project).and_return(false)
...@@ -97,6 +119,10 @@ RSpec.describe IssuablePolicy, models: true do ...@@ -97,6 +119,10 @@ RSpec.describe IssuablePolicy, models: true do
it 'disallows guests from reading timeline events' do it 'disallows guests from reading timeline events' do
expect(permissions(guest, issue)).to be_disallowed(:read_incident_management_timeline_event) expect(permissions(guest, issue)).to be_disallowed(:read_incident_management_timeline_event)
end end
it 'disallows developers from managing timeline events' do
expect(permissions(developer, issue)).to be_disallowed(:admin_incident_management_timeline_event)
end
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Removing an incident timeline event' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:incident) { create(:incident, project: project) }
let_it_be(:timeline_event) { create(:incident_management_timeline_event, incident: incident, project: project) }
let(:variables) { { id: timeline_event.to_global_id.to_s } }
let(:mutation) do
graphql_mutation(:timeline_event_destroy, variables) do
<<~QL
clientMutationId
errors
timelineEvent {
id
author { id username }
incident { id title }
note
noteHtml
editable
action
occurredAt
createdAt
updatedAt
}
QL
end
end
let(:mutation_response) { graphql_mutation_response(:timeline_event_destroy) }
before do
stub_licensed_features(incident_timeline_events: true)
project.add_developer(user)
end
it 'removes incident timeline event', :aggregate_failures do
post_graphql_mutation(mutation, current_user: user)
timeline_event_response = mutation_response['timelineEvent']
expect(response).to have_gitlab_http_status(:success)
expect(timeline_event_response).to include(
'author' => {
'id' => timeline_event.author.to_global_id.to_s,
'username' => timeline_event.author.username
},
'incident' => {
'id' => incident.to_global_id.to_s,
'title' => incident.title
},
'note' => timeline_event.note,
'noteHtml' => timeline_event.note_html,
'editable' => false,
'action' => timeline_event.action,
'occurredAt' => timeline_event.occurred_at.iso8601,
'createdAt' => timeline_event.created_at.iso8601,
'updatedAt' => timeline_event.updated_at.iso8601
)
expect { timeline_event.reload }.to raise_error ActiveRecord::RecordNotFound
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::TimelineEvents::DestroyService do
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be_with_refind(:incident) { create(:incident, project: project) }
let!(:timeline_event) { create(:incident_management_timeline_event, incident: incident, project: project) }
let(:current_user) { user_with_permissions }
let(:params) { {} }
let(:service) { described_class.new(timeline_event, current_user) }
before do
stub_licensed_features(incident_timeline_events: true)
end
before_all do
project.add_developer(user_with_permissions)
project.add_reporter(user_without_permissions)
end
describe '#execute' do
shared_examples 'error response' do |message|
it 'has an informative message' do
expect(execute).to be_error
expect(execute.message).to eq(message)
end
end
subject(:execute) { service.execute }
context 'when current user is anonymous' do
let(:current_user) { nil }
it_behaves_like 'error response', 'You have insufficient permissions to manage timeline events for this incident'
end
context 'when user does not have permissions to remove timeline events' do
let(:current_user) { user_without_permissions }
it_behaves_like 'error response', 'You have insufficient permissions to manage timeline events for this incident'
end
context 'when feature is not available' do
before do
stub_licensed_features(incident_timeline_events: false)
end
it_behaves_like 'error response', 'You have insufficient permissions to manage timeline events for this incident'
end
context 'when an error occurs during removal' do
before do
allow(timeline_event).to receive(:destroy).and_return(false)
timeline_event.errors.add(:note, 'cannot be removed')
end
it_behaves_like 'error response', 'Note cannot be removed'
end
it 'successfully returns the timeline event', :aggregate_failures do
expect(execute).to be_success
result = execute.payload[:timeline_event]
expect(result).to be_a(::IncidentManagement::TimelineEvent)
expect(result.id).to eq(timeline_event.id)
end
end
end
...@@ -40968,6 +40968,9 @@ msgstr "" ...@@ -40968,6 +40968,9 @@ msgstr ""
msgid "You have insufficient permissions to create an on-call schedule for this project" msgid "You have insufficient permissions to create an on-call schedule for this project"
msgstr "" msgstr ""
msgid "You have insufficient permissions to manage timeline events for this incident"
msgstr ""
msgid "You have insufficient permissions to remove an on-call rotation from this project" msgid "You have insufficient permissions to remove an on-call rotation from this project"
msgstr "" msgstr ""
......
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