Initial commit
This commit is contained in:
commit
2683eae6b3
36 files changed
+7369
No files matched your search
@@ -0,0 +1,201 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user