diff options
Diffstat (limited to 'exchange_test.go')
| -rw-r--r-- | exchange_test.go | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/exchange_test.go b/exchange_test.go new file mode 100644 index 0000000..dcb68b1 --- /dev/null +++ b/exchange_test.go @@ -0,0 +1,199 @@ +package exchange + +import ( + "bytes" + "encoding/json" + "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) { + debug = true + + // 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 +} |