Commit 54a01581 authored by Aleksei Lipniagov's avatar Aleksei Lipniagov

Merge branch 'add-timeline-event' into 'master'

Create API for Timeline Event

See merge request gitlab-org/gitlab!78884
parents 83e7fa47 033b1622
...@@ -4362,6 +4362,27 @@ Input type: `TerraformStateUnlockInput` ...@@ -4362,6 +4362,27 @@ 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.timelineEventCreate`
Input type: `TimelineEventCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationtimelineeventcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelineeventcreateincidentid"></a>`incidentId` | [`IssueID!`](#issueid) | Incident ID of the timeline event. |
| <a id="mutationtimelineeventcreatenote"></a>`note` | [`String!`](#string) | Text note of the timeline event. |
| <a id="mutationtimelineeventcreateoccurredat"></a>`occurredAt` | [`Time!`](#time) | Timestamp of when the event occurred. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationtimelineeventcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelineeventcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationtimelineeventcreatetimelineevent"></a>`timelineEvent` | [`TimelineEventType`](#timelineeventtype) | Timeline event. |
### `Mutation.timelineEventDestroy` ### `Mutation.timelineEventDestroy`
Input type: `TimelineEventDestroyInput` Input type: `TimelineEventDestroyInput`
...@@ -80,6 +80,7 @@ module EE ...@@ -80,6 +80,7 @@ module EE
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::Update mount_mutation ::Mutations::IncidentManagement::TimelineEvent::Update
mount_mutation ::Mutations::IncidentManagement::TimelineEvent::Create
mount_mutation ::Mutations::IncidentManagement::TimelineEvent::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
......
...@@ -34,8 +34,8 @@ module Mutations ...@@ -34,8 +34,8 @@ module Mutations
raise_resource_not_available_error! 'Timeline events are not supported for this project' raise_resource_not_available_error! 'Timeline events are not supported for this project'
end end
def timeline_events_available?(timeline_event) def timeline_events_available?(object)
::Gitlab::IncidentManagement.timeline_events_available?(timeline_event.project) ::Gitlab::IncidentManagement.timeline_events_available?(object.project)
end end
end end
end end
......
# frozen_string_literal: true
module Mutations
module IncidentManagement
module TimelineEvent
class Create < Base
graphql_name 'TimelineEventCreate'
argument :incident_id, Types::GlobalIDType[::Issue],
required: true,
description: 'Incident ID of the timeline event.'
argument :note, GraphQL::Types::String,
required: true,
description: 'Text note of the timeline event.'
argument :occurred_at, Types::TimeType,
required: true,
description: 'Timestamp of when the event occurred.'
def resolve(incident_id:, **args)
incident = authorized_find!(id: incident_id)
authorize!(incident)
response ::IncidentManagement::TimelineEvents::CreateService.new(incident, current_user, args).execute
end
private
def find_object(id:)
GitlabSchema.object_from_id(id, expected_type: ::Issue).sync
end
end
end
end
end
# frozen_string_literal: true
module IncidentManagement
module TimelineEvents
DEFAULT_ACTION = 'comment'
class CreateService < TimelineEvents::BaseService
def initialize(incident, user, params)
@project = incident.project
@incident = incident
@user = user
@params = params
end
def execute
return error_no_permissions unless allowed?
timeline_event_params = {
project: project,
incident: incident,
author: user,
note: params[:note],
action: params.fetch(:action, DEFAULT_ACTION),
note_html: params[:note_html].presence || params[:note],
occurred_at: params[:occurred_at]
}
timeline_event = IncidentManagement::TimelineEvent.new(timeline_event_params)
if timeline_event.save
success(timeline_event)
else
error_in_save(timeline_event)
end
end
private
attr_reader :project, :user, :incident, :params
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::IncidentManagement::TimelineEvent::Create do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:incident) { create(:incident, project: project) }
let(:args) { { note: 'note', occurred_at: Time.current } }
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(incident_id: incident.to_global_id, **args) }
context 'when a user has permissions to create a timeline event' do
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
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)
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
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 'Creating 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(:event_occurred_at) { Time.current }
let_it_be(:note) { 'demo note' }
let(:input) { { incident_id: incident.to_global_id.to_s, note: note, occurred_at: event_occurred_at } }
let(:mutation) do
graphql_mutation(:timeline_event_create, input) do
<<~QL
clientMutationId
errors
timelineEvent {
id
author { id username }
incident { id title }
note
editable
action
occurredAt
}
QL
end
end
let(:mutation_response) { graphql_mutation_response(:timeline_event_create) }
before do
stub_licensed_features(incident_timeline_events: true)
project.add_developer(user)
end
it 'creates 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' => user.to_global_id.to_s,
'username' => user.username
},
'incident' => {
'id' => incident.to_global_id.to_s,
'title' => incident.title
},
'note' => note,
'action' => 'comment',
'editable' => false,
'occurredAt' => event_occurred_at.iso8601
)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::TimelineEvents::CreateService 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(: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
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
shared_examples 'success response' do
it 'has timeline event' do
expect(execute).to be_success
result = execute.payload[:timeline_event]
expect(result).to be_a(::IncidentManagement::TimelineEvent)
expect(result.author).to eq(current_user)
expect(result.incident).to eq(incident)
expect(result.project).to eq(project)
expect(result.note).to eq(args[:note])
end
end
subject(:execute) { service.execute }
context 'when current user is blank' 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 create 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 error occurs during creation' do
let(:args) { {} }
it_behaves_like 'error response', "Occurred at can't be blank, Note can't be blank, and Note html can't be blank"
end
context 'with default action' do
let(:args) { { 'note': 'note', 'occurred_at': Time.current } }
it_behaves_like 'success response'
it 'matches the default action', :aggregate_failures do
result = execute.payload[:timeline_event]
expect(result.action).to eq(IncidentManagement::TimelineEvents::DEFAULT_ACTION)
end
end
context 'with non_default action' do
it_behaves_like 'success response'
it 'matches the action from arguments', :aggregate_failures do
result = execute.payload[:timeline_event]
expect(result.action).to eq(args[:action])
end
end
it 'successfully creates a database record', :aggregate_failures do
expect { execute }.to change { ::IncidentManagement::TimelineEvent.count }.by(1)
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