feat(admin): эмулятор МНП — режим реальной записи в МНП на подставного клиента

Третий режим МНП (между «внутренняя имитация» и «боевая запись»): в bj цикл
идёт по обычному пользователю, а поручение в QUIK МНП уходит РЕАЛЬНО, но с
кодом другого, реально существующего в QUIK клиента (обобщение ручного теста
realtest «боевой ЛК → TEST3»).

- QuikMNPSettings: RealWrite + RealClientCode (валидация формата кода).
- Подмена кода — единая точка quikClientCodeFromPort (после ClientCodeMap);
  портфель не трогаем. quikActive: RealWrite только при devMode и непустом
  коде; Emulate (внутренняя имитация) в приоритете.
- Шаги боевого пути помечаются «[на подставного клиента <код>]» — видно в
  «Поручениях QUIK».
- Карточка МНП на /admin/emulators — три взаимоисключающих режима
  (выключено / имитация / реальная запись), поле кода, крупное предупреждение;
  отдельный красный баннер «Реальные поручения в МНП на клиента <код> (режим
  разработки)» — не смешиваем с баннером эмуляции НРД.
- disableEmulatorsIfDevOff сбрасывает и RealWrite (код остаётся, инертен),
  новость явно фиксирует прекращение реальных поручений.
This commit is contained in:
zuevav
2026-10-08 00:45:06 +03:00
parent 8e4cb5de19
commit 6c42e0378d
10 changed files with 293 additions and 81 deletions
+116 -3
View File
@@ -116,14 +116,14 @@ func TestEmulatorsSave(t *testing.T) {
}
rec = post("/admin/emulators/quik", url.Values{
"emulate": {"1"}, "step_delay_sec": {"7"}, "fail_register": {"1"}, "fail_deposit": {"1"},
"mode": {"emulate"}, "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)
if !s.Quik.Emulate || s.Quik.RealWrite || s.MNPEmu.StepDelaySec != 7 || !s.MNPEmu.FailRegister || !s.MNPEmu.FailDeposit || s.MNPEmu.FailSign {
t.Errorf("после сохранения МНП: emulate=%v real=%v emu=%+v", s.Quik.Emulate, s.Quik.RealWrite, s.MNPEmu)
}
// Валидация на хендлере: плохой код отказа — 400, настройки не тронуты.
@@ -301,3 +301,116 @@ func TestDisableEmulatorsIfDevOffNoOp(t *testing.T) {
t.Error("no-op не должен добавлять новость")
}
}
// Режимы МНП взаимоисключающие: хендлер выставляет Emulate/RealWrite по выбранному режиму.
func TestEmulatorsQuikModes(t *testing.T) {
devModeForced = true
defer func() { devModeForced = false }()
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
if err != nil {
t.Fatal(err)
}
mux := http.NewServeMux()
registerEmulators(mux, nil, rc)
post := func(form url.Values) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodPost, "/admin/emulators/quik", 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(url.Values{"mode": {"real"}, "real_client_code": {"BR859"}})
if rec.Code != http.StatusOK {
t.Fatalf("real: %d %s", rec.Code, rec.Body.String())
}
if s := rc.Snapshot().Quik; !s.RealWrite || s.Emulate || s.RealClientCode != "BR859" {
t.Fatalf("режим real: %+v", s)
}
// Внутренняя имитация — наоборот, код сохраняется (инертен без флага).
rec = post(url.Values{"mode": {"emulate"}, "real_client_code": {"BR859"}})
if rec.Code != http.StatusOK {
t.Fatalf("emulate: %d %s", rec.Code, rec.Body.String())
}
if s := rc.Snapshot().Quik; !s.Emulate || s.RealWrite {
t.Fatalf("режим emulate: %+v", s)
}
// real без кода — 400.
if rec := post(url.Values{"mode": {"real"}}); rec.Code != http.StatusBadRequest {
t.Errorf("real без кода: %d, ожидали 400", rec.Code)
}
// Код с недопустимыми символами — 400 (валидация UpdateQuik).
if rec := post(url.Values{"mode": {"real"}, "real_client_code": {"БР 859!"}}); rec.Code != http.StatusBadRequest {
t.Errorf("плохой код: %d, ожидали 400", rec.Code)
}
}
// Подмена кода клиента QUIK в режиме реальной записи — после обычной логики (ClientCodeMap не мешает).
func TestQuikClientCodeRealWrite(t *testing.T) {
s := QuikMNPSettings{ClientCodeMap: map[string]string{"БР-7-7": "BR777"}}
if c := quikClientCodeFromPort(s, "БР-859 Баранкин"); c != "BR859" {
t.Errorf("обычный режим: %q", c)
}
if c := quikClientCodeFromPort(s, "БР-7-7 Погонщиков"); c != "BR777" {
t.Errorf("ClientCodeMap: %q", c)
}
s.RealWrite, s.RealClientCode = true, "BR859"
if c := quikClientCodeFromPort(s, "БР-7-7 Погонщиков"); c != "BR859" {
t.Errorf("подмена имеет приоритет над ClientCodeMap: %q", c)
}
if c := quikClientCodeFromPort(s, "БР-859 Баранкин"); c != "BR859" {
t.Errorf("подмена: %q", c)
}
// Флаг включён, но код не задан — обычная логика.
s.RealClientCode = ""
if c := quikClientCodeFromPort(s, "БР-7-7 Погонщиков"); c != "BR777" {
t.Errorf("real без кода: %q", c)
}
}
// Цикл МНП активен в режиме реальной записи только в dev и с указанным кодом.
func TestQuikActiveRealWrite(t *testing.T) {
if quikActive(QuikMNPSettings{RealWrite: true, RealClientCode: "BR859"}) {
t.Error("вне dev-режима цикл стоит")
}
devModeForced = true
defer func() { devModeForced = false }()
if !quikActive(QuikMNPSettings{RealWrite: true, RealClientCode: "BR859"}) {
t.Error("dev + код: цикл должен работать")
}
if quikActive(QuikMNPSettings{RealWrite: true}) {
t.Error("dev без кода: цикл стоит")
}
// Имитация имеет приоритет над real-записью.
if !quikActive(QuikMNPSettings{Emulate: true, RealWrite: true, RealClientCode: "BR859"}) {
t.Error("emulate + real: работает имитация (цикл активен)")
}
}
// Авто-сброс при окончании dev-режима гасит и реальную запись на подставного клиента.
func TestDisableEmulatorsIfDevOffRealWrite(t *testing.T) {
rc := rcWithExpiredDev(t)
q := rc.Snapshot().Quik
q.RealWrite, q.RealClientCode = true, "BR859"
if err := rc.UpdateQuik(q); err != nil {
t.Fatal(err)
}
disableEmulatorsIfDevOff(rc)
if s := rc.Snapshot().Quik; s.RealWrite {
t.Fatal("RealWrite не сброшен")
}
found := false
for _, n := range rc.Snapshot().News.Items {
if strings.Contains(n.Title, "Эмуляторы отключены") {
found = true
if !strings.Contains(n.Body, "подставного клиента BR859") || !strings.Contains(n.Body, "прекращены") {
t.Errorf("новость без упоминания реальных поручений: %s", n.Body)
}
}
}
if !found {
t.Error("новость не добавлена")
}
}