Files
Bridge-and-Join-s/internal/lkgateway/lkoutcome_test.go
T
fontvielle 32d88fc42a ЛК: итог «Исполнена» по MS010, GUID без учёта регистра, заявка при отказе МНП берётся один раз; тестовый контур не трогает боевой ЛК
Перевод 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
2026-10-23 16:04:44 +03:00

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")
}
}