cd6e2c424f
Было: правило «боевой ЛК + тестовый профиль → принудительно имитатор» не давало выключить эмуляцию, а подмена личности жила отдельным тумблером на другой странице — пользователь не мог понять состояние и выключить. Стало — явная модель (контур НРД и режим эмуляции независимы): - «Выключена» — заявки идут в выбранный контур (NSD.Profile) с реальными данными клиента; - «Подмена личности» — уходят в контур, но у клиентов из списка блок инвестора заменяется на тест-инвестора (Бубликов/Баранкин); DEPO и QUIK — по реальному клиенту; - «Полная имитация» — в НРД ничего не уходит, bj сам отвечает (внутренний имитатор). Режимы взаимоисключающие (ведутся флагами EmulateNSD/RealNSDTest.Enabled), принудительное правило убрано. Единый 3-радио на /admin/emulators (как у QUIK), с показом контура, тест-инвестора и списка паспортов; карточка на /admin/m2m-testing — статус + ссылка на «Эмуляторы». Тесты обновлены под новую модель. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
454 lines
18 KiB
Go
454 lines
18 KiB
Go
package lkgateway
|
||
|
||
import (
|
||
"context"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"net/url"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/license"
|
||
)
|
||
|
||
// Настройки эмуляторов: валидация и обратная совместимость (старый 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{
|
||
"mode": {"imitate"}, "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{
|
||
"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.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, настройки не тронуты.
|
||
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("задержка не прервалась по контексту")
|
||
}
|
||
}
|
||
|
||
// rcWithExpiredDev — действующая лицензия с истёкшим dev_until: devMode уже false.
|
||
func rcWithExpiredDev(t *testing.T) *RuntimeConfig {
|
||
t.Helper()
|
||
priv := testLicenseKey(t)
|
||
rc := testRC(t)
|
||
past := time.Now().UTC().Add(-time.Hour)
|
||
tok := signTestLicense(t, priv, &license.License{Schema: 1, ID: "lic-dev", Tenant: "Тест", Product: "bj-server", Plan: license.PlanPro,
|
||
IssuedAt: past.Add(-24 * time.Hour), ExpiresAt: time.Now().UTC().Add(60 * 24 * time.Hour), DevUntil: &past})
|
||
if err := rc.SaveLicense(LicenseSettings{Key: tok}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if devMode(rc) {
|
||
t.Fatal("devMode с истёкшим dev_until должен быть выключен")
|
||
}
|
||
return rc
|
||
}
|
||
|
||
// dev-режим закончился: включённые эмуляторы сбрасываются из настроек, про это — новость.
|
||
func TestDisableEmulatorsIfDevOff(t *testing.T) {
|
||
rc := rcWithExpiredDev(t)
|
||
s := rc.Snapshot()
|
||
s.LK.EmulateNSD = true
|
||
if err := rc.UpdateLK(s.LK); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s.Quik.Emulate = true
|
||
if err := rc.UpdateQuik(s.Quik); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
disableEmulatorsIfDevOff(rc)
|
||
if s := rc.Snapshot(); s.LK.EmulateNSD || s.Quik.Emulate {
|
||
t.Fatalf("флаги не сброшены: EmulateNSD=%v Quik.Emulate=%v", s.LK.EmulateNSD, s.Quik.Emulate)
|
||
}
|
||
// setup.json на диске тоже без флагов — при следующем dev-режиме эмуляторы не оживут.
|
||
rc2, err := NewRuntimeConfig(rc.path)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if s := rc2.Snapshot(); s.LK.EmulateNSD || s.Quik.Emulate {
|
||
t.Fatal("в setup.json флаги остались включёнными")
|
||
}
|
||
found := false
|
||
for _, n := range rc.Snapshot().News.Items {
|
||
if strings.Contains(n.Title, "Эмуляторы отключены") {
|
||
found = true
|
||
if !strings.Contains(n.Body, "эмулятор НРД") || !strings.Contains(n.Body, "эмулятор МНП") {
|
||
t.Errorf("в новости не перечислено, что выключено: %s", n.Body)
|
||
}
|
||
// WritesEnabled=false — новость предупреждает об остановленном цикле МНП.
|
||
if !strings.Contains(n.Body, "Цикл M2M в МНП остановлен") {
|
||
t.Errorf("нет предупреждения об остановленном цикле МНП: %s", n.Body)
|
||
}
|
||
}
|
||
}
|
||
if !found {
|
||
t.Error("новость об отключении эмуляторов не добавлена")
|
||
}
|
||
}
|
||
|
||
// dev-режим действует: авто-сброс ничего не трогает.
|
||
func TestDisableEmulatorsIfDevOffDevOn(t *testing.T) {
|
||
devModeForced = true
|
||
defer func() { devModeForced = false }()
|
||
rc := testRC(t)
|
||
s := rc.Snapshot()
|
||
s.LK.EmulateNSD = true
|
||
s.Quik.Emulate = true
|
||
if err := rc.UpdateLK(s.LK); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := rc.UpdateQuik(s.Quik); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
disableEmulatorsIfDevOff(rc)
|
||
if s := rc.Snapshot(); !s.LK.EmulateNSD || !s.Quik.Emulate {
|
||
t.Fatal("при действующем dev-режиме флаги не должны сбрасываться")
|
||
}
|
||
}
|
||
|
||
// dev выключен, но флаги уже сняты: no-op — настройки не переписываются, новости нет.
|
||
func TestDisableEmulatorsIfDevOffNoOp(t *testing.T) {
|
||
rc := rcWithExpiredDev(t)
|
||
before := rc.Snapshot().UpdatedAt
|
||
disableEmulatorsIfDevOff(rc)
|
||
after := rc.Snapshot()
|
||
if !after.UpdatedAt.Equal(before) {
|
||
t.Error("no-op не должен переписывать setup.json")
|
||
}
|
||
if len(after.News.Items) != 0 {
|
||
t.Error("no-op не должен добавлять новость")
|
||
}
|
||
}
|
||
|
||
// Подмена личности в НРД («боевой ЛК → TEST3») тоже гаснет по окончании dev-режима:
|
||
// иначе клиент без доступа к dev-вкладкам не вернул бы стандартный режим.
|
||
func TestDisableEmulatorsIfDevOffRealNSD(t *testing.T) {
|
||
rc := rcWithExpiredDev(t)
|
||
s := rc.Snapshot()
|
||
s.LK.RealNSDTest.Enabled = true
|
||
s.LK.RealNSDTest.Docs = "1110 754503"
|
||
if err := rc.UpdateLK(s.LK); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
disableEmulatorsIfDevOff(rc)
|
||
if rc.Snapshot().LK.RealNSDTest.Enabled {
|
||
t.Fatal("подмена личности не сброшена")
|
||
}
|
||
// Паспорта клиентов сохраняем — сбрасываем только включатель.
|
||
if rc.Snapshot().LK.RealNSDTest.Docs != "1110 754503" {
|
||
t.Errorf("список паспортов не должен затираться: %q", rc.Snapshot().LK.RealNSDTest.Docs)
|
||
}
|
||
// На диске тоже выключено — при следующем dev-режиме подмена сама не оживёт.
|
||
rc2, err := NewRuntimeConfig(rc.path)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if rc2.Snapshot().LK.RealNSDTest.Enabled {
|
||
t.Fatal("в setup.json подмена осталась включённой")
|
||
}
|
||
found := false
|
||
for _, n := range rc.Snapshot().News.Items {
|
||
if strings.Contains(n.Title, "Эмуляторы отключены") && strings.Contains(n.Body, "подмена личности") {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Error("в новости не упомянуто отключение подмены личности")
|
||
}
|
||
}
|
||
|
||
// Режимы МНП взаимоисключающие: хендлер выставляет 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("новость не добавлена")
|
||
}
|
||
}
|