@Tags(['integration', 'postgres']) library; import 'package:cyberhybridhub_server/alpaca/alpaca_env.dart'; import 'package:cyberhybridhub_server/alpaca/alpaca_market_data_client.dart'; import 'package:cyberhybridhub_server/trading/market_data_db.dart'; import 'package:cyberhybridhub_server/trading/market_data_ingest.dart'; import 'package:cyberhybridhub_server/trading/trading_config.dart'; import 'package:postgres/postgres.dart'; import 'package:test/test.dart'; import '../helpers/fixture_loader.dart'; import '../helpers/mock_http_client.dart'; import '../helpers/test_db.dart'; void main() { TestDb? testDb; setUpAll(() async { testDb = await TestDb.open(); }); tearDown(() async { if (testDb != null) { await testDb!.truncateTradingTables(); } }); tearDownAll(() async { await testDb?.close(); }); group('MarketDataIngest', () { test('writes last_trade, daily_bar, prev_close snapshots for SPY', () async { if (testDb == null) { markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests'); return; } const String uid = 'market-ingest-metrics-uid'; await testDb!.seedUser(uid); await testDb!.tradingConfigDb.upsertUserConfig( firebaseUid: uid, templateName: 'default_paper_watchlist', config: { 'data_inputs': >[ { 'id': 'primary_watchlist', 'symbols': ['SPY'], }, ], }, enabled: true, ); final EffectiveTradingConfig? config = await testDb!.tradingConfigDb.resolveEffectiveConfig(uid); expect(config, isNotNull); final FixtureLoader fixtures = FixtureLoader(); final MockHttpClient mock = MockHttpClient() ..whenGetJson( '/trades/latest', await fixtures.loadJson('alpaca_latest_trade.json'), ) ..whenGetJson( '/bars', await fixtures.loadJson('alpaca_daily_bars.json'), ); final AlpacaEnv env = AlpacaEnv.fromMap({ 'ALPACA_API_KEY_ID': 'test-key', 'ALPACA_API_SECRET_KEY': 'test-secret', }); final MarketDataIngest ingest = MarketDataIngest( marketDataDb: testDb!.marketDataDb, tradingStateDb: testDb!.userTradingStateDb, alpacaClient: AlpacaMarketDataClient(env: env, httpClient: mock), ); final MarketDataIngestResult result = await ingest.runIfDue( firebaseUid: uid, config: config!, now: DateTime.utc(2026, 5, 23, 12), ); expect(result.inputsFetched, 1); expect(result.snapshotsWritten, 3); final MarketDataSnapshot? lastTrade = await testDb!.marketDataDb.latestForSymbol('SPY', 'last_trade'); final MarketDataSnapshot? dailyBar = await testDb!.marketDataDb.latestForSymbol('SPY', 'daily_bar'); final MarketDataSnapshot? prevClose = await testDb!.marketDataDb.latestForSymbol('SPY', 'prev_close'); expect(lastTrade?.price, 492.15); expect(dailyBar?.price, 500.0); expect(prevClose?.price, 498.0); final Result rows = await testDb!.connection.execute( Sql.named( ''' SELECT metric, price::float FROM market_data_snapshots WHERE symbol = 'SPY' ORDER BY metric ''', ), ); expect(rows, hasLength(3)); }); test('second runIfDue within poll_interval_seconds skips HTTP', () async { if (testDb == null) { markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests'); return; } const String uid = 'market-ingest-poll-uid'; await testDb!.seedUser(uid); await testDb!.tradingConfigDb.upsertUserConfig( firebaseUid: uid, templateName: 'default_paper_watchlist', config: { 'data_inputs': >[ { 'id': 'primary_watchlist', 'source': 'alpaca', 'asset_class': 'us_equity', 'symbols': ['SPY'], 'feed': 'iex', 'poll_interval_seconds': 60, 'metrics': ['last_trade'], }, ], }, enabled: true, ); final EffectiveTradingConfig? config = await testDb!.tradingConfigDb.resolveEffectiveConfig(uid); expect(config, isNotNull); final FixtureLoader fixtures = FixtureLoader(); final MockHttpClient mock = MockHttpClient() ..whenGetJson( '/trades/latest', await fixtures.loadJson('alpaca_latest_trade.json'), ); final AlpacaEnv env = AlpacaEnv.fromMap({ 'ALPACA_API_KEY_ID': 'test-key', 'ALPACA_API_SECRET_KEY': 'test-secret', }); final MarketDataIngest ingest = MarketDataIngest( marketDataDb: testDb!.marketDataDb, tradingStateDb: testDb!.userTradingStateDb, alpacaClient: AlpacaMarketDataClient(env: env, httpClient: mock), ); final DateTime tick = DateTime.utc(2026, 5, 23, 12); await ingest.runIfDue( firebaseUid: uid, config: config!, now: tick, ); final int afterFirst = mock.requests.length; await ingest.runIfDue( firebaseUid: uid, config: config, now: tick.add(const Duration(seconds: 30)), ); expect(afterFirst, 1); expect(mock.requests.length, 1); }); }); }