import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import http from "node:http"; import { createServer, triggerEleventyBuild } from "../server.js"; const TEST_SECRET = "super-secret-test-token"; test("Ingestion API: rejects requests without proper Bearer token", async () => { const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-")); const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-")); process.env.API_SECRET = TEST_SECRET; const server = createServer(tmpDir, tmpPosts); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const port = server.address().port; try { // 1. Missing Authorization header const resNoAuth = await fetch(`http://127.0.0.1:${port}/api/posts`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ slug: "test", content: "test" }) }); assert.strictEqual(resNoAuth.status, 401, "Missing auth header must return 401"); // 2. Invalid Bearer token const resBadAuth = await fetch(`http://127.0.0.1:${port}/api/posts`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer wrong-token" }, body: JSON.stringify({ slug: "test", content: "test" }) }); assert.strictEqual(resBadAuth.status, 401, "Invalid token must return 401"); } finally { await new Promise((resolve) => server.close(resolve)); fs.rmSync(tmpDir, { recursive: true, force: true }); fs.rmSync(tmpPosts, { recursive: true, force: true }); } }); test("Ingestion API: validates required fields and frontmatter schema", async () => { const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-")); const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-")); process.env.API_SECRET = TEST_SECRET; const server = createServer(tmpDir, tmpPosts); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const port = server.address().port; try { // Missing slug const resNoSlug = await fetch(`http://127.0.0.1:${port}/api/posts`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TEST_SECRET}` }, body: JSON.stringify({ content: "---\ntitle: 'Test'\ndate: 2026-09-14\n---\nHello" }) }); assert.strictEqual(resNoSlug.status, 400, "Missing slug must return 400"); // Missing required frontmatter title const resNoTitle = await fetch(`http://127.0.0.1:${port}/api/posts`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TEST_SECRET}` }, body: JSON.stringify({ slug: "no-title-post", content: "---\ndate: 2026-09-14\n---\nHello" }) }); assert.strictEqual(resNoTitle.status, 400, "Missing frontmatter title must return 400"); // Missing required frontmatter date or created const resNoDate = await fetch(`http://127.0.0.1:${port}/api/posts`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TEST_SECRET}` }, body: JSON.stringify({ slug: "no-date-post", content: "---\ntitle: 'Valid Title'\n---\nHello" }) }); assert.strictEqual(resNoDate.status, 400, "Missing frontmatter created/date must return 400"); // Valid with created instead of date const resWithCreated = await fetch(`http://127.0.0.1:${port}/api/posts`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TEST_SECRET}` }, body: JSON.stringify({ slug: "created-tag-post", content: "---\ntitle: 'Valid Title'\ncreated: 2026-09-14\n---\nHello" }) }); assert.strictEqual(resWithCreated.status, 200, "Valid post with created tag must return 200"); } finally { await new Promise((resolve) => server.close(resolve)); fs.rmSync(tmpDir, { recursive: true, force: true }); fs.rmSync(tmpPosts, { recursive: true, force: true }); } }); test("Ingestion API: publishes markdown, normalizes wikilinks, saves assets, and handles delete", async () => { const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-")); const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-")); const tmpAssets = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-assets-")); process.env.API_SECRET = TEST_SECRET; const server = createServer(tmpDir, tmpPosts, tmpAssets); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const port = server.address().port; const rawMarkdown = `--- title: "Reverse Engineering Flock Safety" created: 2026-09-14 tags: ["#security", "#firmware"] excerpt: "A deep dive into firmware." --- Check out [[other-post|Other Post]] and also [[simple-link]]. Here is an image: ![[camera-board.png]] `; // 1x1 transparent PNG base64 const sampleBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; try { // Publish post const resPublish = await fetch(`http://127.0.0.1:${port}/api/posts`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TEST_SECRET}` }, body: JSON.stringify({ slug: "flock-firmware", content: rawMarkdown, assets: [ { filename: "camera-board.png", data: sampleBase64 } ] }) }); assert.strictEqual(resPublish.status, 200, "Valid publish must return 200"); const json = await resPublish.json(); assert.strictEqual(json.status, "published"); assert.strictEqual(json.path, "/posts/flock-firmware/"); // Verify markdown file written and wikilinks normalized const writtenPath = path.join(tmpPosts, "flock-firmware.md"); assert.ok(fs.existsSync(writtenPath), "Post markdown file must be written to disk"); const writtenContent = fs.readFileSync(writtenPath, "utf-8"); assert.match(writtenContent, /^layout:\s*layouts\/post\.njk$/m, "Layout must be injected into frontmatter when missing"); assert.match(writtenContent, /\[Other Post\]\(\/posts\/other-post\/\)/, "Wikilink with alias must be translated"); assert.match(writtenContent, /\[simple-link\]\(\/posts\/simple-link\/\)/, "Simple wikilink must be translated"); assert.match(writtenContent, /!\[camera-board\.png\]\(\/assets\/images\/posts\/camera-board\.png\)/, "Attachment wikilink must be translated"); // Verify asset image was written const assetPath = path.join(tmpAssets, "camera-board.png"); assert.ok(fs.existsSync(assetPath), "Asset file must be saved"); // Create simulated compiled static post files in siteDir const compiledPostDir = path.join(tmpDir, "posts", "flock-firmware"); fs.mkdirSync(compiledPostDir, { recursive: true }); const compiledPostHtml = path.join(compiledPostDir, "index.html"); fs.writeFileSync(compiledPostHtml, "Simulated compiled post"); const standalonePostHtml = path.join(tmpDir, "posts", "flock-firmware.html"); fs.writeFileSync(standalonePostHtml, "Simulated standalone post"); assert.ok(fs.existsSync(compiledPostHtml), "Simulated compiled post must exist before delete"); assert.ok(fs.existsSync(standalonePostHtml), "Simulated standalone post must exist before delete"); // Delete post const resDelete = await fetch(`http://127.0.0.1:${port}/api/posts?slug=flock-firmware`, { method: "DELETE", headers: { Authorization: `Bearer ${TEST_SECRET}` } }); assert.strictEqual(resDelete.status, 200, "Delete must return 200"); assert.ok(!fs.existsSync(writtenPath), "Post markdown file must be deleted"); assert.ok(!fs.existsSync(compiledPostDir), "Compiled post directory must be deleted from _site"); assert.ok(!fs.existsSync(standalonePostHtml), "Compiled standalone HTML must be deleted from _site"); } finally { await new Promise((resolve) => server.close(resolve)); fs.rmSync(tmpDir, { recursive: true, force: true }); fs.rmSync(tmpPosts, { recursive: true, force: true }); fs.rmSync(tmpAssets, { recursive: true, force: true }); } }); test("Ingestion API: adversarial protection against path traversal in slug and DELETE validation", async () => { const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-")); const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-")); process.env.API_SECRET = TEST_SECRET; const server = createServer(tmpDir, tmpPosts); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const port = server.address().port; try { // 1. Path traversal in POST const resTraversal = await fetch(`http://127.0.0.1:${port}/api/posts`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TEST_SECRET}` }, body: JSON.stringify({ slug: "../../etc/evil", content: "---\ntitle: 'Evil'\ndate: 2026-09-14\n---\nEvil content" }) }); // Should sanitize or reject slug containing path traversal if (resTraversal.status === 200) { const evilPath = path.resolve(tmpPosts, "../../etc/evil.md"); assert.ok(!fs.existsSync(evilPath), "Must never write outside posts directory"); } else { assert.strictEqual(resTraversal.status, 400, "Invalid slug must return 400"); } // 2. DELETE without auth token const resDeleteNoAuth = await fetch(`http://127.0.0.1:${port}/api/posts?slug=any-post`, { method: "DELETE" }); assert.strictEqual(resDeleteNoAuth.status, 401, "DELETE without Bearer token must return 401"); // 3. DELETE with missing slug parameter const resDeleteNoSlug = await fetch(`http://127.0.0.1:${port}/api/posts`, { method: "DELETE", headers: { Authorization: `Bearer ${TEST_SECRET}` } }); assert.strictEqual(resDeleteNoSlug.status, 400, "DELETE without slug parameter must return 400"); // 4. DELETE with invalid/empty slug const resDeleteBadSlug = await fetch(`http://127.0.0.1:${port}/api/posts?slug=///`, { method: "DELETE", headers: { Authorization: `Bearer ${TEST_SECRET}` } }); assert.strictEqual(resDeleteBadSlug.status, 400, "DELETE with invalid slug must return 400"); // 5. DELETE non-existent post succeeds gracefully (idempotent) const resDeleteNonExistent = await fetch(`http://127.0.0.1:${port}/api/posts?slug=non-existent-post`, { method: "DELETE", headers: { Authorization: `Bearer ${TEST_SECRET}` } }); assert.strictEqual(resDeleteNonExistent.status, 200, "DELETE non-existent post must return 200 idempotently"); const jsonNonExistent = await resDeleteNonExistent.json(); assert.strictEqual(jsonNonExistent.status, "deleted"); assert.strictEqual(jsonNonExistent.slug, "non-existent-post"); } finally { await new Promise((resolve) => server.close(resolve)); fs.rmSync(tmpDir, { recursive: true, force: true }); fs.rmSync(tmpPosts, { recursive: true, force: true }); } }); test("Ingestion API: triggerEleventyBuild builds site and handles concurrency", async () => { assert.strictEqual(typeof triggerEleventyBuild, "function"); // Trigger two concurrent calls to assert queuing does not crash or throw const [b1, b2] = await Promise.all([triggerEleventyBuild(), triggerEleventyBuild()]); assert.strictEqual(b1, undefined); assert.strictEqual(b2, undefined); });