Files
Bridge-and-Join-s/internal/lkgateway/lkcoverage_live_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

121 lines
3.4 KiB
Go

//go:build live
package lkgateway
import (
"context"
"strings"
"testing"
"time"
)
// Покрытие данных кабинета на реальных договорах: что клиент увидит на каждой странице.
// Только чтение. В вывод идут номера договоров без ФИО.
func TestLiveCabinetCoverage(t *testing.T) {
rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json")
if err != nil {
t.Fatal(err)
}
globalRC = rc
fs := rc.Snapshot().Fancy
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Minute)
defer cancel()
contracts, err := depoListContracts(ctx, fs, "", 60)
if err != nil {
t.Fatalf("список договоров: %v", err)
}
if len(contracts) == 0 {
t.Fatal("в депозитарии нет договоров с остатками")
}
type agg struct{ clients, withData, rows int }
var positions, priced, ops, income, corp, docs agg
var posTotal, posPriced int
from := time.Now().AddDate(-2, 0, 0).Format("2006-01-02")
for _, c := range contracts {
num := c.Contract
if i := strings.Index(num, " "); i > 0 {
num = num[:i] // без ФИО
}
pos, err := depoClientPositions(ctx, fs, c.Contract)
if err != nil {
t.Errorf("%s: портфель: %v", num, err)
continue
}
positions.clients++
if len(pos) > 0 {
positions.withData++
positions.rows += len(pos)
}
isins := make([]string, 0, len(pos))
for _, p := range pos {
if p.ISIN != "" {
isins = append(isins, p.ISIN)
}
}
px := lkPrices(ctx, rc, isins)
got := 0
for _, i := range isins {
if p, ok := px[strings.ToUpper(i)]; ok && p.Price > 0 {
got++
}
}
posTotal += len(isins)
posPriced += got
priced.clients++
if got > 0 {
priced.withData++
}
o, err := lkOperations(ctx, fs, c.Contract, from, "", 200)
if err != nil {
t.Errorf("%s: операции: %v", num, err)
}
ops.clients++
if len(o) > 0 {
ops.withData++
ops.rows += len(o)
}
inc, _, err := lkIncome(ctx, fs, c.Contract, from, "")
if err != nil {
t.Errorf("%s: доходы: %v", num, err)
}
income.clients++
if len(inc) > 0 {
income.withData++
income.rows += len(inc)
}
ca, err := lkCorpActions(ctx, fs, isins)
if err != nil {
t.Errorf("%s: корпоративные действия: %v", num, err)
}
corp.clients++
if len(ca) > 0 {
corp.withData++
corp.rows += len(ca)
}
d, err := lkReadyDocuments(ctx, fs, c.Contract)
if err != nil {
t.Errorf("%s: документы: %v", num, err)
}
docs.clients++
if len(d) > 0 {
docs.withData++
docs.rows += len(d)
}
}
line := func(name string, a agg) {
t.Logf("%-28s данные есть у %3d из %3d клиентов, строк всего %d", name, a.withData, a.clients, a.rows)
}
t.Logf("Выборка: %d договоров, период операций и доходов — с %s", len(contracts), from)
line("Портфель (остатки)", positions)
line("Оценка по ценам QUIK", priced)
if posTotal > 0 {
t.Logf("%-28s оценено %d из %d позиций (%.0f%%)", " доля позиций с ценой", posPriced, posTotal, 100*float64(posPriced)/float64(posTotal))
}
line("Операции по счёту депо", ops)
line("Доходы (купоны, дивиденды)", income)
line("Корпоративные действия", corp)
line("Готовые выписки", docs)
}