Кабинет клиента: документы и корпоративные действия, дистрибутив, обратный канал, защита
Данные - корпоративные действия по бумагам клиента (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>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
//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)
|
||||
}
|
||||
Reference in New Issue
Block a user