67 lines
4.3 KiB
JavaScript
67 lines
4.3 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { spawn, execFileSync } from 'node:child_process'
|
|
import { randomBytes } from 'node:crypto'
|
|
import { readdirSync, statSync, mkdtempSync, rmSync } from 'node:fs'
|
|
import { createServer } from 'node:net'
|
|
import { tmpdir } from 'node:os'
|
|
import { resolve, join, dirname, basename, sep } from 'node:path'
|
|
import { setTimeout as delay } from 'node:timers/promises'
|
|
import { resolveProjectPath } from './project-paths.mjs'
|
|
|
|
assert(process.argv[2], 'Provide the single-binary release directory or executable')
|
|
const input = resolveProjectPath(process.argv[2])
|
|
const directory = statSync(input).isDirectory() ? input : dirname(input)
|
|
const filename = process.platform === 'win32' ? 'linkport.exe' : 'linkport'
|
|
const binary = join(directory, filename)
|
|
assert(input === directory || input === binary, `Expected ${filename}`)
|
|
assert.deepEqual(readdirSync(directory), [filename], 'Release must contain only the executable')
|
|
assert(statSync(binary).isFile(), 'Expected an executable file')
|
|
const inherited = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('LINKPORT_')))
|
|
const version = JSON.parse(execFileSync(binary, ['--version', '--json'], { encoding: 'utf8', windowsHide: true, cwd: directory, env: inherited }))
|
|
assert.equal(version.platform, `${process.platform === 'win32' ? 'windows' : process.platform}/${process.arch === 'x64' ? 'amd64' : process.arch}`)
|
|
assert.match(version.version, /^\d+\.\d+\.\d+/)
|
|
assert.match(version.commit, /^(?:[a-f0-9]{40}|[a-f0-9]{64})(?:-dirty)?$|^unknown$/)
|
|
assert(Number.isFinite(Date.parse(version.buildTime)), 'Build timestamp must be embedded')
|
|
const temporaryRoot = resolve(tmpdir())
|
|
const temp = mkdtempSync(join(temporaryRoot, 'linkport-package-smoke-'))
|
|
let child
|
|
try {
|
|
const listener = createServer()
|
|
await new Promise((resolve, reject) => { listener.once('error', reject); listener.listen(0, '127.0.0.1', resolve) })
|
|
const port = listener.address().port
|
|
await new Promise(resolve => listener.close(resolve))
|
|
const password = randomBytes(24).toString('base64url')
|
|
child = spawn(binary, [], { cwd: temp, windowsHide: true, stdio: ['ignore', 'ignore', 'pipe'], env: { ...inherited, LINKPORT_NO_BROWSER: '1', LINKPORT_AUTH_ENABLED: 'true', LINKPORT_ADMIN_USER: 'package-test', LINKPORT_ADMIN_PASSWORD: password, LINKPORT_DATA: join(temp, 'test.sqlite'), LINKPORT_HTTP_ADDR: `127.0.0.1:${port}`, LINKPORT_SECURE_COOKIE: 'false' } })
|
|
let startupError = ''
|
|
child.stderr.on('data', b => { startupError += b.toString() })
|
|
child.on('error', e => { startupError += e.message })
|
|
const base = `http://127.0.0.1:${port}`
|
|
let healthy = false
|
|
for (let i = 0; i < 60; i++) {
|
|
try { healthy = (await fetch(`${base}/healthz`, { signal: AbortSignal.timeout(500) })).ok } catch {}
|
|
if (healthy) break
|
|
if (child.exitCode !== null) throw new Error(`Package exited during startup: ${startupError}`)
|
|
await delay(100)
|
|
}
|
|
assert(healthy, 'Packaged application did not start')
|
|
const page = await fetch(base)
|
|
assert.equal(page.status, 200)
|
|
const html = await page.text()
|
|
const assets = [...html.matchAll(/(?:src|href)="(\/assets\/[^" ]+)"/g)].map(m => m[1])
|
|
assert(assets.length >= 2, 'Built frontend was not embedded')
|
|
for (const asset of assets) assert.equal((await fetch(base + asset)).status, 200, `Missing embedded ${asset}`)
|
|
const auth = await fetch(`${base}/api/v1/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'package-test', password }) })
|
|
assert.equal(auth.status, 200)
|
|
const cookie = auth.headers.get('set-cookie').split(';')[0]
|
|
const proxies = await fetch(`${base}/api/v1/proxies`, { headers: { Cookie: cookie } })
|
|
assert.equal(proxies.status, 200)
|
|
assert.deepEqual(await proxies.json(), [])
|
|
assert.deepEqual(readdirSync(directory), [filename], 'Verification must not write to the release directory')
|
|
console.log('Single binary verified: embedded version, isolated single-SQLite startup, embedded web assets and administrator login.')
|
|
} finally {
|
|
if (child && child.exitCode === null) { await new Promise(resolve => { child.once('exit', resolve); child.kill() }) }
|
|
const cleanup = resolve(temp)
|
|
assert(cleanup.startsWith(temporaryRoot + sep) && basename(cleanup).startsWith('linkport-package-smoke-'), 'Unexpected cleanup path')
|
|
rmSync(cleanup, { recursive: true, force: true })
|
|
}
|