import test from "node:test"; import assert from "node:assert/strict"; import { spawn } from "node:child_process"; import { once } from "node:events"; import { mkdtemp, rm } from "node:fs/promises"; import { createServer } from "node:net"; import { get as httpGet } from "node:http"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; const projectDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const serverEntry = path.join(projectDir, "dist", "server.cjs"); async function freePort() { const server = createServer(); server.listen(0, "127.0.0.1"); await once(server, "listening"); const address = server.address(); const port = typeof address === "object" && address ? address.port : 0; server.close(); await once(server, "close"); return port; } function waitForServer(child) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error("Testserver startete nicht rechtzeitig.")), 10_000); const onData = (chunk) => { if (chunk.toString().includes("Server running")) { clearTimeout(timeout); child.stdout.off("data", onData); resolve(); } }; child.stdout.on("data", onData); child.once("exit", (code) => { clearTimeout(timeout); reject(new Error(`Testserver endete vorzeitig mit Code ${code}.`)); }); }); } async function stopServer(child) { if (child.exitCode !== null) return; child.kill("SIGTERM"); await once(child, "close"); } function getWithHost(baseUrl, host) { return new Promise((resolve, reject) => { const request = httpGet(baseUrl, { headers: { host } }, (response) => { let body = ""; response.setEncoding("utf8"); response.on("data", (chunk) => { body += chunk; }); response.on("end", () => resolve({ status: response.statusCode, body })); }); request.on("error", reject); }); } test("production hardening and public routing", async (t) => { await t.test("production refuses to start without secrets", async () => { const dataDir = await mkdtemp(path.join(tmpdir(), "author-missing-secrets-")); const env = { ...process.env, NODE_ENV: "production", DATA_DIR: dataDir }; delete env.ADMIN_PASSWORD; delete env.SESSION_SECRET; const child = spawn(process.execPath, [serverEntry], { cwd: projectDir, env, stdio: ["ignore", "pipe", "pipe"] }); let stderr = ""; child.stderr.on("data", (chunk) => { stderr += chunk; }); const [code] = await once(child, "close"); assert.equal(code, 1); assert.match(stderr, /ADMIN_PASSWORD, SESSION_SECRET/); await rm(dataDir, { recursive: true, force: true }); }); await t.test("sessions, rate limits, domains and uploads work securely", async () => { const dataDir = await mkdtemp(path.join(tmpdir(), "author-server-test-")); const port = await freePort(); const baseUrl = `http://127.0.0.1:${port}`; const child = spawn(process.execPath, [serverEntry], { cwd: projectDir, env: { ...process.env, NODE_ENV: "production", PORT: String(port), DATA_DIR: dataDir, ADMIN_PASSWORD: "test-password-12345", SESSION_SECRET: "test-session-secret-12345678901234567890", }, stdio: ["ignore", "pipe", "pipe"], }); try { await waitForServer(child); let response = await fetch(`${baseUrl}/api/admin/session`); assert.equal(response.status, 401); response = await fetch(`${baseUrl}/api/admin/login`, { method: "POST", headers: { "content-type": "application/json", origin: baseUrl }, body: JSON.stringify({ password: "test-password-12345" }), }); assert.equal(response.status, 200); const setCookie = response.headers.get("set-cookie") || ""; assert.match(setCookie, /HttpOnly/i); assert.match(setCookie, /Secure/i); assert.match(setCookie, /SameSite=Strict/i); assert.doesNotMatch(setCookie, /test-password/); const cookie = setCookie.split(";")[0]; response = await fetch(`${baseUrl}/api/admin/session`, { headers: { cookie } }); assert.equal(response.status, 200); response = await fetch(`${baseUrl}/api/admin/author-data`); assert.equal(response.status, 401); response = await fetch(`${baseUrl}/api/admin/author-data`, { headers: { cookie } }); assert.equal(response.status, 200); const adminData = await response.json(); adminData.erotica.customSectionLinks = [ { label: "Zum befreundeten Pseudonym", url: "https://example-pseudonym.test/" }, ]; response = await fetch(`${baseUrl}/api/admin/save-profile`, { method: "POST", headers: { "content-type": "application/json", origin: baseUrl, cookie }, body: JSON.stringify({ profileKey: "erotica", profileData: adminData.erotica, expectedRevision: adminData.revision }), }); assert.equal(response.status, 200); const firstSave = await response.json(); response = await fetch(`${baseUrl}/api/admin/save-profile`, { method: "POST", headers: { "content-type": "application/json", origin: baseUrl, cookie }, body: JSON.stringify({ profileKey: "erotica", profileData: adminData.erotica, expectedRevision: adminData.revision }), }); assert.equal(response.status, 409); response = await fetch(`${baseUrl}/api/admin/save-profile`, { method: "POST", headers: { "content-type": "application/json", origin: baseUrl, cookie }, body: JSON.stringify({ profileKey: "erotica", profileData: { ...adminData.erotica, customSectionLinks: [{ label: "Unsicher", url: "javascript:alert(1)" }] }, expectedRevision: firstSave.revision, }), }); assert.equal(response.status, 400); const publicResponse = await getWithHost(`${baseUrl}/api/author-data?path=/clara`, "annieslone.de"); assert.equal(publicResponse.status, 200); const publicData = JSON.parse(publicResponse.body); assert.deepEqual(Object.keys(publicData).sort(), ["legalDocuments", "profile", "theme"]); assert.equal(publicData.profile.name, "Annie Slone"); assert.equal(publicData.theme, "velvet"); assert.equal(publicData.profile.customDomain, undefined); assert.equal(publicData.profile.customPath, undefined); assert.deepEqual(publicData.profile.customSectionLinks, [ { label: "Zum befreundeten Pseudonym", url: "https://example-pseudonym.test/" }, ]); assert.equal(publicResponse.body.includes("Clara Finch"), false); assert.equal(publicResponse.body.includes("Renee Heart"), false); assert.equal(publicResponse.body.includes("Daniel Hesse"), false); let routedPage = await getWithHost(`${baseUrl}/`, "annieslone.de"); assert.match(routedPage.body, /