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

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)
}
}