Netpulse_SasS/server/internal/store/audit_actions_zones_test.go
byrsapty f6df538020 Аудит: закрито дві найдорожчі сліпі зони — алерти й склад команди
За цілий день активної роботи в журналі не з'явилось нічого: цих
доменів у ньому просто не було. Проєкт це визнавав сам, у
AuditBlindSpots().

Тепер пишуться: правила алертів (створення/зміна/видалення й окремо
вимкнення-увімкнення), канали, драбини ескалації, правила
відповідності; додавання людини в кабінет, зміна ролі, вилучення,
правка профілю й скидання пароля.

Два рішення про зміст запису:
* вимкнення видно з НАЗВИ дії (alr.rule.disable), а не з різниці
  подробиць — питання «хто вимкнув правило, за яким приходив алерт»
  має відповідатись переліком, а не порівнянням;
* config каналу не їде в запис ВЗАГАЛІ — там не лише токен бота, а й
  адреса вебхука (доступ на запис у чужий чат) і заголовок
  Authorization. Замість нього прапорець secret_changed.

Мапи, групи й шаблони свідомо лишились у сліпих зонах: їхня правка
видима в самому об'єкті. Туди ж додано дві нові чесні межі — тригери
від шаблонів пишуться повз обробник, і пробне повідомлення в канал.

Прогнано проти справжньої бази: секрет каналу й пароль учасника в
подробиці не потрапляють.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 01:06:21 +03:00

168 lines
7.9 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 (
"go/token"
"strings"
"testing"
)
// Дві закриті сліпі зони — перевірка з обох боків.
//
// ЧОМУ ЦЬОГО НЕ РОБИТЬ СУСІДНІЙ ТЕСТ
//
// audit_actions_source_test.go перевіряє напрямок «ключ пишеться в
// журнал → у нього є назва» і свідомо не перевіряє зворотного: журнал
// старший за код, і назви до дій збірок, яких уже немає, мають лишатись
// (див. «ЧОГО ТЕСТ НЕ ВИМАГАЄ» у його шапці).
//
// Але для дій, ДОДАНИХ ЗАРАЗ, зворотний напрямок — це і є вся робота.
// Рядок у словнику коштує нічого, і саме тому забути обробник поруч із
// ним нічого не коштує теж: сторінка аудиту покаже нову назву у фільтрі,
// фільтр відпрацює, а подій за ним не буде жодної — тобто вийде та сама
// порожнеча, що й до роботи, тільки тепер вона виглядає як «нічого не
// робили». Тому нижче перелічені ключі мусять не лише мати назву, а й
// справді писатись кодом із server/internal.
//
// Перелік навмисно записаний руками, а не виведений із auditActions за
// префіксом: він фіксує обіцянку конкретної роботи. Наступний, хто
// додасть дію в цій же зоні, або допише її сюди свідомо, або не
// зачепить цей тест узагалі — обидва варіанти чесні.
// closedZoneActions — дії, заради яких і робилась ця робота.
var closedZoneActions = map[string]string{
// Зона А: алертна частина.
AuditActionAlertRuleCreate: "Сповіщення",
AuditActionAlertRuleUpdate: "Сповіщення",
AuditActionAlertRuleDelete: "Сповіщення",
AuditActionAlertRuleEnable: "Сповіщення",
AuditActionAlertRuleDisable: "Сповіщення",
AuditActionChannelCreate: "Сповіщення",
AuditActionChannelUpdate: "Сповіщення",
AuditActionChannelDelete: "Сповіщення",
AuditActionEscalationCreate: "Сповіщення",
AuditActionEscalationUpdate: "Сповіщення",
AuditActionEscalationDelete: "Сповіщення",
// Правила відповідності — та сама зона: вимога, яку зняли, так само
// не лишає по собі нічого.
AuditActionComplianceRuleCreate: "Конфігурації",
AuditActionComplianceRuleUpdate: "Конфігурації",
AuditActionComplianceRuleDelete: "Конфігурації",
// Зона Б: склад команди.
AuditActionMemberAdd: "Адміністрування",
AuditActionMemberRole: "Адміністрування",
AuditActionMemberRemove: "Адміністрування",
AuditActionMemberProfile: "Адміністрування",
AuditActionMemberPassword: "Адміністрування",
}
// closedZoneObjects — типи об'єктів тих самих зон.
var closedZoneObjects = []string{
AuditObjectAlertRule,
AuditObjectChannel,
AuditObjectEscalation,
AuditObjectComplianceRule,
AuditObjectMember,
}
func TestClosedZonesHaveLabels(t *testing.T) {
for key, group := range closedZoneActions {
info, ok := auditActionByKey[key]
if !ok {
t.Errorf("дія %q не має назви у словнику", key)
continue
}
if info.Label == "" {
t.Errorf("дія %q має порожню назву", key)
}
if info.Group != group {
t.Errorf("дія %q у розділі %q, а очікувався %q", key, info.Group, group)
}
}
for _, key := range closedZoneObjects {
if auditObjectTypeByKey[key] == "" {
t.Errorf("тип об'єкта %q не має назви у словнику", key)
}
}
}
// Руйнівні дії позначені саме там, де після дії об'єкта більше немає.
//
// Позначка керує тим, як подію видно на сторінці, і помилка в ній тиха:
// видалення, яке не виглядає видаленням, губиться серед правок рівно
// тоді, коли його шукають.
func TestClosedZonesMarkDestructive(t *testing.T) {
gone := map[string]bool{
AuditActionAlertRuleDelete: true,
AuditActionChannelDelete: true,
AuditActionEscalationDelete: true,
AuditActionComplianceRuleDelete: true,
// Членства більше немає, сесії відкликані. Сам обліковий запис
// лишається — він може працювати в іншому кабінеті, — але
// доступу САМЕ сюди в людини вже нема.
AuditActionMemberRemove: true,
}
for key := range closedZoneActions {
if auditActionByKey[key].Destructive != gone[key] {
t.Errorf("дія %q: позначка «об'єкта більше немає» = %v, очікувалось %v",
key, auditActionByKey[key].Destructive, gone[key])
}
}
}
// Кожна нова дія має справді писатись кодом.
func TestClosedZonesAreActuallyWritten(t *testing.T) {
fset := token.NewFileSet()
files := parseAuditScanTree(t, fset)
consts := auditConstValues(files)
written := map[string]bool{}
for _, u := range collectAuditKeyUses(t, fset, files, consts) {
written[u.key] = true
}
for key := range closedZoneActions {
if !written[key] {
t.Errorf("дія %q є у словнику, але жодне місце коду її не пише — "+
"у фільтрі з'явиться назва, за якою ніколи не буде подій", key)
}
}
for _, key := range closedZoneObjects {
if !written[key] {
t.Errorf("тип об'єкта %q є у словнику, але його ніхто не пише", key)
}
}
}
// Перелік сліпих зон правиться РАЗОМ зі словником — це вимога з шапки
// AuditBlindSpots, і без перевірки вона тримається лише на пам'яті.
func TestBlindSpotsDropClosedZones(t *testing.T) {
spots := AuditBlindSpots()
// Закрите звідти зникло. Шукаємо початок рядка, а не будь-яку
// згадку: рядок про мапи навмисно ЗГАДУЄ правила алертів, щоб
// сказати, що їх звідти прибрано, і збіг за словом «правила» був би
// хибним спрацюванням.
for _, gone := range []string{"Склад команди:", "Мапи, групи, шаблони, правила алертів"} {
for _, s := range spots {
if strings.HasPrefix(s, gone) {
t.Errorf("зона закрита, а рядок про неї лишився: %q", s)
}
}
}
// Свідомо лишене — лишилось, і з причиною. Мапи, групи й шаблони
// прибирати з переліку не можна: їх ця робота не покривала.
found := false
for _, s := range spots {
if strings.HasPrefix(s, "Мапи, групи й шаблони") {
found = true
if !strings.Contains(s, "видима в самому об'єкті") {
t.Errorf("рядок про мапи лишився без причини: %q", s)
}
}
}
if !found {
t.Error("з переліку зникли мапи, групи й шаблони — а вони в журнал не пишуться")
}
}