feat(igw): REST-клиент ИШ НРД по DOC/instr-ish-rest-api.pdf + упаковщик ZIP
Полный клиент Интеграционного шлюза НРД в internal/nsdadapter/igw/:
client.go — REST endpoint'ы по свежей спецификации НРД:
- POST /api/package/{channel}/file — отправка ZIP (Type=archive, File=base64)
возвращает id пакета (поддерживаются варианты id|package_id|ID)
- GET /api/package/status/{id} — статус NEW|SENT|ERROR (с error-полем)
- GET /api/package?channel=&type=M2MTD|M2MER&date=&id=&count=&excludeErrors=
— список входящих от НРД, с files[] и signs[] (ИШ сам проверяет ЭП и
выдаёт VALID|INVALID)
- GET /api/package/{id} — скачать ZIP (raw или base64-в-JSON, авто-детект
по сигнатуре PK\x03\x04)
- Ретраи только на 5xx/сетевые ошибки (4xx — сразу ошибка)
- HTTP-клиент через options, кастомный таймаут, ретраи
pack.go — упаковщик/распаковщик ZIP по разделу 2.3 инструкции:
- PackRequest(req, docName) — M2MTransferRequest→ZIP с config.xml
- PackXML(xml, docName, packageType) — для эталонных сообщений
- UnpackPackage(zip) → {DocXML, WinfXML, Signature, Filenames}
- ParseDecision / ParseResponse через nsdxml.Unmarshal
Покрыто тестами (10/10 PASS):
- send happy path с проверкой формата JSON-body
- retry на 5xx, без ретраев на 4xx
- GetStatus с числовым id
- ListIncoming как массив (новый формат) и как {items:[]} (старый)
- GetPackage raw ZIP + GetPackage с base64-в-JSON
- упаковка/распаковка: 2 файла в ZIP, имена, содержимое config.xml
- распаковка с .sgn и winf.xml
cmd/bj-server/main.go — NSD-poller адаптирован под новый API
(client.ListIncoming(ctx, ListFilter{}) вместо позиционных параметров;
поля Package.ID/Name/Type/State вместо PackageID/PackageType).
Скачана и положена в DOC/ свежая спецификация (798 KB, 15 стр):
DOC/instr-ish-rest-api.pdf — это исходный документ для нашей реализации.
REPORT.md обновлён:
- общая готовность 65% → 70%
- готовность к роботу 80% → 85%
- добавлен раздел про REST-клиент ИШ
- блокер #6 — отсутствие «Руководства по установке ИШ»
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
package igw_test
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/igw"
|
||||
)
|
||||
|
||||
// TestPackXML_StructureMatchesSpec — после упаковки в ZIP должны быть
|
||||
// ровно два файла: doc.xml + config.xml. Config содержит <name> и
|
||||
// <package>. Это структура из раздела 2.3 инструкции НРД.
|
||||
func TestPackXML_StructureMatchesSpec(t *testing.T) {
|
||||
xmlBody := []byte(`<?xml version="1.0" encoding="windows-1251"?><rt:M2MTransferRequest/>`)
|
||||
zipBytes, err := igw.PackXML(xmlBody, "M2MTransferRequest.xml", "#M2MTR")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(r.File) != 2 {
|
||||
t.Fatalf("в ZIP должно быть 2 файла, получено %d", len(r.File))
|
||||
}
|
||||
got := map[string][]byte{}
|
||||
for _, f := range r.File {
|
||||
rc, _ := f.Open()
|
||||
b, _ := io.ReadAll(rc)
|
||||
rc.Close()
|
||||
got[f.Name] = b
|
||||
}
|
||||
if _, ok := got["M2MTransferRequest.xml"]; !ok {
|
||||
t.Errorf("в ZIP нет M2MTransferRequest.xml. Файлы: %v", keys(got))
|
||||
}
|
||||
cfg, ok := got["config.xml"]
|
||||
if !ok {
|
||||
t.Fatalf("в ZIP нет config.xml. Файлы: %v", keys(got))
|
||||
}
|
||||
cfgStr := string(cfg)
|
||||
if !strings.Contains(cfgStr, "<name>M2MTransferRequest.xml</name>") {
|
||||
t.Errorf("config.xml не содержит правильное <name>: %s", cfgStr)
|
||||
}
|
||||
if !strings.Contains(cfgStr, "<package>#M2MTR</package>") {
|
||||
t.Errorf("config.xml не содержит правильное <package>: %s", cfgStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackXML_RejectsBadPackageType(t *testing.T) {
|
||||
_, err := igw.PackXML([]byte("<x/>"), "doc.xml", "M2MTR")
|
||||
if err == nil {
|
||||
t.Fatal("ожидалась ошибка для packageType без #")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnpackPackage_FindsXMLAndWinf — распаковка эмулирует входящий ZIP
|
||||
// от ИШ: M2MTD.xml + winf.xml + .sgn. Проверяем что UnpackPackage
|
||||
// корректно раскладывает по полям.
|
||||
func TestUnpackPackage_FindsXMLAndWinf(t *testing.T) {
|
||||
docBody := []byte("<dn:M2MTransferDecision/>")
|
||||
winfBody := []byte("<winf/>")
|
||||
sgnBody := []byte("BINARY-SIGN-BLOB")
|
||||
|
||||
var buf bytes.Buffer
|
||||
w := zip.NewWriter(&buf)
|
||||
must := func(name string, data []byte) {
|
||||
fw, _ := w.Create(name)
|
||||
_, _ = fw.Write(data)
|
||||
}
|
||||
must("M2MTD20260320140624.XML", docBody)
|
||||
must("winf.xml", winfBody)
|
||||
must("M2MTD20260320140624.XML.sgn", sgnBody)
|
||||
_ = w.Close()
|
||||
|
||||
pkg, err := igw.UnpackPackage(buf.Bytes())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(pkg.DocXML) != string(docBody) {
|
||||
t.Errorf("DocXML mismatch")
|
||||
}
|
||||
if string(pkg.WinfXML) != string(winfBody) {
|
||||
t.Errorf("WinfXML mismatch")
|
||||
}
|
||||
if string(pkg.Signature) != string(sgnBody) {
|
||||
t.Errorf("Signature mismatch")
|
||||
}
|
||||
if len(pkg.Filenames) != 3 {
|
||||
t.Errorf("ожидалось 3 файла в Filenames, получено %d", len(pkg.Filenames))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnpackPackage_EmptyXML(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
w := zip.NewWriter(&buf)
|
||||
fw, _ := w.Create("winf.xml")
|
||||
_, _ = fw.Write([]byte("<winf/>"))
|
||||
_ = w.Close()
|
||||
_, err := igw.UnpackPackage(buf.Bytes())
|
||||
if err == nil {
|
||||
t.Fatal("ожидалась ошибка когда в ZIP нет основного .xml")
|
||||
}
|
||||
}
|
||||
|
||||
func keys(m map[string][]byte) []string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user