32d88fc42a
Перевод 97EAD00C (заявка ЛК #71741, 23.10.2026) исполнен в QUIK и НРД (MS010), но в ЛК статус не изменился, а сделка осталась «Подтверждена»: - связки guid → заявка ЛК искались с учётом регистра: мы шлём GUID строчными, НРД отвечает заглавными — OnLKOutcome молча не находил заявку. Ключи — строчными (lkGUIDKey), старые записи приводятся при загрузке; итог модуля ЛК (lk.transfer_requests) — lower(guid); - после MS010 по всем строкам поручения 16 сделка → «Исполнена» (CompleteSUB16), итог — в ЛК (outOnExecuted, каждый проход order-watch, повтор безвреден); в бою согласие контрагента — ещё не «Исполнена»: статус ждёт MS010, как в тесте «боевой ЛК → TEST3»; имитатор НРД — итог сразу. Тестовый 28 с боевым ЛК взял ту же заявку и после отказа МНП («запрещен для операции шорт» — бумагу уже заблокировал 9) брал её каждые 10 секунд: 108 сделок с новыми GUID и столько же писем. - Сделка заведена, а отправка не прошла (МНП не заблокировал) — заявка помечается взятой и связывается с переводом: дальше — оператор в карточке. - Боевой ЛК при тестовом профиле ИШ (lkProdOnTestNSD): заявки клиентов не берутся (кроме теста «боевой ЛК → тестовый НРД»), портфели не заполняются, предупреждение в настройках ЛК. - Письма: в теме и первой строке — сервер bj и контур НРД («[bj ТЕСТ …]» у тестового); письма о входящих — метка только у тестового. - История FSM — по МСК с датой, «текущее состояние» вместо «сейчас». - Карточка теста «боевой ЛК → тестовый НРД» — только в режиме разработки (включённый тест виден всегда, чтобы его выключить). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
149 lines
5.6 KiB
Go
149 lines
5.6 KiB
Go
package lkgateway
|
|
|
|
import (
|
|
"context"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore"
|
|
)
|
|
|
|
// lkLinksTemp — связки ЛК во временном каталоге, состояние в памяти сброшено.
|
|
func lkLinksTemp(t *testing.T, links string) {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
oldL, oldO, oldR := lkLinksFile, lkOriginFile, realTestFile
|
|
lkLinksFile, lkOriginFile, realTestFile = filepath.Join(dir, "links.json"), filepath.Join(dir, "origin.json"), filepath.Join(dir, "realtest.json")
|
|
if links != "" {
|
|
if err := os.WriteFile(lkLinksFile, []byte(links), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
reset := func() {
|
|
lkLinksMu.Lock()
|
|
lkLinks, lkLoaded = map[string]int{}, false
|
|
lkLinksMu.Unlock()
|
|
lkOriginMu.Lock()
|
|
lkOrigin, lkOriginLoaded = map[string]int{}, false
|
|
lkOriginMu.Unlock()
|
|
realTests.Lock()
|
|
realTests.loaded, realTests.m = false, nil
|
|
realTests.Unlock()
|
|
}
|
|
reset()
|
|
t.Cleanup(func() {
|
|
lkLinksFile, lkOriginFile, realTestFile = oldL, oldO, oldR
|
|
reset()
|
|
})
|
|
}
|
|
|
|
// НРД отвечает GUID заглавными, мы шлём строчными (23.10.2026: статус в ЛК по 97EAD00C не ушёл).
|
|
func TestLKLinksIgnoreGUIDCase(t *testing.T) {
|
|
lkLinksTemp(t, `{"97ead00c-fd0d-49ab-85a1-15eb49f3201a":71741}`) // запись до исправления
|
|
up := "97EAD00C-FD0D-49AB-85A1-15EB49F3201A"
|
|
if !lkLinked(up) {
|
|
t.Fatal("связка не найдена по GUID заглавными")
|
|
}
|
|
lkRecordOrigin("97ead00c-fd0d-49ab-85a1-15eb49f3201a", 71741)
|
|
if id, ok := LKClaimIDByGUID(up); !ok || id != 71741 {
|
|
t.Fatalf("происхождение по GUID заглавными: %d %v", id, ok)
|
|
}
|
|
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
OnLKOutcome(context.Background(), rc, up, "done", "Перевод исполнен") // ЛК не настроен — только закрытие связки
|
|
if lkLinked(up) {
|
|
t.Fatal("после итога связка должна закрыться")
|
|
}
|
|
}
|
|
|
|
// MS010 по нашему поручению 16: сделка «Подтверждена» → «Исполнена», итог — в ЛК.
|
|
func TestOutOnExecuted(t *testing.T) {
|
|
lkLinksTemp(t, "")
|
|
ctx := context.Background()
|
|
repo := m2mcore.NewMemoryRepository()
|
|
svc := NewService(Config{Repository: repo, Sender: &flakySender{}})
|
|
guid := "c02a1d5e-c2af-4799-bab4-953f133c5133"
|
|
req := sendFailRequest()
|
|
now := time.Now()
|
|
if _, err := repo.Create(ctx, &m2mcore.Deal{ID: "d1", GUID: m2m.UUID(guid), State: m2mcore.StateConfirmed, Request: req, CreatedAt: now, UpdatedAt: now}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lkLinkGUID(guid, LKEndpoint{}, 71741)
|
|
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w := &OrderWatch{GUID: guid, Direction: "out", MS010At: now, Lines: map[string]*OrderLineWatch{"42391": {ISIN: "RU0009029540", MS010At: now}}}
|
|
outOnExecuted(ctx, rc, svc, guid, w)
|
|
d, _ := repo.GetByGUID(ctx, m2m.UUID(guid))
|
|
if d.State != m2mcore.StateDone {
|
|
t.Fatalf("сделка: %s, нужно done", d.State)
|
|
}
|
|
if lkLinked(guid) {
|
|
t.Fatal("итог в ЛК не передан — связка не закрыта")
|
|
}
|
|
outOnExecuted(ctx, rc, svc, guid, w) // повтор каждый проход — без ошибок и изменений
|
|
if d, _ := repo.GetByGUID(ctx, m2m.UUID(guid)); d.State != m2mcore.StateDone {
|
|
t.Fatalf("после повтора: %s", d.State)
|
|
}
|
|
}
|
|
|
|
// Тестовый контур НРД с боевым ЛК: заявки клиентов не берутся, портфели не заполняются.
|
|
func TestLKProdOnTestNSD(t *testing.T) {
|
|
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
set := func(env, profile string) bool {
|
|
lk := rc.Snapshot().LK
|
|
lk.ActiveEnv = env
|
|
if err := rc.UpdateLK(lk); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
n := rc.Snapshot().NSD
|
|
n.Profile = profile
|
|
if err := rc.UpdateNSD(n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return lkProdOnTestNSD(rc)
|
|
}
|
|
if !set("prod", "test3-gost") || set("prod", "prod-gost") || set("test", "test3-gost") {
|
|
t.Fatal("боевой ЛК + тестовый контур — только эта пара")
|
|
}
|
|
if tag, line := alertServer("test3-gost"); !strings.HasPrefix(tag, "[bj ТЕСТ ") || !strings.Contains(line, "ТЕСТОВЫЙ контур") {
|
|
t.Fatalf("метка тестового сервера: %q %q", tag, line)
|
|
}
|
|
if tag, _ := alertServer("prod-gost"); strings.Contains(tag, "ТЕСТ") {
|
|
t.Fatalf("метка боевого сервера: %q", tag)
|
|
}
|
|
}
|
|
|
|
// История FSM — по МСК (время хранится в UTC), текущее состояние подписано.
|
|
func TestClaimStagesMSK(t *testing.T) {
|
|
a, err := newAdmin()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in := time.Date(2026, 10, 23, 12, 17, 19, 502000000, time.UTC)
|
|
left := in.Add(time.Minute)
|
|
rec := httptest.NewRecorder()
|
|
render(rec, a.claim, claimData{page: nowPage("Заявка", "claims"), Claim: ClaimView{ID: "0123456789abcdef", Status: "confirmed",
|
|
Stages: []StageView{{State: "awaiting_decision", EnteredAt: in, LeftAt: &left}, {State: "confirmed", EnteredAt: left}}}})
|
|
body := rec.Body.String()
|
|
for _, want := range []string{"23.10 15:17:19.502", "23.10 15:18:19.502", "текущее состояние"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("в истории нет %q", want)
|
|
}
|
|
}
|
|
if strings.Contains(body, "12:17:19") {
|
|
t.Error("время в UTC")
|
|
}
|
|
}
|