Commit 7c9f931d authored by Dylan Griffith's avatar Dylan Griffith

Merge branch '268356-add-escalation-policy-mutation-graphql-new' into 'master'

GraphQL mutation for creating Escalation Policies

See merge request gitlab-org/gitlab!61966
parents e98110f3 a117f471
......@@ -2153,6 +2153,28 @@ Input type: `EpicTreeReorderInput`
| <a id="mutationepictreereorderclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationepictreereordererrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.escalationPolicyCreate`
Input type: `EscalationPolicyCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationescalationpolicycreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationescalationpolicycreatedescription"></a>`description` | [`String`](#string) | The description of the escalation policy. |
| <a id="mutationescalationpolicycreatename"></a>`name` | [`String!`](#string) | The name of the escalation policy. |
| <a id="mutationescalationpolicycreateprojectpath"></a>`projectPath` | [`ID!`](#id) | The project to create the escalation policy for. |
| <a id="mutationescalationpolicycreaterules"></a>`rules` | [`[EscalationRuleInput!]!`](#escalationruleinput) | The steps of the escalation policy. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationescalationpolicycreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationescalationpolicycreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationescalationpolicycreateescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | The escalation policy. |
### `Mutation.exportRequirements`
Input type: `ExportRequirementsInput`
......@@ -15927,6 +15949,18 @@ A node of an epic tree.
| <a id="epictreenodefieldsinputtypenewparentid"></a>`newParentId` | [`EpicID`](#epicid) | ID of the new parent epic. |
| <a id="epictreenodefieldsinputtyperelativeposition"></a>`relativePosition` | [`MoveType`](#movetype) | The type of the switch, after or before allowed. |
### `EscalationRuleInput`
Represents an escalation rule.
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="escalationruleinputelapsedtimeseconds"></a>`elapsedTimeSeconds` | [`Int!`](#int) | The time in seconds before the rule is activated. |
| <a id="escalationruleinputoncallscheduleiid"></a>`oncallScheduleIid` | [`ID!`](#id) | The on-call schedule to notify. |
| <a id="escalationruleinputstatus"></a>`status` | [`EscalationRuleStatus!`](#escalationrulestatus) | The status required to prevent the rule from activating. |
### `JiraUsersMappingInputType`
#### Arguments
......
......@@ -75,6 +75,7 @@ module EE
mount_mutation ::Mutations::IncidentManagement::OncallRotation::Create
mount_mutation ::Mutations::IncidentManagement::OncallRotation::Update
mount_mutation ::Mutations::IncidentManagement::OncallRotation::Destroy
mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Create
mount_mutation ::Mutations::AppSec::Fuzzing::Api::CiConfiguration::Create
prepend(Types::DeprecatedMutations)
......
# frozen_string_literal: true
module Mutations
module IncidentManagement
module EscalationPolicy
class Create < BaseMutation
include ResolvesProject
graphql_name 'EscalationPolicyCreate'
authorize :admin_incident_management_escalation_policy
field :escalation_policy,
::Types::IncidentManagement::EscalationPolicyType,
null: true,
description: 'The escalation policy.'
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'The project to create the escalation policy for.'
argument :name, GraphQL::STRING_TYPE,
required: true,
description: 'The name of the escalation policy.'
argument :description, GraphQL::STRING_TYPE,
required: false,
description: 'The description of the escalation policy.'
argument :rules, [Types::IncidentManagement::EscalationRuleInputType],
required: true,
description: 'The steps of the escalation policy.'
def resolve(project_path:, **args)
@project = authorized_find!(project_path: project_path, **args)
args = prepare_rules_attributes(args)
result = ::IncidentManagement::EscalationPolicies::CreateService.new(
project,
current_user,
args
).execute
response(result)
end
private
attr_reader :project
def find_object(project_path:, **args)
unless project = resolve_project(full_path: project_path).sync
raise_project_not_found
end
unless ::Gitlab::IncidentManagement.escalation_policies_available?(project)
raise_resource_not_available_error! 'Escalation policies are not supported for this project'
end
project
end
def prepare_rules_attributes(args)
args[:rules_attributes] = args.delete(:rules).map(&:to_h)
iids = args[:rules_attributes].collect { |rule| rule[:oncall_schedule_iid] }
found_schedules = schedules_for_iids(iids)
args[:rules_attributes].each do |rule|
iid = rule.delete(:oncall_schedule_iid).to_i
rule[:oncall_schedule] = found_schedules[iid]
raise Gitlab::Graphql::Errors::ResourceNotAvailable, "The oncall schedule for iid #{iid} could not be found" unless rule[:oncall_schedule]
end
args
end
def schedules_for_iids(iids)
schedules = ::IncidentManagement::OncallSchedulesFinder.new(current_user, project, iid: iids).execute
schedules.index_by(&:iid)
end
def response(result)
{
escalation_policy: result.payload[:escalation_policy],
errors: result.errors
}
end
def raise_project_not_found
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'The project could not be found'
end
end
end
end
end
# frozen_string_literal: true
module Types
module IncidentManagement
class EscalationRuleInputType < BaseInputObject
graphql_name 'EscalationRuleInput'
description 'Represents an escalation rule'
argument :oncall_schedule_iid, GraphQL::ID_TYPE, # rubocop: disable Graphql/IDType
description: 'The on-call schedule to notify.',
required: true
argument :elapsed_time_seconds, GraphQL::INT_TYPE,
description: 'The time in seconds before the rule is activated.',
required: true
argument :status, Types::IncidentManagement::EscalationRuleStatusEnum,
description: 'The status required to prevent the rule from activating.',
required: true
end
end
end
......@@ -5,10 +5,12 @@ module IncidentManagement
self.table_name = 'incident_management_escalation_policies'
belongs_to :project
has_many :rules, class_name: 'EscalationRule', inverse_of: :policy, foreign_key: 'policy_id'
has_many :rules, class_name: 'EscalationRule', inverse_of: :policy, foreign_key: 'policy_id', index_errors: true
validates :name, presence: true, uniqueness: { scope: [:project_id] }, length: { maximum: 72 }
validates :description, length: { maximum: 160 }
validates :rules, presence: true
accepts_nested_attributes_for :rules
end
end
......@@ -10,6 +10,7 @@ module IncidentManagement
enum status: AlertManagement::Alert::STATUSES.slice(:acknowledged, :resolved)
validates :status, presence: true
validates :oncall_schedule, presence: true
validates :elapsed_time_seconds,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 24.hours }
......
# frozen_string_literal: true
module IncidentManagement
module EscalationPolicies
class BaseService
def allowed?
user&.can?(:admin_incident_management_escalation_policy, project)
end
def available?
::Gitlab::IncidentManagement.escalation_policies_available?(project)
end
def error(message)
ServiceResponse.error(message: message)
end
def success(escalation_policy)
ServiceResponse.success(payload: { escalation_policy: escalation_policy })
end
def error_no_license
error(_('Escalation policies are not supported for this project'))
end
end
end
end
# frozen_string_literal: true
module IncidentManagement
module EscalationPolicies
class CreateService < EscalationPolicies::BaseService
# @param [Project] project
# @param [User] user
# @param [Hash] params
# @option params [String] name
# @option params [String] description
# @option params [Array<Hash>] rules_attributes
# @option rules [Integer] oncall_schedule_id
# @option rules [Integer] elapsed_time_seconds
# @option rules [String] status
def initialize(project, user, params)
@project = project
@user = user
@params = params
end
def execute
return error_no_license unless available?
return error_no_permissions unless allowed?
return error_no_rules if params[:rules_attributes].blank?
escalation_policy = project.incident_management_escalation_policies.create(params)
return error_in_create(escalation_policy) unless escalation_policy.persisted?
success(escalation_policy)
end
private
attr_reader :project, :user, :params
def error_no_permissions
error(_('You have insufficient permissions to create an escalation policy for this project'))
end
def error_in_create(escalation_policy)
error(escalation_policy.errors.full_messages.to_sentence)
end
def error_no_rules
error(_('A rule must be provided to create an escalation policy'))
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::IncidentManagement::EscalationPolicy::Create do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let(:args) do
{
project_path: project.full_path,
name: 'Escalation policy name',
description: 'Escalation policy description',
rules: [
{
oncall_schedule_iid: oncall_schedule.iid,
elapsed_time_seconds: 300,
status: ::IncidentManagement::EscalationRule.statuses[:acknowledged]
},
{
oncall_schedule_iid: oncall_schedule.iid,
elapsed_time_seconds: 600,
status: ::IncidentManagement::EscalationRule.statuses[:resolved]
}
]
}
end
describe '#resolve' do
subject(:resolve) { mutation_for(project, current_user).resolve(project_path: project.full_path, **args) }
shared_examples 'returns a GraphQL error' do |error|
it { is_expected.to match(escalation_policy: nil, errors: [error])}
end
shared_examples 'raises a resource not available error' do |error|
specify do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, error)
end
end
before do
project.add_maintainer(current_user)
end
context 'project does not have feature' do
before do
stub_licensed_features(oncall_schedules: true)
end
it_behaves_like 'raises a resource not available error', 'Escalation policies are not supported for this project'
end
context 'project has feature' do
before do
stub_licensed_features(oncall_schedules: true, escalation_policies: true)
stub_feature_flags(escalation_policies_mvc: project)
end
context 'user has access to project' do
it 'returns the escalation policy with no errors' do
expect(resolve).to match(
escalation_policy: ::IncidentManagement::EscalationPolicy.last!,
errors: be_empty
)
rules = resolve[:escalation_policy].rules
expect(rules.size).to eq(2)
expect(rules).to match_array([
have_attributes(oncall_schedule_id: oncall_schedule.id, elapsed_time_seconds: 300, status: 'acknowledged'),
have_attributes(oncall_schedule_id: oncall_schedule.id, elapsed_time_seconds: 600, status: 'resolved')
])
end
context 'rules are missing' do
before do
args[:rules] = []
end
it_behaves_like 'returns a GraphQL error', "A rule must be provided to create an escalation policy"
end
context 'schedule that does not belong to the project' do
let!(:other_schedule) { create(:incident_management_oncall_schedule, iid: 2) }
before do
args[:rules][0][:oncall_schedule_iid] = other_schedule.iid
end
it_behaves_like 'raises a resource not available error', 'The oncall schedule for iid 2 could not be found'
context 'user does not have permission for project' do
before do
project.add_reporter(current_user)
end
it_behaves_like 'raises a resource not available error', "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
end
end
end
context 'user does not have permission for project' do
before do
project.add_reporter(current_user)
end
it_behaves_like 'raises a resource not available error', "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
end
end
context 'invalid project path' do
before do
args[:project_path] = 'something/incorrect'
end
it_behaves_like 'raises a resource not available error', 'The project could not be found'
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 escalation policy' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:schedule) { create(:incident_management_oncall_schedule, project: project) }
let(:params) { create_params }
let(:mutation) do
graphql_mutation(:escalation_policy_create, params) do
<<-QL.strip_heredoc
escalationPolicy {
id
name
description
rules {
status
elapsedTimeSeconds
oncallSchedule {
name
iid
}
}
}
errors
QL
end
end
before do
stub_licensed_features(oncall_schedules: true, escalation_policies: true)
stub_feature_flags(escalation_policies_mvc: project)
project.add_maintainer(current_user)
end
subject(:resolve) { post_graphql_mutation(mutation, current_user: current_user) }
it 'successfully creates the policy and rules' do
resolve
expect(mutation_response['errors']).to be_empty
escalation_policy_response = mutation_response['escalationPolicy']
expect(escalation_policy_response['name']).to eq(create_params[:name])
expect(escalation_policy_response['description']).to eq(create_params[:description])
expect(escalation_policy_response['rules'].size).to eq(create_params[:rules].size)
first_rule = escalation_policy_response['rules'].first
expect(first_rule['status']).to eq('ACKNOWLEDGED')
expect(first_rule['elapsedTimeSeconds']).to eq(create_params.dig(:rules, 0, :elapsedTimeSeconds))
expect(first_rule['status']).to eq(create_params.dig(:rules, 0, :status))
end
context 'errors' do
context 'user does not have permission' do
subject(:resolve) { post_graphql_mutation(mutation, current_user: create(:user)) }
it 'raises an error' do
resolve
expect_graphql_errors_to_include("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
end
end
context 'no rules given' do
before do
params[:rules] = []
end
it 'raises an error' do
resolve
expect(mutation_response['errors'][0]).to eq("A rule must be provided to create an escalation policy")
end
end
context 'feature flag disabled' do
before do
stub_feature_flags(escalation_policies_mvc: false)
end
it 'raises an error' do
resolve
expect_graphql_errors_to_include("Escalation policies are not supported for this project")
end
end
end
def mutation_response
graphql_mutation_response(:escalation_policy_create)
end
def create_params
{
projectPath: project.full_path,
name: 'Escalation Policy 1',
description: 'Description',
rules: [
{
oncallScheduleIid: schedule.iid,
elapsedTimeSeconds: 60,
status: 'ACKNOWLEDGED'
}
]
}
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::EscalationPolicies::CreateService do
let_it_be_with_refind(:project) { create(:project) }
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let(:user) { user_with_permissions }
before do
stub_licensed_features(oncall_schedules: true, escalation_policies: true)
stub_feature_flags(escalation_policies_mvc: project)
end
before_all do
project.add_maintainer(user_with_permissions)
end
let(:rule_params) do
[
{
oncall_schedule_id: oncall_schedule.id,
elapsed_time_seconds: 60,
status: :resolved
}
]
end
let(:params) { { name: 'Policy', description: 'Description', rules_attributes: rule_params } }
let(:service) { described_class.new(project, user, params) }
describe '#execute' do
subject(:execute) { service.execute }
shared_examples 'error response' do |message|
it 'does not save the policy and has an informative message' do
expect { execute }.not_to change(IncidentManagement::EscalationPolicy, :count)
expect(execute).to be_error
expect(execute.message).to eq(message)
end
end
context 'when user does not have access' do
let(:user) { create(:user) }
it_behaves_like 'error response', 'You have insufficient permissions to create an escalation policy for this project'
end
context 'when license is not enabled' do
before do
stub_licensed_features(oncall_schedules: true, escalation_policies: false)
end
it_behaves_like 'error response', 'Escalation policies are not supported for this project'
end
context 'validation errors' do
context 'validation error in policy' do
before do
params[:name] = ''
end
it_behaves_like 'error response', "Name can't be blank"
end
context 'no rules are given' do
let(:rule_params) { nil }
it_behaves_like 'error response', 'A rule must be provided to create an escalation policy'
end
context 'oncall schedule is blank' do
before do
rule_params[0][:oncall_schedule_id] = nil
end
it_behaves_like 'error response', "Rules[0] oncall schedule can't be blank"
end
end
context 'valid params' do
it 'creates the policy and rules' do
expect(execute).to be_success
policy = execute.payload[:escalation_policy]
expect(policy).to be_a(::IncidentManagement::EscalationPolicy)
end
end
end
end
......@@ -1483,6 +1483,9 @@ msgstr ""
msgid "A rebase is already in progress."
msgstr ""
msgid "A rule must be provided to create an escalation policy"
msgstr ""
msgid "A secure token that identifies an external storage request."
msgstr ""
......@@ -13105,6 +13108,9 @@ msgstr ""
msgid "Escalation policies"
msgstr ""
msgid "Escalation policies are not supported for this project"
msgstr ""
msgid "EscalationPolicies|+ Add an additional rule"
msgstr ""
......@@ -37423,6 +37429,9 @@ msgstr ""
msgid "You have insufficient permissions to create an HTTP integration for this project"
msgstr ""
msgid "You have insufficient permissions to create an escalation policy for this project"
msgstr ""
msgid "You have insufficient permissions to create an on-call schedule for this project"
msgstr ""
......
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