import test from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import { NEWSLETTER_ERROR_MESSAGE, NEWSLETTER_SUCCESS_MESSAGE, handleNewsletterSubmit, isValidEmail, readFormEmail, submitNewsletterSignup, } from "../js/newsletter.js"; import { NEWSLETTER_API_TOKEN, NEWSLETTER_ENDPOINT, } from "../js/newsletter-config.js"; const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const newsletterHtml = readFileSync(join(root, "newsletter.html"), "utf8"); const indexHtml = readFileSync(join(root, "index.html"), "utf8"); const readingHtml = readFileSync(join(root, "reading.html"), "utf8"); const contactHtml = readFileSync(join(root, "contact.html"), "utf8"); const newsletterJs = readFileSync(join(root, "js/newsletter.js"), "utf8"); const configJs = readFileSync(join(root, "js/newsletter-config.js"), "utf8"); /** Minimal stand-in for a DOM form (has querySelectorAll("[name]")). */ function fakeForm(email) { return { querySelectorAll(selector) { assert.equal(selector, "[name]"); return [{ name: "email", value: email }]; }, }; } /** Record calls and respond — pass an object or a (url, init) => response fn. */ function fakeFetch(respond) { const calls = []; const impl = async (url, init) => { calls.push({ url, init }); return typeof respond === "function" ? respond(url, init) : respond; }; impl.calls = calls; return impl; } // --------------------------------------------------------------------------- // Component tests: the signup form renders on the blog // --------------------------------------------------------------------------- test("newsletter page renders an email field and a submit button", () => { assert.match(newsletterHtml, /]*id="newsletter-form"/); assert.match( newsletterHtml, /]*type="email"[^>]*id="newsletter-email"[^>]*name="email"/ ); assert.match(newsletterHtml, /]*type="submit"[^>]*>Subscribe<\/button>/); }); test("newsletter email field is required and the page loads the wiring module", () => { assert.match(newsletterHtml, /]*id="newsletter-email"[^>]*required/); assert.match( newsletterHtml, /]*type="module"[^>]*src="js\/newsletter\.js"/ ); }); test("newsletter page has a live status region for the result message", () => { assert.match(newsletterHtml, /id="newsletter-status"/); assert.match(newsletterHtml, /role="status"/); assert.match(newsletterHtml, /aria-live="polite"/); }); test("every site page links to the newsletter page, and it marks itself current", () => { for (const html of [indexHtml, readingHtml, contactHtml, newsletterHtml]) { assert.match(html, /]*>Newsletter<\/a>/); } assert.match( newsletterHtml, /]*aria-current="page"[^>]*>Newsletter<\/a>/ ); }); // --------------------------------------------------------------------------- // Config & secrets: token comes from config, never hardcoded in source // --------------------------------------------------------------------------- test("endpoint and token are imported from the config module, never defined in the page logic", () => { assert.match( newsletterJs, /import\s*\{[\s\S]*NEWSLETTER_ENDPOINT[\s\S]*\}\s*from\s*"\.\/newsletter-config\.js"/ ); assert.match( newsletterJs, /import\s*\{[\s\S]*NEWSLETTER_API_TOKEN[\s\S]*\}\s*from\s*"\.\/newsletter-config\.js"/ ); // The page logic must not assign a token literal itself. assert.doesNotMatch(newsletterJs, /NEWSLETTER_API_TOKEN\s*=\s*["'][^"']+["']/); }); test("the committed config carries an empty placeholder token, so no secret is committed", () => { assert.equal(NEWSLETTER_API_TOKEN, ""); assert.equal(typeof NEWSLETTER_ENDPOINT, "string"); assert.ok(NEWSLETTER_ENDPOINT.startsWith("https://")); }); test("no shipped page or script embeds a secret-shaped token literal", () => { const secretShape = /["'][A-Za-z0-9+/_-]{32,}["']/; for (const source of [newsletterJs, configJs, newsletterHtml]) { assert.doesNotMatch(source, secretShape); } }); // --------------------------------------------------------------------------- // Unit tests: submitting POSTs to the configured serverless endpoint // --------------------------------------------------------------------------- test("submitNewsletterSignup POSTs the email to the endpoint with the token in the Authorization header", async () => { const fetchImpl = fakeFetch({ ok: true }); const result = await submitNewsletterSignup("ada@example.com", { endpoint: "https://api.example.com/newsletter", token: "test-token-123", fetchImpl, }); assert.equal(result.ok, true); assert.equal(fetchImpl.calls.length, 1); const { url, init } = fetchImpl.calls[0]; assert.equal(url, "https://api.example.com/newsletter"); assert.equal(init.method, "POST"); assert.equal(init.headers["Content-Type"], "application/json"); assert.equal(init.headers["Authorization"], "Bearer test-token-123"); assert.deepEqual(JSON.parse(init.body), { email: "ada@example.com" }); }); test("submitNewsletterSignup resolves success with a confirmation message on a 2xx response", async () => { const result = await submitNewsletterSignup("ada@example.com", { endpoint: "https://api.example.com/newsletter", token: "test-token-123", fetchImpl: fakeFetch({ ok: true, status: 200 }), }); assert.deepEqual(result, { ok: true, message: NEWSLETTER_SUCCESS_MESSAGE }); }); // --------------------------------------------------------------------------- // Unit tests: missing or invalid token → user-safe error, no secret leak // --------------------------------------------------------------------------- test("a missing token fails with a user-safe error and never calls the network", async () => { const fetchImpl = fakeFetch({ ok: true }); const result = await submitNewsletterSignup("ada@example.com", { endpoint: "https://api.example.com/newsletter", token: "", fetchImpl, }); assert.equal(result.ok, false); assert.equal(result.message, NEWSLETTER_ERROR_MESSAGE); assert.equal(fetchImpl.calls.length, 0); // The message reveals nothing about the failure cause or configuration. assert.ok(!result.message.includes("token")); assert.ok(!result.message.includes("Bearer")); assert.ok(!result.message.includes("secret")); assert.ok(!result.message.includes("api.example.com")); }); test("an invalid token (401 from the endpoint) shows a user-safe error without leaking the secret", async () => { const token = "sekrit-token-abc123"; const fetchImpl = fakeFetch({ ok: false, status: 401 }); const result = await submitNewsletterSignup("ada@example.com", { endpoint: "https://api.example.com/newsletter", token, fetchImpl, }); assert.equal(result.ok, false); assert.equal(result.message, NEWSLETTER_ERROR_MESSAGE); assert.ok(!result.message.includes(token)); assert.ok(!result.message.includes("401")); assert.ok(!result.message.includes("api.example.com")); }); test("a network failure shows the same user-safe error", async () => { const fetchImpl = fakeFetch(() => { throw new Error("network down"); }); const result = await submitNewsletterSignup("ada@example.com", { endpoint: "https://api.example.com/newsletter", token: "test-token-123", fetchImpl, }); assert.equal(result.ok, false); assert.equal(result.message, NEWSLETTER_ERROR_MESSAGE); }); // --------------------------------------------------------------------------- // Unit tests: form handling wires result → status region // --------------------------------------------------------------------------- test("handleNewsletterSubmit shows the confirmation message after a successful signup", async () => { const fetchImpl = fakeFetch({ ok: true }); const statuses = []; const result = await handleNewsletterSubmit(fakeForm("ada@example.com"), { endpoint: "https://api.example.com/newsletter", token: "test-token-123", fetchImpl, setStatus: (message, kind) => statuses.push({ message, kind }), }); assert.equal(result.ok, true); assert.equal(result.message, NEWSLETTER_SUCCESS_MESSAGE); assert.deepEqual(statuses, [ { message: NEWSLETTER_SUCCESS_MESSAGE, kind: "success" }, ]); assert.equal(fetchImpl.calls.length, 1); }); test("handleNewsletterSubmit rejects an invalid email without any network call", async () => { const fetchImpl = fakeFetch({ ok: true }); const statuses = []; const result = await handleNewsletterSubmit(fakeForm("not-an-email"), { token: "test-token-123", fetchImpl, setStatus: (message, kind) => statuses.push({ message, kind }), }); assert.equal(result.ok, false); assert.equal(result.message, NEWSLETTER_ERROR_MESSAGE); assert.equal(fetchImpl.calls.length, 0); assert.deepEqual(statuses, [ { message: NEWSLETTER_ERROR_MESSAGE, kind: "error" }, ]); }); test("handleNewsletterSubmit surfaces a user-safe error when the config token is missing", async () => { const fetchImpl = fakeFetch({ ok: true }); const statuses = []; const result = await handleNewsletterSubmit(fakeForm("ada@example.com"), { endpoint: "https://api.example.com/newsletter", token: "", fetchImpl, setStatus: (message, kind) => statuses.push({ message, kind }), }); assert.equal(result.ok, false); assert.equal(result.message, NEWSLETTER_ERROR_MESSAGE); assert.equal(fetchImpl.calls.length, 0); assert.deepEqual(statuses, [ { message: NEWSLETTER_ERROR_MESSAGE, kind: "error" }, ]); }); // --------------------------------------------------------------------------- // Unit tests: helpers // --------------------------------------------------------------------------- test("readFormEmail returns the trimmed email value", () => { assert.equal(readFormEmail(fakeForm(" ada@example.com ")), "ada@example.com"); assert.equal(readFormEmail(fakeForm("")), ""); }); test("isValidEmail accepts well-formed addresses and rejects malformed ones", () => { assert.equal(isValidEmail("ada@example.com"), true); assert.equal(isValidEmail("a.b+c@sub.example.co.uk"), true); assert.equal(isValidEmail("not-an-email"), false); assert.equal(isValidEmail("ada@"), false); assert.equal(isValidEmail("@example.com"), false); assert.equal(isValidEmail(""), false); });