package exchange import ( "bytes" "encoding/json" "flag" "log" "net/url" "os" "testing" "time" "github.com/google/go-querystring/query" "github.com/shopspring/decimal" "github.com/stretchr/testify/require" ) const ( keyFile = ".key" // $1 CAD ≅ 70¢ USD (±20%) // This is the rate at the time of writing. It may need to be // changed later if something drastic occurs in either country. usdPerCad = 0.70 rateEpsilon = 0.20 floatEpsilon = 1e-7 ) var apiKey string func TestMain(m *testing.M) { flag.BoolVar(&debug, "debug", false, "log debug info to stderr") flag.Parse() // Load API key buf, err := os.ReadFile(keyFile) if err != nil { log.Fatalf("error loading API key: %v\n", err) } apiKey = string(bytes.TrimSpace(buf)) m.Run() } func TestUnmarshalApiError(t *testing.T) { data := ` { "status": { "timestamp": "2018-06-02T22:51:28.209Z", "error_code": 1002, "error_message": "API key missing.", "elapsed": 10, "credit_count": 0 } } ` want := apiError{ status{"API key missing."}, } var info apiError err := json.Unmarshal([]byte(data), &info) require.NoError(t, err) require.Equal(t, want, info) } func TestPriceConversionQuery(t *testing.T) { q := priceConversionQuery{123, "USD", "CAD"} want := url.Values{ "amount": {"123"}, "symbol": {"USD"}, "convert": {"CAD"}, } vals, err := query.Values(q) require.NoError(t, err) require.Equal(t, want, vals) } func TestUnmarshalPriceConversionResponse(t *testing.T) { data := ` { "data": [ { "id": 32134, "symbol": "CAD", "name": "Caduceus Protocol (new)", "amount": 1, "quote": { "USD": { "price": 0.00026617250802605987, "last_updated": "2026-07-09T17:31:05.000Z" } }, "last_updated": "2026-07-09T17:30:00.000Z" }, { "id": 2784, "symbol": "CAD", "name": "Canadian Dollar", "amount": 1, "quote": { "USD": { "price": 0.7060965078585, "last_updated": "2026-07-09T17:31:05.000Z" } }, "last_updated": "2026-07-09T17:31:05.000Z" } ], "status": { "timestamp": "2026-07-09T17:32:29.378Z", "error_code": 0, "error_message": null, "elapsed": 25, "credit_count": 1, "notice": null } } ` want := response[[]priceConversion]{ Data: []priceConversion{ { 32134, map[string]price{ "USD": {decimal.RequireFromString("0.00026617250802605987")}, }, }, { 2784, map[string]price{ "USD": {decimal.RequireFromString("0.7060965078585")}, }, }, }, } var conv response[[]priceConversion] err := json.Unmarshal([]byte(data), &conv) require.NoError(t, err) require.Equal(t, want, conv) } func TestRateCache(t *testing.T) { clnt := NewClient(apiKey) defer clnt.Close() from, to := "CAD", "USD" // Repeat to exercise cache for i := 0; i < 5; i++ { rate, err := clnt.Rate(from, to) require.NoError(t, err) t.Log(rate) require.InEpsilon(t, usdPerCad, rate.InexactFloat64(), rateEpsilon) } } func TestRateCacheExpire(t *testing.T) { ttl := time.Millisecond clnt := NewClient(apiKey, WithTTL(ttl)) defer clnt.Close() from, to := "CAD", "USD" check := func() { rate, err := clnt.Rate(from, to) require.NoError(t, err) t.Log(rate) require.InEpsilon(t, usdPerCad, rate.InexactFloat64(), rateEpsilon) } check() // retrieve time.Sleep(2 * ttl) // expire check() // refresh } func TestRateFiatFiat(t *testing.T) { testRate(t, "AUD", "CAD") } func TestRateFiatCrypto(t *testing.T) { testRate(t, "CAD", "XMR") } func TestRateCryptoCrypto(t *testing.T) { testRate(t, "XMR", "BTC") } func testRate(t *testing.T, from, to string) { c := NewClient(apiKey) defer c.Close() aPerB := rate(t, c, from, to) bPerA := rate(t, c, to, from) require.NotZero(t, aPerB) require.NotZero(t, bPerA) require.InEpsilon(t, bPerA.InexactFloat64(), decimal.NewFromInt(1).Div(aPerB).InexactFloat64(), floatEpsilon) } func rate(t *testing.T, c *Client, from, to string) decimal.Decimal { t.Helper() rate, err := c.Rate(from, to) require.NoError(t, err) t.Logf("1 %s = %v %s\n", from, rate, to) return rate }