package lkgateway import ( "bytes" "context" "path/filepath" "strings" "testing" "time" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore" ) func nsdTestRC(t *testing.T) *RuntimeConfig { t.Helper() rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json")) if err != nil { t.Fatal(err) } n := rc.Snapshot().NSD n.IGWBaseURL = "http://localhost:8090" if err := rc.UpdateNSD(n); err != nil { t.Fatal(err) } return rc } func nsdTestReset(t *testing.T) { t.Helper() old := sendFailFile sendFailFile = filepath.Join(t.TempDir(), "sf.json") sendFails.loaded = false oldHist := nsdHistoryFile nsdHistoryFile = filepath.Join(t.TempDir(), "h.json") t.Cleanup(func() { sendFailFile, sendFails.loaded, nsdHistoryFile = old, false, oldHist igwWatch.mu.Lock() igwWatch.Checked, igwWatch.Up = false, false igwWatch.mu.Unlock() nsdWatch.Lock() nsdWatch.DownSince, nsdWatch.LastPollAt, nsdWatch.LastPollOK = time.Time{}, time.Time{}, time.Time{} nsdWatch.Unlock() nsdH.Lock() nsdH.ProbeSentAt, nsdH.ProbeReplyAt, nsdH.ProbeErr, nsdH.history, nsdH.historyLoaded = time.Time{}, time.Time{}, "", nil, false nsdH.Unlock() }) } func segState(h NSDHealth, key string) string { for _, s := range h.Segs { if s.Key == key { return s.State } } return "" } // Зелёный — только при свежих подтверждениях; сбой опросов и молчание на проверку — красный. func TestNSDHealthStates(t *testing.T) { nsdTestReset(t) rc := nsdTestRC(t) now := time.Now() igwWatch.mu.Lock() igwWatch.Checked, igwWatch.Up = true, true igwWatch.mu.Unlock() nsdWatch.Lock() nsdWatch.LastPollAt, nsdWatch.LastPollOK = now.Add(-10*time.Second), now.Add(-10*time.Second) nsdWatch.Unlock() nsdH.Lock() nsdH.ProbeSentAt, nsdH.ProbeReplyAt = now.Add(-5*time.Minute), now.Add(-4*time.Minute) nsdH.Unlock() h := nsdHealthNow(rc, now) if h.State != segOK || !nsdLinkOK(h) || nsdClientNotice(h) != "" || h.Ago == "" { t.Fatalf("всё работает: %+v", h) } nsdWatch.Lock() nsdWatch.DownSince, nsdWatch.LastErr = now.Add(-5*time.Minute), "#404" nsdWatch.Unlock() h = nsdHealthNow(rc, now) if segState(h, "nsd") != segErr || h.State != segErr || nsdLinkOK(h) || nsdClientNotice(h) == "" { t.Fatalf("НРД отвечает ошибками 5 мин: %+v", h) } nsdWatch.Lock() nsdWatch.DownSince = time.Time{} nsdWatch.LastPollOK = now.Add(-20 * time.Minute) // тишина — не «работает» nsdWatch.Unlock() if h = nsdHealthNow(rc, now); segState(h, "nsd") != segWarn { t.Fatalf("давно нет опросов: %+v", h) } nsdWatch.Lock() nsdWatch.LastPollOK = now nsdWatch.Unlock() nsdH.Lock() nsdH.ProbeSentAt = now.Add(-12 * time.Minute) nsdH.ProbeReplyAt = now.Add(-25 * time.Minute) nsdH.Unlock() if h = nsdHealthNow(rc, now); segState(h, "m2m") != segErr { t.Fatalf("НРД молчит на проверку 12 мин: %+v", h) } } // Автоповтор: НРД снова отвечает — недоставленный перевод уходит сам; старше суток — не трогаем. func TestAutoResend(t *testing.T) { nsdTestReset(t) oldHook, oldQ := sentHook, quikSendHook defer func() { sentHook, quikSendHook = oldHook, oldQ }() sentHook, quikSendHook = nil, nil ctx := context.Background() snd := &flakySender{fail: true} svc := NewService(Config{Repository: m2mcore.NewMemoryRepository(), Sender: snd}) s := &Server{svc: svc} req := sendFailRequest() if _, err := svc.SendBuiltRequest(ctx, req, "inv", nil); err == nil { t.Fatal("ожидалась ошибка доставки") } guid := string(req.Header.GUID) s.autoResend(ctx) // последняя попытка только что — ждём 5 мин if sendFailGet(guid) == nil { t.Fatal("рано повторять") } sendFails.Lock() f := sendFails.m[guid] f.At = time.Now().Add(-6 * time.Minute) sendFails.m[guid] = f sendFails.Unlock() snd.fail = false s.autoResend(ctx) if sendFailGet(guid) != nil { t.Fatal("перевод должен уйти автоматически") } d, _ := svc.Repo().GetByGUID(ctx, req.Header.GUID) if d.State != m2mcore.StateAwaitingDecision { t.Fatalf("после автоповтора: %s", d.State) } // Старше суток — автоповтор останавливается. sendFails.Lock() sendFails.m["old"] = SendFailure{DealID: "x", At: time.Now().Add(-26 * time.Hour), FirstAt: time.Now().Add(-26 * time.Hour), Attempts: 9} sendFails.Unlock() s.autoResend(ctx) if f := sendFailGet("old"); f == nil || !f.GaveUp { t.Fatalf("старше суток: %+v", f) } } func TestNSDStripAndStatusRender(t *testing.T) { nsdTestReset(t) now := time.Now() nsdH.Lock() nsdH.historyLoaded = true nsdH.Unlock() for i := 30; i >= 0; i-- { st := segOK if i < 5 { st = segErr } nsdHistoryAdd(NSDHealth{Configured: true, Segs: []NSDSeg{{State: segOK}, {State: st}, {State: segOK}, {State: segOK}}}, now.Add(-time.Duration(i)*time.Minute)) } rows := nsdStrip([]string{"Шлюз", "НРД", "M2M", "Отправка"}, now) if len(rows) != 4 || len(rows[1].Cells) != 144 || rows[1].Cells[143].State != segErr || rows[0].Bad != "сбоев не было" || !strings.HasPrefix(rows[1].Bad, "сбой") { t.Fatalf("полоса: %+v / %q", rows[1].Cells[143], rows[1].Bad) } a, err := newAdmin() if err != nil { t.Fatal(err) } pg := page{Title: "Статус"} pg.NSDLink = NSDHealth{Configured: true, State: segErr, Segs: []NSDSeg{{Key: "igw", Title: "Шлюз", State: segOK, Text: "отвечает"}, {Key: "nsd", Title: "НРД", State: segErr, Text: "отвечает шлюзу ошибками"}}} var b bytes.Buffer data := statusData{page: pg, Strip: rows, Fails: []SendFailView{{GUID: "d7a182d4-4f79", DealID: "7f1c5cfb", Text: "контур НРД не принял пакет", FirstAt: now, Attempts: 2}}} if err := a.status.ExecuteTemplate(&b, "layout", data); err != nil { t.Fatal(err) } for _, want := range []string{"Связь с НРД", "nsd-cells", "Не доставлены в НРД", "Проверить связь сейчас", "id=\"nsd-chain\"", "отправится сам"} { if !strings.Contains(b.String(), want) { t.Fatalf("нет %q", want) } } }