summaryrefslogtreecommitdiffstats
path: root/exchange_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'exchange_test.go')
-rw-r--r--exchange_test.go199
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
+}