129 lines
4.4 KiB
JavaScript
129 lines
4.4 KiB
JavaScript
/**
|
|
* @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', () => {
|
|
const testState = {
|
|
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,
|
|
}
|
|
testState.serverProcess = spawn('node', ['.output/server/index.mjs'], {
|
|
cwd: projectRoot,
|
|
env,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
})
|
|
testState.serverProcess.stdout?.on('data', d => process.stdout.write(d))
|
|
testState.serverProcess.stderr?.on('data', d => process.stderr.write(d))
|
|
await waitForHealth(90000)
|
|
}, 120000)
|
|
|
|
afterAll(() => {
|
|
if (testState.serverProcess?.pid) {
|
|
testState.serverProcess.kill('SIGTERM')
|
|
}
|
|
})
|
|
|
|
it('serves health on port 3000', async () => {
|
|
const tryProtocols = async (protocols) => {
|
|
if (protocols.length === 0) throw new Error('No protocol succeeded')
|
|
try {
|
|
const res = await fetch(`${protocols[0]}://localhost:${API_PORT}/health`, { method: 'GET', headers: { Accept: 'application/json' } })
|
|
if (res?.ok) return res
|
|
return tryProtocols(protocols.slice(1))
|
|
}
|
|
catch {
|
|
return tryProtocols(protocols.slice(1))
|
|
}
|
|
}
|
|
const res = await tryProtocols(['https', 'http'])
|
|
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()
|
|
})
|
|
})
|