pcbjam/site/test/waitlist.test.ts
Istvan Matejcsok 74afb7b726 harden the marketing site: gate demo overrides, rate-limit + redact waitlist
- gerber-demo/boot.js honors the base/cdn/tag script-source overrides only on a
  loopback hostname, so a shipped page ignores them.
- /api/waitlist gains a best-effort per-IP/per-address rate limit and masks the
  submitter address in the no-key log line.
Adds vitest to the site: test/boot-overrides.test.ts (happy-dom) and
test/waitlist.test.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 12:46:17 +02:00

53 lines
2.1 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
/**
* Waitlist route hardening: per-key rate limiting, and no raw address in logs on
* the no-key path.
*
* The route imports typed Astro env + the Resend SDK; both are stubbed so the
* handler runs in plain node. RESEND_API_KEY is left undefined so the no-key
* branch (the one that logs) is exercised.
*/
vi.mock("astro:env/server", () => ({
RESEND_API_KEY: undefined,
RESEND_SEGMENT_ID: undefined,
WAITLIST_FROM_EMAIL: "hello@pcbjam.com",
WAITLIST_ALLOWED_ORIGINS: "",
}));
vi.mock("resend", () => ({
Resend: class {
emails = { send: async () => ({}) };
contacts = { create: async () => ({}) };
},
}));
const { POST } = await import("../src/pages/api/waitlist.ts");
function post(email: string, ip: string): Promise<Response> {
const request = new Request("https://www.pcbjam.com/api/waitlist", {
method: "POST",
headers: { "content-type": "application/json", "x-forwarded-for": ip },
body: JSON.stringify({ email }),
});
// Astro passes a full context; the handler only reads `request`.
return POST({ request } as unknown as Parameters<typeof POST>[0]) as Promise<Response>;
}
describe("waitlist route hardening", () => {
it("rate-limits a burst from one IP (429 after the per-IP cap)", async () => {
const ip = "203.0.113.9";
const statuses: number[] = [];
for (let i = 0; i < 7; i++) statuses.push((await post(`u${i}@ex.com`, ip)).status);
expect(statuses.filter((s) => s === 200).length).toBe(5); // RATE_MAX_PER_IP
expect(statuses.filter((s) => s === 429).length).toBeGreaterThan(0);
});
it("does not write the raw email to logs in the no-key path", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
await post("secret.person@example.com", "198.51.100.7");
const logged = warn.mock.calls.map((c) => c.join(" ")).join("\n");
expect(logged).not.toContain("secret.person@example.com"); // raw address masked
expect(logged).toContain("@example.com"); // domain kept for debugging
warn.mockRestore();
});
});