Commit ada46f7d authored by Sarah Yasonik's avatar Sarah Yasonik Committed by Vitali Tatarintev

Create pending escalations for incidents on update

Part of the work required to allow users to manually
escalate incidents which were created manually.
parent fc6f07cd
......@@ -78,7 +78,6 @@ module AlertManagement
scope :for_environment, -> (environment) { where(environment: environment) }
scope :for_assignee_username, -> (assignee_username) { joins(:assignees).merge(User.by_username(assignee_username)) }
scope :search, -> (query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) }
scope :open, -> { with_status(open_statuses) }
scope :not_resolved, -> { without_status(:resolved) }
scope :with_prometheus_alert, -> { includes(:prometheus_alert) }
scope :with_threat_monitoring_alerts, -> { where(domain: :threat_monitoring ) }
......@@ -143,18 +142,6 @@ module AlertManagement
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
def self.open_statuses
[:triggered, :acknowledged]
end
def self.open_status?(status)
open_statuses.include?(status)
end
def open?
self.class.open_status?(status_name)
end
def prometheus?
monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
end
......
......@@ -27,6 +27,8 @@ module IncidentManagement
ignored: 'No action will be taken'
}.freeze
OPEN_STATUSES = [:triggered, :acknowledged].freeze
included do
validates :status, presence: true
......@@ -34,6 +36,7 @@ module IncidentManagement
# Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored
# https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior
scope :order_status, -> (sort_order) { order(status: sort_order == :asc ? :desc : :asc) }
scope :open, -> { with_status(OPEN_STATUSES) }
state_machine :status, initial: :triggered do
state :triggered, value: STATUSES[:triggered]
......@@ -89,6 +92,10 @@ module IncidentManagement
@status_names ||= state_machine_statuses.keys
end
def open_status?(status)
OPEN_STATUSES.include?(status)
end
private
def state_machine_statuses
......@@ -99,6 +106,10 @@ module IncidentManagement
def status_event_for(status)
self.class.state_machines[:status].events.transitions_for(self, to: status.to_s.to_sym).first&.event
end
def open?
self.class.open_status?(status_name)
end
end
end
end
......
# frozen_string_literal: true
module IncidentManagement
module IssuableEscalationStatuses
class AfterUpdateService < ::BaseProjectService
def initialize(issuable, current_user)
@issuable = issuable
@escalation_status = issuable.escalation_status
@alert = issuable.alert_management_alert
super(project: issuable.project, current_user: current_user)
end
def execute
after_update
ServiceResponse.success(payload: { escalation_status: escalation_status })
end
private
attr_reader :issuable, :escalation_status, :alert
def after_update
sync_to_alert
end
def sync_to_alert
return unless alert
return unless escalation_status.status_previously_changed?
::AlertManagement::Alerts::UpdateService.new(
alert,
current_user,
status: escalation_status.status_name
).execute
end
end
end
end
::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.prepend_mod
......@@ -213,13 +213,8 @@ module Issues
def handle_escalation_status_change(issue, old_escalation_status)
return unless old_escalation_status.present?
return if issue.escalation_status&.slice(:status, :policy_id) == old_escalation_status
return unless issue.alert_management_alert
::AlertManagement::Alerts::UpdateService.new(
issue.alert_management_alert,
current_user,
status: issue.escalation_status.status_name
).execute
::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new(issue, current_user).execute
end
# rubocop: disable CodeReuse/ActiveRecord
......
......@@ -207,6 +207,8 @@
- 1
- - incident_management_pending_escalations_issue_check
- 1
- - incident_management_pending_escalations_issue_create
- 1
- - integrations_create_external_cross_reference
- 1
- - invalid_gpg_signature_update
......
......@@ -8,6 +8,10 @@ module EE
def escalation_policy
project.incident_management_escalation_policies.first
end
def pending_escalation_target
self
end
end
end
end
......@@ -4,7 +4,7 @@ module IncidentManagement
# Functionality needed for models which represent escalations.
#
# Implemeting classes should alias `target` to the attribute
# of the relevant escalatable.
# of the relevant association for the escalation.
#
# EX) `alias_attribute :target, :alert`
module BasePendingEscalation
......@@ -29,6 +29,10 @@ module IncidentManagement
delegate :project, to: :target
def self.class_for_check_worker
raise NotImplementedError
end
def escalatable
raise NotImplementedError
end
......
......@@ -4,6 +4,7 @@ module EE
module IncidentManagement
module IssuableEscalationStatus
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do
belongs_to :policy, optional: true, class_name: '::IncidentManagement::EscalationPolicy'
......@@ -24,6 +25,11 @@ module EE
end
end
end
override :pending_escalation_target
def pending_escalation_target
issue
end
end
end
end
......@@ -13,6 +13,10 @@ module IncidentManagement
validates :rule_id, uniqueness: { scope: [:alert_id] }
def self.class_for_check_worker
AlertCheckWorker
end
def escalatable
alert
end
......
......@@ -13,6 +13,10 @@ module IncidentManagement
validates :rule_id, uniqueness: { scope: [:issue_id] }
def self.class_for_check_worker
IssueCheckWorker
end
def escalatable
issue.incident_management_issuable_escalation_status
end
......
# frozen_string_literal: true
module EE
module IncidentManagement
module IssuableEscalationStatuses
module AfterUpdateService
extend ::Gitlab::Utils::Override
private
delegate :open_status?, :status_name, to: '::IncidentManagement::IssuableEscalationStatus'
override :after_update
def after_update
super
reset_pending_escalations
end
def reset_pending_escalations
return unless ::Gitlab::IncidentManagement.escalation_policies_available?(project)
return if alert
return unless policy_changed? || open_status_changed?
delete_escalations if had_policy? && had_open_status?
create_escalations if has_policy_now? && has_open_status_now?
end
def policy_changed?
escalation_status.policy_id_previously_changed?
end
def open_status_changed?
had_open_status? != has_open_status_now?
end
def had_policy?
escalation_status.policy_id_previously_was.present?
end
def has_policy_now?
escalation_status.policy_id.present?
end
def had_open_status?
open_status?(status_name(escalation_status.status_previously_was))
end
def has_open_status_now?
escalation_status.open?
end
def delete_escalations
issuable.pending_escalations.delete_all(:delete_all)
end
def create_escalations
::IncidentManagement::PendingEscalations::IssueCreateWorker.perform_async(issuable.id)
end
end
end
end
end
......@@ -2,26 +2,25 @@
module IncidentManagement
module PendingEscalations
class CreateService < BaseService
def initialize(target)
@target = target
@project = target.project
class CreateService < ::BaseProjectService
def initialize(escalatable)
@escalatable = escalatable
@target = escalatable.pending_escalation_target
@process_time = Time.current
super(project: target.project)
end
def execute
return unless ::Gitlab::IncidentManagement.escalation_policies_available?(project) && !target.resolved?
policy = project.incident_management_escalation_policies.first
return unless policy
return unless ::Gitlab::IncidentManagement.escalation_policies_available?(project) && !escalatable.resolved?
return unless policy = escalatable.escalation_policy
create_escalations(policy.active_rules)
end
private
attr_reader :target, :project, :process_time
attr_reader :escalatable, :target, :process_time
def create_escalations(rules)
escalation_ids = rules.map do |rule|
......@@ -33,8 +32,7 @@ module IncidentManagement
end
def create_escalation(rule)
IncidentManagement::PendingEscalations::Alert.create!(
target: target,
target.pending_escalations.create!(
rule: rule,
process_at: rule.elapsed_time_seconds.seconds.after(process_time)
)
......@@ -43,7 +41,11 @@ module IncidentManagement
def process_escalations(escalation_ids)
args = escalation_ids.map { |id| [id] }
::IncidentManagement::PendingEscalations::AlertCheckWorker.bulk_perform_async(args) # rubocop:disable Scalability/BulkPerformWithContext
class_for_check_worker.bulk_perform_async(args) # rubocop:disable Scalability/BulkPerformWithContext
end
def class_for_check_worker
@class_for_check_worker ||= target.pending_escalations.klass.class_for_check_worker
end
end
end
......
......@@ -1119,6 +1119,15 @@
:weight: 1
:idempotent: true
:tags: []
- :name: incident_management_pending_escalations_issue_create
:worker_name: IncidentManagement::PendingEscalations::IssueCreateWorker
:feature_category: :incident_management
:has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 1
:idempotent: true
:tags: []
- :name: ldap_group_sync
:worker_name: LdapGroupSyncWorker
:feature_category: :authentication_and_authorization
......
# frozen_string_literal: true
module IncidentManagement
module PendingEscalations
class IssueCreateWorker
include ApplicationWorker
data_consistency :always
worker_resource_boundary :cpu
urgency :high
idempotent!
feature_category :incident_management
def perform(issue_id)
issue = ::Issue.find_by_id(issue_id)
return unless issue
escalation_status = issue.escalation_status
return unless escalation_status
::IncidentManagement::PendingEscalations::CreateService.new(escalation_status).execute
end
end
end
end
......@@ -8,7 +8,7 @@ FactoryBot.define do
end
rule { association :incident_management_escalation_rule, policy: policy }
issue { association :issue, project: rule.policy.project }
issue { association :incident, project: rule.policy.project }
process_at { 5.minutes.from_now }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::BasePendingEscalation do
let(:klass) do
Class.new(ApplicationRecord) do
include IncidentManagement::BasePendingEscalation
self.table_name = 'incident_management_pending_alert_escalations'
end
end
subject(:instance) { klass.new }
describe '.class_for_check_worker' do
it 'must be implemented' do
expect { klass.class_for_check_worker }.to raise_error(NotImplementedError)
end
end
describe '#escalatable' do
it 'must be implemented' do
expect { instance.escalatable }.to raise_error(NotImplementedError)
end
end
describe '#type' do
it 'must be implemented' do
expect { instance.type }.to raise_error(NotImplementedError)
end
end
end
......@@ -83,4 +83,10 @@ RSpec.describe IncidentManagement::IssuableEscalationStatus do
end
end
end
describe '#pending_escalation_target' do
subject { escalation_status.pending_escalation_target }
it { is_expected.to eq(escalation_status.issue) }
end
end
......@@ -3,5 +3,27 @@
require 'spec_helper'
RSpec.describe IncidentManagement::PendingEscalations::Alert do
include_examples 'IncidentManagement::PendingEscalation model'
let(:pending_escalation) { build(:incident_management_pending_alert_escalation) }
describe '.class_for_check_worker' do
subject { described_class.class_for_check_worker }
it { is_expected.to eq(::IncidentManagement::PendingEscalations::AlertCheckWorker) }
end
describe '#escalatable' do
subject { pending_escalation.escalatable }
it { is_expected.to eq(pending_escalation.alert) }
end
describe '#type' do
subject { pending_escalation.type }
it { is_expected.to eq(:alert) }
end
context 'shared pending escalation features' do
include_examples 'IncidentManagement::PendingEscalation model'
end
end
......@@ -3,5 +3,29 @@
require 'spec_helper'
RSpec.describe IncidentManagement::PendingEscalations::Issue do
include_examples 'IncidentManagement::PendingEscalation model'
let_it_be(:pending_escalation) { create(:incident_management_pending_issue_escalation) }
describe '.class_for_check_worker' do
subject { described_class.class_for_check_worker }
it { is_expected.to eq(::IncidentManagement::PendingEscalations::IssueCheckWorker) }
end
describe '#escalatable' do
let_it_be(:escalatable) { create(:incident_management_issuable_escalation_status, issue: pending_escalation.issue) }
subject { pending_escalation.escalatable }
it { is_expected.to eq(escalatable) }
end
describe '#type' do
subject { pending_escalation.type }
it { is_expected.to eq(:incident) }
end
context 'shared pending escalation features' do
include_examples 'IncidentManagement::PendingEscalation model'
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::IssuableEscalationStatuses::AfterUpdateService do
let_it_be(:current_user) { create(:user) }
let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status) }
let_it_be(:issue, reload: true) { escalation_status.issue }
let_it_be(:project) { issue.project }
let_it_be(:escalation_policy) { create(:incident_management_escalation_policy, project: project) }
let(:status_event) { :acknowledge }
let(:policy) { escalation_policy }
let(:escalations_started_at) { Time.current }
let(:service) { IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new(issue, current_user) }
subject(:result) { service.execute }
before do
project.add_developer(current_user)
stub_licensed_features(oncall_schedules: true, escalation_policies: true)
end
shared_examples 'does not alter the pending escalations' do
specify do
update_issue(status_event, policy, escalations_started_at)
expect(::IncidentManagement::PendingEscalations::IssueCreateWorker).not_to receive(:perform_async)
expect { result }.not_to change(::IncidentManagement::PendingEscalations::Issue, :count)
end
end
context 'when escalation policies feature is unavailable' do
before do
stub_licensed_features(escalation_policies: false)
end
it_behaves_like 'does not alter the pending escalations'
end
context 'when issue is associated with an alert' do
let_it_be(:alert) { create(:alert_management_alert, issue: issue, project: project) }
it_behaves_like 'does not alter the pending escalations'
end
context 'resetting pending escalations' do
using RSpec::Parameterized::TableSyntax
where(:old_status, :new_status, :had_policy, :has_policy, :should_delete, :should_create) do
:triggered | :triggered | false | false | false | false # status unchanged open
:resolved | :resolved | false | false | false | false # status unchanged closed
:triggered | :acknowledged | false | false | false | false # open -> open
:acknowledged | :resolved | false | false | false | false # open -> closed
:resolved | :triggered | false | false | false | false # closed -> open
:resolved | :ignored | false | false | false | false # closed -> closed
:triggered | :triggered | true | false | true | false
:resolved | :resolved | true | false | false | false
:triggered | :acknowledged | true | false | true | false
:acknowledged | :resolved | true | false | true | false
:resolved | :triggered | true | false | false | false
:resolved | :ignored | true | false | false | false
:triggered | :triggered | true | true | false | false
:resolved | :resolved | true | true | false | false
:triggered | :acknowledged | true | true | false | false
:acknowledged | :resolved | true | true | true | false
:resolved | :triggered | true | true | false | true
:resolved | :ignored | true | true | false | false
:triggered | :triggered | false | true | false | true # status unchanged
:resolved | :triggered | false | true | false | true # closed -> open
:acknowledged | :triggered | false | true | false | true # open -> open
end
with_them do
let(:old_policy) { had_policy ? escalation_policy : nil }
let(:old_escalations_started_at) { had_policy ? Time.current : nil }
let(:old_status_event) { escalation_status.status_event_for(old_status) }
let(:policy) { has_policy ? escalation_policy : nil }
let(:escalations_started_at) { has_policy ? Time.current : nil }
let(:status_event) { escalation_status.status_event_for(new_status) }
before do
if had_policy && [:acknowledged, :triggered].include?(old_status)
create(:incident_management_pending_issue_escalation, issue: issue, policy: escalation_policy, project: project)
end
end
it 'deletes or creates pending escalations as required' do
update_issue(old_status_event, old_policy, old_escalations_started_at)
update_issue(status_event, policy, escalations_started_at)
if should_create
expect(::IncidentManagement::PendingEscalations::IssueCreateWorker).to receive(:perform_async).with(issue.id)
else
expect(::IncidentManagement::PendingEscalations::IssueCreateWorker).not_to receive(:perform_async)
end
if should_delete
expect { result }.to change(::IncidentManagement::PendingEscalations::Issue, :count).by(-1)
else
expect { result }.not_to change(::IncidentManagement::PendingEscalations::Issue, :count)
end
end
end
end
def update_issue(status_event, policy, escalations_started_at)
issue.update!(
incident_management_issuable_escalation_status_attributes: {
status_event: status_event,
policy: policy,
escalations_started_at: escalations_started_at
}
)
end
end
......@@ -451,6 +451,7 @@ RSpec.describe Issues::UpdateService do
context 'updating escalation status' do
let(:opts) { { escalation_status: { policy: policy } } }
let(:escalation_update_class) { ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService }
let!(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue) }
let!(:policy) { create(:incident_management_escalation_policy, project: project) }
......@@ -461,13 +462,22 @@ RSpec.describe Issues::UpdateService do
end
# Requires `expoected_policy` and `expected_status` to be defined
shared_examples 'escalation status record has correct values' do
specify do
shared_examples 'updates the escalation status record' do
let(:service_double) { instance_double(escalation_update_class) }
it 'has correct values' do
update_issue(opts)
expect(issue.escalation_status.policy).to eq(expected_policy)
expect(issue.escalation_status.status_name).to eq(expected_status)
end
it 'triggers side-effects' do
expect(escalation_update_class).to receive(:new).with(issue, user).and_return(service_double)
expect(service_double).to receive(:execute)
update_issue(opts)
end
end
shared_examples 'does not change the status record' do
......@@ -476,7 +486,7 @@ RSpec.describe Issues::UpdateService do
end
it 'does not trigger side-effects' do
expect(::AlertManagement::Alerts::UpdateService).not_to receive(:new)
expect(escalation_update_class).not_to receive(:new)
update_issue(opts)
end
......@@ -486,7 +496,7 @@ RSpec.describe Issues::UpdateService do
let(:issue) { create(:incident, project: project) }
context 'setting the escalation policy' do
include_examples 'escalation status record has correct values' do
include_examples 'updates the escalation status record' do
let(:expected_policy) { policy }
let(:expected_status) { :triggered }
end
......@@ -503,8 +513,9 @@ RSpec.describe Issues::UpdateService do
context 'when the policy is already set' do
let!(:escalation_status) { create(:incident_management_issuable_escalation_status, :paging, issue: issue) }
let(:expected_policy) { nil }
include_examples 'escalation status record has correct values' do
include_examples 'updates the escalation status record' do
let(:expected_policy) { nil }
let(:expected_status) { :triggered }
end
......@@ -512,7 +523,7 @@ RSpec.describe Issues::UpdateService do
context 'in addition to other attributes' do
let(:opts) { { escalation_status: { policy: policy, status: 'acknowledged' } } }
include_examples 'escalation status record has correct values' do
include_examples 'updates the escalation status record' do
let(:expected_policy) { nil }
let(:expected_status) { :acknowledged }
end
......
......@@ -4,70 +4,95 @@ require 'spec_helper'
RSpec.describe IncidentManagement::PendingEscalations::CreateService do
let_it_be(:project) { create(:project) }
let_it_be(:target) { create(:alert_management_alert, project: project) }
let_it_be(:rule_count) { 2 }
let!(:escalation_policy) { create(:incident_management_escalation_policy, project: project, rule_count: rule_count) }
let!(:removed_rule) { create(:incident_management_escalation_rule, :removed, policy: escalation_policy) }
let(:rules) { escalation_policy.rules }
let(:service) { described_class.new(target) }
let(:rules) { escalation_policy.rules }
let(:service) { described_class.new(escalatable) }
subject(:execute) { service.execute }
context 'feature not available' do
it 'does nothing' do
expect { execute }.not_to change { IncidentManagement::PendingEscalations::Alert.count }
shared_examples 'creates pending escalations appropriately' do
context 'feature not available' do
it 'does nothing' do
expect { execute }.not_to change { escalation_class.count }
end
end
end
context 'feature available' do
before do
stub_licensed_features(oncall_schedules: true, escalation_policies: true)
end
context 'feature available' do
before do
stub_licensed_features(oncall_schedules: true, escalation_policies: true)
end
context 'target is resolved' do
let(:target) { create(:alert_management_alert, :resolved, project: project) }
context 'target is resolved' do
before do
escalatable.resolve
end
it 'does nothing' do
expect { execute }.not_to change { IncidentManagement::PendingEscalations::Alert.count }
it 'does nothing' do
expect { execute }.not_to change { escalation_class.count }
end
end
end
it 'creates an escalation for each rule for the policy' do
execution_time = Time.current
expect { execute }.to change { IncidentManagement::PendingEscalations::Alert.count }.by(rule_count)
it 'creates an escalation for each rule for the policy' do
execution_time = Time.current
expect { execute }.to change { escalation_class.count }.by(rule_count)
first_escalation, second_escalation = target.pending_escalations.order(created_at: :asc)
first_rule, second_rule = rules
first_escalation, second_escalation = target.pending_escalations.order(created_at: :asc)
first_rule, second_rule = rules
expect_escalation_attributes_with(escalation: first_escalation, target: target, rule: first_rule, execution_time: execution_time)
expect_escalation_attributes_with(escalation: second_escalation, target: target, rule: second_rule, execution_time: execution_time)
end
expect_escalation_attributes_with(escalation: first_escalation, target: target, rule: first_rule, execution_time: execution_time)
expect_escalation_attributes_with(escalation: second_escalation, target: target, rule: second_rule, execution_time: execution_time)
end
context 'when there is no escalation policy for the project' do
let!(:escalation_policy) { nil }
let!(:removed_rule) { nil }
context 'when there is no escalation policy for the project' do
let!(:escalation_policy) { nil }
let!(:removed_rule) { nil }
it 'does nothing' do
expect { execute }.not_to change { IncidentManagement::PendingEscalations::Alert.count }
it 'does nothing' do
expect { execute }.not_to change { escalation_class.count }
end
end
end
it 'creates the escalations and queues the escalation process check' do
expect(IncidentManagement::PendingEscalations::AlertCheckWorker)
.to receive(:bulk_perform_async)
.with([[a_kind_of(Integer)], [a_kind_of(Integer)]])
it 'creates the escalations and queues the escalation process check' do
expect(worker_class)
.to receive(:bulk_perform_async)
.with([[a_kind_of(Integer)], [a_kind_of(Integer)]])
expect { execute }.to change { IncidentManagement::PendingEscalations::Alert.count }.by(rule_count)
end
expect { execute }.to change { escalation_class.count }.by(rule_count)
end
def expect_escalation_attributes_with(escalation:, target:, rule:, execution_time: Time.current)
expect(escalation).to have_attributes(
rule_id: rule.id,
alert_id: target.id,
process_at: be_within(1.minute).of(rule.elapsed_time_seconds.seconds.after(execution_time))
)
def expect_escalation_attributes_with(escalation:, target:, rule:, execution_time: Time.current)
expect(escalation).to have_attributes(
rule_id: rule.id,
foreign_key => target.id,
process_at: be_within(1.minute).of(rule.elapsed_time_seconds.seconds.after(execution_time))
)
end
end
end
context 'for alerts' do
let_it_be(:target) { create(:alert_management_alert, project: project) }
let_it_be(:escalatable) { target }
let(:escalation_class) { IncidentManagement::PendingEscalations::Alert }
let(:worker_class) { IncidentManagement::PendingEscalations::AlertCheckWorker }
let(:foreign_key) { :alert_id }
include_examples 'creates pending escalations appropriately'
end
context 'for incidents' do
let_it_be(:target) { create(:incident, project: project) }
let_it_be(:escalatable) { create(:incident_management_issuable_escalation_status, issue: target) }
let(:escalation_class) { IncidentManagement::PendingEscalations::Issue }
let(:worker_class) { IncidentManagement::PendingEscalations::IssueCheckWorker }
let(:foreign_key) { :issue_id }
include_examples 'creates pending escalations appropriately'
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::PendingEscalations::IssueCreateWorker do
let(:worker) { described_class.new }
let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status) }
let_it_be(:issue) { escalation_status.issue }
describe '#perform' do
subject { worker.perform(*args) }
context 'with valid issue' do
let(:args) { [issue.id.to_s] }
it 'processes the escalation' do
expect_next_instance_of(IncidentManagement::PendingEscalations::CreateService, escalation_status) do |service|
expect(service).to receive(:execute)
end
subject
end
end
context 'without valid issue' do
let(:args) { [non_existing_record_id] }
it 'does nothing' do
expect(IncidentManagement::PendingEscalations::CreateService).not_to receive(:new)
expect { subject }.not_to raise_error
end
end
end
end
......@@ -211,12 +211,6 @@ RSpec.describe AlertManagement::Alert do
end
end
describe '.open' do
subject { described_class.open }
it { is_expected.to contain_exactly(acknowledged_alert, triggered_alert) }
end
describe '.not_resolved' do
subject { described_class.not_resolved }
......@@ -324,33 +318,6 @@ RSpec.describe AlertManagement::Alert do
end
end
describe '.open_status?' do
using RSpec::Parameterized::TableSyntax
where(:status, :is_open_status) do
:triggered | true
:acknowledged | true
:resolved | false
:ignored | false
nil | false
end
with_them do
it 'returns true when the status is open status' do
expect(described_class.open_status?(status)).to eq(is_open_status)
end
end
end
describe '#open?' do
it 'returns true when the status is open status' do
expect(triggered_alert.open?).to be true
expect(acknowledged_alert.open?).to be true
expect(resolved_alert.open?).to be false
expect(ignored_alert.open?).to be false
end
end
describe '#to_reference' do
it { expect(triggered_alert.to_reference).to eq("^alert##{triggered_alert.iid}") }
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::IssuableEscalationStatuses::AfterUpdateService do
let_it_be(:current_user) { create(:user) }
let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, :triggered) }
let_it_be(:issue, reload: true) { escalation_status.issue }
let_it_be(:project) { issue.project }
let_it_be(:alert) { create(:alert_management_alert, issue: issue, project: project) }
let(:status_event) { :acknowledge }
let(:update_params) { { incident_management_issuable_escalation_status_attributes: { status_event: status_event } } }
let(:service) { IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new(issue, current_user) }
subject(:result) do
issue.update!(update_params)
service.execute
end
before do
issue.project.add_developer(current_user)
end
shared_examples 'does not attempt to update the alert' do
specify do
expect(::AlertManagement::Alerts::UpdateService).not_to receive(:new)
expect(result).to be_success
end
end
context 'with status attributes' do
it 'updates an the associated alert with status changes' do
expect(::AlertManagement::Alerts::UpdateService)
.to receive(:new)
.with(alert, current_user, { status: :acknowledged })
.and_call_original
expect(result).to be_success
expect(alert.reload.status).to eq(escalation_status.reload.status)
end
context 'when incident is not associated with an alert' do
before do
alert.destroy!
end
it_behaves_like 'does not attempt to update the alert'
end
context 'when status was not changed' do
let(:status_event) { :trigger }
it_behaves_like 'does not attempt to update the alert'
end
end
end
......@@ -1166,9 +1166,15 @@ RSpec.describe Issues::UpdateService, :mailer do
context 'updating escalation status' do
let(:opts) { { escalation_status: { status: 'acknowledged' } } }
let(:escalation_update_class) { ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService }
shared_examples 'updates the escalation status record' do |expected_status|
let(:service_double) { instance_double(escalation_update_class) }
it 'has correct value' do
expect(escalation_update_class).to receive(:new).with(issue, user).and_return(service_double)
expect(service_double).to receive(:execute)
update_issue(opts)
expect(issue.escalation_status.status_name).to eq(expected_status)
......@@ -1185,7 +1191,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end
it 'does not trigger side-effects' do
expect(::AlertManagement::Alerts::UpdateService).not_to receive(:new)
expect(escalation_update_class).not_to receive(:new)
update_issue(opts)
end
......@@ -1207,6 +1213,7 @@ RSpec.describe Issues::UpdateService, :mailer do
it 'syncs the update back to the alert' do
update_issue(opts)
expect(issue.escalation_status.status_name).to eq(:acknowledged)
expect(alert.reload.status_name).to eq(:acknowledged)
end
end
......
......@@ -95,6 +95,12 @@ RSpec.shared_examples 'a model including Escalatable' do
it { is_expected.to eq([ignored_escalatable, resolved_escalatable, acknowledged_escalatable, triggered_escalatable]) }
end
end
describe '.open' do
subject { all_escalatables.open }
it { is_expected.to contain_exactly(acknowledged_escalatable, triggered_escalatable) }
end
end
describe '.status_value' do
......@@ -133,6 +139,24 @@ RSpec.shared_examples 'a model including Escalatable' do
end
end
describe '.open_status?' do
using RSpec::Parameterized::TableSyntax
where(:status, :is_open_status) do
:triggered | true
:acknowledged | true
:resolved | false
:ignored | false
nil | false
end
with_them do
it 'returns true when the status is open status' do
expect(described_class.open_status?(status)).to eq(is_open_status)
end
end
end
describe '#trigger' do
subject { escalatable.trigger }
......@@ -237,6 +261,15 @@ RSpec.shared_examples 'a model including Escalatable' do
end
end
describe '#open?' do
it 'returns true when the status is open status' do
expect(triggered_escalatable.open?).to be true
expect(acknowledged_escalatable.open?).to be true
expect(resolved_escalatable.open?).to be false
expect(ignored_escalatable.open?).to be false
end
end
private
def factory_from_class(klass)
......
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