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.contactEmail = "kontakt@example.test"; adminData.erotica.instagramUrl = "https://instagram.com/example-author"; adminData.erotica.discordUrl = "https://discord.gg/example-author"; adminData.erotica.books[0].seriesName = "Beispiel-Reihe"; adminData.erotica.books[0].seriesNumber = 1; adminData.erotica.books[0].ebookLink = "https://amazon.example/ebook"; adminData.erotica.books[0].paperbackLink = "https://amazon.example/paperback"; 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(publicData.profile.contactEmail, "kontakt@example.test"); assert.equal(publicData.profile.discordUrl, "https://discord.gg/example-author"); assert.equal(publicData.profile.books[0].seriesName, "Beispiel-Reihe"); assert.equal(publicData.profile.books[0].seriesNumber, 1); 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, /Annie Slone/); routedPage = await getWithHost(`${baseUrl}/`, "www.annieslone.de"); assert.match(routedPage.body, /<title>Annie Slone/); routedPage = await getWithHost(`${baseUrl}/`, "blog.annieslone.de"); assert.match(routedPage.body, /<title>Daniel Hesse/); const validPng = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="; const upload = (fileName, base64Data) => fetch(`${baseUrl}/api/admin/upload-file`, { method: "POST", headers: { "content-type": "application/json", origin: baseUrl, cookie }, body: JSON.stringify({ fileName, base64Data }), }); response = await upload("cover.png", validPng); assert.equal(response.status, 201); const uploaded = await response.json(); assert.match(uploaded.url, /^\/uploads\/[a-z0-9-]+\.png$/); assert.equal(uploaded.mimeType, "image/png"); response = await upload("fake.png", "data:image/png;base64,SGVsbG8="); assert.equal(response.status, 415); response = await upload("wrong.jpg", validPng); assert.equal(response.status, 415); const validPdf = `data:application/pdf;base64,${Buffer.from("%PDF-1.4\n%%EOF").toString("base64")}`; response = await fetch(`${baseUrl}/api/admin/upload-sample`, { method: "POST", headers: { "content-type": "application/json", origin: baseUrl, cookie }, body: JSON.stringify({ fileName: "leseprobe.pdf", base64Data: validPdf }), }); assert.equal(response.status, 201); const sample = await response.json(); assert.match(sample.url, /^\/downloads\/[a-z0-9-]+\.pdf$/); response = await fetch(`${baseUrl}${sample.url}`); assert.equal(response.status, 200); assert.match(response.headers.get("content-disposition") || "", /attachment/); response = await fetch(`${baseUrl}/api/admin/upload-publication`, { method: "POST", headers: { "content-type": "application/json", origin: baseUrl, cookie }, body: JSON.stringify({ fileName: "geschichte.pdf", base64Data: validPdf, format: "pdf" }), }); assert.equal(response.status, 201); const publicationPdf = await response.json(); const epubBuffer = Buffer.concat([Buffer.from([0x50, 0x4b, 0x03, 0x04]), Buffer.alloc(60), Buffer.from("application/epub+zip")]); response = await fetch(`${baseUrl}/api/admin/upload-publication`, { method: "POST", headers: { "content-type": "application/json", origin: baseUrl, cookie }, body: JSON.stringify({ fileName: "geschichte.epub", base64Data: `data:application/epub+zip;base64,${epubBuffer.toString("base64")}`, format: "epub" }), }); assert.equal(response.status, 201); const publicationEpub = await response.json(); response = await fetch(`${baseUrl}${publicationEpub.url}`); assert.equal(response.status, 200); assert.equal(response.headers.get("content-type"), "application/epub+zip"); adminData.erotica.books[0].samplePdfUrl = sample.url; adminData.erotica.downloadsTitle = "Kostenlose Geschichten"; adminData.erotica.downloads = [{ id: "download_test", title: "Testgeschichte", description: "Eine kurze Beschreibung.", imageUrl: "", pdfUrl: publicationPdf.url, epubUrl: publicationEpub.url, publishedAt: "2026-08-18", }]; 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: firstSave.revision }), }); assert.equal(response.status, 200); const sampleSave = await response.json(); const downloadPublicResponse = await getWithHost(`${baseUrl}/api/author-data`, "annieslone.de"); const downloadPublicData = JSON.parse(downloadPublicResponse.body); assert.equal(downloadPublicData.profile.downloads[0].title, "Testgeschichte"); assert.equal(downloadPublicData.profile.downloads[0].epubUrl, publicationEpub.url); response = await fetch(`${baseUrl}/api/admin/samples/${path.basename(sample.url)}`, { method: "DELETE", headers: { origin: baseUrl, cookie } }); assert.equal(response.status, 409); response = await fetch(`${baseUrl}/api/admin/publications/${path.basename(publicationEpub.url)}`, { method: "DELETE", headers: { origin: baseUrl, cookie } }); assert.equal(response.status, 409); response = await fetch(`${baseUrl}${uploaded.url}`); assert.equal(response.status, 200); assert.equal(response.headers.get("x-content-type-options"), "nosniff"); assert.match(response.headers.get("cache-control") || "", /immutable/); adminData.erotica.avatarUrl = uploaded.url; 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: sampleSave.revision }), }); assert.equal(response.status, 200); const referencedSave = await response.json(); response = await fetch(`${baseUrl}/api/admin/uploads/${path.basename(uploaded.url)}`, { method: "DELETE", headers: { origin: baseUrl, cookie }, }); assert.equal(response.status, 409); adminData.erotica.avatarUrl = ""; adminData.erotica.books[0].samplePdfUrl = ""; adminData.erotica.downloads = []; 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: referencedSave.revision }), }); assert.equal(response.status, 200); response = await fetch(`${baseUrl}/api/admin/uploads/${path.basename(uploaded.url)}`, { method: "DELETE", headers: { origin: baseUrl, cookie }, }); assert.equal(response.status, 200); response = await fetch(`${baseUrl}${uploaded.url}`); assert.equal(response.status, 404); response = await fetch(`${baseUrl}/api/admin/samples/${path.basename(sample.url)}`, { method: "DELETE", headers: { origin: baseUrl, cookie } }); assert.equal(response.status, 200); response = await fetch(`${baseUrl}/api/admin/publications/${path.basename(publicationPdf.url)}`, { method: "DELETE", headers: { origin: baseUrl, cookie } }); assert.equal(response.status, 200); response = await fetch(`${baseUrl}/api/admin/publications/${path.basename(publicationEpub.url)}`, { method: "DELETE", headers: { origin: baseUrl, cookie } }); assert.equal(response.status, 200); response = await fetch(`${baseUrl}/api/admin/logout`, { method: "POST", headers: { origin: baseUrl, cookie } }); assert.equal(response.status, 200); response = await fetch(`${baseUrl}/api/admin/session`, { headers: { cookie } }); assert.equal(response.status, 401); const attempts = []; for (let index = 0; index < 6; index += 1) { const attempt = await fetch(`${baseUrl}/api/admin/login`, { method: "POST", headers: { "content-type": "application/json", origin: baseUrl }, body: JSON.stringify({ password: "wrong-password" }), }); attempts.push(attempt.status); } assert.deepEqual(attempts, [401, 401, 401, 401, 401, 429]); response = await fetch(`${baseUrl}/`); assert.equal(response.headers.get("x-frame-options"), "DENY"); assert.ok(response.headers.get("content-security-policy")); response = await fetch(`${baseUrl}/does-not-exist`); assert.equal(response.status, 404); assert.match(response.headers.get("x-robots-tag") || "", /noindex/); const notFoundBody = await response.text(); assert.match(notFoundBody, /Seite nicht gefunden/); assert.equal(notFoundBody.includes("Daniel Hesse"), false); } finally { await stopServer(child); await rm(dataDir, { recursive: true, force: true }); } }); });