import assert from 'node:assert/strict' import { spawn } from 'node:child_process' import { createServer } from 'node:net' import path from 'node:path' import test from 'node:test' import { fileURLToPath } from 'node:url' import { startMockWiki } from './mock-wiki.js' const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url))) function freePort() { return new Promise((resolve, reject) => { const server = createServer() server.once('error', reject) server.listen(0, '127.0.0.1', () => { const { port } = server.address() server.close((error) => (error ? reject(error) : resolve(port))) }) }) } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)) } async function startApp({ port, wikiUrl, apiKey = 'test-key', extraEnv = {} }) { const env = { ...process.env, NODE_ENV: 'test', HOST: '127.0.0.1', PORT: String(port), WIKI_URL: wikiUrl || '', WIKI_API_KEY: apiKey, ...extraEnv, } delete env.MAX_PAGES delete env.AUTH_USER delete env.AUTH_PASSWORD const child = spawn(process.execPath, ['src/server.js'], { cwd: ROOT, env, stdio: ['ignore', 'pipe', 'pipe'], }) let output = '' child.stdout.on('data', (chunk) => { output += chunk }) child.stderr.on('data', (chunk) => { output += chunk }) const baseUrl = `http://127.0.0.1:${port}` for (let attempt = 0; attempt < 100; attempt += 1) { if (child.exitCode !== null) break try { const response = await fetch(`${baseUrl}/healthz`) if (response.ok) return { baseUrl, child, output: () => output } } catch { // The server is not listening yet. } await sleep(50) } child.kill('SIGKILL') throw new Error(`server did not start:\n${output}`) } async function stopApp(app) { if (!app) return if (app.child.exitCode === null) { await new Promise((resolve) => { app.child.once('exit', resolve) app.child.kill('SIGTERM') setTimeout(() => app.child.kill('SIGKILL'), 5_000).unref() }) } } test('serves the wiki page list and keeps the connection details server-side', async () => { const wiki = await startMockWiki() const port = await freePort() const app = await startApp({ port, wikiUrl: wiki.url }) try { const configResponse = await fetch(`${app.baseUrl}/api/config`) assert.equal(configResponse.status, 200) assert.match(configResponse.headers.get('x-request-id') || '', /^[0-9a-f-]{36}$/) const config = await configResponse.json() assert.equal(config.wikiUrlConfigured, true) assert.equal(config.wikiUrl, wiki.url) assert.equal(config.apiKeyConfigured, true) assert.equal('apiKey' in config, false, 'the API key must never be sent to the browser') const pagesResponse = await fetch(`${app.baseUrl}/api/pages`) assert.equal(pagesResponse.status, 200) const pagesPayload = await pagesResponse.json() assert.equal(pagesPayload.baseUrl, wiki.url) assert.equal(pagesPayload.defaultLocale, 'en') assert.equal(pagesPayload.namespacing, true) assert.equal(pagesPayload.pages.length, wiki.pages.length) const docs = pagesPayload.pages.find((page) => page.path === 'docs') assert.deepEqual(docs, { path: 'docs', locale: 'en', title: 'Documentation', description: 'Documentation root', isPublished: true, }) assert.equal( pagesPayload.pages.find((page) => page.path === 'docs/draft').isPublished, false, ) const paths = pagesPayload.pages.map((page) => page.path) assert.deepEqual(paths, [...paths].sort((a, b) => a.localeCompare(b, 'en'))) } finally { await stopApp(app) await wiki.close() } }) test('rejects an export without a starting page and does not require a browser URL', async () => { const wiki = await startMockWiki() const port = await freePort() const app = await startApp({ port, wikiUrl: wiki.url }) try { const response = await fetch(`${app.baseUrl}/api/exports`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ startPage: '' }), }) assert.equal(response.status, 400) const payload = await response.json() assert.equal(payload.error, 'The starting page is required.') // A browser-supplied baseUrl/API key must be ignored: the server uses // WIKI_URL and WIKI_API_KEY only. A missing page fails before Chromium // starts, so this test does not need a browser. const accepted = await fetch(`${app.baseUrl}/api/exports`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ baseUrl: 'https://attacker.example', apiKey: 'attacker-key', startPage: 'missing-page', maxPages: 1, }), }) assert.equal(accepted.status, 202) const { job } = await accepted.json() assert.equal(job.request.baseUrl, wiki.url) assert.equal('apiKey' in job.request, false) assert.equal('apiKey' in job, false) let status = job for (let attempt = 0; attempt < 100 && status.status !== 'error'; attempt += 1) { await sleep(50) status = (await (await fetch(`${app.baseUrl}/api/exports/${job.id}`)).json()).job } assert.equal(status.status, 'error') assert.equal(status.error.code, 'START_PAGE_NOT_FOUND') } finally { await stopApp(app) await wiki.close() } }) test('reports missing and invalid WIKI_URL without crashing', async () => { const port = await freePort() const app = await startApp({ port, wikiUrl: '' }) try { const config = await (await fetch(`${app.baseUrl}/api/config`)).json() assert.equal(config.wikiUrlConfigured, false) assert.equal(config.wikiUrl, null) const pagesResponse = await fetch(`${app.baseUrl}/api/pages`) assert.equal(pagesResponse.status, 503) const pagesPayload = await pagesResponse.json() assert.equal(pagesPayload.code, 'WIKI_URL_MISSING') } finally { await stopApp(app) } const invalidPort = await freePort() const invalidApp = await startApp({ port: invalidPort, wikiUrl: 'ftp://not-a-wiki' }) try { const pagesResponse = await fetch(`${invalidApp.baseUrl}/api/pages`) assert.equal(pagesResponse.status, 503) const payload = await pagesResponse.json() assert.equal(payload.code, 'WIKI_URL_MISSING') assert.match(payload.error, /WIKI_URL is invalid/) } finally { await stopApp(invalidApp) } })