Мониторинг связи с НРД: участки Шлюз/НРД/M2M/Отправка, сквозная проверка справочником раз в 10 мин, история за сутки на «Статусе», цепочка в шапке (30 с), автоповтор недоставленных до 24 ч, баннер и статус «ожидает отправки» в кабинете клиента
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user