nathanAndrinoid a7c370b743
Some checks failed
Admin portal tests / test (push) Has been cancelled
Fix database connection pooling and UI question filtering
- Replaced single Postgres Connection with Pool across the server to prevent runTx locking
- Added _filterForEnabledSources to client's _fetchUnansweredForPanel to strictly enforce local configuration

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 22:04:06 +00:00

191 lines
6.3 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_source_preferences_db.dart';
import 'package:cyberhybridhub_server/pipeline/question_pipeline.dart';
import 'package:cyberhybridhub_server/question_dispatcher.dart';
import 'package:cyberhybridhub_server/question_service.dart';
import 'package:cyberhybridhub_server/questions_db.dart';
import 'package:cyberhybridhub_server/spelling/spelling_items_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–018.
class TestDb {
TestDb._(this.db, this._connection, this.databaseUrl);
final ProfileDb db;
final Pool _connection;
final String databaseUrl;
Pool 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();
await SpellingItemsDb(profileDb.connection).ensureCatalogSeeded();
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 Pool admin = Pool.withEndpoints(
[_endpointFromUrl(adminUrl)],
settings: const PoolSettings(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);
QuestionSourcePreferencesDb get questionSourcePreferencesDb =>
QuestionSourcePreferencesDb(_connection);
SpellingItemsDb get spellingItemsDb => SpellingItemsDb(_connection);
/// QuestionService backed by a no-op hub (no live SignalR clients in tests).
QuestionService questionService() => QuestionService(
questionsDb: questionsDb,
hubConnections: QuestionsHubConnections(),
questionSourcePreferencesDb: questionSourcePreferencesDb,
);
QuestionPipeline questionPipeline({bool testMode = true}) => QuestionPipeline(
questionsDb: questionsDb,
questionService: questionService(),
questionSourcePreferencesDb: questionSourcePreferencesDb,
spellingItemsDb: spellingItemsDb,
testMode: testMode,
);
QuestionDispatcher questionDispatcher({bool testMode = true}) {
final QuestionPipeline pipeline = questionPipeline(testMode: testMode);
final QuestionDispatcher dispatcher = QuestionDispatcher(
questionsDb: questionsDb,
questionService: questionService(),
questionPipeline: pipeline,
preferencesDb: questionSourcePreferencesDb,
);
pipeline.attachQuestionDispatcher(dispatcher);
return dispatcher;
}
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();
}