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