202 lines
6.3 KiB
JavaScript
202 lines
6.3 KiB
JavaScript
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)
|
|
}
|
|
})
|