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

99 lines
2.8 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.
//go:build live
package lkgateway
import (
"context"
"sort"
"strings"
"testing"
"time"
)
// Почему у части позиций нет цены: какие это бумаги и есть ли они вообще в экспорте QUIK.
func TestLiveUnpricedBreakdown(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.Fatal(err)
}
type info struct {
name string
balAcc int
n int
}
un := map[string]*info{}
for _, c := range contracts {
pos, err := depoClientPositions(ctx, fs, c.Contract)
if err != nil {
continue
}
var isins []string
for _, p := range pos {
if p.ISIN != "" {
isins = append(isins, p.ISIN)
}
}
px := lkPrices(ctx, rc, isins)
for _, p := range pos {
if p.ISIN == "" {
k := "(без ISIN) " + p.SecurityName
if un[k] == nil {
un[k] = &info{name: p.SecurityName, balAcc: p.BalAcc}
}
un[k].n++
continue
}
if v, ok := px[strings.ToUpper(p.ISIN)]; ok && v.Price > 0 {
continue
}
if un[p.ISIN] == nil {
un[p.ISIN] = &info{name: p.SecurityName, balAcc: p.BalAcc}
}
un[p.ISIN].n++
}
}
// есть ли такие бумаги в экспорте вообще (любой класс, даже без торгов)
db, err := nprDB(rc)
if err != nil {
t.Fatalf("база экспорта: %v", err)
}
var keys []string
for k := range un {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool { return un[keys[i]].n > un[keys[j]].n })
var foreign, noISIN, inQuikNoTrade, absent int
for _, k := range keys {
i := un[k]
switch {
case strings.HasPrefix(k, "(без ISIN)"):
noISIN += i.n
t.Logf("без ISIN ×%d счёт %d %s", i.n, i.balAcc, i.name)
continue
case !strings.HasPrefix(strings.ToUpper(k), "RU"):
foreign += i.n
t.Logf("иностранная ×%d счёт %d %s %s", i.n, i.balAcc, k, i.name)
continue
}
var classes string
_ = db.QueryRow(ctx, `SELECT COALESCE(string_agg(DISTINCT "ClassCode", ','), '') FROM "Securities" WHERE upper("ISINCode") = $1`, strings.ToUpper(k)).Scan(&classes)
if classes == "" {
absent += i.n
t.Logf("нет в экспорте ×%d счёт %d %s %s", i.n, i.balAcc, k, i.name)
} else {
inQuikNoTrade += i.n
t.Logf("есть, без цены ×%d счёт %d %s %s классы: %s", i.n, i.balAcc, k, i.name, classes)
}
}
t.Logf("ИТОГО без цены: иностранные %d · без ISIN %d · российские вне экспорта QUIK %d · есть в экспорте, но без цены %d",
foreign, noISIN, absent, inQuikNoTrade)
}