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)), ``) { 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) } }) }