Commit 5e40a799 authored by Sarah Yasonik's avatar Sarah Yasonik Committed by Vitali Tatarintev

Add mutation to edit escalation policies

parent 11be5622
...@@ -2154,6 +2154,28 @@ Input type: `EscalationPolicyDestroyInput` ...@@ -2154,6 +2154,28 @@ Input type: `EscalationPolicyDestroyInput`
| <a id="mutationescalationpolicydestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationescalationpolicydestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationescalationpolicydestroyescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | The escalation policy. | | <a id="mutationescalationpolicydestroyescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | The escalation policy. |
### `Mutation.escalationPolicyUpdate`
Input type: `EscalationPolicyUpdateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationescalationpolicyupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationescalationpolicyupdatedescription"></a>`description` | [`String`](#string) | The description of the escalation policy. |
| <a id="mutationescalationpolicyupdateid"></a>`id` | [`IncidentManagementEscalationPolicyID!`](#incidentmanagementescalationpolicyid) | The ID of the on-call schedule to create the on-call rotation in. |
| <a id="mutationescalationpolicyupdatename"></a>`name` | [`String`](#string) | The name of the escalation policy. |
| <a id="mutationescalationpolicyupdaterules"></a>`rules` | [`[EscalationRuleInput!]`](#escalationruleinput) | The steps of the escalation policy. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationescalationpolicyupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationescalationpolicyupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationescalationpolicyupdateescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | The escalation policy. |
### `Mutation.exportRequirements` ### `Mutation.exportRequirements`
Input type: `ExportRequirementsInput` Input type: `ExportRequirementsInput`
......
...@@ -77,6 +77,7 @@ module EE ...@@ -77,6 +77,7 @@ module EE
mount_mutation ::Mutations::IncidentManagement::OncallRotation::Update mount_mutation ::Mutations::IncidentManagement::OncallRotation::Update
mount_mutation ::Mutations::IncidentManagement::OncallRotation::Destroy mount_mutation ::Mutations::IncidentManagement::OncallRotation::Destroy
mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Create mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Create
mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Update
mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Destroy mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Destroy
mount_mutation ::Mutations::AppSec::Fuzzing::API::CiConfiguration::Create mount_mutation ::Mutations::AppSec::Fuzzing::API::CiConfiguration::Create
......
...@@ -21,7 +21,45 @@ module Mutations ...@@ -21,7 +21,45 @@ module Mutations
end end
def find_object(id:) def find_object(id:)
GitlabSchema.object_from_id(id, expected_type: ::IncidentManagement::EscalationPolicy) GitlabSchema.object_from_id(id, expected_type: ::IncidentManagement::EscalationPolicy).sync
end
# Provide more granular error message for feature availability
# ahead of role-based authorization
def authorize!(object)
raise_feature_not_available! if object && !escalation_policies_available?(object)
super
end
def raise_feature_not_available!
raise_resource_not_available_error! 'Escalation policies are not supported for this project'
end
def escalation_policies_available?(policy)
::Gitlab::IncidentManagement.escalation_policies_available?(policy.project)
end
def prepare_rules_attributes(project, args)
return args unless rules = args.delete(:rules)
iids = rules.collect { |rule| rule[:oncall_schedule_iid] }
found_schedules = schedules_for_iids(project, iids)
rules_attributes = rules.map { |rule| prepare_rule(found_schedules, rule.to_h) }
args.merge(rules_attributes: rules_attributes)
end
def prepare_rule(schedules, rule)
iid = rule.delete(:oncall_schedule_iid).to_i
rule.merge(oncall_schedule: schedules[iid])
end
def schedules_for_iids(project, iids)
schedules = ::IncidentManagement::OncallSchedulesFinder.new(current_user, project, iid: iids).execute
schedules.index_by(&:iid)
end end
end end
end end
......
...@@ -3,18 +3,11 @@ ...@@ -3,18 +3,11 @@
module Mutations module Mutations
module IncidentManagement module IncidentManagement
module EscalationPolicy module EscalationPolicy
class Create < BaseMutation class Create < Base
include ResolvesProject include ResolvesProject
graphql_name 'EscalationPolicyCreate' 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, argument :project_path, GraphQL::ID_TYPE,
required: true, required: true,
description: 'The project to create the escalation policy for.' description: 'The project to create the escalation policy for.'
...@@ -32,9 +25,8 @@ module Mutations ...@@ -32,9 +25,8 @@ module Mutations
description: 'The steps of the escalation policy.' description: 'The steps of the escalation policy.'
def resolve(project_path:, **args) def resolve(project_path:, **args)
@project = authorized_find!(project_path: project_path, **args) project = authorized_find!(project_path: project_path, **args)
args = prepare_rules_attributes(project, args)
args = prepare_rules_attributes(args)
result = ::IncidentManagement::EscalationPolicies::CreateService.new( result = ::IncidentManagement::EscalationPolicies::CreateService.new(
project, project,
...@@ -47,51 +39,12 @@ module Mutations ...@@ -47,51 +39,12 @@ module Mutations
private private
attr_reader :project
def find_object(project_path:, **args) def find_object(project_path:, **args)
unless project = resolve_project(full_path: project_path).sync 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 end
def raise_project_not_found def escalation_policies_available?(project)
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'The project could not be found' ::Gitlab::IncidentManagement.escalation_policies_available?(project)
end end
end end
end end
......
# frozen_string_literal: true
module Mutations
module IncidentManagement
module EscalationPolicy
class Update < Base
graphql_name 'EscalationPolicyUpdate'
argument :id, ::Types::GlobalIDType[::IncidentManagement::EscalationPolicy],
required: true,
description: 'The ID of the on-call schedule to create the on-call rotation in.'
argument :name, GraphQL::STRING_TYPE,
required: false,
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: false,
description: 'The steps of the escalation policy.'
def resolve(id:, **args)
policy = authorized_find!(id: id)
args = prepare_rules_attributes(policy.project, args)
response ::IncidentManagement::EscalationPolicies::UpdateService.new(
policy,
current_user,
args
).execute
end
end
end
end
end
...@@ -7,8 +7,8 @@ module IncidentManagement ...@@ -7,8 +7,8 @@ module IncidentManagement
user&.can?(:admin_incident_management_escalation_policy, project) user&.can?(:admin_incident_management_escalation_policy, project)
end end
def available? def invalid_schedules?
::Gitlab::IncidentManagement.escalation_policies_available?(project) params[:rules_attributes]&.any? { |attrs| attrs[:oncall_schedule]&.project != project }
end end
def error(message) def error(message)
...@@ -19,8 +19,20 @@ module IncidentManagement ...@@ -19,8 +19,20 @@ module IncidentManagement
ServiceResponse.success(payload: { escalation_policy: escalation_policy }) ServiceResponse.success(payload: { escalation_policy: escalation_policy })
end end
def error_no_license def error_no_permissions
error(_('Escalation policies are not supported for this project')) error(_('You have insufficient permissions to configure escalation policies for this project'))
end
def error_no_rules
error(_('Escalation policies must have at least one rule'))
end
def error_bad_schedules
error(_('All escalations rules must have a schedule in the same project as the policy'))
end
def error_in_save(policy)
error(policy.errors.full_messages.to_sentence)
end end
end end
end end
......
...@@ -9,9 +9,9 @@ module IncidentManagement ...@@ -9,9 +9,9 @@ module IncidentManagement
# @option params [String] name # @option params [String] name
# @option params [String] description # @option params [String] description
# @option params [Array<Hash>] rules_attributes # @option params [Array<Hash>] rules_attributes
# @option rules [Integer] oncall_schedule_id # @option params[:rules_attributes] [IncidentManagement::OncallSchedule] oncall_schedule
# @option rules [Integer] elapsed_time_seconds # @option params[:rules_attributes] [Integer] elapsed_time_seconds
# @option rules [String] status # @option params[:rules_attributes] [String] status
def initialize(project, user, params) def initialize(project, user, params)
@project = project @project = project
@user = user @user = user
...@@ -19,13 +19,13 @@ module IncidentManagement ...@@ -19,13 +19,13 @@ module IncidentManagement
end end
def execute def execute
return error_no_license unless available?
return error_no_permissions unless allowed? return error_no_permissions unless allowed?
return error_no_rules if params[:rules_attributes].blank? return error_no_rules if params[:rules_attributes].blank?
return error_bad_schedules if invalid_schedules?
escalation_policy = project.incident_management_escalation_policies.create(params) escalation_policy = project.incident_management_escalation_policies.create(params)
return error_in_create(escalation_policy) unless escalation_policy.persisted? return error_in_save(escalation_policy) unless escalation_policy.persisted?
success(escalation_policy) success(escalation_policy)
end end
...@@ -33,18 +33,6 @@ module IncidentManagement ...@@ -33,18 +33,6 @@ module IncidentManagement
private private
attr_reader :project, :user, :params 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 end
end end
...@@ -12,23 +12,18 @@ module IncidentManagement ...@@ -12,23 +12,18 @@ module IncidentManagement
end end
def execute def execute
return error_no_license unless available?
return error_no_permissions unless allowed? return error_no_permissions unless allowed?
if escalation_policy.destroy if escalation_policy.destroy
success(escalation_policy) success(escalation_policy)
else else
error(escalation_policy.errors.full_messages.to_sentence) error_in_save(escalation_policy)
end end
end end
private private
attr_reader :escalation_policy, :user, :project attr_reader :escalation_policy, :user, :project
def error_no_permissions
error(_('You have insufficient permissions to remove an escalation policy from this project'))
end
end end
end end
end end
# frozen_string_literal: true
module IncidentManagement
module EscalationPolicies
class UpdateService < EscalationPolicies::BaseService
include Gitlab::Utils::StrongMemoize
# @param escalation_policy [IncidentManagement::EscalationPolicy]
# @param user [User]
# @param params [Hash]
# @option params [String] name
# @option params [String] description
# @option params [Array<Hash>] rules_attributes
# The attributes of the full set of
# the policy's expected escalation rules.
# @option params[:rules_attributes] [IncidentManagement::OncallSchedule] oncall_schedule
# @option params[:rules_attributes] [Integer] elapsed_time_seconds
# @option params[:rules_attributes] [String, Integer, Symbol] status
def initialize(escalation_policy, user, params)
@escalation_policy = escalation_policy
@user = user
@params = params
@project = escalation_policy.project
end
def execute
return error_no_permissions unless allowed?
return error_no_rules if empty_rules?
return error_bad_schedules if invalid_schedules?
reconcile_rules!
if escalation_policy.update(params)
success(escalation_policy)
else
error_in_save(escalation_policy)
end
end
private
attr_reader :escalation_policy, :user, :params, :project
def empty_rules?
params[:rules_attributes] && params[:rules_attributes].empty?
end
# Limits rules_attributes to only new records & prepares
# to delete existing rules which are no longer needed
# when the policy is saved.
#
# Context: Rules are managed via `accepts_nested_attributes_for`
# on the IncidentManagement::EscalationPolicy.
# `accepts_nested_attributes_for` requires explicit
# removal of records, so we'll limit `rules_attributes`
# to new records, then rely on `autosave` to actually
# destroy the unwanted rules after marking them for
# deletion.
def reconcile_rules!
return unless rules_attributes = params.delete(:rules_attributes)
params[:rules_attributes] = remove_obsolete_rules(rules_attributes).to_a
end
def remove_obsolete_rules(rules_attrs)
expected_rules = rules_attrs.to_set { |attrs| normalize(::IncidentManagement::EscalationRule.new(**attrs)) }
escalation_policy.rules.each_with_object(expected_rules) do |existing_rule, new_rules|
# Exclude an expected rule which already corresponds to a persisted record - it's a no-op.
next if new_rules.delete?(normalize(existing_rule))
# Destroy a persisted record, since we don't expect this rule to be on the policy anymore.
existing_rule.mark_for_destruction
end
end
def normalize(rule)
rule.slice(:oncall_schedule_id, :elapsed_time_seconds, :status)
end
end
end
end
...@@ -36,7 +36,10 @@ RSpec.describe Mutations::IncidentManagement::EscalationPolicy::Create do ...@@ -36,7 +36,10 @@ RSpec.describe Mutations::IncidentManagement::EscalationPolicy::Create do
shared_examples 'raises a resource not available error' do |error| shared_examples 'raises a resource not available error' do |error|
specify do specify do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, error) expect { resolve }.to raise_error(
Gitlab::Graphql::Errors::ResourceNotAvailable,
error || Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
)
end end
end end
...@@ -79,7 +82,7 @@ RSpec.describe Mutations::IncidentManagement::EscalationPolicy::Create do ...@@ -79,7 +82,7 @@ RSpec.describe Mutations::IncidentManagement::EscalationPolicy::Create do
args[:rules] = [] args[:rules] = []
end end
it_behaves_like 'returns a GraphQL error', "A rule must be provided to create an escalation policy" it_behaves_like 'returns a GraphQL error', 'Escalation policies must have at least one rule'
end end
context 'schedule that does not belong to the project' do context 'schedule that does not belong to the project' do
...@@ -89,14 +92,14 @@ RSpec.describe Mutations::IncidentManagement::EscalationPolicy::Create do ...@@ -89,14 +92,14 @@ RSpec.describe Mutations::IncidentManagement::EscalationPolicy::Create do
args[:rules][0][:oncall_schedule_iid] = other_schedule.iid args[:rules][0][:oncall_schedule_iid] = other_schedule.iid
end end
it_behaves_like 'raises a resource not available error', 'The oncall schedule for iid 2 could not be found' it_behaves_like 'returns a GraphQL error', 'All escalations rules must have a schedule in the same project as the policy'
context 'user does not have permission for project' do context 'user does not have permission for project' do
before do before do
project.add_reporter(current_user) project.add_reporter(current_user)
end 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" it_behaves_like 'raises a resource not available error'
end end
end end
end end
...@@ -106,7 +109,7 @@ RSpec.describe Mutations::IncidentManagement::EscalationPolicy::Create do ...@@ -106,7 +109,7 @@ RSpec.describe Mutations::IncidentManagement::EscalationPolicy::Create do
project.add_reporter(current_user) project.add_reporter(current_user)
end 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" it_behaves_like 'raises a resource not available error'
end end
end end
...@@ -115,7 +118,7 @@ RSpec.describe Mutations::IncidentManagement::EscalationPolicy::Create do ...@@ -115,7 +118,7 @@ RSpec.describe Mutations::IncidentManagement::EscalationPolicy::Create do
args[:project_path] = 'something/incorrect' args[:project_path] = 'something/incorrect'
end end
it_behaves_like 'raises a resource not available error', 'The project could not be found' it_behaves_like 'raises a resource not available error'
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::IncidentManagement::EscalationPolicy::Update do
let_it_be(:maintainer) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let_it_be_with_reload(:escalation_policy) { create(:incident_management_escalation_policy, project: project, rule_count: 2) }
let_it_be_with_reload(:escalation_rules) { escalation_policy.rules }
let_it_be_with_reload(:first_rule) { escalation_rules.first }
let(:args) do
{
id: policy_id,
name: name,
rules: rule_args,
description: 'Updated escalation policy description'
}
end
let(:policy_id) { GitlabSchema.id_from_object(escalation_policy).to_s }
let(:name) { 'Updated escalation policy name' }
let(:rule_args) { nil }
let(:expected_rules) { escalation_rules }
before do
project.add_maintainer(maintainer)
project.add_reporter(reporter)
stub_licensed_features(oncall_schedules: true, escalation_policies: true)
stub_feature_flags(escalation_policies_mvc: project)
end
describe '#resolve' do
let(:current_user) { maintainer }
subject(:resolve) { mutation_for(current_user).resolve(**args) }
# Requires `expected_rules` to be defined
shared_examples 'successful update with no errors' do
it 'returns the updated escalation policy' do
expect(resolve).to match(
escalation_policy: escalation_policy,
errors: be_empty
)
expect(resolve[:escalation_policy]).to have_attributes(escalation_policy.reload.attributes)
expect(escalation_policy).to have_attributes(args.slice(:name, :description))
expect(escalation_policy.rules).to match_array(expected_rules)
end
end
shared_examples 'failed update with a top-level access error' do |error|
specify do
expect { resolve }.to raise_error(
Gitlab::Graphql::Errors::ResourceNotAvailable,
error || Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
)
end
end
context 'when the policy cannot be found' do
let(:policy_id) { Gitlab::GlobalId.build(nil, model_name: ::IncidentManagement::EscalationPolicy.name, id: non_existing_record_id).to_s }
it_behaves_like 'failed update with a top-level access error'
end
context 'when project does not have feature' do
before do
stub_licensed_features(oncall_schedules: true, escalation_policies: false)
end
it_behaves_like 'failed update with a top-level access error', 'Escalation policies are not supported for this project'
end
context 'when user does not have permissions to update the policy' do
let(:current_user) { reporter }
it_behaves_like 'failed update with a top-level access error'
end
context 'when there is an error in updating' do
let(:name) { 'name' * 100 }
it 'returns errors in the body of the response' do
expect(resolve).to eq(
escalation_policy: nil,
errors: ['Name is too long (maximum is 72 characters)']
)
end
end
context 'when rules are excluded' do
let(:rule_args) { nil }
it_behaves_like 'successful update with no errors'
end
context 'when rules are included but empty' do
let(:rule_args) { [] }
it 'returns errors in the body of the response' do
expect(resolve).to eq(
escalation_policy: nil,
errors: ['Escalation policies must have at least one rule']
)
end
end
context 'with rule updates' do
let(:oncall_schedule_iid) { oncall_schedule.iid }
let(:rule_args) do
[
{
oncall_schedule_iid: first_rule.oncall_schedule.iid,
elapsed_time_seconds: first_rule.elapsed_time_seconds,
status: first_rule.status.to_sym
},
{
oncall_schedule_iid: oncall_schedule_iid,
elapsed_time_seconds: 800,
status: :acknowledged
}
]
end
let(:expected_rules) do
[
first_rule,
have_attributes(oncall_schedule_id: oncall_schedule.id, elapsed_time_seconds: 800, status: 'acknowledged')
]
end
it_behaves_like 'successful update with no errors'
context 'when schedule does not exist' do
let(:error_message) { eq("The oncall schedule for iid #{non_existing_record_iid} could not be found") }
let(:oncall_schedule_iid) { non_existing_record_iid }
it 'returns errors in the body of the response' do
expect(resolve).to eq(
escalation_policy: nil,
errors: ['All escalations rules must have a schedule in the same project as the policy']
)
end
context 'the user does not have permission to update policies regardless' do
let(:current_user) { reporter }
it_behaves_like 'failed update with a top-level access error'
end
end
end
end
private
def mutation_for(user)
described_class.new(object: nil, context: { current_user: user }, field: nil)
end
end
...@@ -75,7 +75,7 @@ RSpec.describe 'creating escalation policy' do ...@@ -75,7 +75,7 @@ RSpec.describe 'creating escalation policy' do
it 'raises an error' do it 'raises an error' do
resolve resolve
expect(mutation_response['errors'][0]).to eq("A rule must be provided to create an escalation policy") expect(mutation_response['errors'][0]).to eq('Escalation policies must have at least one rule')
end end
end end
...@@ -87,7 +87,7 @@ RSpec.describe 'creating escalation policy' do ...@@ -87,7 +87,7 @@ RSpec.describe 'creating escalation policy' do
it 'raises an error' do it 'raises an error' do
resolve resolve
expect_graphql_errors_to_include("Escalation policies are not supported for this project") expect_graphql_errors_to_include('Escalation policies are not supported for this project')
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Updating an escalation policy' do
include GraphqlHelpers
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(:schedule) { escalation_policy.rules.first.oncall_schedule }
let(:variables) do
{
id: escalation_policy.to_global_id.to_s,
name: 'Updated Policy Name',
description: 'Updated Description',
rules: [rule_variables]
}
end
let(:rule_variables) do
{
oncallScheduleIid: schedule.iid,
elapsedTimeSeconds: 60,
status: 'ACKNOWLEDGED'
}
end
let(:mutation) do
graphql_mutation(:escalation_policy_update, variables) do
<<~QL
errors
escalationPolicy {
id
name
description
rules {
status
elapsedTimeSeconds
oncallSchedule { iid }
}
}
QL
end
end
let(:mutation_response) { graphql_mutation_response(:escalation_policy_update) }
before do
stub_licensed_features(oncall_schedules: true, escalation_policies: true)
stub_feature_flags(escalation_policies_mvc: project)
project.add_maintainer(user)
end
it 'updates the escalation policy' do
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(
'errors' => [],
'escalationPolicy' => {
'id' => escalation_policy.to_global_id.to_s,
'name' => variables[:name],
'description' => variables[:description],
'rules' => [{
'status' => rule_variables[:status],
'elapsedTimeSeconds' => rule_variables[:elapsedTimeSeconds],
'oncallSchedule' => { 'iid' => schedule.iid.to_s }
}]
}
)
expect(escalation_policy.reload).to have_attributes(
name: variables[:name],
description: variables[:description],
rules: [
have_attributes(
oncall_schedule: schedule,
status: rule_variables[:status].downcase,
elapsed_time_seconds: rule_variables[:elapsedTimeSeconds]
)
]
)
end
end
...@@ -21,7 +21,7 @@ RSpec.describe IncidentManagement::EscalationPolicies::CreateService do ...@@ -21,7 +21,7 @@ RSpec.describe IncidentManagement::EscalationPolicies::CreateService do
let(:rule_params) do let(:rule_params) do
[ [
{ {
oncall_schedule_id: oncall_schedule.id, oncall_schedule: oncall_schedule,
elapsed_time_seconds: 60, elapsed_time_seconds: 60,
status: :resolved status: :resolved
} }
...@@ -45,7 +45,7 @@ RSpec.describe IncidentManagement::EscalationPolicies::CreateService do ...@@ -45,7 +45,7 @@ RSpec.describe IncidentManagement::EscalationPolicies::CreateService do
context 'when user does not have access' do context 'when user does not have access' do
let(:user) { create(:user) } let(:user) { create(:user) }
it_behaves_like 'error response', 'You have insufficient permissions to create an escalation policy for this project' it_behaves_like 'error response', 'You have insufficient permissions to configure escalation policies for this project'
end end
context 'when license is not enabled' do context 'when license is not enabled' do
...@@ -53,7 +53,7 @@ RSpec.describe IncidentManagement::EscalationPolicies::CreateService do ...@@ -53,7 +53,7 @@ RSpec.describe IncidentManagement::EscalationPolicies::CreateService do
stub_licensed_features(oncall_schedules: true, escalation_policies: false) stub_licensed_features(oncall_schedules: true, escalation_policies: false)
end end
it_behaves_like 'error response', 'Escalation policies are not supported for this project' it_behaves_like 'error response', 'You have insufficient permissions to configure escalation policies for this project'
end end
context 'validation errors' do context 'validation errors' do
...@@ -68,15 +68,23 @@ RSpec.describe IncidentManagement::EscalationPolicies::CreateService do ...@@ -68,15 +68,23 @@ RSpec.describe IncidentManagement::EscalationPolicies::CreateService do
context 'no rules are given' do context 'no rules are given' do
let(:rule_params) { nil } let(:rule_params) { nil }
it_behaves_like 'error response', 'A rule must be provided to create an escalation policy' it_behaves_like 'error response', 'Escalation policies must have at least one rule'
end end
context 'oncall schedule is blank' do context 'oncall schedule is blank' do
before do before do
rule_params[0][:oncall_schedule_id] = nil rule_params[0][:oncall_schedule] = nil
end end
it_behaves_like 'error response', "Rules[0] oncall schedule can't be blank" it_behaves_like 'error response', 'All escalations rules must have a schedule in the same project as the policy'
end
context 'oncall schedule is on the wrong project' do
before do
rule_params[0][:oncall_schedule] = create(:incident_management_oncall_schedule)
end
it_behaves_like 'error response', 'All escalations rules must have a schedule in the same project as the policy'
end end
end end
......
...@@ -34,13 +34,13 @@ RSpec.describe IncidentManagement::EscalationPolicies::DestroyService do ...@@ -34,13 +34,13 @@ RSpec.describe IncidentManagement::EscalationPolicies::DestroyService do
context 'when the current_user is anonymous' do context 'when the current_user is anonymous' do
let(:current_user) { nil } let(:current_user) { nil }
it_behaves_like 'error response', 'You have insufficient permissions to remove an escalation policy from this project' it_behaves_like 'error response', 'You have insufficient permissions to configure escalation policies for this project'
end end
context 'when the current_user does not have permissions to remove escalation policies' do context 'when the current_user does not have permissions to remove escalation policies' do
let(:current_user) { user_without_permissions } let(:current_user) { user_without_permissions }
it_behaves_like 'error response', 'You have insufficient permissions to remove an escalation policy from this project' it_behaves_like 'error response', 'You have insufficient permissions to configure escalation policies for this project'
end end
context 'when license is not enabled' do context 'when license is not enabled' do
...@@ -48,7 +48,7 @@ RSpec.describe IncidentManagement::EscalationPolicies::DestroyService do ...@@ -48,7 +48,7 @@ RSpec.describe IncidentManagement::EscalationPolicies::DestroyService do
stub_licensed_features(oncall_schedules: true, escalation_policies: false) stub_licensed_features(oncall_schedules: true, escalation_policies: false)
end end
it_behaves_like 'error response', 'Escalation policies are not supported for this project' it_behaves_like 'error response', 'You have insufficient permissions to configure escalation policies for this project'
end end
context 'when feature is not available' do context 'when feature is not available' do
...@@ -56,7 +56,7 @@ RSpec.describe IncidentManagement::EscalationPolicies::DestroyService do ...@@ -56,7 +56,7 @@ RSpec.describe IncidentManagement::EscalationPolicies::DestroyService do
stub_feature_flags(escalation_policies_mvc: false) stub_feature_flags(escalation_policies_mvc: false)
end end
it_behaves_like 'error response', 'Escalation policies are not supported for this project' it_behaves_like 'error response', 'You have insufficient permissions to configure escalation policies for this project'
end end
context 'when an error occurs during removal' do context 'when an error occurs during removal' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::EscalationPolicies::UpdateService 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(:oncall_schedule) { create(:incident_management_oncall_schedule, project: project) }
let_it_be_with_reload(:escalation_policy) { create(:incident_management_escalation_policy, project: project, rule_count: 2) }
let_it_be_with_reload(:escalation_rules) { escalation_policy.rules }
let(:service) { described_class.new(escalation_policy, current_user, params) }
let(:current_user) { user_with_permissions }
let(:params) do
{
name: 'Updated escalation policy name',
description: 'Updated escalation policy description',
rules_attributes: rule_params
}
end
let(:rule_params) { [*existing_rules_params, new_rule_params] }
let(:existing_rules_params) do
escalation_rules.map do |rule|
rule.slice(:oncall_schedule, :elapsed_time_seconds)
.merge(status: rule.status.to_sym)
end
end
let(:new_rule_params) do
{
oncall_schedule: oncall_schedule,
elapsed_time_seconds: 800,
status: :acknowledged
}
end
let(:new_rule) { have_attributes(**new_rule_params.except(:status), status: 'acknowledged') }
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
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
# Requires `expected_rules` to be defined
shared_examples 'successful update with no errors' do
it 'returns the updated escalation policy' do
expect(execute).to be_success
expect(execute.payload).to eq(escalation_policy: escalation_policy.reload)
expect(escalation_policy).to have_attributes(params.slice(:name, :description))
expect(escalation_policy.rules).to match_array(expected_rules)
end
end
subject(:execute) { service.execute }
context 'when the current_user is anonymous' do
let(:current_user) { nil }
it_behaves_like 'error response', 'You have insufficient permissions to configure escalation policies for this project'
end
context 'when the current_user does not have permissions to update escalation policies' do
let(:current_user) { user_without_permissions }
it_behaves_like 'error response', 'You have insufficient permissions to configure escalation policies 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', 'You have insufficient permissions to configure escalation policies for this project'
end
context 'when feature is not available' do
before do
stub_feature_flags(escalation_policies_mvc: false)
end
it_behaves_like 'error response', 'You have insufficient permissions to configure escalation policies for this project'
end
context 'when only new rules are added' do
let(:expected_rules) { [*escalation_rules, new_rule] }
it_behaves_like 'successful update with no errors'
end
context 'when all old rules are replaced' do
let(:rule_params) { [new_rule_params] }
let(:expected_rules) { [new_rule] }
it_behaves_like 'successful update with no errors'
end
context 'when some rules are preserved, added, and deleted' do
let(:rule_params) { [existing_rules_params.first, new_rule_params] }
let(:expected_rules) { [escalation_rules.first, new_rule] }
it_behaves_like 'successful update with no errors'
end
context 'when rules are unchanged' do
let(:rule_params) { existing_rules_params }
let(:expected_rules) { escalation_rules }
it_behaves_like 'successful update with no errors'
end
context 'when rules are excluded' do
let(:expected_rules) { escalation_rules }
before do
params.delete(:rules_attributes)
end
it_behaves_like 'successful update with no errors'
end
context 'when rules are explicitly nil' do
let(:rule_params) { nil }
let(:expected_rules) { escalation_rules }
it_behaves_like 'successful update with no errors'
end
context 'when rules are explicitly empty' do
let(:rule_params) { [] }
let(:expected_rules) { escalation_rules }
it_behaves_like 'error response', 'Escalation policies must have at least one rule'
end
context 'when the on-call schedule is not present on the rule' do
let(:rule_params) { [new_rule_params.except(:oncall_schedule)] }
it_behaves_like 'error response', 'All escalations rules must have a schedule in the same project as the policy'
end
context 'when the on-call schedule is not on the project' do
let(:other_schedule) { create(:incident_management_oncall_schedule) }
let(:rule_params) { [new_rule_params.merge(oncall_schedule: other_schedule)] }
it_behaves_like 'error response', 'All escalations rules must have a schedule in the same project as the policy'
end
context 'when an error occurs during update' do
before do
params[:name] = ''
end
it_behaves_like 'error response', "Name can't be blank"
end
end
end
...@@ -1474,9 +1474,6 @@ msgstr "" ...@@ -1474,9 +1474,6 @@ msgstr ""
msgid "A rebase is already in progress." msgid "A rebase is already in progress."
msgstr "" msgstr ""
msgid "A rule must be provided to create an escalation policy"
msgstr ""
msgid "A secure token that identifies an external storage request." msgid "A secure token that identifies an external storage request."
msgstr "" msgstr ""
...@@ -3225,6 +3222,9 @@ msgstr "" ...@@ -3225,6 +3222,9 @@ msgstr ""
msgid "All epics" msgid "All epics"
msgstr "" msgstr ""
msgid "All escalations rules must have a schedule in the same project as the policy"
msgstr ""
msgid "All groups and projects" msgid "All groups and projects"
msgstr "" msgstr ""
...@@ -13030,7 +13030,7 @@ msgstr "" ...@@ -13030,7 +13030,7 @@ msgstr ""
msgid "Escalation policies" msgid "Escalation policies"
msgstr "" msgstr ""
msgid "Escalation policies are not supported for this project" msgid "Escalation policies must have at least one rule"
msgstr "" msgstr ""
msgid "EscalationPolicies|+ Add an additional rule" msgid "EscalationPolicies|+ Add an additional rule"
...@@ -37395,21 +37395,18 @@ msgstr "" ...@@ -37395,21 +37395,18 @@ msgstr ""
msgid "You have imported from this project %{numberOfPreviousImports} times before. Each new import will create duplicate issues." msgid "You have imported from this project %{numberOfPreviousImports} times before. Each new import will create duplicate issues."
msgstr "" msgstr ""
msgid "You have insufficient permissions to create a Todo for this alert" msgid "You have insufficient permissions to configure escalation policies for this project"
msgstr "" msgstr ""
msgid "You have insufficient permissions to create an HTTP integration for this project" msgid "You have insufficient permissions to create a Todo for this alert"
msgstr "" msgstr ""
msgid "You have insufficient permissions to create an escalation policy for this project" msgid "You have insufficient permissions to create an HTTP integration for this project"
msgstr "" msgstr ""
msgid "You have insufficient permissions to create an on-call schedule for this project" msgid "You have insufficient permissions to create an on-call schedule for this project"
msgstr "" msgstr ""
msgid "You have insufficient permissions to remove an escalation policy from this project"
msgstr ""
msgid "You have insufficient permissions to remove an on-call rotation from this project" msgid "You have insufficient permissions to remove an on-call rotation from this project"
msgstr "" 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