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`
| <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. |
### `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`
Input type: `TodoCreateInput`
......@@ -79,6 +79,7 @@ module EE
mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Create
mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Update
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::Coverage::Corpus::Create, feature_flag: :corpus_management
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
enable :read_incident_management_timeline_event
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
enable :upload_issuable_metric_image
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 @@
require 'spec_helper'
RSpec.describe IssuablePolicy, models: true do
let(:non_member) { create(:user) }
let(:guest) { create(:user) }
let(:reporter) { create(:user) }
let_it_be(:non_member) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let(:guest_issue) { create(:issue, project: project, author: guest) }
let(:reporter_issue) { create(:issue, project: project, author: reporter) }
......@@ -13,6 +14,7 @@ RSpec.describe IssuablePolicy, models: true do
before do
project.add_guest(guest)
project.add_reporter(reporter)
project.add_developer(developer)
allow(::Gitlab::IncidentManagement).to receive(:timeline_events_available?).with(project).and_return(true)
end
......@@ -48,6 +50,14 @@ RSpec.describe IssuablePolicy, models: true do
expect(permissions(guest, issue)).to be_allowed(:read_incident_management_timeline_event)
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
before do
allow(::Gitlab::IncidentManagement).to receive(:timeline_events_available?).with(project).and_return(false)
......@@ -56,6 +66,10 @@ RSpec.describe IssuablePolicy, models: true do
it 'disallows guests from reading timeline events' do
expect(permissions(guest, issue)).to be_disallowed(:read_incident_management_timeline_event)
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
......@@ -89,6 +103,14 @@ RSpec.describe IssuablePolicy, models: true do
expect(permissions(guest, issue)).to be_allowed(:read_incident_management_timeline_event)
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
before do
allow(::Gitlab::IncidentManagement).to receive(:timeline_events_available?).with(project).and_return(false)
......@@ -97,6 +119,10 @@ RSpec.describe IssuablePolicy, models: true do
it 'disallows guests from reading timeline events' do
expect(permissions(guest, issue)).to be_disallowed(:read_incident_management_timeline_event)
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
......
# 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 ""
msgid "You have insufficient permissions to create an on-call schedule for this project"
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"
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