feat: живой цикл M2M с НРД + мастер установки ключа на флешку

Инфраструктура M2M (живой обмен с НРД через ИШ):
- обработка M2MTransferResponse: ERROR(M2Mxx) → заявка Отклонена, сохранение
  ответа; INFO → ждём Decision; идемпотентность поллера
- fallback-корреляция ответов с нулевым GUID (M2M14/M2M17) по FIFO
- сырой XML ответа НРД в карточке заявки (для пересылки в ТП)
- тестовый пакет роботу приведён к эталону m2m_robot_samples (CostInfo=Yes,
  4 бумаги, IsolationStatus, DocumentSeries=сценарий); override паспорта
- редирект из теста сразу в карточку заявки

Мастер установки ключа Валидаты на флешку (admin/setup/keywizard):
- пошаговый: загрузка .7z+пароль → выбор флешки → запись → справочник
  сертификатов (CRL) → перезапуск+проверка ИШ → готово
- привилегированный воркер (bj-keymedia) в host-namespace через файл-обмен,
  bj-server остаётся в песочнице
- сохранение структуры профиля архива (spr<N>), перечисление съёмных USB

Прочее:
- пакет-доказательство для ТП НРД + форма регистрации участника M2M
- эталонные образцы робота (DOC/m2m_robot_samples)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-06-19 00:03:21 +03:00
parent 6e503433d4
commit 9737c787f9
110 changed files with 10771 additions and 1690 deletions
+85
View File
@@ -0,0 +1,85 @@
package m2mcore_test
import (
"context"
"testing"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore"
)
// makeResponse строит M2MTransferResponse сервиса МОСТ с заданным статусом и
// одним кодом/текстом (как в реальном ответе НРД, напр. M2M14).
func makeResponse(guid m2m.UUID, status m2m.StatusCode, code, text string) *m2m.M2MTransferResponse {
return &m2m.M2MTransferResponse{
GUID: guid,
StatusCode: status,
Responses: []m2m.Response{{Code: code, Text: text}},
}
}
// TestReceiveServiceResponseError — ERROR (отказ сервиса, напр. M2M14)
// переводит сделку в Rejected и сохраняет ответ для отображения в карточке.
func TestReceiveServiceResponseError(t *testing.T) {
d := newTestDeal(t)
resp := makeResponse(d.GUID, m2m.StatusError, "M2M14",
"Код ЭДО НРД отправителя отсутствует в справочнике участников M2M")
if err := d.ReceiveServiceResponse(context.Background(), resp, nil); err != nil {
t.Fatalf("ReceiveServiceResponse(ERROR): %v", err)
}
if d.State != m2mcore.StateRejected {
t.Errorf("состояние %s, ожидалось %s", d.State, m2mcore.StateRejected)
}
if d.Response == nil || d.Response.StatusCode != m2m.StatusError {
t.Errorf("ответ НРД не сохранён в сделке: %+v", d.Response)
}
if len(d.Response.Responses) == 0 || d.Response.Responses[0].Code != "M2M14" {
t.Errorf("код ответа не сохранён: %+v", d.Response)
}
}
// TestReceiveServiceResponseInfo — INFO (принято в обработку) сохраняет ответ,
// но НЕ меняет состояние: ждём M2MTransferDecision контрагента.
func TestReceiveServiceResponseInfo(t *testing.T) {
d := newTestDeal(t)
before := d.State
resp := makeResponse(d.GUID, m2m.StatusInfo, "01", "Запрос на перевод принят в обработку")
if err := d.ReceiveServiceResponse(context.Background(), resp, nil); err != nil {
t.Fatalf("ReceiveServiceResponse(INFO): %v", err)
}
if d.State != before {
t.Errorf("состояние изменилось на %s, ожидалось без изменений (%s)", d.State, before)
}
if d.Response == nil || d.Response.StatusCode != m2m.StatusInfo {
t.Errorf("ответ НРД не сохранён: %+v", d.Response)
}
}
// TestReceiveServiceResponseTerminalIdempotent — повторный ERROR на уже
// терминальной (Rejected) сделке не валит FSM, ответ обновляется.
func TestReceiveServiceResponseTerminalIdempotent(t *testing.T) {
d := newTestDeal(t)
first := makeResponse(d.GUID, m2m.StatusError, "M2M14", "первый отказ")
if err := d.ReceiveServiceResponse(context.Background(), first, nil); err != nil {
t.Fatalf("первый ERROR: %v", err)
}
// Поллер ИШ может прислать тот же пакет повторно — не должно паниковать
// и не должно ломать состояние недопустимым переходом Rejected→Rejected.
second := makeResponse(d.GUID, m2m.StatusError, "M2M14", "повторный отказ")
if err := d.ReceiveServiceResponse(context.Background(), second, nil); err != nil {
t.Fatalf("повторный ERROR на терминальной сделке: %v", err)
}
if d.State != m2mcore.StateRejected {
t.Errorf("состояние %s, ожидалось %s", d.State, m2mcore.StateRejected)
}
}
// TestReceiveServiceResponseNil — защита от nil.
func TestReceiveServiceResponseNil(t *testing.T) {
d := newTestDeal(t)
if err := d.ReceiveServiceResponse(context.Background(), nil, nil); err == nil {
t.Error("ожидалась ошибка на nil-ответе")
}
}