За цілий день активної роботи в журналі не з'явилось нічого: цих доменів у ньому просто не було. Проєкт це визнавав сам, у AuditBlindSpots(). Тепер пишуться: правила алертів (створення/зміна/видалення й окремо вимкнення-увімкнення), канали, драбини ескалації, правила відповідності; додавання людини в кабінет, зміна ролі, вилучення, правка профілю й скидання пароля. Два рішення про зміст запису: * вимкнення видно з НАЗВИ дії (alr.rule.disable), а не з різниці подробиць — питання «хто вимкнув правило, за яким приходив алерт» має відповідатись переліком, а не порівнянням; * config каналу не їде в запис ВЗАГАЛІ — там не лише токен бота, а й адреса вебхука (доступ на запис у чужий чат) і заголовок Authorization. Замість нього прапорець secret_changed. Мапи, групи й шаблони свідомо лишились у сліпих зонах: їхня правка видима в самому об'єкті. Туди ж додано дві нові чесні межі — тригери від шаблонів пишуться повз обробник, і пробне повідомлення в канал. Прогнано проти справжньої бази: секрет каналу й пароль учасника в подробиці не потрапляють. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
168 lines
7.9 KiB
Go
168 lines
7.9 KiB
Go
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("з переліку зникли мапи, групи й шаблони — а вони в журнал не пишуться")
|
||
}
|
||
}
|