Files
Bridge-and-Join-s/internal/nsdadapter/igw/client_test.go
T
fontvielle de41aea00c 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 — отсутствие «Руководства по установке ИШ»
2026-05-14 17:10:17 +03:00

209 lines
6.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/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]any{"id": 123})
}))
defer srv.Close()
c := igw.NewClient(srv.URL, igw.WithRetry(0, time.Millisecond))
id, err := c.SendPackage(context.Background(), "CH1", "#M2MTR", []byte("PK\x03\x04zipbody"))
if err != nil {
t.Fatal(err)
}
if id != "123" {
t.Errorf("id = %q, ожидалось 123", id)
}
}
func TestSendPackageRetryOn500(t *testing.T) {
calls := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
calls++
if calls < 2 {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
_ = 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(), "CH1", "#M2MTR", []byte("x"))
if err != nil {
t.Fatal(err)
}
if id != "999" {
t.Errorf("id = %q, ожидалось 999", id)
}
if calls < 2 {
t.Errorf("ожидалось хотя бы 2 попытки, получено %d", calls)
}
}
func TestSendPackage4xxNoRetry(t *testing.T) {
calls := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
calls++
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"error":"bad"}`))
}))
defer srv.Close()
c := igw.NewClient(srv.URL, igw.WithRetry(3, time.Millisecond))
_, err := c.SendPackage(context.Background(), "CH1", "#M2MTR", []byte("x"))
if err == nil {
t.Fatal("ожидалась ошибка на 400")
}
if calls != 1 {
t.Errorf("4xx не должен ретраиться, попыток = %d", calls)
}
}
// 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/123" {
t.Errorf("неожиданный путь %q", r.URL.Path)
}
w.WriteHeader(http.StatusOK)
_, _ = 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(), "123")
if err != nil {
t.Fatal(err)
}
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) {
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(`[{
"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(), igw.ListFilter{
Channel: "CH1",
Type: "M2MTD",
})
if err != nil {
t.Fatal(err)
}
if len(pkgs) != 1 {
t.Fatalf("ожидался 1 пакет, получено %d", len(pkgs))
}
p := pkgs[0]
if p.ID != 22423 {
t.Errorf("ID = %d, ожидалось 22423", p.ID)
}
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)
}
}
// 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)
}
}