package alerting import ( "strings" "testing" "time" "github.com/netpulse/netpulse/server/internal/store" ) // Взведення драбини перевіряється з боку «не взвели». // // Тест «драбину взведено» доводить мало: він проходить і в реалізації, // яка взводить її завжди. Кожен із випадків нижче — це людина, яку // система розбудила б о третій ночі без причини, або (у випадку з // каналами) сповіщення, яке о нульовій хвилині свідомо не надіслали, а // о п'ятнадцятій раптом надіслали б. const policy = "p1" func act() store.RuleAction { return store.RuleAction{EscalationPolicyID: policy, Source: "icmp"} } // Контроль: нормальний випадок драбину взводить. func TestEscalationArmedForFiringAlert(t *testing.T) { if !shouldArmEscalation(store.Alert{State: "firing"}, act(), true, true) { t.Fatal("алерт, про який справді повідомили, має отримати драбину") } } // Правило без політики — типовий стан, і він має лишатись мовчазним. // // Це головна вимога до оновлення: жоден наявний кабінет не має раптом // почати будити людей тільки тому, що з'явилась нова можливість. func TestNoPolicyMeansNoEscalation(t *testing.T) { empty := store.RuleAction{Source: "icmp"} if shouldArmEscalation(store.Alert{State: "firing"}, empty, true, true) { t.Fatal("правило без політики не має ескалювати") } // Правила взагалі немає (його видалили, алерт лишився). if shouldArmEscalation(store.Alert{State: "firing"}, store.RuleAction{}, false, true) { t.Fatal("алерт без правила ескалювати нікуди") } } // Придушений алерт драбини не отримує: заглушення означає «не // турбувати», а драбина — це рівно турбування, тільки за розкладом. func TestSuppressedAlertIsNotArmed(t *testing.T) { for _, st := range []string{"suppressed", "acknowledged", "resolved", "expired"} { if shouldArmEscalation(store.Alert{State: st}, act(), true, true) { t.Errorf("стан %q отримав драбину", st) } } } // Найтонший випадок: алерту не було куди піти. // // Каналів не знайшлось через поріг серйозності, вимкнений канал або // порожній перелік. Взвести драбину означало б надіслати о 15-й хвилині // те, що на нульовій ми свідомо не надсилали, — і людина прочитала б це // як несправність. Нею це й було б. // // Тиха година сюди НЕ належить: там канали є, просто зараз ніч, і // targets() повідомляє це окремо. Раніше вона потрапляла в цю ж гілку й // роззброювала драбину назавжди — див. TestQuietHourStillArms. func TestEscalationNotArmedWhenNothingWasSent(t *testing.T) { if shouldArmEscalation(store.Alert{State: "firing"}, act(), true, false) { t.Fatal("драбина взведена попри те, що перше сповіщення нікуди не пішло") } } // --------------------------------------------------------------------- // Шапка ескалації має з першого рядка пояснювати, чому це третє // повідомлення про ту саму аварію. Без цього розбуджений керівник // вимкне канал, вирішивши, що розсилка зациклилась. func TestEscalationHeaderExplainsItself(t *testing.T) { start := time.Now().Add(-45 * time.Minute) snap := store.EscalationSnapshot{ PolicyName: "Нічне чергування", Steps: []store.EscalationStep{ {AfterMin: 15}, {AfterMin: 45}, }, Alert: store.Alert{StartedAt: start}, } head := escalationHeader(snap, store.EscalationDecision{StepIdx: 1}) for _, want := range []string{"ЕСКАЛАЦІЯ", "2/2", "Нічне чергування", "не підтверджено"} { if !strings.Contains(head, want) { t.Errorf("у шапці немає %q:\n%s", want, head) } } // Повтор має бути видно окремо: «друга сходинка» і «друга сходинка // втретє» — це різні новини для того, хто їх читає. rep := escalationHeader(snap, store.EscalationDecision{StepIdx: 1, RepeatIdx: 2}) if !strings.Contains(rep, "повтор 2") { t.Errorf("номер повтору не показано:\n%s", rep) } }