/** * @vitest-environment node * * Integration test: built server on 3000 (API) and 8089 (CoT). Uses temp DB and bootstrap * user so CoT auth can be asserted (socket stays open on success). */ import { describe, it, expect, beforeAll, afterAll } from 'vitest' import { spawn, execSync } from 'node:child_process' import { connect } from 'node:tls' import { existsSync, mkdirSync } from 'node:fs' import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' import { tmpdir } from 'node:os' import { buildAuthCotXml } from '../helpers/fakeAtakClient.js' process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' const __dirname = dirname(fileURLToPath(import.meta.url)) const projectRoot = join(__dirname, '../..') const devCertsDir = join(projectRoot, '.dev-certs') const devKey = join(devCertsDir, 'key.pem') const devCert = join(devCertsDir, 'cert.pem') const API_PORT = 3000 const COT_PORT = 8089 const COT_AUTH_USER = 'test' const COT_AUTH_PASS = 'test' function ensureDevCerts() { if (existsSync(devKey) && existsSync(devCert)) return mkdirSync(devCertsDir, { recursive: true }) execSync( `openssl req -x509 -newkey rsa:2048 -keyout "${devKey}" -out "${devCert}" -days 365 -nodes -subj "/CN=localhost" -addext "subjectAltName=IP:127.0.0.1,DNS:localhost"`, { cwd: projectRoot, stdio: 'pipe' }, ) } const FETCH_TIMEOUT_MS = 5000 async function waitForHealth(timeoutMs = 90000) { const start = Date.now() while (Date.now() - start < timeoutMs) { for (const protocol of ['https', 'http']) { try { const baseURL = `${protocol}://localhost:${API_PORT}` const ctrl = new AbortController() const t = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS) const res = await fetch(`${baseURL}/health`, { method: 'GET', signal: ctrl.signal }) clearTimeout(t) if (res.ok) return baseURL } catch { // try next } } await new Promise(r => setTimeout(r, 1000)) } throw new Error(`Health not OK on ${API_PORT} within ${timeoutMs}ms`) } describe('Server and CoT integration', () => { let serverProcess = null beforeAll(async () => { ensureDevCerts() const serverPath = join(projectRoot, '.output', 'server', 'index.mjs') if (!existsSync(serverPath)) { execSync('npm run build', { cwd: projectRoot, stdio: 'pipe' }) } const dbPath = join(tmpdir(), `kestrelos-it-${process.pid}-${Date.now()}.db`) const env = { ...process.env, DB_PATH: dbPath, BOOTSTRAP_EMAIL: COT_AUTH_USER, BOOTSTRAP_PASSWORD: COT_AUTH_PASS, } serverProcess = spawn('node', ['.output/server/index.mjs'], { cwd: projectRoot, env, stdio: ['ignore', 'pipe', 'pipe'], }) serverProcess.stdout?.on('data', d => process.stdout.write(d)) serverProcess.stderr?.on('data', d => process.stderr.write(d)) await waitForHealth(90000) }, 120000) afterAll(() => { if (serverProcess?.pid) { serverProcess.kill('SIGTERM') } }) it('serves health on port 3000', async () => { let res for (const protocol of ['https', 'http']) { try { res = await fetch(`${protocol}://localhost:${API_PORT}/health`, { method: 'GET', headers: { Accept: 'application/json' } }) if (res?.ok) break } catch { // try next } } expect(res?.ok).toBe(true) const body = await res.json() expect(body).toHaveProperty('status', 'ok') expect(body).toHaveProperty('endpoints') expect(body.endpoints).toHaveProperty('ready', '/health/ready') }) it('CoT on 8089: TAK client auth with username/password succeeds (socket stays open)', async () => { const payload = buildAuthCotXml({ username: COT_AUTH_USER, password: COT_AUTH_PASS }) const socket = await new Promise((resolve, reject) => { const s = connect(COT_PORT, '127.0.0.1', { rejectUnauthorized: false }, () => { s.write(payload, () => resolve(s)) }) s.on('error', reject) s.setTimeout(8000, () => reject(new Error('connect timeout'))) }) await new Promise(r => setTimeout(r, 600)) expect(socket.destroyed).toBe(false) socket.destroy() }) })