Commit cd15de7b authored by Vitali Tatarintev's avatar Vitali Tatarintev

Add Graphql mutation to update timeline event

Changelog: added
EE: true
parent 5b99f05f
......@@ -4355,6 +4355,27 @@ Input type: `TimelineEventDestroyInput`
| <a id="mutationtimelineeventdestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationtimelineeventdestroytimelineevent"></a>`timelineEvent` | [`TimelineEventType`](#timelineeventtype) | Timeline event. |
### `Mutation.timelineEventUpdate`
Input type: `TimelineEventUpdateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationtimelineeventupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelineeventupdateid"></a>`id` | [`IncidentManagementTimelineEventID!`](#incidentmanagementtimelineeventid) | ID of the timeline event to update. |
| <a id="mutationtimelineeventupdatenote"></a>`note` | [`String`](#string) | Text note of the timeline event. |
| <a id="mutationtimelineeventupdateoccurredat"></a>`occurredAt` | [`Time`](#time) | Timestamp when the event occurred. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationtimelineeventupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelineeventupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationtimelineeventupdatetimelineevent"></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::Update
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
......
# frozen_string_literal: true
module Mutations
module IncidentManagement
module TimelineEvent
class Update < Base
graphql_name 'TimelineEventUpdate'
argument :id, ::Types::GlobalIDType[::IncidentManagement::TimelineEvent],
required: true,
description: 'ID of the timeline event to update.'
argument :note, GraphQL::Types::String,
required: false,
description: 'Text note of the timeline event.'
argument :occurred_at, Types::TimeType,
required: false,
description: 'Timestamp when the event occurred.'
def resolve(id:, **args)
timeline_event = authorized_find!(id: id)
response ::IncidentManagement::TimelineEvents::UpdateService.new(
timeline_event,
current_user,
args
).execute
end
end
end
end
end
# frozen_string_literal: true
module IncidentManagement
module TimelineEvents
# @param timeline_event [IncidentManagement::TimelineEvent]
# @param user [User]
# @param params [Hash]
# @option params [string] note
# @option params [datetime] occurred_at
class UpdateService < TimelineEvents::BaseService
def initialize(timeline_event, user, params)
@timeline_event = timeline_event
@incident = timeline_event.incident
@user = user
@note = params[:note]
@occurred_at = params[:occurred_at]
end
def execute
return error_no_permissions unless allowed?
if timeline_event.update(update_params)
success(timeline_event)
else
error_in_save(timeline_event)
end
end
private
attr_reader :timeline_event, :incident, :user, :note, :occurred_at
def update_params
{ updated_by_user: user, note: note.presence, occurred_at: occurred_at.presence }.compact
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::IncidentManagement::TimelineEvent::Update do
let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:incident) { create(:incident, project: project) }
let_it_be_with_reload(:timeline_event) { create(:incident_management_timeline_event, project: project, incident: incident) }
let(:args) do
{
id: timeline_event_id,
note: note,
occurred_at: occurred_at
}
end
let(:note) { 'Updated Note' }
let(:timeline_event_id) { GitlabSchema.id_from_object(timeline_event).to_s }
let(:occurred_at) { 1.minute.ago }
before do
project.add_developer(developer)
project.add_reporter(reporter)
stub_licensed_features(incident_timeline_events: true)
end
describe '#resolve' do
let(:current_user) { developer }
subject(:resolve) { mutation_for(current_user).resolve(**args) }
shared_examples 'failed update with a top-level access error' do |error|
specify do
expect { resolve }.to raise_error(
Gitlab::Graphql::Errors::ResourceNotAvailable,
error || Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
)
end
end
context 'when licenced feature is available' do
context 'when user has permissions to update the timeline event' do
context 'when timeline event exists' do
it 'updates the timeline event' do
expect { resolve }.to change { timeline_event.reload.note }.to(note)
.and change { timeline_event.reload.occurred_at.to_s }.to(occurred_at.to_s)
end
it 'returns updated timeline event' do
expect(resolve).to eq(
timeline_event: timeline_event.reload,
errors: []
)
end
context 'when there is a validation error' do
let(:occurred_at) { 'invalid date' }
it 'does not update the timeline event' do
expect { resolve }.not_to change { timeline_event.reload.updated_at }
end
it 'responds with error' do
expect(resolve).to eq(
timeline_event: nil,
errors: ["Occurred at can't be blank"]
)
end
end
end
context 'when timeline event cannot be found' do
let(:timeline_event_id) do
Gitlab::GlobalId.build(
nil,
model_name: ::IncidentManagement::TimelineEvent.name,
id: non_existing_record_id
).to_s
end
it_behaves_like 'failed update with a top-level access error'
end
end
context 'when user does not have permissions to update the timeline event' do
let(:current_user) { reporter }
it_behaves_like 'failed update with a top-level access error'
end
end
context 'when licensed feature is not available' do
before do
stub_licensed_features(incident_timeline_events: false)
end
it_behaves_like 'failed update with a top-level access error', 'Timeline events are not supported for this project'
end
end
private
def mutation_for(user)
described_class.new(object: nil, context: { current_user: user }, field: nil)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Updating 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_with_reload(:timeline_event) { create(:incident_management_timeline_event, incident: incident, project: project) }
let(:occurred_at) { 1.minute.ago.iso8601 }
let(:variables) do
{
id: timeline_event.to_global_id.to_s,
note: 'Updated note',
occurred_at: occurred_at
}
end
let(:mutation) do
graphql_mutation(:timeline_event_update, variables) do
<<~QL
clientMutationId
errors
timelineEvent {
id
author { id username }
updatedByUser { id username }
incident { id title }
note
noteHtml
occurredAt
createdAt
updatedAt
}
QL
end
end
let(:mutation_response) { graphql_mutation_response(:timeline_event_update) }
before do
stub_licensed_features(incident_timeline_events: true)
project.add_developer(user)
end
it 'updates the timeline event', :aggregate_failures do
post_graphql_mutation(mutation, current_user: user)
timeline_event_response = mutation_response['timelineEvent']
timeline_event.reload
expect(response).to have_gitlab_http_status(:success)
expect(timeline_event_response).to include(
'id' => timeline_event.to_global_id.to_s,
'author' => {
'id' => timeline_event.author.to_global_id.to_s,
'username' => timeline_event.author.username
},
'updatedByUser' => {
'id' => user.to_global_id.to_s,
'username' => user.username
},
'incident' => {
'id' => incident.to_global_id.to_s,
'title' => incident.title
},
'note' => 'Updated note',
'noteHtml' => timeline_event.note_html,
'occurredAt' => occurred_at,
'createdAt' => timeline_event.created_at.iso8601,
'updatedAt' => timeline_event.updated_at.iso8601
)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::TimelineEvents::UpdateService do
let_it_be(: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, project: project, incident: incident) }
let(:occurred_at) { 1.minute.ago }
let(:params) { { note: 'Updated note', occurred_at: occurred_at } }
before do
stub_licensed_features(incident_timeline_events: true)
end
describe '#execute' do
shared_examples 'successful response' do
it 'responds with success', :aggregate_failures do
expect(execute).to be_success
expect(execute.payload).to eq(timeline_event: timeline_event.reload)
end
end
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) { described_class.new(timeline_event, user, params).execute }
context 'when user has permissions' do
before do
project.add_developer(user)
end
it_behaves_like 'successful response'
it 'updates attributes' do
expect { execute }.to change { timeline_event.note }.to(params[:note])
.and change { timeline_event.occurred_at }.to(params[:occurred_at])
end
context 'when note is nil' do
let(:params) { { occurred_at: occurred_at } }
it_behaves_like 'successful response'
it 'does not update the note' do
expect { execute }.not_to change { timeline_event.reload.note }
end
it 'updates occurred_at' do
expect { execute }.to change { timeline_event.occurred_at }.to(params[:occurred_at])
end
end
context 'when note is blank' do
let(:params) { { note: '', occurred_at: occurred_at } }
it_behaves_like 'successful response'
it 'does not update the note' do
expect { execute }.not_to change { timeline_event.reload.note }
end
it 'updates occurred_at' do
expect { execute }.to change { timeline_event.occurred_at }.to(params[:occurred_at])
end
end
context 'when occurred_at is nil' do
let(:params) { { note: 'Updated note' } }
it_behaves_like 'successful response'
it 'updates the note' do
expect { execute }.to change { timeline_event.note }.to(params[:note])
end
it 'does not update occurred_at' do
expect { execute }.not_to change { timeline_event.reload.occurred_at }
end
end
end
context 'when user does not have permissions' do
before do
project.add_reporter(user)
end
it_behaves_like 'error response', 'You have insufficient permissions to manage timeline events for this incident'
end
context 'when licensed 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
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