Files
Bridge-and-Join-s/internal/lkgateway/notifyadmin_test.go
T
zuevav afc5776347 Индикатор «Шлюз НРД», проверка ключа ИШ, папки шлюза по сети, сотрудники из домена, переключатель QUIK в настройках
- журнал ИШ: номер процесса выравнивается пробелами — опросы НРД не распознавались (серый индикатор)
- «Проверить СКЗИ и ключ»: Валидата, датчик случайных чисел, подпись профилем ИШ (а не старый bj-crypto);
  индикатор «Криптографии» — по готовности ключа; карточка автозагрузки УЦ — только для разработки
- шаг «Шлюз НРД»: папки обмена по сети (Samba): логин/пароль, ручная настройка не трогается
- «Учётные записи»: необязательно — подключить домен, проверить связь, найти сотрудника и добавить с ролью;
  роль из bj действует, пока группы домена с ролями не сопоставлены
- настройка QUIK МНП: «Только чтение / Чтение и запись», тонкая настройка цикла свёрнута
- индикатор «Обновления» — только когда есть что обновить; «Мастер настройки» в меню и в настройках

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-07 00:24:19 +03:00

222 lines
11 KiB
Go
Raw 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 lkgateway
import (
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
)
// Сбои и обновления — отдельные типы (по умолчанию — администратору системы), деловые — отделам.
func TestAlertKindAdmin(t *testing.T) {
cases := map[string]string{
"ИШ НРД недоступен": "infra",
"Перезапуск ИШ НРД не помог (попытка 1 из 3)": "infra",
"НРД не отвечает шлюзу": "infra",
"Компонент не отвечает: poll-incoming": "infra",
"Служба «Подпись» не работает": "infra",
"DEPO: вход логином bj_user не удался": "infra",
"Доступно обновление bj 2026.10.06.24": "updates",
"ИШ обновлён до 107.0.930": "updates",
"Лицензия: сервер лицензий отказал в продлении": "updates",
"Поддержка bj заканчивается через 10 дней": "updates",
"Сертификат ключа организации истекает через 5": "updates",
"НРД: депо-поручение не исполнено в срок 5 минут": "orders",
"QUIK МНП: не удалось зачислить бумаги": "quik",
"Перевод не доставлен в НРД": "system",
"DEPO: джоба не обработала перевод ab12": "system",
}
for title, want := range cases {
if got := alertKind(title); got != want {
t.Errorf("%q: %s, ожидалось %s", title, got, want)
}
}
}
// Существующая установка: администратор (адрес из «Почты») получает всё, что получал раньше; выключенные
// уведомления о переводах — пустой маршрут. Новая установка — маршруты по умолчанию.
func TestNotifyRoutesMigration(t *testing.T) {
var old Settings
old.Alerts.SMTP.To = "it@example.ru"
old.Incoming.Emails = "depo@example.ru"
migrateNotifyRoutes(&old)
r := old.Incoming.Routes
for _, k := range []string{"orders", "quik", "infra", "updates", "system"} {
if !old.Incoming.RouteHas(k, "admin") {
t.Errorf("%s: администратор должен остаться получателем (%q)", k, r[k])
}
}
if r["incoming"] != "" || old.Incoming.RouteHas("incoming", "depo") || !old.Incoming.Enabled || old.Incoming.RoutesV != 2 {
t.Errorf("выключенные уведомления о переводах: %+v", old.Incoming)
}
if !old.Incoming.RouteHas("infra", "back") || !old.Incoming.RouteHas("orders", "depo") {
t.Errorf("сбои — тем же отделам, что «прочие» раньше: %v", r)
}
if got := notifyRecipients(old, "infra"); got != "it@example.ru" {
t.Errorf("сбои: %q", got)
}
migrateNotifyRoutes(&old) // повторно — без изменений
if old.Incoming.Routes["orders"] != r["orders"] || strings.Count(old.Incoming.Routes["orders"], "admin") != 1 {
t.Errorf("повторная миграция: %v", old.Incoming.Routes)
}
var fresh Settings
migrateNotifyRoutes(&fresh)
fresh.Alerts.SMTP.To = "it@example.ru"
fresh.Incoming.Emails, fresh.Incoming.EmailsBack = "depo@example.ru", "back@example.ru"
if fresh.Incoming.Routes != nil || !fresh.Incoming.Enabled {
t.Fatalf("новая установка: %+v", fresh.Incoming)
}
want := map[string]string{
"incoming": "depo@example.ru, back@example.ru",
"orders": "depo@example.ru, back@example.ru",
"infra": "it@example.ru",
"updates": "it@example.ru",
"system": "it@example.ru, back@example.ru",
}
for k, w := range want {
if got := notifyRecipients(fresh, k); got != w {
t.Errorf("%s: %q, ожидалось %q", k, got, w)
}
}
}
// Служба пишет администратору, только пролежав две проверки подряд, один раз; поднялась — второе письмо.
func TestUnitsWatchTick(t *testing.T) {
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
if err != nil {
t.Fatal(err)
}
unitsWatch.Lock()
unitsWatch.down, unitsWatch.alerted = map[string]int{}, map[string]bool{}
unitsWatch.Unlock()
down := []watchedUnit{{Key: "bj-npr", Title: "Риск-монитор", Unit: "bj-npr", State: "failed/failed"}}
up := []watchedUnit{{Key: "bj-npr", Title: "Риск-монитор", Unit: "bj-npr", OK: true}}
count := func(sub string) int {
n := 0
for _, it := range rc.Snapshot().News.Items {
if strings.Contains(it.Title, sub) {
n++
}
}
return n
}
unitsWatchTick(rc, down)
if count("не работает") != 0 {
t.Fatal("после одной проверки письма быть не должно")
}
unitsWatchTick(rc, down)
unitsWatchTick(rc, down)
if count("не работает") != 1 {
t.Fatalf("оповещений о сбое: %d", count("не работает"))
}
unitsWatchTick(rc, up)
if count("снова работает") != 1 {
t.Fatalf("оповещений о восстановлении: %d", count("снова работает"))
}
}
// Адрес хранилища копий — как его вводят люди: в проводнике \\сервер\папка, или сервер и общая папка по разным полям.
func TestSMBLocation(t *testing.T) {
cases := []struct{ share, dir, wantShare, wantDir string }{
{`\\10.10.10.11\`, "backup_test", "//10.10.10.11/backup_test", ""},
{`\\10.10.10.11\backup_test`, "", "//10.10.10.11/backup_test", ""},
{`\\nas\backup\bj\copies`, "x", "//nas/backup", "bj/copies/x"},
{"//nas/backup", "bj-backup", "//nas/backup", "bj-backup"},
{"smb://nas/backup/", "", "//nas/backup", ""},
{"nas", "", "//nas", ""},
{"", "bj", "", "bj"},
}
for _, c := range cases {
s, d := smbLocation(c.share, c.dir)
if s != c.wantShare || d != c.wantDir {
t.Errorf("%q + %q → %q %q, ожидалось %q %q", c.share, c.dir, s, d, c.wantShare, c.wantDir)
}
}
if smbShareOK("//nas") || !smbShareOK("//nas/backup") || smbWin("//nas/backup") != `\\nas\backup` {
t.Error("smbShareOK/smbWin")
}
}
// Поле «Проверочное письмо — кому»: адрес администратора по умолчанию не дублируется, свой — запоминается.
func TestSMTPTestTo(t *testing.T) {
s := SMTPSettings{To: "it@example.ru", TestTo: "old@example.ru"}
req := func(body string) *http.Request {
r := httptest.NewRequest(http.MethodPost, "/admin/setup/alerts-smtp-test", strings.NewReader(body))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return r
}
if got := smtpTestTo(req("test_to=me%40example.ru"), s); got != "me@example.ru" {
t.Errorf("свой адрес: %q", got)
}
if got := smtpTestTo(req("test_to=it%40example.ru"), s); got != "" {
t.Errorf("адрес администратора не хранится отдельно: %q", got)
}
if got := smtpTestTo(req("host=x"), s); got != "old@example.ru" {
t.Errorf("без поля — прежний: %q", got)
}
}
// Форма «Создать учётную запись» из мастера не меняет существующего пользователя с тем же логином.
func TestCreateUserLoginTaken(t *testing.T) {
old := usersFile
usersFile = filepath.Join(t.TempDir(), "users.json")
defer func() { usersFile = old }()
prev := globalUsers
globalUsers = &userStore{users: map[string]*User{}}
defer func() { globalUsers = prev }()
if err := globalUsers.Put(&User{Login: "admin", Role: "admin", Source: "local", Active: true}); err != nil {
t.Fatal(err)
}
h := &setupHandlers{}
r := httptest.NewRequest(http.MethodPost, "/admin/setup/users", strings.NewReader("login=admin&role=viewer&create=1&password=12345678&return_to=%2Fadmin%2Fwizard%3Fstep%3D13"))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
h.saveUser(w, r)
if u := globalUsers.Get("admin"); u == nil || u.Role != "admin" {
t.Fatalf("существующий администратор изменён: %+v", u)
}
if loc := w.Header().Get("Location"); !strings.HasPrefix(loc, "/admin/wizard?step=13") || !strings.Contains(loc, "flash=") {
t.Fatalf("возврат в мастер: %q", loc)
}
if doneUsers() {
t.Fatal("один администратор — шаг не выполнен")
}
}
// Старый root-установщик не знает igate-dist (рабочий сервер 06.10.2026: установщик .06.2) — сначала ставится
// новый установщик, igate-dist откладывается на второй заход; новый установщик — всё одним заходом.
func TestDeferForInstaller(t *testing.T) {
st := func(name, inst string, upd, isNew bool) release.ComponentState {
return release.ComponentState{Component: release.Component{Name: name}, Installed: inst, Update: upd, New: isNew}
}
plan := []release.ComponentState{st("bj-component-install", "2026.10.06.2", true, false), st("igate-dist", "", true, true), st("bj-server", "2026.10.06.26", true, false)}
kept, deferred := deferForInstaller(plan, plan)
if len(kept) != 2 || len(deferred) != 1 || deferred[0] != "igate-dist" {
t.Fatalf("старый установщик: оставлено %v, отложено %v", kept, deferred)
}
plan[0] = st("bj-component-install", "2026.10.06.15", true, false)
if kept, deferred := deferForInstaller(plan, plan); len(kept) != 3 || len(deferred) != 0 {
t.Fatalf("установщик знает igate-dist: оставлено %d, отложено %v", len(kept), deferred)
}
plan[0] = st("bj-component-install", "2026.10.06.2", false, false) // установщик не обновляется — откладывать нечем
if kept, deferred := deferForInstaller(plan, plan); len(kept) != 3 || len(deferred) != 0 {
t.Fatalf("без нового установщика: оставлено %d, отложено %v", len(kept), deferred)
}
}
// Роль, назначенная в bj доменному сотруднику (мастер, «Добавить из домена»), действует, когда группы домена
// с ролями не сопоставлены; без неё — доступ снимается, как раньше.
func TestADRecheckBJRole(t *testing.T) {
u := &User{Login: "ivanov", Source: "ad", Role: "operator", BJRole: "operator", Active: true}
if changed, drop, _ := adApplyRecheck(u, &adResult{}, nil, &adNoGroupError{sam: "ivanov"}); drop || u.Role != "operator" || changed {
t.Fatalf("роль из bj должна остаться: changed=%v drop=%v role=%q", changed, drop, u.Role)
}
v := &User{Login: "petrov", Source: "ad", Role: "operator", Active: true}
if _, drop, _ := adApplyRecheck(v, &adResult{}, nil, &adNoGroupError{sam: "petrov"}); !drop || v.Role != "" {
t.Fatalf("без роли из bj доступ снимается: drop=%v role=%q", drop, v.Role)
}
}