4cbefa41d5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
188 lines
6.2 KiB
Go
188 lines
6.2 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|