Badges / coveralls (push) Successful in 1m4s
Allow us to generate year over year reports without having to rerun everything from the beginning. Co-authored-by: Natercio Moniz <[email protected]>
152 lines
4.9 KiB
Go
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 }
|