import { once } from 'node:events' import path from 'node:path' import { fileURLToPath } from 'node:url' import pg from 'pg' import jwt from 'jsonwebtoken' import type { CaseState } from '../src/types.js' import { importMysteryTemplate } from '../scripts/importMysteryTemplate.js' import { runMigrations } from './migrations.js' const { Client } = pg const baseDatabaseUrl = process.env.TEST_DATABASE_URL if (!baseDatabaseUrl) throw new Error('TEST_DATABASE_URL is required for the browser harness') const databaseName = `osint_e2e_${process.pid}_${Date.now()}` const adminUrl = new URL(baseDatabaseUrl) adminUrl.pathname = '/postgres' const adminClient = new Client({ connectionString: adminUrl.toString() }) await adminClient.connect() await adminClient.query(`CREATE DATABASE "${databaseName}"`) const testUrl = new URL(baseDatabaseUrl) testUrl.pathname = `/${databaseName}` const databaseUrl = testUrl.toString() const migrationsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'migrations') await runMigrations(databaseUrl, migrationsDir, () => undefined) const port = Number(process.env.E2E_PORT || 18788) process.env.DATABASE_URL = databaseUrl process.env.LEVEL_EDITING_ENABLED = 'true' process.env.JWT_SECRET = 'osint-e2e-jwt-secret' process.env.PORT = String(port) process.env.OSINT_MANAGED_SERVER = 'true' process.env.ASSET_STORAGE_DRIVER = 'memory' process.env.OCR_LANGUAGES = 'eng' const { server, pool } = await import('./index.js') if (!server.listening) await once(server, 'listening') const baseUrl = `http://127.0.0.1:${port}` const adminToken = jwt.sign({ sub: 'e2e-admin', role: 'admin' }, process.env.JWT_SECRET) const adminHeaders = { 'content-type': 'application/json', authorization: `Bearer ${adminToken}` } const documentId = '22222222-2222-4222-8222-222222222222' const folderId = '11111111-1111-4111-8111-111111111111' const created = await fetch(`${baseUrl}/api/levels`, { method: 'POST', headers: adminHeaders, body: JSON.stringify({ id: 'e2e-level', title: 'Browser Safety Test', subtitle: 'Disposable test level' }), }) if (!created.ok) throw new Error(`Could not create browser test level: ${created.status}`) const state = await created.json() as CaseState state.brief = { body: 'Classify the named people and organizations in this investigation.', concepts: [ { id: '44444444-4444-4444-8444-444444444444', label: 'Ada Lovelace', context: 'Named as the correspondent.', expectedPartyKind: 'person' }, { id: '55555555-5555-4555-8555-555555555555', label: 'Difference Engine Bureau', context: 'Issued the archive notice.', expectedPartyKind: 'organization' }, ] } state.exhibits = [{ id: documentId, type: 'document', title: 'Dated source image', publishedAt: '2021-04-17T12:00:00.000Z', body: [], regions: [], fileType: 'image', metadata: {}, x: 980, y: 360, width: 174, height: 145, rotation: 0, zIndex: 2, hidden: false, }, { id: folderId, type: 'folder', title: 'BROWSER TEST FOLDER', content: 'Disposable evidence', x: 600, y: 360, width: 260, height: 166, rotation: 0, zIndex: 1, hidden: false, isOpen: false, }] state.relations = [{ id: `contains:${folderId}:${documentId}`, fromExhibitId: folderId, toExhibitId: documentId, type: 'contains', sortOrder: 0, }] state.connections = [] state.viewport = { x: 0, y: 28, zoom: 0.7 } const saved = await fetch(`${baseUrl}/api/levels/${state.id}?edit=1`, { method: 'PUT', headers: adminHeaders, body: JSON.stringify(state), }) if (!saved.ok) throw new Error(`Could not seed browser test level: ${saved.status}`) const glassHarbor = await importMysteryTemplate(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'mysteries', 'glass-harbor', 'mystery.json'), baseUrl, adminToken) const revealAuction = await fetch(`${baseUrl}/api/levels/${glassHarbor.playableLevel.id}/flags/lead.auction_catalogue`, { method: 'PUT', headers: adminHeaders, }) if (!revealAuction.ok) throw new Error(`Could not reveal the acceptance-test auction catalogue: ${revealAuction.status}`) await importMysteryTemplate(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'mysteries', 'barricelli-scene-7', 'mystery.json'), baseUrl, adminToken) let shuttingDown = false async function shutdown(exitCode: number) { if (shuttingDown) return shuttingDown = true if (server.listening) await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())) await pool.end() await adminClient.query(`DROP DATABASE IF EXISTS "${databaseName}"`) await adminClient.end() process.exit(exitCode) } process.on('SIGTERM', () => void shutdown(0)) process.on('SIGINT', () => void shutdown(0)) process.on('uncaughtException', error => { console.error(error); void shutdown(1) }) process.on('unhandledRejection', error => { console.error(error); void shutdown(1) }) console.log(`Browser safety harness ready on ${baseUrl}`)