Commit dbf2aac0 authored by Vitali Tatarintev's avatar Vitali Tatarintev

Merge branch 'sy-create-escalations-for-incidents' into 'master'

Create pending escalations for incidents on update

See merge request gitlab-org/gitlab!76986
parents e8ffe5f0 ada46f7d
...@@ -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
......
...@@ -211,6 +211,8 @@ ...@@ -211,6 +211,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