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:
@@ -2,32 +2,53 @@ package igw_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdadapter/igw"
|
||||
)
|
||||
|
||||
// TestSendPackageHappyPath — отправка ZIP, ИШ возвращает id.
|
||||
// Сценарий по DOC/instr-ish-rest-api.pdf раздел 2.5.1.
|
||||
func TestSendPackageHappyPath(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/package/TEST3/file" {
|
||||
if r.URL.Path != "/api/package/CH1/file" {
|
||||
t.Errorf("неожиданный путь %q", r.URL.Path)
|
||||
}
|
||||
// Проверим что тело — это {Type: "archive", File: base64}.
|
||||
var body struct {
|
||||
Type string `json:"Type"`
|
||||
File string `json:"File"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode body: %v", err)
|
||||
}
|
||||
if body.Type != "archive" {
|
||||
t.Errorf("Type = %q, ожидалось archive", body.Type)
|
||||
}
|
||||
if body.File == "" {
|
||||
t.Errorf("File пустой")
|
||||
}
|
||||
if _, err := base64.StdEncoding.DecodeString(body.File); err != nil {
|
||||
t.Errorf("File не base64: %v", err)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"package_id": "pkg-123"})
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"id": 123})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := igw.NewClient(srv.URL, igw.WithRetry(0, time.Millisecond))
|
||||
id, err := c.SendPackage(context.Background(), "TEST3", "#M2MTR", []byte("<xml/>"))
|
||||
id, err := c.SendPackage(context.Background(), "CH1", "#M2MTR", []byte("PK\x03\x04zipbody"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if id != "pkg-123" {
|
||||
t.Errorf("package_id = %q, ожидалось %q", id, "pkg-123")
|
||||
if id != "123" {
|
||||
t.Errorf("id = %q, ожидалось 123", id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,17 +61,17 @@ func TestSendPackageRetryOn500(t *testing.T) {
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"package_id": "pkg-retry"})
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"id": 999})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := igw.NewClient(srv.URL, igw.WithRetry(3, time.Millisecond))
|
||||
id, err := c.SendPackage(context.Background(), "TEST3", "#M2MTR", []byte("x"))
|
||||
id, err := c.SendPackage(context.Background(), "CH1", "#M2MTR", []byte("x"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if id != "pkg-retry" {
|
||||
t.Errorf("ожидалось pkg-retry, получено %q", id)
|
||||
if id != "999" {
|
||||
t.Errorf("id = %q, ожидалось 999", id)
|
||||
}
|
||||
if calls < 2 {
|
||||
t.Errorf("ожидалось хотя бы 2 попытки, получено %d", calls)
|
||||
@@ -67,7 +88,7 @@ func TestSendPackage4xxNoRetry(t *testing.T) {
|
||||
defer srv.Close()
|
||||
|
||||
c := igw.NewClient(srv.URL, igw.WithRetry(3, time.Millisecond))
|
||||
_, err := c.SendPackage(context.Background(), "TEST3", "#M2MTR", []byte("x"))
|
||||
_, err := c.SendPackage(context.Background(), "CH1", "#M2MTR", []byte("x"))
|
||||
if err == nil {
|
||||
t.Fatal("ожидалась ошибка на 400")
|
||||
}
|
||||
@@ -76,60 +97,112 @@ func TestSendPackage4xxNoRetry(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetStatus — формат ответа по разделу 2.5.2 инструкции:
|
||||
// {id: 123, name: "#M2MTR...zip", status: SENT|NEW|ERROR, error: "..."}.
|
||||
func TestGetStatus(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/package/status/pkg-1" {
|
||||
if r.URL.Path != "/api/package/status/123" {
|
||||
t.Errorf("неожиданный путь %q", r.URL.Path)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"package_id":"pkg-1","state":"delivered","updated_at":"2026-03-02T14:30:00Z"}`))
|
||||
_, _ = w.Write([]byte(`{"id":123,"name":"#M2MTR20260320140624.zip","status":"SENT"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := igw.NewClient(srv.URL, igw.WithRetry(0, time.Millisecond))
|
||||
st, err := c.GetStatus(context.Background(), "pkg-1")
|
||||
st, err := c.GetStatus(context.Background(), "123")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st.State != "delivered" {
|
||||
t.Errorf("state = %q, ожидалось delivered", st.State)
|
||||
if st.Status != "SENT" {
|
||||
t.Errorf("status = %q, ожидалось SENT", st.Status)
|
||||
}
|
||||
if !strings.Contains(st.Name, "M2MTR") {
|
||||
t.Errorf("name = %q, ожидалось содержать M2MTR", st.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestListIncoming — формат ответа по разделу 2.6: массив пакетов с полями
|
||||
// channel/id/name/type/state/files/signs.
|
||||
func TestListIncoming(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !contains(r.URL.RawQuery, "channel=TEST3") {
|
||||
t.Errorf("в query нет channel: %s", r.URL.RawQuery)
|
||||
q := r.URL.Query()
|
||||
if q.Get("channel") != "CH1" {
|
||||
t.Errorf("channel = %q, ожидалось CH1", q.Get("channel"))
|
||||
}
|
||||
if q.Get("type") != "M2MTD" {
|
||||
t.Errorf("type = %q, ожидалось M2MTD", q.Get("type"))
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"items":[{"package_id":"p1","package_type":"#M2MTD","channel":"TEST3","received_at":"2026-03-02T14:00:00Z","body":""}]}`))
|
||||
_, _ = w.Write([]byte(`[{
|
||||
"channel":"CH1",
|
||||
"id":22423,
|
||||
"name":"#M2MTD20260320140624.ZIP",
|
||||
"type":"M2MTD",
|
||||
"state":"RECEIVED",
|
||||
"files":[{"id":30112,"name":"M2MTD20260320140624.XML"}],
|
||||
"signs":[{"serial":"40:50:14","subject":"INN=007702165310,CN=НРД","status":"VALID"}]
|
||||
}]`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := igw.NewClient(srv.URL, igw.WithRetry(0, time.Millisecond))
|
||||
pkgs, err := c.ListIncoming(context.Background(), "TEST3", time.Now().Add(-time.Hour), "#M2MTD")
|
||||
pkgs, err := c.ListIncoming(context.Background(), igw.ListFilter{
|
||||
Channel: "CH1",
|
||||
Type: "M2MTD",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(pkgs) != 1 || pkgs[0].PackageType != "#M2MTD" {
|
||||
t.Errorf("неожиданный результат: %+v", pkgs)
|
||||
if len(pkgs) != 1 {
|
||||
t.Fatalf("ожидался 1 пакет, получено %d", len(pkgs))
|
||||
}
|
||||
body, err := pkgs[0].DecodeBody()
|
||||
if err != nil {
|
||||
t.Errorf("DecodeBody: %v", err)
|
||||
p := pkgs[0]
|
||||
if p.ID != 22423 {
|
||||
t.Errorf("ID = %d, ожидалось 22423", p.ID)
|
||||
}
|
||||
if body != nil {
|
||||
t.Errorf("ожидалось пустое тело")
|
||||
if p.State != "RECEIVED" {
|
||||
t.Errorf("State = %q, ожидалось RECEIVED", p.State)
|
||||
}
|
||||
if len(p.Signs) != 1 || p.Signs[0].Status != "VALID" {
|
||||
t.Errorf("Signs неверные: %+v", p.Signs)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (indexOf(s, substr) >= 0)
|
||||
}
|
||||
|
||||
func indexOf(s, substr string) int {
|
||||
for i := 0; i+len(substr) <= len(s); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return i
|
||||
// TestGetPackage — скачивание содержимого. ИШ может возвращать либо чистый
|
||||
// ZIP, либо JSON с base64-полем. Тестируем оба случая.
|
||||
func TestGetPackageRawZIP(t *testing.T) {
|
||||
zipBytes := []byte("PK\x03\x04zip-content-here")
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/package/22423" {
|
||||
t.Errorf("неожиданный путь %q", r.URL.Path)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
_, _ = w.Write(zipBytes)
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := igw.NewClient(srv.URL, igw.WithRetry(0, time.Millisecond))
|
||||
body, err := c.GetPackage(context.Background(), 22423)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(body) != string(zipBytes) {
|
||||
t.Errorf("body = %q, ожидалось %q", body, zipBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPackageBase64InJSON(t *testing.T) {
|
||||
zipBytes := []byte("PK\x03\x04zip-from-base64")
|
||||
encoded := base64.StdEncoding.EncodeToString(zipBytes)
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"file":"` + encoded + `"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := igw.NewClient(srv.URL, igw.WithRetry(0, time.Millisecond))
|
||||
body, err := c.GetPackage(context.Background(), 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(body) != string(zipBytes) {
|
||||
t.Errorf("decoded = %q, ожидалось %q", body, zipBytes)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user