diff --git a/test/obsidian-plugin.test.js b/test/obsidian-plugin.test.js new file mode 100644 index 0000000..ce76b43 --- /dev/null +++ b/test/obsidian-plugin.test.js @@ -0,0 +1,162 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +const pluginPath = path.resolve("/home/venus/Documents/Notes/.obsidian/plugins/blog-publisher/main.js"); + +// Pure helper function to be tested and exported/used by plugin +export function insertOrUpdateFrontmatterProperty(content, key, value) { + if (typeof content !== "string") return ""; + + const fmRegex = /^---\r?\n([\s\S]*?)\r?\n---/; + const match = content.match(fmRegex); + + if (match) { + let fmBlock = match[1]; + const keyRegex = new RegExp(`^${key}:.*$`, "m"); + + if (value === null || value === undefined) { + // Remove property if value is null + fmBlock = fmBlock.replace(new RegExp(`^${key}:.*\\r?\\n?`, "m"), ""); + } else if (keyRegex.test(fmBlock)) { + // Replace existing property + fmBlock = fmBlock.replace(keyRegex, `${key}: "${value}"`); + } else { + // Append property to frontmatter + fmBlock = `${fmBlock}\n${key}: "${value}"`; + } + + return content.replace(fmRegex, `---\n${fmBlock.trim()}\n---`); + } else if (value) { + return `---\n${key}: "${value}"\n---\n\n${content}`; + } + + return content; +} + +export function computePublicUrl(serverUrl, postPath) { + const base = (serverUrl || "").replace(/\/+$/, ""); + const p = (postPath || "").startsWith("/") ? postPath : `/${postPath || ""}`; + return `${base}${p}`; +} + +test("Obsidian Plugin: computePublicUrl normalizes slashes correctly", () => { + assert.equal( + computePublicUrl("https://riverrooks.dev", "/posts/test-note/"), + "https://riverrooks.dev/posts/test-note/" + ); + assert.equal( + computePublicUrl("https://riverrooks.dev/", "/posts/test-note/"), + "https://riverrooks.dev/posts/test-note/" + ); + assert.equal( + computePublicUrl("https://riverrooks.dev///", "posts/test-note/"), + "https://riverrooks.dev/posts/test-note/" + ); + assert.equal( + computePublicUrl("http://localhost:8080", "/posts/test-note/"), + "http://localhost:8080/posts/test-note/" + ); +}); + +test("Obsidian Plugin: insertOrUpdateFrontmatterProperty adds public-url when missing", () => { + const original = `--- +title: "Sample Note" +date: 2026-09-23 +publish: true +--- + +# Content here`; + + const updated = insertOrUpdateFrontmatterProperty( + original, + "public-url", + "https://riverrooks.dev/posts/sample-note/" + ); + + assert.match( + updated, + /public-url:\s*"https:\/\/riverrooks\.dev\/posts\/sample-note\/"/, + "Must insert public-url into frontmatter" + ); + assert.match(updated, /title:\s*"Sample Note"/, "Preserves title"); + assert.match(updated, /publish:\s*true/, "Preserves publish flag"); + assert.match(updated, /# Content here/, "Preserves body content"); +}); + +test("Obsidian Plugin: insertOrUpdateFrontmatterProperty updates existing public-url", () => { + const original = `--- +title: "Updated Note" +date: 2026-09-23 +public-url: "https://old.domain/posts/sample-note/" +--- + +Body`; + + const updated = insertOrUpdateFrontmatterProperty( + original, + "public-url", + "https://riverrooks.dev/posts/updated-note/" + ); + + assert.match( + updated, + /public-url:\s*"https:\/\/riverrooks\.dev\/posts\/updated-note\/"/, + "Must update public-url to new URL" + ); + assert.doesNotMatch( + updated, + /old\.domain/, + "Old URL must be replaced" + ); +}); + +test("Obsidian Plugin: insertOrUpdateFrontmatterProperty removes public-url on unpublish", () => { + const original = `--- +title: "Unpublished Note" +date: 2026-09-23 +public-url: "https://riverrooks.dev/posts/sample-note/" +--- + +Body`; + + const updated = insertOrUpdateFrontmatterProperty(original, "public-url", null); + + assert.doesNotMatch(updated, /public-url:/, "Must remove public-url when value is null"); + assert.match(updated, /title:\s*"Unpublished Note"/, "Preserves other frontmatter"); +}); + +test("Obsidian Plugin: Adversarial edge cases for frontmatter property insertion", () => { + // 1. Empty content + assert.equal(insertOrUpdateFrontmatterProperty("", "public-url", "https://url"), "---\npublic-url: \"https://url\"\n---\n\n"); + // 2. Non-string inputs + assert.equal(insertOrUpdateFrontmatterProperty(null, "public-url", "https://url"), ""); + assert.equal(insertOrUpdateFrontmatterProperty(undefined, "public-url", "https://url"), ""); + // 3. Note without frontmatter + const plain = "# Just a header\nSome text."; + const result = insertOrUpdateFrontmatterProperty(plain, "public-url", "https://url"); + assert.match(result, /^---\npublic-url:\s*"https:\/\/url"\n---/); +}); + +test("Obsidian Plugin: main.js in vault includes public-url insertion and recursion guard", () => { + assert.ok(fs.existsSync(pluginPath), `Plugin main.js must exist at ${pluginPath}`); + const pluginContent = fs.readFileSync(pluginPath, "utf-8"); + + // 1. Must handle public-url + assert.match(pluginContent, /public-url/i, "main.js must reference 'public-url'"); + + // 2. Must guard against infinite loops during frontmatter updates + assert.match( + pluginContent, + /isUpdatingFrontmatter/i, + "main.js must maintain a guard to prevent metadataCache recursion loops" + ); + + // 3. Must use processFrontMatter or frontmatter updater + assert.match( + pluginContent, + /processFrontMatter|updateFrontmatter/i, + "main.js must implement frontmatter updating logic" + ); +});