Commit 028b597d authored by Sarah Yasonik's avatar Sarah Yasonik Committed by Stan Hu

Add escalation status for incident issues

parent 06e8a68b
...@@ -46,7 +46,7 @@ module AlertManagement ...@@ -46,7 +46,7 @@ module AlertManagement
def by_status(collection) def by_status(collection)
values = AlertManagement::Alert.status_names & Array(params[:status]) values = AlertManagement::Alert.status_names & Array(params[:status])
values.present? ? collection.for_status(values) : collection values.present? ? collection.with_status(values) : collection
end end
def by_search(collection) def by_search(collection)
......
...@@ -13,20 +13,7 @@ module AlertManagement ...@@ -13,20 +13,7 @@ module AlertManagement
include Presentable include Presentable
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include Referable include Referable
include ::IncidentManagement::Escalatable
STATUSES = {
triggered: 0,
acknowledged: 1,
resolved: 2,
ignored: 3
}.freeze
STATUS_DESCRIPTIONS = {
triggered: 'Investigation has not started',
acknowledged: 'Someone is actively investigating the problem',
resolved: 'No further work is required',
ignored: 'No action will be taken on the alert'
}.freeze
belongs_to :project belongs_to :project
belongs_to :issue, optional: true belongs_to :issue, optional: true
...@@ -44,6 +31,9 @@ module AlertManagement ...@@ -44,6 +31,9 @@ module AlertManagement
sha_attribute :fingerprint sha_attribute :fingerprint
# Allow :ended_at to be managed by Escalatable
alias_attribute :resolved_at, :ended_at
TITLE_MAX_LENGTH = 200 TITLE_MAX_LENGTH = 200
DESCRIPTION_MAX_LENGTH = 1_000 DESCRIPTION_MAX_LENGTH = 1_000
SERVICE_MAX_LENGTH = 100 SERVICE_MAX_LENGTH = 100
...@@ -57,7 +47,6 @@ module AlertManagement ...@@ -57,7 +47,6 @@ module AlertManagement
validates :project, presence: true validates :project, presence: true
validates :events, presence: true validates :events, presence: true
validates :severity, presence: true validates :severity, presence: true
validates :status, presence: true
validates :started_at, presence: true validates :started_at, presence: true
validates :fingerprint, allow_blank: true, uniqueness: { validates :fingerprint, allow_blank: true, uniqueness: {
scope: :project, scope: :project,
...@@ -80,52 +69,10 @@ module AlertManagement ...@@ -80,52 +69,10 @@ module AlertManagement
threat_monitoring: 1 threat_monitoring: 1
} }
state_machine :status, initial: :triggered do
state :triggered, value: STATUSES[:triggered]
state :acknowledged, value: STATUSES[:acknowledged]
state :resolved, value: STATUSES[:resolved] do
validates :ended_at, presence: true
end
state :ignored, value: STATUSES[:ignored]
state :triggered, :acknowledged, :ignored do
validates :ended_at, absence: true
end
event :trigger do
transition any => :triggered
end
event :acknowledge do
transition any => :acknowledged
end
event :resolve do
transition any => :resolved
end
event :ignore do
transition any => :ignored
end
before_transition to: [:triggered, :acknowledged, :ignored] do |alert, _transition|
alert.ended_at = nil
end
before_transition to: :resolved do |alert, transition|
ended_at = transition.args.first
alert.ended_at = ended_at || Time.current
end
end
delegate :iid, to: :issue, prefix: true, allow_nil: true delegate :iid, to: :issue, prefix: true, allow_nil: true
delegate :details_url, to: :present delegate :details_url, to: :present
scope :for_iid, -> (iid) { where(iid: iid) } scope :for_iid, -> (iid) { where(iid: iid) }
scope :for_status, -> (status) { with_status(status) }
scope :for_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) } scope :for_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) }
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)) }
...@@ -146,36 +93,14 @@ module AlertManagement ...@@ -146,36 +93,14 @@ module AlertManagement
scope :order_severity, -> (sort_order) { order(severity: sort_order == :asc ? :desc : :asc) } scope :order_severity, -> (sort_order) { order(severity: sort_order == :asc ? :desc : :asc) }
scope :order_severity_with_open_prometheus_alert, -> { open.with_prometheus_alert.order(severity: :asc, started_at: :desc) } scope :order_severity_with_open_prometheus_alert, -> { open.with_prometheus_alert.order(severity: :asc, started_at: :desc) }
# Ascending sort order sorts statuses: Ignored > Resolved > Acknowledged > Triggered
# 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 :counts_by_project_id, -> { group(:project_id).count } scope :counts_by_project_id, -> { group(:project_id).count }
alias_method :state, :status_name alias_method :state, :status_name
def self.state_machine_statuses
@state_machine_statuses ||= state_machines[:status].states.to_h { |s| [s.name, s.value] }
end
private_class_method :state_machine_statuses
def self.status_value(name)
state_machine_statuses[name]
end
def self.status_name(raw_status)
state_machine_statuses.key(raw_status)
end
def self.counts_by_status def self.counts_by_status
group(:status).count.transform_keys { |k| status_name(k) } group(:status).count.transform_keys { |k| status_name(k) }
end end
def self.status_names
@status_names ||= state_machine_statuses.keys
end
def self.sort_by_attribute(method) def self.sort_by_attribute(method)
case method.to_s case method.to_s
when 'started_at_asc' then order_start_time(:asc) when 'started_at_asc' then order_start_time(:asc)
...@@ -229,15 +154,6 @@ module AlertManagement ...@@ -229,15 +154,6 @@ module AlertManagement
self.class.open_status?(status_name) self.class.open_status?(status_name)
end end
def status_event_for(status)
self.class.state_machines[:status].events.transitions_for(self, to: status.to_s.to_sym).first&.event
end
def change_status_to(new_status)
event = status_event_for(new_status)
event && fire_status_event(event)
end
def prometheus? def prometheus?
monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus] monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
end end
......
# frozen_string_literal: true
module IncidentManagement
# Shared functionality for a `#status` field, representing
# whether action is required. In EE, this corresponds
# to paging functionality with EscalationPolicies.
#
# This module is only responsible for setting the status and
# possible status-related timestamps (EX triggered_at/resolved_at)
# for the implementing class. The relationships between these
# values and other related timestamps/logic should be managed from
# the object class itself. (EX Alert#ended_at = Alert#resolved_at)
module Escalatable
extend ActiveSupport::Concern
STATUSES = {
triggered: 0,
acknowledged: 1,
resolved: 2,
ignored: 3
}.freeze
STATUS_DESCRIPTIONS = {
triggered: 'Investigation has not started',
acknowledged: 'Someone is actively investigating the problem',
resolved: 'The problem has been addressed',
ignored: 'No action will be taken'
}.freeze
included do
validates :status, presence: true
# Ascending sort order sorts statuses: Ignored > Resolved > Acknowledged > Triggered
# 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) }
state_machine :status, initial: :triggered do
state :triggered, value: STATUSES[:triggered]
state :acknowledged, value: STATUSES[:acknowledged]
state :resolved, value: STATUSES[:resolved] do
validates :resolved_at, presence: true
end
state :ignored, value: STATUSES[:ignored]
state :triggered, :acknowledged, :ignored do
validates :resolved_at, absence: true
end
event :trigger do
transition any => :triggered
end
event :acknowledge do
transition any => :acknowledged
end
event :resolve do
transition any => :resolved
end
event :ignore do
transition any => :ignored
end
before_transition to: [:triggered, :acknowledged, :ignored] do |escalatable, _transition|
escalatable.resolved_at = nil
end
before_transition to: :resolved do |escalatable, transition|
resolved_at = transition.args.first
escalatable.resolved_at = resolved_at || Time.current
end
end
class << self
def status_value(name)
state_machine_statuses[name]
end
def status_name(raw_status)
state_machine_statuses.key(raw_status)
end
def status_names
@status_names ||= state_machine_statuses.keys
end
private
def state_machine_statuses
@state_machine_statuses ||= state_machines[:status].states.to_h { |s| [s.name, s.value] }
end
end
def status_event_for(status)
self.class.state_machines[:status].events.transitions_for(self, to: status.to_s.to_sym).first&.event
end
end
end
end
# frozen_string_literal: true
module IncidentManagement
class IssuableEscalationStatus < ApplicationRecord
include ::IncidentManagement::Escalatable
self.table_name = 'incident_management_issuable_escalation_statuses'
belongs_to :issue
validates :issue, presence: true, uniqueness: true
end
end
IncidentManagement::IssuableEscalationStatus.prepend_mod_with('IncidentManagement::IssuableEscalationStatus')
...@@ -77,6 +77,7 @@ class Issue < ApplicationRecord ...@@ -77,6 +77,7 @@ class Issue < ApplicationRecord
has_one :issuable_severity has_one :issuable_severity
has_one :sentry_issue has_one :sentry_issue
has_one :alert_management_alert, class_name: 'AlertManagement::Alert' has_one :alert_management_alert, class_name: 'AlertManagement::Alert'
has_one :incident_management_issuable_escalation_status, class_name: 'IncidentManagement::IssuableEscalationStatus'
has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_many :prometheus_alerts, through: :prometheus_alert_events has_many :prometheus_alerts, through: :prometheus_alert_events
......
# frozen_string_literal: true
class CreateIncidentManagementIssuableEscalationStatuses < ActiveRecord::Migration[6.1]
ISSUE_IDX = 'index_uniq_im_issuable_escalation_statuses_on_issue_id'
POLICY_IDX = 'index_im_issuable_escalation_statuses_on_policy_id'
def change
create_table :incident_management_issuable_escalation_statuses do |t|
t.timestamps_with_timezone
t.references :issue, foreign_key: { on_delete: :cascade }, index: { unique: true, name: ISSUE_IDX }, null: false
t.references :policy, foreign_key: { to_table: :incident_management_escalation_policies, on_delete: :nullify }, index: { name: POLICY_IDX }
t.datetime_with_timezone :escalations_started_at
t.datetime_with_timezone :resolved_at
t.integer :status, default: 0, null: false, limit: 2
end
end
end
ce20c699d6e6d6baf812c926dde08485764faa2fdeb8af14808670bf692aab00
\ No newline at end of file
...@@ -14020,6 +14020,26 @@ CREATE SEQUENCE incident_management_escalation_rules_id_seq ...@@ -14020,6 +14020,26 @@ CREATE SEQUENCE incident_management_escalation_rules_id_seq
ALTER SEQUENCE incident_management_escalation_rules_id_seq OWNED BY incident_management_escalation_rules.id; ALTER SEQUENCE incident_management_escalation_rules_id_seq OWNED BY incident_management_escalation_rules.id;
CREATE TABLE incident_management_issuable_escalation_statuses (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
issue_id bigint NOT NULL,
policy_id bigint,
escalations_started_at timestamp with time zone,
resolved_at timestamp with time zone,
status smallint DEFAULT 0 NOT NULL
);
CREATE SEQUENCE incident_management_issuable_escalation_statuses_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE incident_management_issuable_escalation_statuses_id_seq OWNED BY incident_management_issuable_escalation_statuses.id;
CREATE TABLE incident_management_oncall_participants ( CREATE TABLE incident_management_oncall_participants (
id bigint NOT NULL, id bigint NOT NULL,
oncall_rotation_id bigint NOT NULL, oncall_rotation_id bigint NOT NULL,
...@@ -20403,6 +20423,8 @@ ALTER TABLE ONLY incident_management_escalation_policies ALTER COLUMN id SET DEF ...@@ -20403,6 +20423,8 @@ ALTER TABLE ONLY incident_management_escalation_policies ALTER COLUMN id SET DEF
ALTER TABLE ONLY incident_management_escalation_rules ALTER COLUMN id SET DEFAULT nextval('incident_management_escalation_rules_id_seq'::regclass); ALTER TABLE ONLY incident_management_escalation_rules ALTER COLUMN id SET DEFAULT nextval('incident_management_escalation_rules_id_seq'::regclass);
ALTER TABLE ONLY incident_management_issuable_escalation_statuses ALTER COLUMN id SET DEFAULT nextval('incident_management_issuable_escalation_statuses_id_seq'::regclass);
ALTER TABLE ONLY incident_management_oncall_participants ALTER COLUMN id SET DEFAULT nextval('incident_management_oncall_participants_id_seq'::regclass); ALTER TABLE ONLY incident_management_oncall_participants ALTER COLUMN id SET DEFAULT nextval('incident_management_oncall_participants_id_seq'::regclass);
ALTER TABLE ONLY incident_management_oncall_rotations ALTER COLUMN id SET DEFAULT nextval('incident_management_oncall_rotations_id_seq'::regclass); ALTER TABLE ONLY incident_management_oncall_rotations ALTER COLUMN id SET DEFAULT nextval('incident_management_oncall_rotations_id_seq'::regclass);
...@@ -21811,6 +21833,9 @@ ALTER TABLE ONLY incident_management_escalation_policies ...@@ -21811,6 +21833,9 @@ ALTER TABLE ONLY incident_management_escalation_policies
ALTER TABLE ONLY incident_management_escalation_rules ALTER TABLE ONLY incident_management_escalation_rules
ADD CONSTRAINT incident_management_escalation_rules_pkey PRIMARY KEY (id); ADD CONSTRAINT incident_management_escalation_rules_pkey PRIMARY KEY (id);
ALTER TABLE ONLY incident_management_issuable_escalation_statuses
ADD CONSTRAINT incident_management_issuable_escalation_statuses_pkey PRIMARY KEY (id);
ALTER TABLE ONLY incident_management_oncall_participants ALTER TABLE ONLY incident_management_oncall_participants
ADD CONSTRAINT incident_management_oncall_participants_pkey PRIMARY KEY (id); ADD CONSTRAINT incident_management_oncall_participants_pkey PRIMARY KEY (id);
...@@ -24053,6 +24078,8 @@ CREATE INDEX index_identities_on_saml_provider_id ON identities USING btree (sam ...@@ -24053,6 +24078,8 @@ CREATE INDEX index_identities_on_saml_provider_id ON identities USING btree (sam
CREATE INDEX index_identities_on_user_id ON identities USING btree (user_id); CREATE INDEX index_identities_on_user_id ON identities USING btree (user_id);
CREATE INDEX index_im_issuable_escalation_statuses_on_policy_id ON incident_management_issuable_escalation_statuses USING btree (policy_id);
CREATE UNIQUE INDEX index_im_oncall_schedules_on_project_id_and_iid ON incident_management_oncall_schedules USING btree (project_id, iid); CREATE UNIQUE INDEX index_im_oncall_schedules_on_project_id_and_iid ON incident_management_oncall_schedules USING btree (project_id, iid);
CREATE UNIQUE INDEX index_import_export_uploads_on_group_id ON import_export_uploads USING btree (group_id) WHERE (group_id IS NOT NULL); CREATE UNIQUE INDEX index_import_export_uploads_on_group_id ON import_export_uploads USING btree (group_id) WHERE (group_id IS NOT NULL);
...@@ -25371,6 +25398,8 @@ CREATE INDEX index_u2f_registrations_on_key_handle ON u2f_registrations USING bt ...@@ -25371,6 +25398,8 @@ CREATE INDEX index_u2f_registrations_on_key_handle ON u2f_registrations USING bt
CREATE INDEX index_u2f_registrations_on_user_id ON u2f_registrations USING btree (user_id); CREATE INDEX index_u2f_registrations_on_user_id ON u2f_registrations USING btree (user_id);
CREATE UNIQUE INDEX index_uniq_im_issuable_escalation_statuses_on_issue_id ON incident_management_issuable_escalation_statuses USING btree (issue_id);
CREATE UNIQUE INDEX index_unique_issue_metrics_issue_id ON issue_metrics USING btree (issue_id); CREATE UNIQUE INDEX index_unique_issue_metrics_issue_id ON issue_metrics USING btree (issue_id);
CREATE INDEX index_unit_test_failures_failed_at ON ci_unit_test_failures USING btree (failed_at DESC); CREATE INDEX index_unit_test_failures_failed_at ON ci_unit_test_failures USING btree (failed_at DESC);
...@@ -27109,6 +27138,9 @@ ALTER TABLE ONLY dast_site_validations ...@@ -27109,6 +27138,9 @@ ALTER TABLE ONLY dast_site_validations
ALTER TABLE ONLY vulnerability_findings_remediations ALTER TABLE ONLY vulnerability_findings_remediations
ADD CONSTRAINT fk_rails_28a8d0cf93 FOREIGN KEY (vulnerability_occurrence_id) REFERENCES vulnerability_occurrences(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_28a8d0cf93 FOREIGN KEY (vulnerability_occurrence_id) REFERENCES vulnerability_occurrences(id) ON DELETE CASCADE;
ALTER TABLE ONLY incident_management_issuable_escalation_statuses
ADD CONSTRAINT fk_rails_29abffe3b9 FOREIGN KEY (policy_id) REFERENCES incident_management_escalation_policies(id) ON DELETE SET NULL;
ALTER TABLE ONLY resource_state_events ALTER TABLE ONLY resource_state_events
ADD CONSTRAINT fk_rails_29af06892a FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_29af06892a FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
...@@ -28360,6 +28392,9 @@ ALTER TABLE incident_management_pending_alert_escalations ...@@ -28360,6 +28392,9 @@ ALTER TABLE incident_management_pending_alert_escalations
ALTER TABLE ONLY board_group_recent_visits ALTER TABLE ONLY board_group_recent_visits
ADD CONSTRAINT fk_rails_f410736518 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_f410736518 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY incident_management_issuable_escalation_statuses
ADD CONSTRAINT fk_rails_f4c811fd28 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
ALTER TABLE ONLY resource_state_events ALTER TABLE ONLY resource_state_events
ADD CONSTRAINT fk_rails_f5827a7ccd FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; ADD CONSTRAINT fk_rails_f5827a7ccd FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
...@@ -14635,8 +14635,8 @@ Alert status values. ...@@ -14635,8 +14635,8 @@ Alert status values.
| Value | Description | | Value | Description |
| ----- | ----------- | | ----- | ----------- |
| <a id="alertmanagementstatusacknowledged"></a>`ACKNOWLEDGED` | Someone is actively investigating the problem. | | <a id="alertmanagementstatusacknowledged"></a>`ACKNOWLEDGED` | Someone is actively investigating the problem. |
| <a id="alertmanagementstatusignored"></a>`IGNORED` | No action will be taken on the alert. | | <a id="alertmanagementstatusignored"></a>`IGNORED` | No action will be taken. |
| <a id="alertmanagementstatusresolved"></a>`RESOLVED` | No further work is required. | | <a id="alertmanagementstatusresolved"></a>`RESOLVED` | The problem has been addressed. |
| <a id="alertmanagementstatustriggered"></a>`TRIGGERED` | Investigation has not started. | | <a id="alertmanagementstatustriggered"></a>`TRIGGERED` | Investigation has not started. |
### `ApiFuzzingScanMode` ### `ApiFuzzingScanMode`
......
# frozen_string_literal: true
module EE
module IncidentManagement
module IssuableEscalationStatus
extend ActiveSupport::Concern
prepended do
belongs_to :policy, optional: true, class_name: '::IncidentManagement::EscalationPolicy'
validate :presence_or_absence_of_policy_attrs
state_machine :status, initial: :triggered do
before_transition to: :triggered do |escalation_status|
escalation_status.escalations_started_at = escalation_status.policy_id ? Time.current : nil
end
end
private
def presence_or_absence_of_policy_attrs
if policy_id.present? ^ escalations_started_at.present?
errors.add(:policy, 'must be set with escalations_started_at')
end
end
end
end
end
end
...@@ -8,7 +8,7 @@ module IncidentManagement ...@@ -8,7 +8,7 @@ module IncidentManagement
belongs_to :oncall_schedule, class_name: 'OncallSchedule', foreign_key: 'oncall_schedule_id', optional: true belongs_to :oncall_schedule, class_name: 'OncallSchedule', foreign_key: 'oncall_schedule_id', optional: true
belongs_to :user, optional: true belongs_to :user, optional: true
enum status: AlertManagement::Alert::STATUSES.slice(:acknowledged, :resolved) enum status: ::IncidentManagement::Escalatable::STATUSES.slice(:acknowledged, :resolved)
validates :status, presence: true validates :status, presence: true
validates :elapsed_time_seconds, validates :elapsed_time_seconds,
......
# frozen_string_literal: true
FactoryBot.modify do
factory :incident_management_issuable_escalation_status, class: 'IncidentManagement::IssuableEscalationStatus' do
trait :paging do
policy { association :incident_management_escalation_policy, project: issue.project }
escalations_started_at { Time.current }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::IssuableEscalationStatus do
let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, :paging, :acknowledged) }
subject { escalation_status }
describe 'validations' do
context 'when policy and escalation start time are both provided' do
it { is_expected.to be_valid }
end
context 'when neither policy and escalation start time are provided' do
let(:escalation_status) { build(:incident_management_issuable_escalation_status) }
it { is_expected.to be_valid }
end
context 'when escalation start time is provided without a policy' do
it 'is invalid' do
escalation_status.policy_id = nil
expect(escalation_status).to be_invalid
expect(escalation_status.errors.messages[:policy]).to eq(['must be set with escalations_started_at'])
end
end
context 'when policy is provided without an escalation start time' do
it 'is invalid' do
escalation_status.escalations_started_at = nil
expect(escalation_status).to be_invalid
expect(escalation_status.errors.messages[:policy]).to eq(['must be set with escalations_started_at'])
end
end
end
describe '#trigger' do
subject(:trigger) { escalation_status.trigger }
context 'with escalation policy' do
it 'updates escalations_started_at' do
expect { trigger }.to change(escalation_status, :escalations_started_at)
expect(escalation_status.escalations_started_at).to be_present
end
end
context 'without escalation policy' do
let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status) }
it 'does not change escalations_started_at' do
expect { trigger }.to not_change(escalation_status, :escalations_started_at)
expect(escalation_status.reload.escalations_started_at).to be_nil
end
end
end
[:acknowledge, :ignore, :resolve].each do |status_event|
describe "#{status_event}" do
subject { escalation_status.send(status_event) }
it 'does not change escalations_started_at' do
expect { subject }.not_to change(escalation_status, :escalations_started_at)
expect(escalation_status.reload.escalations_started_at).to be_present
end
end
end
end
...@@ -11,7 +11,7 @@ RSpec.describe IncidentManagement::PendingEscalations::ProcessService do ...@@ -11,7 +11,7 @@ RSpec.describe IncidentManagement::PendingEscalations::ProcessService do
let!(:escalation_policy) { create(:incident_management_escalation_policy, project: project, rules: [escalation_rule]) } let!(:escalation_policy) { create(:incident_management_escalation_policy, project: project, rules: [escalation_rule]) }
let(:alert) { create(:alert_management_alert, project: project, **alert_params) } let(:alert) { create(:alert_management_alert, project: project, **alert_params) }
let(:alert_params) { { status: AlertManagement::Alert::STATUSES[:triggered] } } let(:alert_params) { { status: ::IncidentManagement::Escalatable::STATUSES[:triggered] } }
let(:target) { alert } let(:target) { alert }
let(:process_at) { 5.minutes.ago } let(:process_at) { 5.minutes.ago }
......
# frozen_string_literal: true
FactoryBot.define do
factory :incident_management_issuable_escalation_status, class: 'IncidentManagement::IssuableEscalationStatus' do
issue
triggered
trait :triggered do
status { ::IncidentManagement::IssuableEscalationStatus.status_value(:triggered) }
end
trait :acknowledged do
status { ::IncidentManagement::IssuableEscalationStatus.status_value(:acknowledged) }
end
trait :resolved do
status { ::IncidentManagement::IssuableEscalationStatus.status_value(:resolved) }
resolved_at { Time.current }
end
trait :ignored do
status { ::IncidentManagement::IssuableEscalationStatus.status_value(:ignored) }
end
end
end
...@@ -57,6 +57,7 @@ issues: ...@@ -57,6 +57,7 @@ issues:
- issue_email_participants - issue_email_participants
- test_reports - test_reports
- requirement - requirement
- incident_management_issuable_escalation_status
work_item_type: work_item_type:
- issues - issues
events: events:
......
This diff is collapsed.
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::IssuableEscalationStatus do
let_it_be(:issue) { create(:issue) }
subject(:escalation_status) { build(:incident_management_issuable_escalation_status, issue: issue) }
it { is_expected.to be_valid }
describe 'associations' do
it { is_expected.to belong_to(:issue) }
end
describe 'validatons' do
it { is_expected.to validate_presence_of(:issue) }
it { is_expected.to validate_uniqueness_of(:issue) }
end
it_behaves_like 'a model including Escalatable'
end
...@@ -33,6 +33,7 @@ RSpec.describe Issue do ...@@ -33,6 +33,7 @@ RSpec.describe Issue do
it { is_expected.to have_many(:prometheus_alerts) } it { is_expected.to have_many(:prometheus_alerts) }
it { is_expected.to have_many(:issue_email_participants) } it { is_expected.to have_many(:issue_email_participants) }
it { is_expected.to have_many(:timelogs).autosave(true) } it { is_expected.to have_many(:timelogs).autosave(true) }
it { is_expected.to have_one(:incident_management_issuable_escalation_status) }
describe 'versions.most_recent' do describe 'versions.most_recent' do
it 'returns the most recent version' do it 'returns the most recent version' do
......
# frozen_string_literal: true
RSpec.shared_examples 'a model including Escalatable' do
# rubocop:disable Rails/SaveBang -- Usage of factory symbol as argument causes a false-positive
let_it_be(:escalatable_factory) { factory_from_class(described_class) }
let_it_be(:triggered_escalatable, reload: true) { create(escalatable_factory, :triggered) }
let_it_be(:acknowledged_escalatable, reload: true) { create(escalatable_factory, :acknowledged) }
let_it_be(:resolved_escalatable, reload: true) { create(escalatable_factory, :resolved) }
let_it_be(:ignored_escalatable, reload: true) { create(escalatable_factory, :ignored) }
context 'validations' do
it { is_expected.to validate_presence_of(:status) }
context 'when status is triggered' do
subject { triggered_escalatable }
context 'when resolved_at is blank' do
it { is_expected.to be_valid }
end
context 'when resolved_at is present' do
before do
triggered_escalatable.resolved_at = Time.current
end
it { is_expected.to be_invalid }
end
end
context 'when status is acknowledged' do
subject { acknowledged_escalatable }
context 'when resolved_at is blank' do
it { is_expected.to be_valid }
end
context 'when resolved_at is present' do
before do
acknowledged_escalatable.resolved_at = Time.current
end
it { is_expected.to be_invalid }
end
end
context 'when status is resolved' do
subject { resolved_escalatable }
context 'when resolved_at is blank' do
before do
resolved_escalatable.resolved_at = nil
end
it { is_expected.to be_invalid }
end
context 'when resolved_at is present' do
it { is_expected.to be_valid }
end
end
context 'when status is ignored' do
subject { ignored_escalatable }
context 'when resolved_at is blank' do
it { is_expected.to be_valid }
end
context 'when resolved_at is present' do
before do
ignored_escalatable.resolved_at = Time.current
end
it { is_expected.to be_invalid }
end
end
end
context 'scopes' do
let(:all_escalatables) { described_class.where(id: [triggered_escalatable, acknowledged_escalatable, ignored_escalatable, resolved_escalatable])}
describe '.order_status' do
subject { all_escalatables.order_status(order) }
context 'descending' do
let(:order) { :desc }
# Downward arrow in UI always corresponds to default sort
it { is_expected.to eq([triggered_escalatable, acknowledged_escalatable, resolved_escalatable, ignored_escalatable]) }
end
context 'ascending' do
let(:order) { :asc }
it { is_expected.to eq([ignored_escalatable, resolved_escalatable, acknowledged_escalatable, triggered_escalatable]) }
end
end
end
describe '.status_value' do
using RSpec::Parameterized::TableSyntax
where(:status, :status_value) do
:triggered | 0
:acknowledged | 1
:resolved | 2
:ignored | 3
:unknown | nil
end
with_them do
it 'returns status value by its name' do
expect(described_class.status_value(status)).to eq(status_value)
end
end
end
describe '.status_name' do
using RSpec::Parameterized::TableSyntax
where(:raw_status, :status) do
0 | :triggered
1 | :acknowledged
2 | :resolved
3 | :ignored
-1 | nil
end
with_them do
it 'returns status name by its values' do
expect(described_class.status_name(raw_status)).to eq(status)
end
end
end
describe '#trigger' do
subject { escalatable.trigger }
context 'when escalatable is in triggered state' do
let(:escalatable) { triggered_escalatable }
it 'does not change the escalatable status' do
expect { subject }.not_to change { escalatable.reload.status }
end
end
context 'when escalatable is not in triggered state' do
let(:escalatable) { resolved_escalatable }
it 'changes the escalatable status to triggered' do
expect { subject }.to change { escalatable.triggered? }.to(true)
end
it 'resets resolved at' do
expect { subject }.to change { escalatable.reload.resolved_at }.to nil
end
end
end
describe '#acknowledge' do
subject { escalatable.acknowledge }
let(:escalatable) { resolved_escalatable }
it 'changes the escalatable status to acknowledged' do
expect { subject }.to change { escalatable.acknowledged? }.to(true)
end
it 'resets ended at' do
expect { subject }.to change { escalatable.reload.resolved_at }.to nil
end
end
describe '#resolve' do
let!(:resolved_at) { Time.current }
subject do
escalatable.resolved_at = resolved_at
escalatable.resolve
end
context 'when escalatable is already resolved' do
let(:escalatable) { resolved_escalatable }
it 'does not change the escalatable status' do
expect { subject }.not_to change { resolved_escalatable.reload.status }
end
end
context 'when escalatable is not resolved' do
let(:escalatable) { triggered_escalatable }
it 'changes escalatable status to "resolved"' do
expect { subject }.to change { escalatable.resolved? }.to(true)
end
end
end
describe '#ignore' do
subject { escalatable.ignore }
let(:escalatable) { resolved_escalatable }
it 'changes the escalatable status to ignored' do
expect { subject }.to change { escalatable.ignored? }.to(true)
end
it 'resets ended at' do
expect { subject }.to change { escalatable.reload.resolved_at }.to nil
end
end
describe '#status_event_for' do
using RSpec::Parameterized::TableSyntax
where(:for_status, :event) do
:triggered | :trigger
'triggered' | :trigger
:acknowledged | :acknowledge
'acknowledged' | :acknowledge
:resolved | :resolve
'resolved' | :resolve
:ignored | :ignore
'ignored' | :ignore
:unknown | nil
nil | nil
'' | nil
1 | nil
end
with_them do
let(:escalatable) { build(escalatable_factory) }
it 'returns event by status name' do
expect(escalatable.status_event_for(for_status)).to eq(event)
end
end
end
private
def factory_from_class(klass)
klass.name.underscore.tr('/', '_')
end
end
# rubocop:enable Rails/SaveBang
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