package trading212 import ( "bytes" "io" "net/http" "testing" "time" "github.com/nmoniz/any2anexoj/internal" "github.com/nmoniz/any2anexoj/internal/ofigi" "github.com/shopspring/decimal" ) func TestRecordSerializer_RoundTrip(t *testing.T) { want := Record{ symbol: "XX1234567890", timestamp: time.Date(2025, 7, 3, 10, 44, 29, 0, time.UTC), kind: internal.KindBuy, quantity: ShouldParseDecimal(t, "2.4387014200"), price: ShouldParseDecimal(t, "7.3690000000"), fees: ShouldParseDecimal(t, "0.02"), taxes: ShouldParseDecimal(t, "0.25"), natureGetter: func() internal.Nature { return internal.NatureG01 }, } s := NewRecordSerializer(NewFigiClientSecurityTypeStub(t, "Common Stock")) data, err := s.MarshalRecord(t.Context(), want) if err != nil { t.Fatalf("MarshalRecord: %v", err) } got, err := s.UnmarshalRecord(t.Context(), data) if err != nil { t.Fatalf("UnmarshalRecord: %v", err) } if got.Symbol() != want.Symbol() { t.Errorf("Symbol: want %q but got %q", want.Symbol(), got.Symbol()) } if got.Kind() != want.Kind() { t.Errorf("Kind: want %v but got %v", want.Kind(), got.Kind()) } if !got.Price().Equal(want.Price()) { t.Errorf("Price: want %v but got %v", want.Price(), got.Price()) } if !got.Quantity().Equal(want.Quantity()) { t.Errorf("Quantity: want %v but got %v", want.Quantity(), got.Quantity()) } if !got.Fees().Equal(want.Fees()) { t.Errorf("Fees: want %v but got %v", want.Fees(), got.Fees()) } if !got.Taxes().Equal(want.Taxes()) { t.Errorf("Taxes: want %v but got %v", want.Taxes(), got.Taxes()) } if !got.Timestamp().Equal(want.Timestamp()) { t.Errorf("Timestamp: want %v but got %v", want.Timestamp(), got.Timestamp()) } } func TestRecordSerializer_UnmarshalRecord_NatureTriggersOpenFIGI(t *testing.T) { var calls int client := &http.Client{ Timeout: time.Second, Transport: RoundTripFunc(func(req *http.Request) (*http.Response, error) { calls++ return &http.Response{ Status: http.StatusText(http.StatusOK), StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(`[{"data":[{"securityType":"Common Stock"}]}]`)), Request: req, }, nil }), } s := NewRecordSerializer(ofigi.NewOpenFIGI(client, "")) original := Record{ symbol: "XX1234567890", timestamp: time.Date(2025, 7, 3, 10, 44, 29, 0, time.UTC), kind: internal.KindBuy, quantity: ShouldParseDecimal(t, "2.4387014200"), price: ShouldParseDecimal(t, "7.3690000000"), fees: ShouldParseDecimal(t, "0.02"), taxes: ShouldParseDecimal(t, "0.25"), // Pre-populated so MarshalRecord doesn't accidentally trigger an // OpenFIGI call when encoding the original record. natureGetter: func() internal.Nature { return internal.NatureG01 }, } data, err := s.MarshalRecord(t.Context(), original) if err != nil { t.Fatalf("MarshalRecord: %v", err) } if calls != 0 { t.Fatalf("OpenFIGI called during MarshalRecord: %d", calls) } got, err := s.UnmarshalRecord(t.Context(), data) if err != nil { t.Fatalf("UnmarshalRecord: %v", err) } // Nature must not have been resolved yet — natureGetter is lazy. if calls != 0 { t.Fatalf("OpenFIGI called before Nature(): %d", calls) } if nature := got.Nature(); nature != internal.NatureG01 { t.Errorf("Nature: want %v but got %v", internal.NatureG01, nature) } if calls != 1 { t.Errorf("OpenFIGI request count: want 1 but got %d", calls) } // Subsequent Nature() calls should not re-trigger the request (the // underlying sync.OnceValue caches the result on the client too). if nature := got.Nature(); nature != internal.NatureG01 { t.Errorf("Nature (cached): want %v but got %v", internal.NatureG01, nature) } if calls != 1 { t.Errorf("OpenFIGI request count after re-read: want 1 but got %d", calls) } } func TestRecordSerializer_MarshalRecord_WrongType(t *testing.T) { s := NewRecordSerializer(NewFigiClientSecurityTypeStub(t, "Common Stock")) _, err := s.MarshalRecord(t.Context(), stubRecord{}) if err == nil { t.Fatal("want error but got nil") } } // stubRecord is a non-trading212 implementation of internal.Record used to // verify that MarshalRecord rejects unrelated record types. type stubRecord struct{} func (stubRecord) Symbol() string { return "STUB" } func (stubRecord) Nature() internal.Nature { return internal.NatureUnknown } func (stubRecord) BrokerCountry() int64 { return 0 } func (stubRecord) AssetCountry() int64 { return 0 } func (stubRecord) Kind() internal.Kind { return internal.KindUnknown } func (stubRecord) Price() decimal.Decimal { return decimal.Zero } func (stubRecord) Quantity() decimal.Decimal { return decimal.Zero } func (stubRecord) Timestamp() time.Time { return time.Time{} } func (stubRecord) Fees() decimal.Decimal { return decimal.Zero } func (stubRecord) Taxes() decimal.Decimal { return decimal.Zero }