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 ...@@ -78,7 +78,6 @@ module AlertManagement
scope :for_environment, -> (environment) { where(environment: environment) } scope :for_environment, -> (environment) { where(environment: environment) }
scope :for_assignee_username, -> (assignee_username) { joins(:assignees).merge(User.by_username(assignee_username)) } 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 :search, -> (query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) }
scope :open, -> { with_status(open_statuses) }
scope :not_resolved, -> { without_status(:resolved) } scope :not_resolved, -> { without_status(:resolved) }
scope :with_prometheus_alert, -> { includes(:prometheus_alert) } scope :with_prometheus_alert, -> { includes(:prometheus_alert) }
scope :with_threat_monitoring_alerts, -> { where(domain: :threat_monitoring ) } scope :with_threat_monitoring_alerts, -> { where(domain: :threat_monitoring ) }
...@@ -143,18 +142,6 @@ module AlertManagement ...@@ -143,18 +142,6 @@ module AlertManagement
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end 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? def prometheus?
monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus] monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
end end
......
...@@ -27,6 +27,8 @@ module IncidentManagement ...@@ -27,6 +27,8 @@ module IncidentManagement
ignored: 'No action will be taken' ignored: 'No action will be taken'
}.freeze }.freeze
OPEN_STATUSES = [:triggered, :acknowledged].freeze
included do included do
validates :status, presence: true validates :status, presence: true
...@@ -34,6 +36,7 @@ module IncidentManagement ...@@ -34,6 +36,7 @@ module IncidentManagement
# Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored # Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored
# https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior # 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 :order_status, -> (sort_order) { order(status: sort_order == :asc ? :desc : :asc) }
scope :open, -> { with_status(OPEN_STATUSES) }
state_machine :status, initial: :triggered do state_machine :status, initial: :triggered do
state :triggered, value: STATUSES[:triggered] state :triggered, value: STATUSES[:triggered]
...@@ -89,6 +92,10 @@ module IncidentManagement ...@@ -89,6 +92,10 @@ module IncidentManagement
@status_names ||= state_machine_statuses.keys @status_names ||= state_machine_statuses.keys
end end
def open_status?(status)
OPEN_STATUSES.include?(status)
end
private private
def state_machine_statuses def state_machine_statuses
...@@ -99,6 +106,10 @@ module IncidentManagement ...@@ -99,6 +106,10 @@ module IncidentManagement
def status_event_for(status) def status_event_for(status)
self.class.state_machines[:status].events.transitions_for(self, to: status.to_s.to_sym).first&.event self.class.state_machines[:status].events.transitions_for(self, to: status.to_s.to_sym).first&.event
end end
def open?
self.class.open_status?(status_name)
end
end end
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 ...@@ -213,13 +213,8 @@ module Issues
def handle_escalation_status_change(issue, old_escalation_status) def handle_escalation_status_change(issue, old_escalation_status)
return unless old_escalation_status.present? return unless old_escalation_status.present?
return if issue.escalation_status&.slice(:status, :policy_id) == old_escalation_status return if issue.escalation_status&.slice(:status, :policy_id) == old_escalation_status
return unless issue.alert_management_alert
::AlertManagement::Alerts::UpdateService.new( ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new(issue, current_user).execute
issue.alert_management_alert,
current_user,
status: issue.escalation_status.status_name
).execute
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
......
...@@ -207,6 +207,8 @@ ...@@ -207,6 +207,8 @@
- 1 - 1
- - incident_management_pending_escalations_issue_check - - incident_management_pending_escalations_issue_check
- 1 - 1
- - incident_management_pending_escalations_issue_create
- 1
- - integrations_create_external_cross_reference - - integrations_create_external_cross_reference
- 1 - 1
- - invalid_gpg_signature_update - - invalid_gpg_signature_update
......
...@@ -8,6 +8,10 @@ module EE ...@@ -8,6 +8,10 @@ module EE
def escalation_policy def escalation_policy
project.incident_management_escalation_policies.first project.incident_management_escalation_policies.first
end end
def pending_escalation_target
self
end
end end
end end
end end
...@@ -4,7 +4,7 @@ module IncidentManagement ...@@ -4,7 +4,7 @@ module IncidentManagement
# Functionality needed for models which represent escalations. # Functionality needed for models which represent escalations.
# #
# Implemeting classes should alias `target` to the attribute # Implemeting classes should alias `target` to the attribute
# of the relevant escalatable. # of the relevant association for the escalation.
# #
# EX) `alias_attribute :target, :alert` # EX) `alias_attribute :target, :alert`
module BasePendingEscalation module BasePendingEscalation
...@@ -29,6 +29,10 @@ module IncidentManagement ...@@ -29,6 +29,10 @@ module IncidentManagement
delegate :project, to: :target delegate :project, to: :target
def self.class_for_check_worker
raise NotImplementedError
end
def escalatable def escalatable
raise NotImplementedError raise NotImplementedError
end end
......
...@@ -4,6 +4,7 @@ module EE ...@@ -4,6 +4,7 @@ module EE
module IncidentManagement module IncidentManagement
module IssuableEscalationStatus module IssuableEscalationStatus
extend ActiveSupport::Concern extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do prepended do
belongs_to :policy, optional: true, class_name: '::IncidentManagement::EscalationPolicy' belongs_to :policy, optional: true, class_name: '::IncidentManagement::EscalationPolicy'
...@@ -24,6 +25,11 @@ module EE ...@@ -24,6 +25,11 @@ module EE
end end
end end
end end
override :pending_escalation_target
def pending_escalation_target
issue
end
end end
end end
end end
...@@ -13,6 +13,10 @@ module IncidentManagement ...@@ -13,6 +13,10 @@ module IncidentManagement
validates :rule_id, uniqueness: { scope: [:alert_id] } validates :rule_id, uniqueness: { scope: [:alert_id] }
def self.class_for_check_worker
AlertCheckWorker
end
def escalatable def escalatable
alert alert
end end
......
...@@ -13,6 +13,10 @@ module IncidentManagement ...@@ -13,6 +13,10 @@ module IncidentManagement
validates :rule_id, uniqueness: { scope: [:issue_id] } validates :rule_id, uniqueness: { scope: [:issue_id] }
def self.class_for_check_worker
IssueCheckWorker
end
def escalatable def escalatable
issue.incident_management_issuable_escalation_status issue.incident_management_issuable_escalation_status
end 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 @@ ...@@ -2,26 +2,25 @@
module IncidentManagement module IncidentManagement
module PendingEscalations module PendingEscalations
class CreateService < BaseService class CreateService < ::BaseProjectService
def initialize(target) def initialize(escalatable)
@target = target @escalatable = escalatable
@project = target.project @target = escalatable.pending_escalation_target
@process_time = Time.current @process_time = Time.current
super(project: target.project)
end end
def execute def execute
return unless ::Gitlab::IncidentManagement.escalation_policies_available?(project) && !target.resolved? return unless ::Gitlab::IncidentManagement.escalation_policies_available?(project) && !escalatable.resolved?
return unless policy = escalatable.escalation_policy
policy = project.incident_management_escalation_policies.first
return unless policy
create_escalations(policy.active_rules) create_escalations(policy.active_rules)
end end
private private
attr_reader :target, :project, :process_time attr_reader :escalatable, :target, :process_time
def create_escalations(rules) def create_escalations(rules)
escalation_ids = rules.map do |rule| escalation_ids = rules.map do |rule|
...@@ -33,8 +32,7 @@ module IncidentManagement ...@@ -33,8 +32,7 @@ module IncidentManagement
end end
def create_escalation(rule) def create_escalation(rule)
IncidentManagement::PendingEscalations::Alert.create!( target.pending_escalations.create!(
target: target,
rule: rule, rule: rule,
process_at: rule.elapsed_time_seconds.seconds.after(process_time) process_at: rule.elapsed_time_seconds.seconds.after(process_time)
) )
...@@ -43,7 +41,11 @@ module IncidentManagement ...@@ -43,7 +41,11 @@ module IncidentManagement
def process_escalations(escalation_ids) def process_escalations(escalation_ids)
args = escalation_ids.map { |id| [id] } 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 end
end end
......
...@@ -1119,6 +1119,15 @@ ...@@ -1119,6 +1119,15 @@
:weight: 1 :weight: 1
:idempotent: true :idempotent: true
:tags: [] :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 - :name: ldap_group_sync
:worker_name: LdapGroupSyncWorker :worker_name: LdapGroupSyncWorker
:feature_category: :authentication_and_authorization :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 ...@@ -8,7 +8,7 @@ FactoryBot.define do
end end
rule { association :incident_management_escalation_rule, policy: policy } 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 } process_at { 5.minutes.from_now }
end end
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 ...@@ -83,4 +83,10 @@ RSpec.describe IncidentManagement::IssuableEscalationStatus do
end end
end end
end end
describe '#pending_escalation_target' do
subject { escalation_status.pending_escalation_target }
it { is_expected.to eq(escalation_status.issue) }
end
end end
...@@ -3,5 +3,27 @@ ...@@ -3,5 +3,27 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe IncidentManagement::PendingEscalations::Alert do 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 end
...@@ -3,5 +3,29 @@ ...@@ -3,5 +3,29 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe IncidentManagement::PendingEscalations::Issue do 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 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 ...@@ -451,6 +451,7 @@ RSpec.describe Issues::UpdateService do
context 'updating escalation status' do context 'updating escalation status' do
let(:opts) { { escalation_status: { policy: policy } } } 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!(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue) }
let!(:policy) { create(:incident_management_escalation_policy, project: project) } let!(:policy) { create(:incident_management_escalation_policy, project: project) }
...@@ -461,13 +462,22 @@ RSpec.describe Issues::UpdateService do ...@@ -461,13 +462,22 @@ RSpec.describe Issues::UpdateService do
end end
# Requires `expoected_policy` and `expected_status` to be defined # Requires `expoected_policy` and `expected_status` to be defined
shared_examples 'escalation status record has correct values' do shared_examples 'updates the escalation status record' do
specify do let(:service_double) { instance_double(escalation_update_class) }
it 'has correct values' do
update_issue(opts) update_issue(opts)
expect(issue.escalation_status.policy).to eq(expected_policy) expect(issue.escalation_status.policy).to eq(expected_policy)
expect(issue.escalation_status.status_name).to eq(expected_status) expect(issue.escalation_status.status_name).to eq(expected_status)
end 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 end
shared_examples 'does not change the status record' do shared_examples 'does not change the status record' do
...@@ -476,7 +486,7 @@ RSpec.describe Issues::UpdateService do ...@@ -476,7 +486,7 @@ RSpec.describe Issues::UpdateService do
end end
it 'does not trigger side-effects' do 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) update_issue(opts)
end end
...@@ -486,7 +496,7 @@ RSpec.describe Issues::UpdateService do ...@@ -486,7 +496,7 @@ RSpec.describe Issues::UpdateService do
let(:issue) { create(:incident, project: project) } let(:issue) { create(:incident, project: project) }
context 'setting the escalation policy' do 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_policy) { policy }
let(:expected_status) { :triggered } let(:expected_status) { :triggered }
end end
...@@ -503,8 +513,9 @@ RSpec.describe Issues::UpdateService do ...@@ -503,8 +513,9 @@ RSpec.describe Issues::UpdateService do
context 'when the policy is already set' do context 'when the policy is already set' do
let!(:escalation_status) { create(:incident_management_issuable_escalation_status, :paging, issue: issue) } 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_policy) { nil }
let(:expected_status) { :triggered } let(:expected_status) { :triggered }
end end
...@@ -512,7 +523,7 @@ RSpec.describe Issues::UpdateService do ...@@ -512,7 +523,7 @@ RSpec.describe Issues::UpdateService do
context 'in addition to other attributes' do context 'in addition to other attributes' do
let(:opts) { { escalation_status: { policy: policy, status: 'acknowledged' } } } 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_policy) { nil }
let(:expected_status) { :acknowledged } let(:expected_status) { :acknowledged }
end end
......
...@@ -4,70 +4,95 @@ require 'spec_helper' ...@@ -4,70 +4,95 @@ require 'spec_helper'
RSpec.describe IncidentManagement::PendingEscalations::CreateService do RSpec.describe IncidentManagement::PendingEscalations::CreateService do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:target) { create(:alert_management_alert, project: project) }
let_it_be(:rule_count) { 2 } let_it_be(:rule_count) { 2 }
let!(:escalation_policy) { create(:incident_management_escalation_policy, project: project, rule_count: rule_count) } 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!(: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 } subject(:execute) { service.execute }
context 'feature not available' do shared_examples 'creates pending escalations appropriately' do
it 'does nothing' do context 'feature not available' 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
end
context 'feature available' do context 'feature available' do
before do before do
stub_licensed_features(oncall_schedules: true, escalation_policies: true) stub_licensed_features(oncall_schedules: true, escalation_policies: true)
end end
context 'target is resolved' do context 'target is resolved' do
let(:target) { create(:alert_management_alert, :resolved, project: project) } before do
escalatable.resolve
end
it 'does nothing' do it 'does nothing' do
expect { execute }.not_to change { IncidentManagement::PendingEscalations::Alert.count } expect { execute }.not_to change { escalation_class.count }
end
end end
end
it 'creates an escalation for each rule for the policy' do it 'creates an escalation for each rule for the policy' do
execution_time = Time.current execution_time = Time.current
expect { execute }.to change { IncidentManagement::PendingEscalations::Alert.count }.by(rule_count) expect { execute }.to change { escalation_class.count }.by(rule_count)
first_escalation, second_escalation = target.pending_escalations.order(created_at: :asc) first_escalation, second_escalation = target.pending_escalations.order(created_at: :asc)
first_rule, second_rule = rules 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: 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) expect_escalation_attributes_with(escalation: second_escalation, target: target, rule: second_rule, execution_time: execution_time)
end end
context 'when there is no escalation policy for the project' do context 'when there is no escalation policy for the project' do
let!(:escalation_policy) { nil } let!(:escalation_policy) { nil }
let!(:removed_rule) { nil } let!(:removed_rule) { nil }
it 'does nothing' do it 'does nothing' do
expect { execute }.not_to change { IncidentManagement::PendingEscalations::Alert.count } expect { execute }.not_to change { escalation_class.count }
end
end end
end
it 'creates the escalations and queues the escalation process check' do it 'creates the escalations and queues the escalation process check' do
expect(IncidentManagement::PendingEscalations::AlertCheckWorker) expect(worker_class)
.to receive(:bulk_perform_async) .to receive(:bulk_perform_async)
.with([[a_kind_of(Integer)], [a_kind_of(Integer)]]) .with([[a_kind_of(Integer)], [a_kind_of(Integer)]])
expect { execute }.to change { IncidentManagement::PendingEscalations::Alert.count }.by(rule_count) expect { execute }.to change { escalation_class.count }.by(rule_count)
end end
def expect_escalation_attributes_with(escalation:, target:, rule:, execution_time: Time.current) def expect_escalation_attributes_with(escalation:, target:, rule:, execution_time: Time.current)
expect(escalation).to have_attributes( expect(escalation).to have_attributes(
rule_id: rule.id, rule_id: rule.id,
alert_id: target.id, foreign_key => target.id,
process_at: be_within(1.minute).of(rule.elapsed_time_seconds.seconds.after(execution_time)) process_at: be_within(1.minute).of(rule.elapsed_time_seconds.seconds.after(execution_time))
) )
end
end 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 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 ...@@ -211,12 +211,6 @@ RSpec.describe AlertManagement::Alert do
end end
end end
describe '.open' do
subject { described_class.open }
it { is_expected.to contain_exactly(acknowledged_alert, triggered_alert) }
end
describe '.not_resolved' do describe '.not_resolved' do
subject { described_class.not_resolved } subject { described_class.not_resolved }
...@@ -324,33 +318,6 @@ RSpec.describe AlertManagement::Alert do ...@@ -324,33 +318,6 @@ RSpec.describe AlertManagement::Alert do
end end
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 describe '#to_reference' do
it { expect(triggered_alert.to_reference).to eq("^alert##{triggered_alert.iid}") } it { expect(triggered_alert.to_reference).to eq("^alert##{triggered_alert.iid}") }
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, :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 ...@@ -1166,9 +1166,15 @@ RSpec.describe Issues::UpdateService, :mailer do
context 'updating escalation status' do context 'updating escalation status' do
let(:opts) { { escalation_status: { status: 'acknowledged' } } } let(:opts) { { escalation_status: { status: 'acknowledged' } } }
let(:escalation_update_class) { ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService }
shared_examples 'updates the escalation status record' do |expected_status| shared_examples 'updates the escalation status record' do |expected_status|
let(:service_double) { instance_double(escalation_update_class) }
it 'has correct value' do 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) update_issue(opts)
expect(issue.escalation_status.status_name).to eq(expected_status) expect(issue.escalation_status.status_name).to eq(expected_status)
...@@ -1185,7 +1191,7 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -1185,7 +1191,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end end
it 'does not trigger side-effects' do 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) update_issue(opts)
end end
...@@ -1207,6 +1213,7 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -1207,6 +1213,7 @@ RSpec.describe Issues::UpdateService, :mailer do
it 'syncs the update back to the alert' do it 'syncs the update back to the alert' do
update_issue(opts) update_issue(opts)
expect(issue.escalation_status.status_name).to eq(:acknowledged)
expect(alert.reload.status_name).to eq(:acknowledged) expect(alert.reload.status_name).to eq(:acknowledged)
end end
end end
......
...@@ -95,6 +95,12 @@ RSpec.shared_examples 'a model including Escalatable' do ...@@ -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]) } it { is_expected.to eq([ignored_escalatable, resolved_escalatable, acknowledged_escalatable, triggered_escalatable]) }
end end
end end
describe '.open' do
subject { all_escalatables.open }
it { is_expected.to contain_exactly(acknowledged_escalatable, triggered_escalatable) }
end
end end
describe '.status_value' do describe '.status_value' do
...@@ -133,6 +139,24 @@ RSpec.shared_examples 'a model including Escalatable' do ...@@ -133,6 +139,24 @@ RSpec.shared_examples 'a model including Escalatable' do
end end
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 describe '#trigger' do
subject { escalatable.trigger } subject { escalatable.trigger }
...@@ -237,6 +261,15 @@ RSpec.shared_examples 'a model including Escalatable' do ...@@ -237,6 +261,15 @@ RSpec.shared_examples 'a model including Escalatable' do
end end
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 private
def factory_from_class(klass) 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