Commit b3a15a14 authored by Alper Akgun's avatar Alper Akgun

Merge branch '344058-create-timeline-event-from-comment-graphql-mutation' into 'master'

Add mutation to promote timeline event from a note

See merge request gitlab-org/gitlab!80633
parents 58f540fa 1a3c1a90
......@@ -4432,6 +4432,25 @@ 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.timelineEventPromoteFromNote`
Input type: `TimelineEventPromoteFromNoteInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationtimelineeventpromotefromnoteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelineeventpromotefromnotenoteid"></a>`noteId` | [`NoteID!`](#noteid) | Note ID from which the timeline event promoted. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationtimelineeventpromotefromnoteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelineeventpromotefromnoteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationtimelineeventpromotefromnotetimelineevent"></a>`timelineEvent` | [`TimelineEventType`](#timelineeventtype) | Timeline event. |
### `Mutation.timelineEventUpdate`
Input type: `TimelineEventUpdateInput`
......@@ -79,8 +79,9 @@ 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::Create
mount_mutation ::Mutations::IncidentManagement::TimelineEvent::PromoteFromNote
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 PromoteFromNote < Base
graphql_name 'TimelineEventPromoteFromNote'
argument :note_id, Types::GlobalIDType[::Note],
required: true,
description: 'Note ID from which the timeline event promoted.'
def resolve(note_id:)
note = find_object(id: note_id)
incident = note&.noteable
authorize!(incident)
response ::IncidentManagement::TimelineEvents::CreateService.new(
incident,
current_user,
promoted_from_note: note,
note: note.note,
occurred_at: note.created_at
).execute
end
private
def find_object(id:)
GitlabSchema.object_from_id(id, expected_type: ::Note).sync
end
def authorize!(object)
raise_noteable_not_incident! if object && !object.try(:incident?)
super
end
def raise_noteable_not_incident!
raise_resource_not_available_error! 'Note does not belong to an incident'
end
end
end
end
end
......@@ -22,7 +22,8 @@ module IncidentManagement
note: params[:note],
action: params.fetch(:action, DEFAULT_ACTION),
note_html: params[:note_html].presence || params[:note],
occurred_at: params[:occurred_at]
occurred_at: params[:occurred_at],
promoted_from_note: params[:promoted_from_note]
}
timeline_event = IncidentManagement::TimelineEvent.new(timeline_event_params)
......
......@@ -19,44 +19,32 @@ RSpec.describe Mutations::IncidentManagement::TimelineEvent::Create do
subject(:resolve) { mutation_for(project, current_user).resolve(incident_id: incident.to_global_id, **args) }
context 'when a user has permissions to create a timeline event' do
let(:expected_timeline_event) do
instance_double(
'IncidentManagement::TimelineEvent',
note: args[:note],
occurred_at: args[:occurred_at].to_s,
incident: incident,
author: current_user,
promoted_from_note: nil
)
end
before do
project.add_developer(current_user)
end
context 'when TimelineEvents::CreateService responds with success' do
it 'adds timeline event to database' do
expect { resolve }.to change(IncidentManagement::TimelineEvent, :count).by(1)
end
end
it_behaves_like 'creating an incident timeline event'
context 'when TimelineEvents::CreateService responds with an error' do
let(:args) { {} }
it 'returns errors' do
expect(resolve).to eq(timeline_event: nil, errors: ["Occurred at can't be blank, Note can't be blank, and Note html can't be blank"])
end
end
end
context 'when a user has no permissions to create 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)
it_behaves_like 'responding with an incident timeline errors',
errors: ["Occurred at can't be blank, Note can't be blank, and Note html can't be blank"]
end
end
context 'when timeline event feature is not available' do
before do
stub_licensed_features(incident_timeline_events: false)
end
it 'raises and error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
it_behaves_like 'failing to create an incident timeline event'
end
private
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::IncidentManagement::TimelineEvent::PromoteFromNote do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:incident) { create(:incident, project: project) }
let_it_be(:comment) { create(:note, project: project, noteable: incident) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:issue_comment) { create(:note, project: project, noteable: issue) }
let_it_be(:alert) { create(:alert_management_alert, project: project) }
let_it_be(:alert_comment) { create(:note, project: project, noteable: alert) }
let(:args) { { note_id: comment.to_global_id.to_s } }
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 create timeline event' do
let(:expected_timeline_event) do
instance_double(
'IncidentManagement::TimelineEvent',
note: comment.note,
occurred_at: comment.created_at.to_s,
incident: incident,
author: current_user,
promoted_from_note: comment
)
end
before do
project.add_developer(current_user)
end
it_behaves_like 'creating an incident timeline event'
context 'when TimelineEvents::CreateService responds with an error' do
before do
allow_next_instance_of(::IncidentManagement::TimelineEvents::CreateService) do |service|
allow(service).to receive(:execute).and_return(
ServiceResponse.error(payload: { timeline_event: nil }, message: 'Some error')
)
end
end
it_behaves_like 'responding with an incident timeline errors', errors: ['Some error']
end
end
context 'when note does not exist' do
let(:args) { { note_id: 'gid://gitlab/Note/0' } }
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when note does not belong to an incident' do
let(:args) { { note_id: issue_comment.to_global_id.to_s } }
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when note belongs to anything else but issuable' do
let(:args) { { note_id: alert_comment.to_global_id.to_s } }
it 'raises an error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
it_behaves_like 'failing to create an incident timeline event'
end
private
def mutation_for(project, user)
described_class.new(object: project, context: { current_user: user }, field: nil)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Promote an incident timeline event from a comment' 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(:comment) { create(:note, project: project, noteable: incident) }
let(:input) { { note_id: comment.to_global_id.to_s } }
let(:mutation) do
graphql_mutation(:timeline_event_promote_from_note, input) do
<<~QL
clientMutationId
errors
timelineEvent {
author { id username }
incident { id title }
promotedFromNote { id }
note
action
editable
occurredAt
}
QL
end
end
let(:mutation_response) { graphql_mutation_response(:timeline_event_promote_from_note) }
before do
stub_licensed_features(incident_timeline_events: true)
project.add_developer(user)
end
it 'creates incident timeline event from the note', :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' => user.to_global_id.to_s,
'username' => user.username
},
'incident' => {
'id' => incident.to_global_id.to_s,
'title' => incident.title
},
'promotedFromNote' => {
'id' => comment.to_global_id.to_s
},
'note' => comment.note,
'action' => 'comment',
'editable' => false,
'occurredAt' => comment.created_at.iso8601
)
end
end
......@@ -7,9 +7,18 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
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_it_be(:comment) { create(:note, project: project, noteable: incident) }
let(:args) do
{
note: 'note',
occurred_at: Time.current,
action: 'new comment',
promoted_from_note: comment
}
end
let(:current_user) { user_with_permissions }
let(:args) { { 'note': 'note', 'occurred_at': Time.current, 'action': 'new comment' } }
let(:service) { described_class.new(incident, current_user, args) }
before do
......@@ -30,7 +39,7 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
end
shared_examples 'success response' do
it 'has timeline event' do
it 'has timeline event', :aggregate_failures do
expect(execute).to be_success
result = execute.payload[:timeline_event]
......@@ -39,6 +48,7 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
expect(result.incident).to eq(incident)
expect(result.project).to eq(project)
expect(result.note).to eq(args[:note])
expect(result.promoted_from_note).to eq(comment)
end
end
......@@ -71,7 +81,7 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
end
context 'with default action' do
let(:args) { { 'note': 'note', 'occurred_at': Time.current } }
let(:args) { { note: 'note', occurred_at: Time.current, promoted_from_note: comment } }
it_behaves_like 'success response'
......
# frozen_string_literal: true
require 'spec_helper'
# Requres:
# * subject with a 'resolve' name
# * Defined expected timeline event via `let(:expected_timeline_event) { instance_double(...) }`
RSpec.shared_examples 'creating an incident timeline event' do
it 'creates a timeline event' do
expect { resolve }.to change(IncidentManagement::TimelineEvent, :count).by(1)
end
it 'responds with a timeline event', :aggregate_failures do
response = resolve
timeline_event = IncidentManagement::TimelineEvent.last!
expect(response).to match(timeline_event: timeline_event, errors: be_empty)
expect(timeline_event.promoted_from_note).to eq(expected_timeline_event.promoted_from_note)
expect(timeline_event.note).to eq(expected_timeline_event.note)
expect(timeline_event.occurred_at.to_s).to eq(expected_timeline_event.occurred_at)
expect(timeline_event.incident).to eq(expected_timeline_event.incident)
expect(timeline_event.author).to eq(expected_timeline_event.author)
end
end
# Requres
# * subject with a 'resolve' name
# * a user factory with a 'current_user' name
RSpec.shared_examples 'failing to create an incident timeline event' do
context 'when a user has no permissions to create 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 event 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
# Requres:
# * subject with a 'resolve' name
RSpec.shared_examples 'responding with an incident timeline errors' do |errors:|
it 'returns errors' do
expect(resolve).to eq(timeline_event: nil, errors: errors)
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