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