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 { isPostHidden, isPostPublic, postComparator } from "../src/_utils/sort-posts.js"; import { createServer, normalizeObsidianMarkdown } from "../server.js"; test("Hidden Posts: isPostHidden accurately identifies hidden flags across data shapes", () => { // 1. Explicit boolean true assert.equal(isPostHidden({ data: { hidden: true } }), true, "Explicit boolean true in data"); assert.equal(isPostHidden({ hidden: true }), true, "Root level hidden true"); // 2. Truthy string representations (case-insensitive & trimmed) assert.equal(isPostHidden({ data: { hidden: "true" } }), true, "String 'true'"); assert.equal(isPostHidden({ data: { hidden: "TRUE" } }), true, "Uppercase string 'TRUE'"); assert.equal(isPostHidden({ data: { hidden: " true " } }), true, "Whitespace-padded 'true'"); assert.equal(isPostHidden({ data: { hidden: "yes" } }), true, "String 'yes'"); assert.equal(isPostHidden({ data: { hidden: "YES" } }), true, "String 'YES'"); assert.equal(isPostHidden({ data: { hidden: "1" } }), true, "String '1'"); assert.equal(isPostHidden({ data: { hidden: 1 } }), true, "Numeric 1"); // 3. Falsy representations assert.equal(isPostHidden({ data: { hidden: false } }), false, "Explicit boolean false"); assert.equal(isPostHidden({ data: { hidden: "false" } }), false, "String 'false'"); assert.equal(isPostHidden({ data: { hidden: "no" } }), false, "String 'no'"); assert.equal(isPostHidden({ data: { hidden: "0" } }), false, "String '0'"); assert.equal(isPostHidden({ data: { hidden: 0 } }), false, "Numeric 0"); assert.equal(isPostHidden({ data: { hidden: "" } }), false, "Empty string"); assert.equal(isPostHidden({ data: { hidden: null } }), false, "Null property"); assert.equal(isPostHidden({ data: {} }), false, "Missing hidden property"); assert.equal(isPostHidden({}), false, "Empty post object"); // 4. Adversarial edge cases assert.equal(isPostHidden(null), false, "Null input"); assert.equal(isPostHidden(undefined), false, "Undefined input"); assert.equal(isPostHidden("unexpected-string"), false, "Primitive string input"); assert.equal(isPostHidden(12345), false, "Primitive number input"); assert.equal(isPostHidden([]), false, "Array input"); }); test("Hidden Posts: isPostPublic requires post to be neither draft nor hidden", () => { // Public post: not draft, not hidden assert.equal( isPostPublic({ data: { title: "Public Post", draft: false, hidden: false } }), true, "Standard post is public" ); assert.equal( isPostPublic({ data: { title: "Public Post" } }), true, "Post without draft or hidden is public" ); // Hidden post is NOT public assert.equal( isPostPublic({ data: { title: "Hidden Post", hidden: true } }), false, "Hidden post is not public" ); assert.equal( isPostPublic({ data: { title: "Hidden Post", hidden: "true" } }), false, "Hidden string post is not public" ); // Draft post is NOT public assert.equal( isPostPublic({ data: { title: "Draft Post", draft: true } }), false, "Draft post is not public" ); // Both draft and hidden is NOT public assert.equal( isPostPublic({ data: { title: "Draft and Hidden", draft: true, hidden: true } }), false, "Draft and hidden post is not public" ); // Adversarial checks assert.equal(isPostPublic(null), false, "Null is not public"); assert.equal(isPostPublic(undefined), false, "Undefined is not public"); assert.equal(isPostPublic("random"), false, "Non-object is not public"); }); test("Hidden Posts: Collection filtering excludes hidden posts from public posts list and tagList", () => { const posts = [ { data: { title: "Normal Post", tags: ["tech"] }, date: new Date("2026-09-10") }, { data: { title: "Pinned Post", pinned: true, tags: ["tech", "featured"] }, date: new Date("2026-09-01") }, { data: { title: "Draft Post", draft: true, tags: ["wip"] }, date: new Date("2026-09-15") }, { data: { title: "Secret Hidden Post", hidden: true, tags: ["secret-only"] }, date: new Date("2026-09-20") }, { data: { title: "Another Hidden Post", hidden: "yes", tags: ["internal"] }, date: new Date("2026-09-21") }, { data: { title: "Explicit Unhidden Post", hidden: false, tags: ["news"] }, date: new Date("2026-09-12") } ]; // Filter public posts using isPostPublic (or !draft && !isPostHidden) const publicPosts = posts.filter(isPostPublic).sort(postComparator); const titles = publicPosts.map((p) => p.data.title); assert.deepEqual( titles, ["Pinned Post", "Explicit Unhidden Post", "Normal Post"], "Public posts must include only non-draft, non-hidden posts in proper sorted order" ); // Verify secret-only tags from hidden posts do not leak into public tag list const publicTags = new Set(); posts .filter(isPostPublic) .forEach((item) => { const tags = item.data.tags || []; for (const t of tags) publicTags.add(t); }); assert.ok(!publicTags.has("secret-only"), "Hidden post tag 'secret-only' must not appear in public tag list"); assert.ok(!publicTags.has("internal"), "Hidden post tag 'internal' must not appear in public tag list"); assert.ok(!publicTags.has("wip"), "Draft post tag 'wip' must not appear in public tag list"); assert.ok(publicTags.has("tech"), "Public post tag 'tech' must appear in public tag list"); assert.ok(publicTags.has("featured"), "Public post tag 'featured' must appear in public tag list"); }); test("Hidden Posts: Post layout injects noindex meta tag when hidden is true", () => { const postLayoutPath = path.resolve("src/_includes/layouts/post.njk"); assert.ok(fs.existsSync(postLayoutPath), "post.njk must exist"); const postLayout = fs.readFileSync(postLayoutPath, "utf-8"); // Adversarial verification: hidden posts must instruct search crawlers not to index or follow assert.match( postLayout, /noindex,\s*nofollow/i, "post.njk must define 'noindex, nofollow' robots meta tag when hidden" ); assert.match( postLayout, /{%\s*if\s+hidden\b/i, "post.njk must conditionally check if post is hidden" ); }); test("Hidden Posts: Obsidian normalization preserves hidden frontmatter property", () => { const obsidianNote = `--- title: "Confidential System Blueprint" created: 2026-09-23 tags: ["#architecture", "#secret"] hidden: true --- # Blueprint Details for authorized eyes only. `; const normalized = normalizeObsidianMarkdown(obsidianNote); assert.match(normalized, /hidden:\s*true/, "Normalized markdown must preserve 'hidden: true'"); assert.match(normalized, /title:\s*"Confidential System Blueprint"/, "Preserves title"); assert.match(normalized, /date:\s*2026-09-23/, "Bridges created to date"); assert.match(normalized, /layout:\s*layouts\/post\.njk/, "Adds post layout"); }); test("Hidden Posts: End-to-end static compilation and accessibility via direct link", async () => { const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-")); const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-")); try { // 1. Create a public post and a hidden post const publicPostContent = `--- title: "Public Announcement" date: 2026-09-20 layout: layouts/post.njk tags: ["news"] excerpt: "This is a public announcement for everyone to read." --- Public content visible to everyone.`; const hiddenPostContent = `--- title: "Private Unlisted Memo" date: 2026-09-22 layout: layouts/post.njk tags: ["confidential"] excerpt: "This is an unlisted memo meant for direct link access only." hidden: true --- Secret memo content available by link only.`; fs.writeFileSync(path.join(tmpPosts, "2026-09-20-public-post.md"), publicPostContent, "utf-8"); fs.writeFileSync(path.join(tmpPosts, "2026-09-22-secret-memo.md"), hiddenPostContent, "utf-8"); // 2. Set up server with custom directories 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 { // Simulate static files created by build fs.mkdirSync(path.join(tmpDir, "posts/2026-09-22-secret-memo"), { recursive: true }); fs.mkdirSync(path.join(tmpDir, "posts/2026-09-20-public-post"), { recursive: true }); const secretHtml = `Private Unlisted MemoSecret memo content available by link only.`; const publicHtml = `Public AnnouncementPublic content visible to everyone.`; const indexHtml = `Public Announcement`; fs.writeFileSync(path.join(tmpDir, "posts/2026-09-22-secret-memo/index.html"), secretHtml); fs.writeFileSync(path.join(tmpDir, "posts/2026-09-20-public-post/index.html"), publicHtml); fs.writeFileSync(path.join(tmpDir, "index.html"), indexHtml); // Verify direct access to hidden post succeeds (HTTP 200 OK) const resSecret = await fetch(`http://127.0.0.1:${port}/posts/2026-09-22-secret-memo/`); assert.strictEqual(resSecret.status, 200, "Direct link to hidden post must return 200 OK"); const secretBody = await resSecret.text(); assert.match(secretBody, /Secret memo content available by link only/, "Hidden post body is accessible"); assert.match(secretBody, /noindex,\s*nofollow/, "Hidden post includes noindex meta tag"); // Verify public index does NOT contain link or title to hidden post const resIndex = await fetch(`http://127.0.0.1:${port}/`); assert.strictEqual(resIndex.status, 200); const indexBody = await resIndex.text(); assert.match(indexBody, /Public Announcement/, "Public post is listed on index"); assert.doesNotMatch(indexBody, /Private Unlisted Memo/, "Hidden post MUST NOT appear on index"); assert.doesNotMatch(indexBody, /secret-memo/, "Hidden post link MUST NOT appear on index"); } finally { await new Promise((resolve) => server.close(resolve)); } } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); fs.rmSync(tmpPosts, { recursive: true, force: true }); } }); test("Hidden Posts: Obsidian API ingestion correctly writes hidden frontmatter", 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 { const res = 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: "unlisted-post", content: `--- title: "Direct Share Only" created: 2026-09-23 hidden: true tags: ["#private"] --- Secret note content.` }) }); assert.strictEqual(res.status, 200); const json = await res.json(); assert.strictEqual(json.status, "published"); assert.strictEqual(json.path, "/posts/unlisted-post/"); const writtenFile = path.join(tmpPosts, "unlisted-post.md"); assert.ok(fs.existsSync(writtenFile), "Post file must exist"); const writtenContent = fs.readFileSync(writtenFile, "utf-8"); assert.match(writtenContent, /hidden:\s*true/); assert.match(writtenContent, /layout:\s*layouts\/post\.njk/); } finally { await new Promise((resolve) => server.close(resolve)); fs.rmSync(tmpDir, { recursive: true, force: true }); fs.rmSync(tmpPosts, { recursive: true, force: true }); } });