package crypto_test import ( "context" "encoding/json" "sort" "testing" decimal "github.com/alpacahq/alpacadecimal" "github.com/google/go-cmp/cmp" "s1d3sw1ped/robinhood-agentic-mcp/client" "s1d3sw1ped/robinhood-agentic-mcp/crypto" "s1d3sw1ped/robinhood-agentic-mcp/internal/rhntest" ) func TestCrypto_toolNames(t *testing.T) { t.Parallel() qty := decimal.RequireFromString("0.001") px := decimal.RequireFromString("3000") stop := decimal.RequireFromString("50000") dollars := decimal.RequireFromString("100") tests := []struct { name string call func(*crypto.Client) error wantName string wantArgs map[string]any }{ { name: "Pairs", call: func(c *crypto.Client) error { _, err := c.Pairs(context.Background(), crypto.PairsRequest{ Cursor: "c1", Limit: 25, }) return err }, wantName: "get_currency_pairs", wantArgs: map[string]any{"cursor": "c1", "limit": 25}, }, { name: "Quotes", call: func(c *crypto.Client) error { _, err := c.Quotes(context.Background(), crypto.QuotesRequest{ Symbols: []string{"BTC-USD", "ETH-USD"}, Timezone: "America/New_York", RHSAccountNumber: "123456789", }) return err }, wantName: "get_crypto_quotes", wantArgs: map[string]any{ "symbols": []string{"BTC-USD", "ETH-USD"}, "timezone": "America/New_York", "rhs_account_number": "123456789", }, }, { name: "Positions", call: func(c *crypto.Client) error { _, err := c.Positions(context.Background(), crypto.PositionsRequest{ RHSAccountNumber: "123456789", Cursor: "c1", }) return err }, wantName: "get_crypto_positions", wantArgs: map[string]any{"rhs_account_number": "123456789", "cursor": "c1"}, }, { name: "Orders", call: func(c *crypto.Client) error { _, err := c.Orders(context.Background(), crypto.OrdersRequest{ RHSAccountNumber: "123456789", OrderID: "o1", State: "filled", StateGroup: "closed", Side: client.Buy, Symbol: "BTC-USD", CreatedAtGTE: "2026-08-18T00:00:00Z", UpdatedAtGTE: "2026-08-18T13:30:00Z", Cursor: "c1", }) return err }, wantName: "get_crypto_orders", wantArgs: map[string]any{ "rhs_account_number": "123456789", "order_id": "o1", "state": "filled", "state_group": "closed", "side": "buy", "symbol": "BTC-USD", "created_at_gte": "2026-08-18T00:00:00Z", "updated_at_gte": "2026-08-18T13:30:00Z", "cursor": "c1", }, }, { name: "PreviewOrder", call: func(c *crypto.Client) error { _, err := c.PreviewOrder(context.Background(), crypto.PlaceOrderRequest{ RHSAccountNumber: "123456789", Symbol: "ETH", Side: client.Buy, Type: client.Limit, DollarAmount: &dollars, LimitPrice: &px, TimeInForce: client.GTC, RefID: "buy:eth", }) return err }, wantName: "preview_crypto_order", wantArgs: map[string]any{ "rhs_account_number": "123456789", "symbol": "ETH", "side": "buy", "type": "limit", "dollar_amount": "100", "limit_price": "3000", "time_in_force": "gtc", }, }, { name: "PlaceOrder", call: func(c *crypto.Client) error { _, err := c.PlaceOrder(context.Background(), crypto.PlaceOrderRequest{ RHSAccountNumber: "123456789", Symbol: "BTC-USD", Side: client.Sell, Type: client.StopLoss, Qty: &qty, StopPrice: &stop, TimeInForce: client.GFW, RefID: "sell:btc", }) return err }, wantName: "place_crypto_order", wantArgs: map[string]any{ "rhs_account_number": "123456789", "symbol": "BTC-USD", "side": "sell", "type": "stop_loss", "quantity": "0.001", "stop_price": "50000", "time_in_force": "gfw", "ref_id": "sell:btc", }, }, { name: "CancelOrder", call: func(c *crypto.Client) error { return c.CancelOrder(context.Background(), crypto.CancelOrderRequest{ RHSAccountNumber: "123456789", OrderID: "o1", }) }, wantName: "cancel_crypto_order", wantArgs: map[string]any{"rhs_account_number": "123456789", "order_id": "o1"}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() var gotName string var gotArgs map[string]any c := crypto.New(client.Func(func(ctx context.Context, name string, args map[string]any) (json.RawMessage, error) { gotName, gotArgs = name, args return json.RawMessage(`{"id":"o1"}`), nil })) if err := tc.call(c); err != nil { t.Fatal(err) } if gotName != tc.wantName { t.Fatalf("%s %+v", gotName, gotArgs) } if diff := cmp.Diff(tc.wantArgs, gotArgs); diff != "" { t.Fatal(diff) } if tc.name == "PreviewOrder" { if _, ok := gotArgs["ref_id"]; ok { t.Fatalf("ref_id on preview: %+v", gotArgs) } } if tc.name == "PlaceOrder" { if _, ok := gotArgs["idempotency_key"]; ok { t.Fatalf("idempotency_key on place: %+v", gotArgs) } if gotArgs["rhs_account_number"] != "123456789" { t.Fatalf("rhs_account_number %v", gotArgs["rhs_account_number"]) } if gotArgs["type"] != "stop_loss" { t.Fatalf("type %v", gotArgs["type"]) } } }) } } func TestTools(t *testing.T) { t.Parallel() want := []string{ "cancel_crypto_order", "get_crypto_orders", "get_crypto_positions", "get_crypto_quotes", "get_currency_pairs", "place_crypto_order", "preview_crypto_order", } got := append([]string(nil), crypto.Tools()...) sort.Strings(got) if diff := cmp.Diff(want, got); diff != "" { t.Fatal(diff) } } func TestQuotes_rhntest(t *testing.T) { t.Parallel() s := rhntest.New(t) s.Set("get_crypto_quotes", json.RawMessage(`{}`)) c := crypto.New(&client.Client{URL: s.URL}) _, err := c.Quotes(context.Background(), crypto.QuotesRequest{ Symbols: []string{"BTC-USD", "ETH-USD"}, RHSAccountNumber: "123456789", }) if err != nil { t.Fatal(err) } if s.LastName() != "get_crypto_quotes" { t.Fatalf("%s", s.LastName()) } want := map[string]any{ "symbols": []any{"BTC-USD", "ETH-USD"}, "rhs_account_number": "123456789", } if diff := cmp.Diff(want, s.LastArgs()); diff != "" { t.Fatal(diff) } } func TestPlaceOrder_rhntest(t *testing.T) { t.Parallel() s := rhntest.New(t) s.Set("place_crypto_order", json.RawMessage(`{"id":"o1"}`)) c := crypto.New(&client.Client{URL: s.URL}) qty := decimal.RequireFromString("0.001") stop := decimal.RequireFromString("50000") got, err := c.PlaceOrder(context.Background(), crypto.PlaceOrderRequest{ RHSAccountNumber: "123456789", Symbol: "BTC-USD", Side: client.Sell, Type: client.StopLoss, Qty: &qty, StopPrice: &stop, TimeInForce: client.GFW, RefID: "sell:btc", }) if err != nil { t.Fatal(err) } if got.ID != "o1" { t.Fatalf("%+v", got) } if s.LastName() != "place_crypto_order" { t.Fatalf("%s", s.LastName()) } want := map[string]any{ "rhs_account_number": "123456789", "symbol": "BTC-USD", "side": "sell", "type": "stop_loss", "quantity": "0.001", "stop_price": "50000", "time_in_force": "gfw", "ref_id": "sell:btc", } if diff := cmp.Diff(want, s.LastArgs()); diff != "" { t.Fatal(diff) } }