Files
fontvielle 1d6ab86a57 feat(m2m): доменная модель сообщений + парсер windows-1251 + round-trip тесты
- internal/m2m/types.go: enum'ы и simple-типы из XSD НРД (M2MSchemas_260408)
- internal/m2m/validators.go: pattern-валидаторы ReferenceID/ISIN/INN/UUID/SecurityCode/IdentityDocSerial/AccountID + перечисления
- internal/m2m/messages.go: структуры 6 типов сообщений M2M, choice-типы через указатели, IsM2M=true автоматически в MarshalXML
- internal/nsdxml/datetime.go: тип NSDDateTime (формат "YYYY-MM-DDThh:mm:ss(МСК+N)")
- internal/nsdxml/codec.go: Marshal/Unmarshal XML в windows-1251 (собственный кодек CP1251, без внешних зависимостей)
- internal/m2m/messages_test.go: round-trip тесты на 6 примерах + 2 эталонах из DOC/

Покрытие: m2m 73.9%, nsdxml 92.5%. make ci зелёный.

Отклонение от спеки: вместо golang.org/x/text/encoding/charmap собственная
таблица CP1251 на ~60 строк, потому что прокси zetit блокирует
proxy.golang.org, goproxy.cn и redirect-хосты Go-модулей.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 00:30:46 +03:00

201 lines
7.8 KiB
Go
Raw Permalink 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 m2m_test
import (
"errors"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/nsdxml"
)
// validator — общий интерфейс для каскадной валидации в тестах.
type validator interface {
Validate() error
}
// roundTripCase описывает приёмочный кейс round-trip на эталонном файле.
type roundTripCase struct {
path string
mk func() any
}
// roundTripCases — соответствие "файл -> ожидаемый Go-тип сообщения".
var roundTripCases = []roundTripCase{
{filepath.Join("..", "..", "DOC", "Примеры", "M2MTransferRequest.xml"), func() any { return new(m2m.M2MTransferRequest) }},
{filepath.Join("..", "..", "DOC", "Примеры", "M2MTransferDecision.xml"), func() any { return new(m2m.M2MTransferDecision) }},
{filepath.Join("..", "..", "DOC", "Примеры", "M2MTransferResponse.xml"), func() any { return new(m2m.M2MTransferResponse) }},
{filepath.Join("..", "..", "DOC", "Примеры", "M2MTransferHandbook.xml"), func() any { return new(m2m.M2MTransferHandbook) }},
{filepath.Join("..", "..", "DOC", "Примеры", "M2MTransferHandbookRequest.xml"), func() any { return new(m2m.M2MTransferHandbookRequest) }},
{filepath.Join("..", "..", "DOC", "Примеры", "M2MTransferParticipantForm.xml"), func() any { return new(m2m.M2MTransferParticipantForm) }},
{filepath.Join("..", "..", "DOC", "Эталонные сообщения", "M2MTransferRequest_эталон.xml"), func() any { return new(m2m.M2MTransferRequest) }},
{filepath.Join("..", "..", "DOC", "Эталонные сообщения", "M2MTransferDecision_эталон.xml"), func() any { return new(m2m.M2MTransferDecision) }},
}
func TestRoundTrip(t *testing.T) {
for _, c := range roundTripCases {
c := c
t.Run(filepath.Base(c.path), func(t *testing.T) {
b1, err := os.ReadFile(c.path)
if err != nil {
t.Fatalf("read %s: %v", c.path, err)
}
s1 := c.mk()
if err := nsdxml.Unmarshal(b1, s1); err != nil {
t.Fatalf("unmarshal оригинала: %v", err)
}
if v, ok := s1.(validator); ok {
if err := v.Validate(); err != nil {
t.Fatalf("Validate после первого Unmarshal: %v", err)
}
}
b2, err := nsdxml.Marshal(s1)
if err != nil {
t.Fatalf("marshal: %v", err)
}
// Проверяем, что пролог windows-1251 проставлен.
if !strings.HasPrefix(string(nsdxml.DecodeWindows1251(b2)), `<?xml version="1.0" encoding="windows-1251"?>`) {
t.Fatalf("в выходе нет windows-1251 пролога")
}
s2 := c.mk()
if err := nsdxml.Unmarshal(b2, s2); err != nil {
t.Fatalf("unmarshal после Marshal: %v", err)
}
if !reflect.DeepEqual(s1, s2) {
t.Errorf("round-trip структуры разошлись:\nS1 = %+v\nS2 = %+v", s1, s2)
}
})
}
}
func TestValidatorsPositive(t *testing.T) {
cases := []struct {
name string
v validator
}{
{"ReferenceId", m2m.ReferenceID("M2M2026030200001")},
{"ISIN", m2m.ISIN("RU0007661625")},
{"OrganizationINN", m2m.OrganizationINN("7702070139")},
{"DeponentCode", m2m.DeponentCode("MC0079200000")},
{"UUID", m2m.UUID("c02a1d5e-c2af-4799-bab4-953f133c5133")},
{"SecurityCode", m2m.SecurityCode("MM0766162534")},
{"IdentityDocSerial", m2m.IdentityDocSerial("4512")},
{"AccountId", m2m.AccountID("31MC0021900000F01")},
{"StatusCode", m2m.StatusInfo},
{"IIAContractType", m2m.IIAContractT03},
{"SecurityClassification", m2m.SecurityBond},
{"SecurityCategory", m2m.CategoryOrdn},
{"IdentityDocumentCode", m2m.DocCode21},
{"IsolationStatus", m2m.IsolationSGDN},
{"Decimal16", m2m.Decimal16("2500.75")},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if err := c.v.Validate(); err != nil {
t.Errorf("ожидалось без ошибок, получено: %v", err)
}
})
}
}
func TestValidatorsNegative(t *testing.T) {
cases := []struct {
name string
v validator
}{
{"ReferenceId короткий", m2m.ReferenceID("M2M123")},
{"ReferenceId без префикса", m2m.ReferenceID("XYZ2026030200001")},
{"ISIN короткий", m2m.ISIN("RU0007")},
{"ISIN с lowercase", m2m.ISIN("ru0007661625")},
{"INN короткий", m2m.OrganizationINN("123")},
{"INN с буквами", m2m.OrganizationINN("770207013A")},
{"DeponentCode пустой", m2m.DeponentCode("")},
{"DeponentCode lowercase", m2m.DeponentCode("mc007920")},
{"DeponentCode слишком длинный", m2m.DeponentCode("AAAAAAAAAAAAA")},
{"UUID без дефисов", m2m.UUID("c02a1d5ec2af4799bab4953f133c5133aaaa")},
{"UUID с не-hex символом", m2m.UUID("c02a1d5e-c2af-4799-babZ-953f133c5133")},
{"UUID короткий", m2m.UUID("c02a1d5e-c2af-4799-bab4-953f133c513")},
{"SecurityCode короткий", m2m.SecurityCode("ABC")},
{"SecurityCode с lowercase", m2m.SecurityCode("mm0766162534")},
{"IdentityDocSerial пустой", m2m.IdentityDocSerial("")},
{"IdentityDocSerial с пробелом", m2m.IdentityDocSerial("45 12")},
{"AccountId пустой", m2m.AccountID("")},
{"AccountId длиннее 50", m2m.AccountID(strings.Repeat("A", 51))},
{"StatusCode unknown", m2m.StatusCode("OK")},
{"IIAContractType T01", m2m.IIAContractType("T01")},
{"SecurityClassification unknown", m2m.SecurityClassification("STCK")},
{"SecurityCategory unknown", m2m.SecurityCategory("XXXX")},
{"IdentityDocumentCode 99", m2m.IdentityDocumentCode("99")},
{"IsolationStatus FOO", m2m.IsolationStatus("FOO")},
{"Decimal16 пустой", m2m.Decimal16("")},
{"Decimal16 с буквами", m2m.Decimal16("12a.5")},
{"Decimal16 слишком много дробных", m2m.Decimal16("1.12345678901234567")},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
err := c.v.Validate()
if err == nil {
t.Errorf("ожидалась ошибка")
return
}
if !errors.Is(err, m2m.ErrInvalid) {
t.Errorf("ожидалась ErrInvalid, получено: %v", err)
}
})
}
}
func TestChoiceValidators(t *testing.T) {
t.Run("CostInfo пустой", func(t *testing.T) {
if err := (m2m.CostInfo{}).Validate(); !errors.Is(err, m2m.ErrChoice) {
t.Errorf("ожидалась ErrChoice, получено: %v", err)
}
})
t.Run("CostInfo оба поля", func(t *testing.T) {
c := m2m.CostInfo{
Yes: &m2m.CostInfoYes{Code: "MC0010300032"},
No: &m2m.CostInfoNo{},
}
if err := c.Validate(); !errors.Is(err, m2m.ErrChoice) {
t.Errorf("ожидалась ErrChoice, получено: %v", err)
}
})
t.Run("Quantity пустой", func(t *testing.T) {
if err := (m2m.Quantity{}).Validate(); !errors.Is(err, m2m.ErrChoice) {
t.Errorf("ожидалась ErrChoice, получено: %v", err)
}
})
t.Run("Quantity оба поля", func(t *testing.T) {
whole := uint64(100)
frac := m2m.Decimal16("1.5")
q := m2m.Quantity{Whole: &whole, Fractional: &frac}
if err := q.Validate(); !errors.Is(err, m2m.ErrChoice) {
t.Errorf("ожидалась ErrChoice, получено: %v", err)
}
})
t.Run("SecurityDetails пустой", func(t *testing.T) {
if err := (m2m.SecurityDetails{}).Validate(); !errors.Is(err, m2m.ErrChoice) {
t.Errorf("ожидалась ErrChoice, получено: %v", err)
}
})
t.Run("IdentificationDetails пустой", func(t *testing.T) {
if err := (m2m.IdentificationDetails{}).Validate(); !errors.Is(err, m2m.ErrChoice) {
t.Errorf("ожидалась ErrChoice, получено: %v", err)
}
})
t.Run("DecisionTransfer пустой", func(t *testing.T) {
if err := (m2m.DecisionTransfer{}).Validate(); !errors.Is(err, m2m.ErrChoice) {
t.Errorf("ожидалась ErrChoice, получено: %v", err)
}
})
}