[Story] Newsletter signup (serverless) #15

Merged
kpcto merged 5 commits from feature/14 into main 2026-08-26 11:44:11 +00:00
Showing only changes of commit 49690a7934 - Show all commits
+278
View File
@@ -0,0 +1,278 @@
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, /<form[^>]*id="newsletter-form"/);
assert.match(
newsletterHtml,
/<input[^>]*type="email"[^>]*id="newsletter-email"[^>]*name="email"/
);
assert.match(newsletterHtml, /<button[^>]*type="submit"[^>]*>Subscribe<\/button>/);
});
test("newsletter email field is required and the page loads the wiring module", () => {
assert.match(newsletterHtml, /<input[^>]*id="newsletter-email"[^>]*required/);
assert.match(
newsletterHtml,
/<script[^>]*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, /<a href="newsletter\.html"[^>]*>Newsletter<\/a>/);
}
assert.match(
newsletterHtml,
/<a href="newsletter\.html"[^>]*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);
});