158 lines
5.0 KiB
Dart
158 lines
5.0 KiB
Dart
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–010.
|
||
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<TestDb?> 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<void> _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: <String, dynamic>{'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<void> truncateTradingTables() async {
|
||
await _connection.execute(
|
||
'''
|
||
TRUNCATE TABLE
|
||
trade_orders,
|
||
market_data_snapshots,
|
||
market_data_sync_runs,
|
||
tradable_assets,
|
||
user_trading_state,
|
||
user_trading_config,
|
||
questions,
|
||
user_pipeline_state,
|
||
users
|
||
RESTART IDENTITY CASCADE
|
||
''',
|
||
);
|
||
}
|
||
|
||
Future<void> 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: <String, dynamic>{
|
||
'uid': firebaseUid,
|
||
'email': '$firebaseUid@test.local',
|
||
},
|
||
);
|
||
}
|
||
|
||
Future<void> close() => db.close();
|
||
}
|