238 lines
6.3 KiB
Go
238 lines
6.3 KiB
Go
package ingestor
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/tech/sendico/fx/ingestor/internal/config"
|
|
mmarket "github.com/tech/sendico/fx/ingestor/internal/model"
|
|
"github.com/tech/sendico/fx/storage"
|
|
"github.com/tech/sendico/fx/storage/model"
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func TestParseDecimal(t *testing.T) {
|
|
got, err := parseDecimal("123.456")
|
|
if err != nil {
|
|
t.Fatalf("parseDecimal returned error: %v", err)
|
|
}
|
|
if got.String() != "15432/125" { // 123.456 expressed as a rational
|
|
t.Fatalf("unexpected rational value: %s", got.String())
|
|
}
|
|
|
|
if _, err := parseDecimal("not-a-number"); err == nil {
|
|
t.Fatalf("parseDecimal should fail on invalid decimal string")
|
|
}
|
|
}
|
|
|
|
func TestInvertPrices(t *testing.T) {
|
|
bid, err := parseDecimal("2")
|
|
if err != nil {
|
|
t.Fatalf("parseDecimal: %v", err)
|
|
}
|
|
ask, err := parseDecimal("4")
|
|
if err != nil {
|
|
t.Fatalf("parseDecimal: %v", err)
|
|
}
|
|
|
|
invBid, invAsk := invertPrices(bid, ask)
|
|
if diff := cmp.Diff("0.5", invAsk.FloatString(1)); diff != "" {
|
|
t.Fatalf("unexpected inverted ask (-want +got):\n%s", diff)
|
|
}
|
|
if diff := cmp.Diff("0.25", invBid.FloatString(2)); diff != "" {
|
|
t.Fatalf("unexpected inverted bid (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func TestServiceUpsertPairStoresSnapshot(t *testing.T) {
|
|
store := &ratesStoreStub{}
|
|
svc := testService(store, map[mmarket.Driver]mmarket.Connector{
|
|
mmarket.DriverBinance: &connectorStub{
|
|
id: mmarket.DriverBinance,
|
|
ticker: &mmarket.Ticker{
|
|
Symbol: "EURUSDT",
|
|
BidPrice: "1.0000",
|
|
AskPrice: "1.2000",
|
|
Provider: "binance",
|
|
},
|
|
},
|
|
})
|
|
|
|
pair := config.Pair{
|
|
PairConfig: config.PairConfig{
|
|
Base: "USDT",
|
|
Quote: "EUR",
|
|
Symbol: "EURUSDT",
|
|
Provider: "binance",
|
|
},
|
|
Source: mmarket.DriverBinance,
|
|
}
|
|
|
|
if err := svc.upsertPair(context.Background(), pair); err != nil {
|
|
t.Fatalf("upsertPair returned error: %v", err)
|
|
}
|
|
if len(store.snapshots) != 1 {
|
|
t.Fatalf("expected 1 snapshot stored, got %d", len(store.snapshots))
|
|
}
|
|
snap := store.snapshots[0]
|
|
if snap.Pair.Base != "USDT" || snap.Pair.Quote != "EUR" {
|
|
t.Fatalf("unexpected currency pair stored: %+v", snap.Pair)
|
|
}
|
|
if snap.Provider != "binance" {
|
|
t.Fatalf("unexpected provider: %s", snap.Provider)
|
|
}
|
|
if snap.Bid != "1.00000000" || snap.Ask != "1.20000000" {
|
|
t.Fatalf("unexpected bid/ask: %s / %s", snap.Bid, snap.Ask)
|
|
}
|
|
if snap.Mid != "1.10000000" {
|
|
t.Fatalf("unexpected mid price: %s", snap.Mid)
|
|
}
|
|
if snap.SpreadBps != "1818.18181818" {
|
|
t.Fatalf("unexpected spread bps: %s", snap.SpreadBps)
|
|
}
|
|
}
|
|
|
|
func TestServiceUpsertPairInvertsPrices(t *testing.T) {
|
|
store := &ratesStoreStub{}
|
|
svc := testService(store, map[mmarket.Driver]mmarket.Connector{
|
|
mmarket.DriverCoinGecko: &connectorStub{
|
|
id: mmarket.DriverCoinGecko,
|
|
ticker: &mmarket.Ticker{
|
|
Symbol: "RUBUSDT",
|
|
BidPrice: "2",
|
|
AskPrice: "4",
|
|
Provider: "coingecko",
|
|
},
|
|
},
|
|
})
|
|
|
|
pair := config.Pair{
|
|
PairConfig: config.PairConfig{
|
|
Base: "RUB",
|
|
Quote: "USDT",
|
|
Symbol: "RUBUSDT",
|
|
Provider: "coingecko",
|
|
Invert: true,
|
|
},
|
|
Source: mmarket.DriverCoinGecko,
|
|
}
|
|
|
|
if err := svc.upsertPair(context.Background(), pair); err != nil {
|
|
t.Fatalf("upsertPair returned error: %v", err)
|
|
}
|
|
|
|
snap := store.snapshots[0]
|
|
if snap.Bid != "0.25000000" || snap.Ask != "0.50000000" {
|
|
t.Fatalf("unexpected inverted bid/ask: %s / %s", snap.Bid, snap.Ask)
|
|
}
|
|
}
|
|
|
|
func TestServicePollOnceReturnsFirstError(t *testing.T) {
|
|
errFetch := merrors.Internal("fetch failed")
|
|
connectorSuccess := &connectorStub{
|
|
id: mmarket.DriverBinance,
|
|
ticker: &mmarket.Ticker{
|
|
Symbol: "EURUSDT",
|
|
BidPrice: "1",
|
|
AskPrice: "1",
|
|
Provider: "binance",
|
|
},
|
|
}
|
|
connectorFail := &connectorStub{
|
|
id: mmarket.DriverCoinGecko,
|
|
err: errFetch,
|
|
}
|
|
|
|
store := &ratesStoreStub{}
|
|
svc := testService(store, map[mmarket.Driver]mmarket.Connector{
|
|
mmarket.DriverBinance: connectorSuccess,
|
|
mmarket.DriverCoinGecko: connectorFail,
|
|
})
|
|
svc.pairs = []config.Pair{
|
|
{PairConfig: config.PairConfig{Base: "USDT", Quote: "EUR", Symbol: "EURUSDT"}, Source: mmarket.DriverBinance},
|
|
{PairConfig: config.PairConfig{Base: "USDT", Quote: "RUB", Symbol: "RUBUSDT"}, Source: mmarket.DriverCoinGecko},
|
|
}
|
|
|
|
err := svc.pollOnce(context.Background())
|
|
if err == nil {
|
|
t.Fatalf("pollOnce expected to return error")
|
|
}
|
|
if !errors.Is(err, errFetch) {
|
|
t.Fatalf("pollOnce returned unexpected error: %v", err)
|
|
}
|
|
if connectorSuccess.calls != 1 {
|
|
t.Fatalf("expected success connector called once, got %d", connectorSuccess.calls)
|
|
}
|
|
if connectorFail.calls != 1 {
|
|
t.Fatalf("expected failing connector called once, got %d", connectorFail.calls)
|
|
}
|
|
if len(store.snapshots) != 1 {
|
|
t.Fatalf("expected snapshot stored only for successful pair, got %d", len(store.snapshots))
|
|
}
|
|
}
|
|
|
|
// -- test helpers -----------------------------------------------------------------
|
|
|
|
type ratesStoreStub struct {
|
|
snapshots []*model.RateSnapshot
|
|
err error
|
|
}
|
|
|
|
func (r *ratesStoreStub) UpsertSnapshot(_ context.Context, snapshot *model.RateSnapshot) error {
|
|
if r.err != nil {
|
|
return r.err
|
|
}
|
|
cp := *snapshot
|
|
r.snapshots = append(r.snapshots, &cp)
|
|
return nil
|
|
}
|
|
|
|
func (r *ratesStoreStub) LatestSnapshot(context.Context, model.CurrencyPair, string) (*model.RateSnapshot, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
type repositoryStub struct {
|
|
rates storage.RatesStore
|
|
}
|
|
|
|
func (r *repositoryStub) Ping(context.Context) error { return nil }
|
|
func (r *repositoryStub) Rates() storage.RatesStore { return r.rates }
|
|
func (r *repositoryStub) Quotes() storage.QuotesStore { return nil }
|
|
func (r *repositoryStub) Pairs() storage.PairStore { return nil }
|
|
func (r *repositoryStub) Currencies() storage.CurrencyStore { return nil }
|
|
|
|
type connectorStub struct {
|
|
id mmarket.Driver
|
|
ticker *mmarket.Ticker
|
|
err error
|
|
calls int
|
|
}
|
|
|
|
func (c *connectorStub) ID() mmarket.Driver {
|
|
return c.id
|
|
}
|
|
|
|
func (c *connectorStub) FetchTicker(_ context.Context, symbol string) (*mmarket.Ticker, error) {
|
|
c.calls++
|
|
if c.ticker != nil {
|
|
cp := *c.ticker
|
|
cp.Symbol = symbol
|
|
return &cp, c.err
|
|
}
|
|
return nil, c.err
|
|
}
|
|
|
|
func testService(store storage.RatesStore, connectors map[mmarket.Driver]mmarket.Connector) *Service {
|
|
return &Service{
|
|
logger: zap.NewNop(),
|
|
cfg: &config.Config{},
|
|
rates: store,
|
|
connectors: connectors,
|
|
pairs: nil,
|
|
metrics: nil,
|
|
}
|
|
}
|