Netpulse_SasS/server/internal/store/alerts_rule_channels_test.go
byrsapty 954be1d643
All checks were successful
CI / hygiene (push) Successful in 9s
CI / web (push) Successful in 1m15s
CI / server (push) Successful in 1m34s
CI / agent (push) Successful in 2m59s
Ескалації: закриваю те, що минулого разу закрив наполовину
За другою рецензією:

* scripts/dbtest.sh писав у шапці «не напрямляйте на робочу базу» й
  нічого для цього не робив — перевірено, пішов котити міграції на базу
  з бойовим іменем. Тепер вимагає probe/test в імені.
* sendText ковтав помилку, тож журнал ескалацій писав «надіслано» на
  сходинці, жодне повідомлення якої не дійшло. Три результати замість
  двох: no_channels, failed, sent.
* stopped_at IS NULL рятував лише від ack; гасіння правилом і
  ResolveMissing рядка драбини не чіпають, і сходинка дзвонила за
  погашеним алертом. Додано перевірку стану алерту в тому ж UPDATE.
* escalate() блокував весь тік движка — мертвий вебхук одного кабінету
  зупиняв обчислення правил усім. Винесено в RunEscalations.
* алерт, народжений під заглушенням, не сповіщався ніколи: ні при
  народженні, ні коли вікно скінчилось. Тепер перехід suppressed→firing
  сповіщається, а драбина рахує час від першого сповіщення.
* alr.rules.channel_ids приймав чужі канали, глушачи і сповіщення, і
  драбину. Перевірка як для сходинок; DeleteChannel чистить посилання.

І перше, що зловив прогін проти справжньої бази: nil-зріз каналів їде
явним NULL повз DEFAULT '{}' — правило без каналів давало 500.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:44:38 +03:00

93 lines
5.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package store
import (
"errors"
"strings"
"testing"
)
// Перевірки правила, яке шле в канал.
//
// Та сама вада, що вже закрита для сходинок драбини, тільки дорожча.
// alr.rules.channel_ids має тип uuid[], зовнішнього ключа на масив у
// Postgres немає — тож у правило лягав будь-який UUID: хоч вигаданий,
// хоч підставлений із сусіднього кабінету. У формі таке правило
// виглядало налаштованим на канал.
//
// Ціна: targets() для правила з явно вказаними каналами маршрути вже не
// питає, жодного каналу за мертвим id не знаходить, sent лишається
// нулем — і через нуль shouldArmEscalation не взводить навіть драбину.
// Тобто одна помилка в ідентифікаторі глушить і сповіщення, і резервний
// механізм, заведений рівно на випадок, коли сповіщення не спрацювало.
// Головний випадок: канал, якого в кабінеті немає.
func TestRuleChannelUnknownRefused(t *testing.T) {
err := ValidateRuleChannels([]string{"ch-duty", "ch-gone"}, knownChannels("ch-duty", "ch-lead"))
if err == nil {
t.Fatal("правило з неіснуючим каналом збереглося — воно нікого не сповістить")
}
if !errors.Is(err, ErrInvalid) {
t.Fatalf("відмова має бути ErrInvalid (інакше HTTP віддасть 500): %v", err)
}
// Без ідентифікатора у відмові людина не знає, який саме рядок
// виправляти: у переліку каналів правила їх буває кілька.
if !strings.Contains(err.Error(), "ch-gone") {
t.Fatalf("відмова не називає ідентифікатор: %q", err)
}
// І де взяти правильний. Відмова, яка лише каже «ні», змушує людину
// здогадуватись — саме цього уникає формулювання сходинок драбини.
if !strings.Contains(err.Error(), "Канали") {
t.Fatalf("відмова не підказує, де взяти правильний канал: %q", err)
}
}
// Дзеркальний випадок: усе на місці — відмови бути не має.
func TestRuleChannelsKnownAccepted(t *testing.T) {
if err := ValidateRuleChannels([]string{"ch-duty", "ch-lead"},
knownChannels("ch-duty", "ch-lead", "ch-boss")); err != nil {
t.Fatalf("справне правило не збереглося: %v", err)
}
}
// Канал чужого кабінету — той самий випадок, і це головне в ньому.
//
// Перевірка не знає слова «чужий»: їй дають перелік каналів ЦЬОГО
// кабінету, прочитаний під RLS у транзакції запису. Тому підставлений
// UUID сусіда не проходить не як окремий випадок, а як частина
// загального правила — і його не можна забути закрити окремо.
func TestRuleChannelFromOtherTenantRefused(t *testing.T) {
const foreign = "00000000-0000-4000-8000-000000000002"
err := ValidateRuleChannels([]string{foreign}, knownChannels("ch-duty"))
if err == nil {
t.Fatal("канал чужого кабінету прийнято в правило")
}
if !strings.Contains(err.Error(), foreign) {
t.Fatalf("відмова не називає ідентифікатор: %q", err)
}
}
// Порожній перелік — не помилка, а типовий стан правила.
//
// Порожньо означає «за загальними маршрутами кабінету»; відмовляти тут
// означало б вимагати канал у кожного правила, тобто зламати всі наявні
// однією перевіркою.
func TestRuleChannelsEmptyAllowed(t *testing.T) {
if err := ValidateRuleChannels(nil, knownChannels()); err != nil {
t.Fatalf("правило без каналів відхилено: %v", err)
}
if err := ValidateRuleChannels([]string{}, knownChannels("ch-duty")); err != nil {
t.Fatalf("порожній перелік каналів відхилено: %v", err)
}
}
// Вимкнений канал — не привід відмовляти, як і в драбині.
//
// Перелік каналів кабінету не фільтрується за enabled навмисно:
// «вимкнув Telegram на час переїзду» не має ламати збереження правила,
// у якому він стоїть. Про вимкнений канал говорить форма, і це інша
// розмова, ніж «такого каналу немає».
func TestRuleChannelDisabledStillValid(t *testing.T) {
if err := ValidateRuleChannels([]string{"ch-off"}, knownChannels("ch-off")); err != nil {
t.Fatalf("вимкнений канал відхилено: %v", err)
}
}