Files
Bridge-and-Join-s/internal/lkgateway/lkagent_test.go
T
zuevav 2a7442c935 Кабинет клиента: документы и корпоративные действия, дистрибутив, обратный канал, защита
Данные
- корпоративные действия по бумагам клиента (U_CA_ACTS), готовые выписки (U_OP_INF_EXTRACT),
  заявки клиента на документ (lk.doc_requests) с оповещением сотрудникам; страница «Документы»
- дистрибутив кабинета из настроек ядра: архив с программой, службой, настройками,
  установщиком и инструкцией; ключ доступа на странице показан замаскированным

Связь без публичного адреса у ядра
- режим ожидания (BJ_MODE=agent): кабинет копит запросы, ядро само подключается к нему,
  выполняет их обработчиком /api/lk/* и возвращает ответы; 4 параллельных подключения,
  тот же ключ модуля в обе стороны, состояние канала в настройках
- сквозной тест: настоящий кабинет + настоящий цикл ядра

Защита (по итогам аудита)
- вход: лимит попыток по адресу и логину, одинаковое время ответа для несуществующего логина,
  пароль от 10 символов
- сессии ядра: на диске отпечаток токена и ссылка на учётную запись, без персональных данных;
  карточка клиента читается из БД на каждый запрос (блокировка действует сразу); абсолютный
  срок 24 ч; привязка к браузеру, к сети — по настройке
- вход сотрудника в кабинет клиента пишется в lk.audit и поднимает оповещение
- кабинет: CSRF на всех формах, /impersonate только POST, куки HttpOnly+Secure+SameSite=Strict,
  выход гасит все сессии, заголовки CSP/X-Frame-Options/nosniff/HSTS, no-store, таймауты сервера,
  ограничение тела, recover; сообщения через куку вместо адреса; /health наружу без подробностей
- шрифты вшиты в программу: страница не обращается к сторонним сайтам
- экранирование LIKE в выборках по номеру договора; ключ и токен без суррогата при отказе rand
- исправлено: ошибка входа не показывалась клиенту (шаблон ждал Error, приходил Flash)

Тесты: lksecurity_test, lkagent_test, lkdist_test, modules/lk (CSRF, заголовки, режим ожидания),
живые lkcoverage/lkunpriced, scripts/lk-sec-smoke.sh — проверка работающего кабинета снаружи.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 23:47:10 +03:00

91 lines
3.1 KiB
Go

package lkgateway
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/modules/lk"
)
// Режим ожидания целиком: настоящий кабинет (modules/lk) в режиме agent и настоящий цикл
// ядра. Кабинет ставит запрос в очередь, ядро своим исходящим подключением забирает его,
// выполняет обработчиком /api/lk/* и возвращает ответ.
func TestLKAgentEndToEnd(t *testing.T) {
const key = "lk-agent-test-key"
cabinet, err := lk.New(lk.Config{BJKey: key, Agent: true})
if err != nil {
t.Fatal(err)
}
cab := httptest.NewServer(cabinet.Handler())
defer cab.Close()
rc, err := NewRuntimeConfig(t.TempDir() + "/setup.json")
if err != nil {
t.Fatal(err)
}
lkCfg := rc.Snapshot().LK
lkCfg.APIKey, lkCfg.AgentMode, lkCfg.CabinetURL = key, true, cab.URL
if err := rc.UpdateLK(lkCfg); err != nil {
t.Fatal(err)
}
mux := http.NewServeMux()
registerLKAPI(mux, rc, nil)
core := &Server{rc: rc, mux: mux}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go core.lkAgentLoop(ctx)
// Администратор проверяет связь: кабинет спрашивает ядро через очередь.
req, _ := http.NewRequest(http.MethodGet, cab.URL+"/health", nil)
req.Header.Set("X-BJ-Key", key)
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
var out struct {
OK bool `json:"ok"`
Mode string `json:"mode"`
BJ map[string]any `json:"bj"`
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK || !out.OK || out.Mode != "agent" || out.BJ["service"] != "bj-server" {
t.Fatalf("ответ ядра через очередь: код %d, %+v", resp.StatusCode, out)
}
if st := lkAgentStatus(rc.Snapshot()); !st.Connected || st.Served == 0 {
t.Fatalf("состояние канала: %+v", st)
}
// Без ключа наружу — только признак жизни, без адресов и подробностей.
resp2, err := client.Get(cab.URL + "/health")
if err != nil {
t.Fatal(err)
}
defer resp2.Body.Close()
var pub map[string]any
_ = json.NewDecoder(resp2.Body).Decode(&pub)
if len(pub) != 1 || pub["ok"] != true {
t.Fatalf("/health без ключа раскрывает лишнее: %+v", pub)
}
}
// Ядро выполняет через канал только запросы к API кабинета — ничего другого.
func TestLKAgentRejectsForeignPath(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]string{"id": "1", "method": "GET", "path": "/admin/setup"})
}))
defer srv.Close()
if task, err := lkAgentFetch(context.Background(), srv.Client(), srv.URL, "k"); err == nil || task != nil {
t.Fatalf("чужой путь принят: %+v", task)
}
}