Commit 02c59bd4 authored by Sarah Yasonik's avatar Sarah Yasonik Committed by Vitali Tatarintev

Add escalation policy field and mutation for incidents

Allows users to set an escalation policy for an incident,
which should start escalations for the incident after
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76986.
Gated behind :incident_escalations feature flag.
parent 50df24e4
......@@ -2941,6 +2941,27 @@ Input type: `IssueSetEpicInput`
| <a id="mutationissuesetepicerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationissuesetepicissue"></a>`issue` | [`Issue`](#issue) | Issue after mutation. |
### `Mutation.issueSetEscalationPolicy`
Input type: `IssueSetEscalationPolicyInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationissuesetescalationpolicyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationissuesetescalationpolicyescalationpolicyid"></a>`escalationPolicyId` | [`IncidentManagementEscalationPolicyID`](#incidentmanagementescalationpolicyid) | Global ID of the escalation policy to assign to the issue. Policy will be removed if absent or set to null. |
| <a id="mutationissuesetescalationpolicyiid"></a>`iid` | [`String!`](#string) | IID of the issue to mutate. |
| <a id="mutationissuesetescalationpolicyprojectpath"></a>`projectPath` | [`ID!`](#id) | Project the issue to mutate is in. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationissuesetescalationpolicyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationissuesetescalationpolicyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationissuesetescalationpolicyissue"></a>`issue` | [`Issue`](#issue) | Issue after mutation. |
### `Mutation.issueSetIteration`
Input type: `IssueSetIterationInput`
......@@ -10294,6 +10315,7 @@ Relationship between an epic and an issue.
| <a id="epicissueemailsdisabled"></a>`emailsDisabled` | [`Boolean!`](#boolean) | Indicates if a project has email notifications disabled: `true` if email notifications are disabled. |
| <a id="epicissueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. |
| <a id="epicissueepicissueid"></a>`epicIssueId` | [`ID!`](#id) | ID of the epic-issue relation. |
| <a id="epicissueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. |
| <a id="epicissuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. |
| <a id="epicissuehidden"></a>`hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. |
| <a id="epicissuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. |
......@@ -11476,6 +11498,7 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount).
| <a id="issueduedate"></a>`dueDate` | [`Time`](#time) | Due date of the issue. |
| <a id="issueemailsdisabled"></a>`emailsDisabled` | [`Boolean!`](#boolean) | Indicates if a project has email notifications disabled: `true` if email notifications are disabled. |
| <a id="issueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. |
| <a id="issueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. |
| <a id="issuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. |
| <a id="issuehidden"></a>`hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. |
| <a id="issuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. |
......@@ -41,6 +41,9 @@ module EE
field :metric_images, [::Types::MetricImageType], null: true,
description: 'Metric images associated to the issue.'
field :escalation_policy, ::Types::IncidentManagement::EscalationPolicyType, null: true,
description: 'Escalation policy associated with the issue. Available for issues which support escalation.'
def iteration
::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Iteration, object.sprint_id).find
end
......@@ -68,6 +71,10 @@ module EE
def health_status
object.supports_health_status? ? object.health_status : nil
end
def escalation_policy
object.escalation_policies_available? ? object.escalation_status&.policy : nil
end
end
end
end
......
......@@ -12,6 +12,7 @@ module EE
mount_mutation ::Mutations::Issues::SetIteration
mount_mutation ::Mutations::Issues::SetWeight
mount_mutation ::Mutations::Issues::SetEpic
mount_mutation ::Mutations::Issues::SetEscalationPolicy
mount_mutation ::Mutations::Issues::PromoteToEpic
mount_mutation ::Mutations::EpicTree::Reorder
mount_mutation ::Mutations::Epics::Update
......
# frozen_string_literal: true
module Mutations
module Issues
class SetEscalationPolicy < Base
graphql_name 'IssueSetEscalationPolicy'
argument :escalation_policy_id,
::Types::GlobalIDType[::IncidentManagement::EscalationPolicy],
required: false,
loads: Types::IncidentManagement::EscalationPolicyType,
description: 'Global ID of the escalation policy to assign to the issue. ' \
'Policy will be removed if absent or set to null.'
def resolve(project_path:, iid:, escalation_policy:)
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
authorize_escalation_status!(project)
::Issues::UpdateService.new(
project: project,
current_user: current_user,
params: { escalation_status: { policy: escalation_policy } }
).execute(issue)
{
issue: issue,
errors: errors_on_object(issue)
}
end
private
def authorize_escalation_status!(project)
return if Ability.allowed?(current_user, :update_escalation_status, project)
raise_resource_not_available_error!
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Issues::SetEscalationPolicy do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:escalation_policy) { create(:incident_management_escalation_policy, project: project) }
let_it_be(:issue, reload: true) { create(:incident, project: project) }
let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, issue: issue) }
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
describe '#resolve' do
let(:args) { { escalation_policy: escalation_policy } }
let(:mutated_issue) { result[:issue] }
subject(:result) { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, **args) }
it_behaves_like 'permission level for issue mutation is correctly verified', true
before do
stub_licensed_features(oncall_schedules: true, escalation_policies: true)
end
context 'when the user can update the issue' do
before do
project.add_reporter(user)
end
it_behaves_like 'permission level for issue mutation is correctly verified', true
context 'when the user can update the escalation status' do
before do
project.add_developer(user)
end
it 'returns the issue with the escalation policy' do
expect(mutated_issue).to eq(issue)
expect(mutated_issue.escalation_status.policy).to eq(escalation_policy)
expect(result[:errors]).to be_empty
end
it 'returns errors when issue update fails' do
issue.update_column(:author_id, nil)
expect(result[:errors]).not_to be_empty
end
context 'when passing escalation_policy_id as nil' do
let(:args) { { escalation_policy: nil } }
before do
escalation_status.update!(policy: escalation_policy, escalations_started_at: Time.current)
end
it 'removes the iteration' do
expect(mutated_issue.escalation_status.policy).to eq(nil)
end
end
end
end
end
end
......@@ -13,6 +13,7 @@ RSpec.describe GitlabSchema.types['Issue'] do
it { expect(described_class).to have_graphql_field(:blocked_by_issues) }
it { expect(described_class).to have_graphql_field(:sla_due_at) }
it { expect(described_class).to have_graphql_field(:metric_images) }
it { expect(described_class).to have_graphql_field(:escalation_policy) }
context 'N+1 queries' do
let_it_be(:user) { create(:user) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Setting the escalation policy of an issue' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:incident, project: project) }
let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue) }
let_it_be(:escalation_policy) { create(:incident_management_escalation_policy, project: project) }
let_it_be(:user) { create(:user) }
let(:policy_input) { global_id_of(escalation_policy) }
let(:input) { { project_path: project.full_path, iid: issue.iid.to_s, escalation_policy_id: policy_input } }
let(:current_user) { user }
let(:mutation) do
graphql_mutation(:issue_set_escalation_policy, input) do
<<~QL
clientMutationId
errors
issue {
iid
escalationPolicy {
id
name
}
}
QL
end
end
let(:mutation_response) { graphql_mutation_response(:issue_set_escalation_policy) }
before do
stub_licensed_features(oncall_schedules: true, escalation_policies: true)
project.add_developer(user)
end
context 'when user does not have permission to edit the escalation status' do
let(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns a top-level access error'
end
it 'sets given escalation_policy to the escalation status for the issue' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['errors']).to be_empty
expect(mutation_response['issue']['escalationPolicy']['id']).to eq(global_id_of(escalation_policy))
expect(mutation_response['issue']['escalationPolicy']['name']).to eq(escalation_policy.name)
expect(escalation_status.reload.policy).to eq(escalation_policy)
end
context 'when escalation_policy_id is nil' do
let(:policy_input) { nil }
before do
escalation_status.update!(policy_id: escalation_policy.id, escalations_started_at: Time.current)
end
it 'removes existing escalation policy' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['errors']).to be_empty
expect(escalation_status.reload.policy).to be_nil
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