import 'dart:convert'; import 'package:cyberhybridhub_server/alpaca/alpaca_env.dart'; import 'package:cyberhybridhub_server/alpaca/alpaca_models.dart'; import 'package:cyberhybridhub_server/alpaca/alpaca_trading_client.dart'; import 'package:http/http.dart' as http; import 'package:test/test.dart'; import '../helpers/fixture_loader.dart'; import '../helpers/mock_http_client.dart'; void main() { late AlpacaEnv env; late FixtureLoader fixtures; setUp(() { env = AlpacaEnv.fromMap({ 'ALPACA_API_KEY_ID': 'test-key-id', 'ALPACA_API_SECRET_KEY': 'test-secret', 'ALPACA_TRADING_BASE_URL': AlpacaEnv.defaultPaperTradingUrl, 'ALPACA_DATA_BASE_URL': AlpacaEnv.defaultDataUrl, 'ALPACA_DATA_FEED': 'iex', }); fixtures = FixtureLoader(); }); group('AlpacaTradingClient.submitOrder', () { test('POSTs to /v2/orders with credentials and parses response', () async { final MockHttpClient http = MockHttpClient(); http.whenPost( '/v2/orders', Response.json( await fixtures.loadJson('alpaca_order_accepted.json'), statusCode: 201, ), ); final AlpacaTradingClient client = AlpacaTradingClient(env: env, httpClient: http); final AlpacaOrderRequest request = AlpacaOrderRequest( symbol: 'SPY', side: 'buy', type: 'market', timeInForce: 'day', clientOrderId: 'test-uid-spy_dip-q123', notional: 10, ); final AlpacaOrderResponse response = await client.submitOrder(request); expect(response.id, '904837e3-3b76-47ec-b432-046db621571b'); expect(response.clientOrderId, 'test-uid-spy_dip-q123'); expect(response.status, 'accepted'); expect(response.symbol, 'SPY'); expect(response.side, 'buy'); expect(response.type, 'market'); expect(response.notional, 10); expect(http.requests, hasLength(1)); final request_ = http.requests.first; expect(request_.method, 'POST'); expect(request_.url.path, '/v2/orders'); expect(request_.headers['APCA-API-KEY-ID'], 'test-key-id'); expect(request_.headers['APCA-API-SECRET-KEY'], 'test-secret'); expect(request_.headers['content-type'], contains('application/json')); final Map sent = jsonDecode(http.capturedBodies.first) as Map; expect(sent['symbol'], 'SPY'); expect(sent['side'], 'buy'); expect(sent['type'], 'market'); expect(sent['time_in_force'], 'day'); expect(sent['client_order_id'], 'test-uid-spy_dip-q123'); expect(sent['notional'], 10); }); test('throws AlpacaTradingDuplicateClientOrderIdException on 422 dup', () async { final MockHttpClient http = MockHttpClient(); http.whenPost( '/v2/orders', Response.json( await fixtures.loadJson('alpaca_order_duplicate_client_id.json'), statusCode: 422, ), ); final AlpacaTradingClient client = AlpacaTradingClient(env: env, httpClient: http); expect( () => client.submitOrder( AlpacaOrderRequest( symbol: 'SPY', side: 'buy', type: 'market', timeInForce: 'day', clientOrderId: 'duplicate', notional: 10, ), ), throwsA(isA()), ); }); test('refuses live URL when ALPACA_ALLOW_LIVE=false', () async { final AlpacaEnv liveEnv = AlpacaEnv.fromMap({ 'ALPACA_API_KEY_ID': 'k', 'ALPACA_API_SECRET_KEY': 's', 'ALPACA_TRADING_BASE_URL': 'https://api.alpaca.markets', }); final AlpacaTradingClient client = AlpacaTradingClient(env: liveEnv, httpClient: MockHttpClient()); expect( () => client.submitOrder( AlpacaOrderRequest( symbol: 'SPY', side: 'buy', type: 'market', timeInForce: 'day', clientOrderId: 'x', notional: 10, ), ), throwsStateError, ); }); }); group('AlpacaTradingClient.getOrderByClientOrderId', () { test('returns parsed response when found', () async { final MockHttpClient http = MockHttpClient(); http.whenGet( '/v2/orders:by_client_order_id', Response.json(await fixtures.loadJson('alpaca_order_accepted.json')), ); final AlpacaTradingClient client = AlpacaTradingClient(env: env, httpClient: http); final AlpacaOrderResponse? order = await client.getOrderByClientOrderId('test-uid-spy_dip-q123'); expect(order, isNotNull); expect(order!.id, '904837e3-3b76-47ec-b432-046db621571b'); expect(http.requests.single.url.queryParameters['client_order_id'], 'test-uid-spy_dip-q123'); }); test('returns null on 404', () async { final MockHttpClient http = MockHttpClient(); http.whenGet( '/v2/orders:by_client_order_id', Response.json({'error': 'not found'}, statusCode: 404), ); final AlpacaTradingClient client = AlpacaTradingClient(env: env, httpClient: http); expect(await client.getOrderByClientOrderId('missing'), isNull); }); }); } /// Tiny helper for building canned `http.Response` values from JSON fixtures. class Response { static http.Response json(Map body, {int statusCode = 200}) { return http.Response( jsonEncode(body), statusCode, headers: {'content-type': 'application/json'}, ); } }