Files
any2anexoj/internal/trading212/serializer_test.go
T
natercioandnatercio 9bd4230ff1
Badges / coveralls (push) Successful in 1m4s
Internal state persistence (#27)
Allow us to generate year over year reports without having to rerun everything from the beginning.

Co-authored-by: Natercio Moniz <[email protected]>
2026-08-03 00:33:06 +01:00

152 lines
4.9 KiB
Go

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 }