feat(admin): dev-страница «Эмуляторы» — переключатели и настройки эмуляции НРД и МНП QUIK
В режиме разработки новая страница /admin/emulators с двумя клавишами: - «Эмулятор НРД» (флаг LK.EmulateNSD): заявки ЛК и ручного ввода идут через внутренний имитатор, в НРД ничего не отправляется. Настройки: задержка решения (0–300 с), исход (подтвердить/отклонить/не отвечать), код отказа M2M01–M2M09/18. Сендер перенастраивается в рантайме через потокобезопасный mock.Sender.Reconfigure — без перезапуска. - «Эмулятор МНП QUIK» (флаг QuikMNP.Emulate): шаги цикла без записи в боевую БД МНП. Настройки: задержка шага (0–60 с), точечные отказы по шагам (регистрация/подпись/отказ/зачисление) — эмулированный отказ ведёт себя как боевой: шаг OK=false, состояние не продвигается, повтор на следующем проходе. Настройки хранятся в setup.json (nsd_emu/mnp_emu), обратная совместимость — дефолты. Баннер «Режим эмуляции» показывается и при ручном включении эмуляторов (раньше зависел только от отсутствия ИШ). Пункт меню «Эмуляторы» и две плитки на дашборде — только в dev. Пути занесены в devOnlyPath. Попутно: исправлен race в TestLicenseActivateAndRenew (cleanup восстанавливал DefaultLicensePublicKey без moduleEnforceMu — падал под -race и на чистом HEAD).
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
package lkgateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Настройки эмуляторов: валидация и обратная совместимость (старый setup.json без полей = дефолты).
|
||||
func TestEmulatorSettingsValidation(t *testing.T) {
|
||||
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, s := range []NSDEmuSettings{
|
||||
{DelaySec: -1}, {DelaySec: 301},
|
||||
{Outcome: "explode"}, {RejectCode: "99"}, {RejectCode: "M2M07"},
|
||||
} {
|
||||
if err := rc.UpdateNSDEmu(s); err == nil {
|
||||
t.Errorf("UpdateNSDEmu(%+v): ожидалась ошибка", s)
|
||||
}
|
||||
}
|
||||
if err := rc.UpdateNSDEmu(NSDEmuSettings{DelaySec: 45, Outcome: "reject", RejectCode: "18"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, s := range []MNPEmuSettings{{StepDelaySec: -1}, {StepDelaySec: 61}} {
|
||||
if err := rc.UpdateMNPEmu(s); err == nil {
|
||||
t.Errorf("UpdateMNPEmu(%+v): ожидалась ошибка", s)
|
||||
}
|
||||
}
|
||||
if err := rc.UpdateMNPEmu(MNPEmuSettings{StepDelaySec: 5, FailRegister: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Перечитываем файл: настройки переживают перезапуск.
|
||||
rc2, err := NewRuntimeConfig(rc.path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := rc2.Snapshot()
|
||||
if s.NSDEmu.DelaySec != 45 || s.NSDEmu.Outcome != "reject" || s.NSDEmu.RejectCode != "18" {
|
||||
t.Errorf("NSDEmu после перезагрузки: %+v", s.NSDEmu)
|
||||
}
|
||||
if !s.MNPEmu.FailRegister || s.MNPEmu.StepDelaySec != 5 {
|
||||
t.Errorf("MNPEmu после перезагрузки: %+v", s.MNPEmu)
|
||||
}
|
||||
// Дефолты для нулевых полей (старый setup.json).
|
||||
d := NSDEmuSettings{}.Defaults()
|
||||
if d.DelaySec != 30 || d.Outcome != "confirm" || d.RejectCode != "07" {
|
||||
t.Errorf("дефолты NSDEmu: %+v", d)
|
||||
}
|
||||
}
|
||||
|
||||
// Страница «Эмуляторы» — только в режиме разработки: вне него 404 (гейт в authMiddleware).
|
||||
func TestEmulatorsDevGate(t *testing.T) {
|
||||
for _, p := range []string{"/admin/emulators", "/admin/emulators/nsd", "/admin/emulators/quik"} {
|
||||
if !devOnlyPath(p) || requiredPerm(p, http.MethodGet) != "dev" {
|
||||
t.Errorf("%s должен быть служебным", p)
|
||||
}
|
||||
}
|
||||
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
registerEmulators(mux, nil, rc)
|
||||
h := authMiddleware(rc, mux)
|
||||
for _, m := range []string{http.MethodGet, http.MethodPost} {
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(m, "/admin/emulators", nil))
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("%s вне режима разработки: %d, ожидали 404", m, rec.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Сохранение со страницы «Эмуляторы»: ajax-POST пишет настройки и флаги.
|
||||
func TestEmulatorsSave(t *testing.T) {
|
||||
devModeForced = true
|
||||
defer func() { devModeForced = false }()
|
||||
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a, err := newAdmin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
registerEmulators(mux, a, rc)
|
||||
|
||||
post := func(path string, form url.Values) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("X-BJ-Ajax", "1")
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
rec := post("/admin/emulators/nsd", url.Values{
|
||||
"emulate": {"1"}, "delay_sec": {"45"}, "outcome": {"reject"}, "reject_code": {"18"},
|
||||
})
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("nsd: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
s := rc.Snapshot()
|
||||
if !s.LK.EmulateNSD || s.NSDEmu.DelaySec != 45 || s.NSDEmu.Outcome != "reject" || s.NSDEmu.RejectCode != "18" {
|
||||
t.Errorf("после сохранения НРД: emulate=%v emu=%+v", s.LK.EmulateNSD, s.NSDEmu)
|
||||
}
|
||||
|
||||
rec = post("/admin/emulators/quik", url.Values{
|
||||
"emulate": {"1"}, "step_delay_sec": {"7"}, "fail_register": {"1"}, "fail_deposit": {"1"},
|
||||
})
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("quik: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
s = rc.Snapshot()
|
||||
if !s.Quik.Emulate || s.MNPEmu.StepDelaySec != 7 || !s.MNPEmu.FailRegister || !s.MNPEmu.FailDeposit || s.MNPEmu.FailSign {
|
||||
t.Errorf("после сохранения МНП: emulate=%v emu=%+v", s.Quik.Emulate, s.MNPEmu)
|
||||
}
|
||||
|
||||
// Валидация на хендлере: плохой код отказа — 400, настройки не тронуты.
|
||||
rec = post("/admin/emulators/nsd", url.Values{"delay_sec": {"10"}, "outcome": {"reject"}, "reject_code": {"42"}})
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("плохой код отказа: %d, ожидали 400", rec.Code)
|
||||
}
|
||||
rec = post("/admin/emulators/quik", url.Values{"step_delay_sec": {"600"}})
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("задержка 600: %d, ожидали 400", rec.Code)
|
||||
}
|
||||
|
||||
// Страница собирается и показывает обе карточки.
|
||||
rec = httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/admin/emulators", nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("GET: %d", rec.Code)
|
||||
}
|
||||
for _, want := range []string{"Эмулятор НРД", "Эмулятор МНП QUIK", "в НРД ничего не отправляется", "M2M18"} {
|
||||
if !strings.Contains(rec.Body.String(), want) {
|
||||
t.Errorf("на странице нет %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Эмулированный отказ МНП: шаг завершается как боевой отказ — QuikStep OK=false, Err заполнена,
|
||||
// состояние связки не продвигается; повторный вызов без отказа проходит.
|
||||
func TestMNPEmuFailRegister(t *testing.T) {
|
||||
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
q := rc.Snapshot().Quik
|
||||
q.Emulate = true
|
||||
if err := rc.UpdateQuik(q); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := rc.UpdateMNPEmu(MNPEmuSettings{FailRegister: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
l, err := QuikOutRegister(context.Background(), rc, "emu-fail-test-guid", "CL1", "", "RU0009029540", 10, "7702070139", "", false)
|
||||
if err == nil {
|
||||
t.Fatal("ожидался отказ регистрации")
|
||||
}
|
||||
if l == nil || l.State != "error" || !strings.Contains(l.Err, "имитация отказа") {
|
||||
t.Fatalf("связка после отказа: %+v", l)
|
||||
}
|
||||
if st := l.LastStep(); st == nil || st.OK || st.Action != "register" {
|
||||
t.Fatalf("шаг: %+v", st)
|
||||
}
|
||||
// Сняли отказ — повтор (как следующий проход цикла) проходит.
|
||||
if err := rc.UpdateMNPEmu(MNPEmuSettings{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
l, err = QuikOutRegister(context.Background(), rc, "emu-fail-test-guid", "CL1", "", "RU0009029540", 10, "7702070139", "", false)
|
||||
if err != nil || l.State != "registered" {
|
||||
t.Fatalf("повтор без отказа: %v, %+v", err, l)
|
||||
}
|
||||
}
|
||||
|
||||
// Задержка шага эмулятора МНП учитывает контекст: отмена/таймаут прерывают ожидание.
|
||||
func TestMNPEmuStepDelayCtx(t *testing.T) {
|
||||
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
q := rc.Snapshot().Quik
|
||||
q.Emulate = true
|
||||
if err := rc.UpdateQuik(q); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := rc.UpdateMNPEmu(MNPEmuSettings{StepDelaySec: 60}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
start := time.Now()
|
||||
if _, err := QuikOutRegister(ctx, rc, "emu-delay-test-guid", "CL1", "", "RU0009029540", 5, "", "", false); err == nil {
|
||||
t.Fatal("ожидался обрыв по контексту")
|
||||
}
|
||||
if time.Since(start) > 5*time.Second {
|
||||
t.Fatal("задержка не прервалась по контексту")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user