Commit 0bfca6b5 authored by Vitali Tatarintev's avatar Vitali Tatarintev

Merge branch 'sy-add-graphql-support-for-incident-escalation-policy' into 'master'

Add mutation for setting escalation policy on incidents

See merge request gitlab-org/gitlab!76987
parents 909295c8 02c59bd4
...@@ -2941,6 +2941,27 @@ Input type: `IssueSetEpicInput` ...@@ -2941,6 +2941,27 @@ Input type: `IssueSetEpicInput`
| <a id="mutationissuesetepicerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationissuesetepicerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationissuesetepicissue"></a>`issue` | [`Issue`](#issue) | Issue after 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` ### `Mutation.issueSetIteration`
Input type: `IssueSetIterationInput` Input type: `IssueSetIterationInput`
...@@ -10294,6 +10315,7 @@ Relationship between an epic and an issue. ...@@ -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="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="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="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="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="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. | | <a id="epicissuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. |
...@@ -11476,6 +11498,7 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount). ...@@ -11476,6 +11498,7 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount).
| <a id="issueduedate"></a>`dueDate` | [`Time`](#time) | Due date of the issue. | | <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="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="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="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="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. | | <a id="issuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. |
...@@ -41,6 +41,9 @@ module EE ...@@ -41,6 +41,9 @@ module EE
field :metric_images, [::Types::MetricImageType], null: true, field :metric_images, [::Types::MetricImageType], null: true,
description: 'Metric images associated to the issue.' 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 def iteration
::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Iteration, object.sprint_id).find ::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Iteration, object.sprint_id).find
end end
...@@ -68,6 +71,10 @@ module EE ...@@ -68,6 +71,10 @@ module EE
def health_status def health_status
object.supports_health_status? ? object.health_status : nil object.supports_health_status? ? object.health_status : nil
end end
def escalation_policy
object.escalation_policies_available? ? object.escalation_status&.policy : nil
end
end end
end end
end end
......
...@@ -12,6 +12,7 @@ module EE ...@@ -12,6 +12,7 @@ module EE
mount_mutation ::Mutations::Issues::SetIteration mount_mutation ::Mutations::Issues::SetIteration
mount_mutation ::Mutations::Issues::SetWeight mount_mutation ::Mutations::Issues::SetWeight
mount_mutation ::Mutations::Issues::SetEpic mount_mutation ::Mutations::Issues::SetEpic
mount_mutation ::Mutations::Issues::SetEscalationPolicy
mount_mutation ::Mutations::Issues::PromoteToEpic mount_mutation ::Mutations::Issues::PromoteToEpic
mount_mutation ::Mutations::EpicTree::Reorder mount_mutation ::Mutations::EpicTree::Reorder
mount_mutation ::Mutations::Epics::Update 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 ...@@ -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(:blocked_by_issues) }
it { expect(described_class).to have_graphql_field(:sla_due_at) } 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(:metric_images) }
it { expect(described_class).to have_graphql_field(:escalation_policy) }
context 'N+1 queries' do context 'N+1 queries' do
let_it_be(:user) { create(:user) } 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