feat(nsd-adapter): REST-клиент ИШ НРД + маршрутизация типов пакетов

- internal/nsdadapter/igw/client.go: REST-клиент ИШ (SendPackage, GetStatus, ListIncoming) с base64-JSON, ретраями на 5xx, 4xx без ретраев
- internal/nsdadapter/router.go: маршрутизация MessageKind -> PackageType ЭДО (#M2MTR, #M2MTD, #M2MER, SUBBR/SUBER/SUB16, Assets_investment)
- internal/nsdadapter/sender.go: реализация m2mcore.NSDSender (Send/SendDecision) через REST ИШ, сериализация Request/Decision в windows-1251
- internal/nsdadapter/config.go: профили guest/test3/prod × gost/rsa (URL ИШ, канал, контейнер ключа, retry)
- internal/nsdadapter/onyx/onyx.go: скелет резервного канала WS ONYX (ждёт PR-6 crypto-service для подписи)
- cmd/nsd-adapter/main.go: HTTP /healthz + фоновый поллер входящих по типам ЭДО; idle-режим без BJ_NSD_PROFILE

make ci зелёный. Без внешних Go-зависимостей.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
fontvielle
2026-05-14 00:55:20 +03:00
parent 9e6e95f431
commit a8cdeeb838
10 changed files with 803 additions and 1 deletions
+108
View File
@@ -0,0 +1,108 @@
package nsdadapter_test
import (
"context"
"testing"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/igw"
)
type mockIGW struct {
gotChannel string
gotType string
gotBody []byte
returnID string
returnErr error
}
func (m *mockIGW) SendPackage(_ context.Context, channel, packageType string, body []byte) (string, error) {
m.gotChannel = channel
m.gotType = packageType
m.gotBody = append([]byte(nil), body...)
return m.returnID, m.returnErr
}
func (m *mockIGW) ListIncoming(_ context.Context, _ string, _ time.Time, _ string) ([]igw.Package, error) {
return nil, nil
}
// validRequest строит минимально валидный M2MTransferRequest для теста Sender.
func validRequest() *m2m.M2MTransferRequest {
whole := uint64(100)
isin := m2m.ISIN("RU0007661625")
return &m2m.M2MTransferRequest{
Header: m2m.RequestHeader{
GUID: m2m.UUID("c02a1d5e-c2af-4799-bab4-953f133c5133"),
SenderCode: "MC0079200000",
ReceiverCode: "MC0010300000",
CostInfo: m2m.CostInfo{No: &m2m.CostInfoNo{}},
},
Data: m2m.RequestData{
InvestorInformation: m2m.InvestorInformation{
LastName: "Иванов",
FirstName: "Иван",
IdentityDocument: m2m.IdentityDocument{
DocumentType: m2m.DocCode21,
DocumentNumber: m2m.IdentityDocSerial("654321"),
},
},
TransferringDepository: m2m.SettlementRequisites{INN: "7702070139"},
ReceivingDepository: m2m.SettlementRequisites{INN: "7802031669"},
TransferredSecurities: m2m.RequestTransferredSecurities{
Securities: []m2m.RequestSecurity{
{
ReferenceID: "M2M2026030200001",
SecurityCode: "MM0766162534",
SecurityDetails: m2m.SecurityDetails{ISIN: &isin},
Quantity: m2m.Quantity{Whole: &whole},
SettlementAccount: []m2m.RequestSettlementAccount{
{
SettlementRequisites: m2m.SettlementRequisites{INN: "7702070139"},
SettlementLocation: m2m.SettlementDepositoryLocation{
DeponentCode: "DP789456",
AccountID: "31MC0021900000F01",
SectionID: "P001",
},
},
},
IsolationStatus: m2m.IsolationSGDN,
},
},
},
},
}
}
func TestSenderSend(t *testing.T) {
profile, err := nsdadapter.LookupProfile("test3-gost")
if err != nil {
t.Fatal(err)
}
mock := &mockIGW{returnID: "pkg-1"}
s := nsdadapter.NewSender(profile, mock)
_, err = s.Send(context.Background(), validRequest())
if err != nil {
t.Fatalf("Send: %v", err)
}
if mock.gotChannel != "TEST3" {
t.Errorf("channel = %q, ожидалось TEST3", mock.gotChannel)
}
if mock.gotType != string(nsdadapter.PackageM2MTransferRequest) {
t.Errorf("type = %q, ожидалось %s", mock.gotType, nsdadapter.PackageM2MTransferRequest)
}
if len(mock.gotBody) == 0 {
t.Error("body пустой")
}
}
func TestSenderSendNilRequest(t *testing.T) {
profile, _ := nsdadapter.LookupProfile("guest-gost")
s := nsdadapter.NewSender(profile, &mockIGW{})
if _, err := s.Send(context.Background(), nil); err == nil {
t.Error("ожидалась ошибка на nil request")
}
}