160 lines
5.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 001016.
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_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<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();
}