import 'dart:io'; import 'package:cyberhybridhub_server/db.dart'; import 'package:cyberhybridhub_server/question_service.dart'; import 'package:cyberhybridhub_server/questions_db.dart'; import 'package:cyberhybridhub_server/signalr/questions_hub_connections.dart'; import 'package:cyberhybridhub_server/trading/market_data_db.dart'; import 'package:cyberhybridhub_server/trading/trade_orders_db.dart'; import 'package:cyberhybridhub_server/trading/trading_config_db.dart'; import 'package:cyberhybridhub_server/trading/user_trading_state_db.dart'; import 'package:dotenv/dotenv.dart'; import 'package:postgres/postgres.dart'; /// Integration test Postgres: [cyberhybridhub_test] with migrations 001–017. class TestDb { TestDb._(this.db, this._connection, this.databaseUrl); final ProfileDb db; final Connection _connection; final String databaseUrl; Connection get connection => _connection; static const String testDatabaseName = 'cyberhybridhub_test'; static Future open() async { String? baseUrl = Platform.environment['TEST_DATABASE_URL'] ?? Platform.environment['DATABASE_URL']; if (baseUrl == null || baseUrl.isEmpty) { final DotEnv env = DotEnv(includePlatformEnvironment: true) ..load(['.env']); baseUrl = env['DATABASE_URL']; } if (baseUrl == null || baseUrl.isEmpty) { return null; } final Uri uri = Uri.parse(baseUrl); final String testUrl = uri.replace(path: '/$testDatabaseName').toString(); await _ensureTestDatabaseExists(baseUrl); final ProfileDb profileDb = await ProfileDb.connect(testUrl); final Directory migrationsDir = Directory('migrations'); if (!migrationsDir.existsSync()) { final Directory serverDir = Directory.current.path.endsWith('server') ? Directory.current : Directory('server'); if (serverDir.existsSync()) { Directory.current = serverDir.path; } } await profileDb.migrate(); return TestDb._(profileDb, profileDb.connection, testUrl); } static Future _ensureTestDatabaseExists(String databaseUrl) async { final Uri uri = Uri.parse(databaseUrl); final String adminDb = uri.pathSegments.isNotEmpty && uri.pathSegments.first.isNotEmpty ? uri.pathSegments.first : 'postgres'; final String adminUrl = uri.replace(path: '/$adminDb').toString(); final Connection admin = await Connection.open( _endpointFromUrl(adminUrl), settings: const ConnectionSettings(sslMode: SslMode.disable), ); try { final Result exists = await admin.execute( Sql.named( 'SELECT 1 FROM pg_database WHERE datname = @name', ), parameters: {'name': testDatabaseName}, ); if (exists.isEmpty) { try { await admin.execute('CREATE DATABASE $testDatabaseName'); } on ServerException catch (e) { // Parallel test files may race on CREATE DATABASE. if (e.code != '23505' && e.code != '42P04') { rethrow; } } } } finally { await admin.close(); } } static Endpoint _endpointFromUrl(String databaseUrl) { final Uri uri = Uri.parse(databaseUrl); return Endpoint( host: uri.host.isEmpty ? 'localhost' : uri.host, port: uri.hasPort ? uri.port : 5432, database: uri.pathSegments.isNotEmpty ? uri.pathSegments.last : 'postgres', username: uri.userInfo.isNotEmpty ? uri.userInfo.split(':').first : null, password: uri.userInfo.contains(':') ? uri.userInfo.split(':').skip(1).join(':') : null, ); } MarketDataDb get marketDataDb => MarketDataDb(_connection); TradingConfigDb get tradingConfigDb => TradingConfigDb(_connection); TradeOrdersDb get tradeOrdersDb => TradeOrdersDb(_connection); UserTradingStateDb get userTradingStateDb => UserTradingStateDb(_connection); QuestionsDb get questionsDb => QuestionsDb(_connection); /// QuestionService backed by a no-op hub (no live SignalR clients in tests). QuestionService questionService() => QuestionService( questionsDb: questionsDb, hubConnections: QuestionsHubConnections(), ); Future truncateTradingTables() async { await _connection.execute( ''' TRUNCATE TABLE trade_orders, market_history_prospective_assignments, market_history_prospective_questions, market_data_snapshots, market_data_sync_runs, tradable_assets, user_trading_state, user_trading_config, questions, user_pipeline_state, users RESTART IDENTITY CASCADE ''', ); } Future seedUser(String firebaseUid) async { await _connection.execute( Sql.named( ''' INSERT INTO users (firebase_uid, email) VALUES (@uid, @email) ON CONFLICT (firebase_uid) DO NOTHING ''', ), parameters: { 'uid': firebaseUid, 'email': '$firebaseUid@test.local', }, ); } Future close() => db.close(); }