import { describe, expect, it, vi } from "vitest"; import type { Note } from "../src/lib/content.ts"; import { checkOrder, clientIp, DRAFT_LIMIT, encodeDraft, formValues, inquiryFields, inquirySent, isBot, orderFields, orderSent, parseDraft, parseOrderSent, RateLimiter, saveAndSend, validateInquiry, validateOrderFields, } from "../src/lib/forms.ts"; import { SiteIndex } from "../src/lib/site.ts"; import { storeState } from "../src/lib/store.ts"; import { parseSentSummary } from "../src/lib/work.ts"; const MB = 1024 * 1024; const form = (fields: Record) => { const f = new FormData(); for (const [k, v] of Object.entries(fields)) f.append(k, v); return f; }; const base = { name: "Jane Doe", email: "jane@example.com", details: "A senior role on the platform team." }; describe("inquiry validation", () => { it("takes a job inquiry and only the job questions", () => { const r = validateInquiry( form({ ...base, type: "job", job_company: "Acme", job_kind: "Contract", job_link: "https://acme.example/jobs/1", patiun_quantity: "60", freelance_need: "An app" }), { maxBytes: MB }, ); expect(r.ok).toBe(true); if (!r.ok) return; expect(r.value.rows).toEqual([ { label: "Company", value: "Acme" }, { label: "Kind of role", value: "Contract" }, { label: "Posting", value: "https://acme.example/jobs/1" }, ]); expect(inquiryFields(r.value).at(-1)).toEqual({ label: "Details", value: base.details }); }); it("marks what is wrong", () => { const r = validateInquiry(form({ type: "nope", name: "", email: "x@", details: "hi", job_link: "javascript:alert(1)" }), { maxBytes: MB }); expect(r.ok).toBe(false); if (r.ok) return; expect(Object.keys(r.errors).sort()).toEqual(["details", "email", "name", "type"]); }); it("checks choices, links and dates for the chosen type", () => { const bad = validateInquiry( form({ ...base, type: "freelance", freelance_need: "World peace", freelance_site: "ftp://x", freelance_timing: "date", freelance_date: "soon" }), { maxBytes: MB }, ); expect(bad.ok).toBe(false); if (!bad.ok) expect(Object.keys(bad.errors).sort()).toEqual(["freelance_date", "freelance_need", "freelance_site"]); const good = validateInquiry(form({ ...base, type: "patiun", patiun_item: "Deck box", patiun_timing: "date", patiun_date: "2026-12-01" }), { maxBytes: MB }); expect(good.ok && good.value.rows).toEqual([ { label: "Item", value: "Deck box" }, { label: "Need it by", value: "Dec 1, 2026" }, ]); }); it("limits attachments by type and size", () => { const ok = validateInquiry(form({ ...base, type: "patiun", attachment: new File(["1 Sol Ring"], "deck.dek") }), { maxBytes: MB }); expect(ok.ok && ok.value.attachment?.name).toBe("deck.dek"); const exe = validateInquiry(form({ ...base, type: "patiun", attachment: new File(["MZ"], "x.exe") }), { maxBytes: MB }); expect(!exe.ok && exe.errors.attachment).toBeTruthy(); const big = validateInquiry(form({ ...base, type: "patiun", attachment: new File([new Uint8Array(MB + 1)], "big.png") }), { maxBytes: MB }); expect(!big.ok && big.errors.attachment).toContain("1 MB"); const empty = validateInquiry(form({ ...base, type: "patiun", attachment: new File([], "") }), { maxBytes: MB }); expect(empty.ok && empty.value.attachment).toBeNull(); }); it("builds the sent cookie the sent page reads", () => { const r = validateInquiry(form({ ...base, type: "patiun", patiun_quantity: "60 cards" }), { maxBytes: MB }); if (!r.ok) throw new Error("should pass"); const parsed = parseSentSummary(encodeURIComponent(JSON.stringify(inquirySent(r.value)))); expect(parsed).toEqual({ type: "patiun", name: "Jane Doe", email: "jane@example.com", rows: [{ label: "How many", value: "60 cards" }] }); }); it("spots the honeypot", () => { expect(isBot(form({ leave_blank: "http://spam" }))).toBe(true); expect(isBot(form({ leave_blank: "" }))).toBe(false); }); }); // Sample store notes, written for these tests. const settings = (open: Record) => ({ path: "Store/_settings.md", type: "store-settings", name: "_settings", slug: "settings", title: "Store settings", body: "", acceptedAt: "2026-10-01", data: { type: "store-settings", title: "Store settings", categories: Object.entries(open).map(([name, ordersOpen]) => ({ name, ordersOpen })) }, }) as unknown as Note; const line = (slug: string, data: Record) => ({ path: `Store/${slug}.md`, type: "product-line", name: slug, slug, title: slug.replace(/-/g, " "), body: "", acceptedAt: "2026-10-01", data: { type: "product-line", title: slug, category: "Tabletop", channel: "here", mode: "made to order", summary: "", priceTiers: [], ...data }, }) as unknown as Note; const store = (open: Record, lines: Note[]) => storeState(new SiteIndex([settings(open), ...lines])); const cards = line("proxy-cards", { slots: { open: 2, total: 5 }, options: [ { group: "Finish", name: "Matte", inStock: true }, { group: "Finish", name: "Foil", inStock: false }, ], }); const orderForm = (extra: Record = {}) => form({ name: "Bo", email: "bo@example.com", line: "proxy-cards", quantity: "75 cards", terms: "accepted", ...extra }); const fieldsOf = (f: FormData) => { const r = validateOrderFields(f); if (!r.ok) throw new Error(JSON.stringify(r.errors)); return r.value; }; describe("order validation", () => { it("needs the terms, a line, a quantity and contact details", () => { const r = validateOrderFields(form({ line: "../x" })); expect(!r.ok && Object.keys(r.errors).sort()).toEqual(["email", "line", "name", "quantity", "terms"]); }); it("takes an order for an open line with room and an in-stock option", () => { const f = orderForm({ "option.proxy-cards.Finish": "Matte", "option.other-line.Color": "Red" }); const r = checkOrder(fieldsOf(f), f, store({ Tabletop: true }, [cards])); expect(r.ok).toBe(true); if (!r.ok) return; expect(r.value.options).toEqual([{ label: "Finish", value: "Matte" }]); expect(orderFields(r.value).map((x) => x.label)).toEqual(["Line", "Finish", "Quantity", "Terms"]); expect(parseOrderSent(encodeURIComponent(JSON.stringify(orderSent(r.value))))?.rows).toEqual([ { label: "Finish", value: "Matte" }, { label: "Quantity", value: "75 cards" }, ]); }); it("turns down a closed category, whatever the post says", () => { const f = orderForm(); expect(checkOrder(fieldsOf(f), f, store({ Tabletop: false }, [cards]))).toMatchObject({ ok: false, code: "closed" }); // No settings note at all counts as closed. expect(checkOrder(fieldsOf(f), f, storeState(new SiteIndex([cards])))).toMatchObject({ ok: false, code: "closed" }); }); it("turns down a line with no open slot or no stock", () => { const f = orderForm(); const full = line("proxy-cards", { slots: { open: 0, total: 5 } }); expect(checkOrder(fieldsOf(f), f, store({ Tabletop: true }, [full]))).toMatchObject({ ok: false, code: "full" }); const sold = line("proxy-cards", { mode: "in stock", quantity: 0 }); expect(checkOrder(fieldsOf(f), f, store({ Tabletop: true }, [sold]))).toMatchObject({ ok: false, code: "full" }); const stocked = line("proxy-cards", { mode: "in stock", quantity: 3 }); expect(checkOrder(fieldsOf(f), f, store({ Tabletop: true }, [stocked])).ok).toBe(true); }); it("turns down lines sold elsewhere or unknown, and out-of-stock options", () => { const f = orderForm(); const etsy = line("proxy-cards", { channel: "Patiun.com", slots: { open: 2, total: 5 } }); expect(checkOrder(fieldsOf(f), f, store({ Tabletop: true }, [etsy]))).toMatchObject({ ok: false, code: "invalid" }); expect(checkOrder(fieldsOf(f), f, store({ Tabletop: true }, []))).toMatchObject({ ok: false, code: "invalid" }); const foil = orderForm({ "option.proxy-cards.Finish": "Foil" }); expect(checkOrder(fieldsOf(foil), foil, store({ Tabletop: true }, [cards]))).toMatchObject({ ok: false, code: "invalid" }); const made = orderForm({ "option.proxy-cards.Finish": "Gold leaf" }); expect(checkOrder(fieldsOf(made), made, store({ Tabletop: true }, [cards]))).toMatchObject({ ok: false, code: "invalid" }); }); }); describe("try-again drafts", () => { it("round-trips and fits in a cookie", () => { const values = formValues(form({ name: "Jane", details: "x".repeat(20_000), leave_blank: "", "cf-turnstile-response": "tok", terms: "accepted" })); expect(Object.keys(values)).toEqual(["name", "details"]); const raw = encodeDraft({ code: "invalid", errors: { email: "Add an email" }, values }); expect(encodeURIComponent(raw).length).toBeLessThanOrEqual(DRAFT_LIMIT); const d = parseDraft(encodeURIComponent(raw)); expect(d?.code).toBe("invalid"); expect(d?.values.name).toBe("Jane"); expect(d?.errors.email).toBe("Add an email"); expect(parseDraft("garbage")).toBeUndefined(); expect(parseDraft(JSON.stringify({ code: "nope" }))).toBeUndefined(); }); }); describe("saving and emailing", () => { const quiet = { log: () => {} }; it("records a failed email and still succeeds when the copy was saved", async () => { const report = vi.fn(async () => {}); const r = await saveAndSend({ ...quiet, save: async () => ({ id: 7, receipt: "rcpt" }), email: async () => { throw new Error("ECONNREFUSED"); }, report, }); expect(r).toEqual({ ok: true, savedId: 7, email: "failed" }); expect(report).toHaveBeenCalledWith(7, "rcpt", "failed", "ECONNREFUSED"); }); it("still emails when saving fails, and fails only when both do", async () => { const email = vi.fn(async (id: number | null) => (id === null ? ("sent" as const) : ("skipped" as const))); const report = vi.fn(async () => {}); const failSave = async (): Promise<{ id: number; receipt: string }> => { throw new Error("down"); }; expect(await saveAndSend({ ...quiet, save: failSave, email, report })).toEqual({ ok: true, savedId: null, email: "sent" }); expect(report).not.toHaveBeenCalled(); const r = await saveAndSend({ ...quiet, save: failSave, email: async () => { throw new Error("smtp down"); }, report, }); expect(r.ok).toBe(false); // Nothing configured for mail and nothing saved: not a success either. expect((await saveAndSend({ ...quiet, save: failSave, email: async () => "skipped", report })).ok).toBe(false); }); it("counts a skipped email as fine when the copy was saved", async () => { const report = vi.fn(async () => {}); const r = await saveAndSend({ ...quiet, save: async () => ({ id: 1, receipt: "r" }), email: async () => "skipped", report }); expect(r.ok).toBe(true); expect(report).toHaveBeenCalledWith(1, "r", "skipped", undefined); }); }); describe("rate limit and address", () => { it("allows N posts per window per address", () => { let t = 0; const rl = new RateLimiter(() => 2, () => 1000, () => t); expect([rl.hit("a"), rl.hit("a"), rl.hit("a"), rl.hit("b")]).toEqual([false, false, true, false]); t = 1001; expect(rl.hit("a")).toBe(false); }); it("reads the forwarded address first", () => { expect(clientIp(new Headers({ "x-forwarded-for": "203.0.113.9, 10.0.0.1" }), "10.0.0.2")).toBe("203.0.113.9"); expect(clientIp(new Headers(), "10.0.0.2")).toBe("10.0.0.2"); expect(clientIp(new Headers({ "cf-connecting-ip": "198.51.100.7", "x-forwarded-for": "1.2.3.4, 10.0.0.1" }))).toBe("198.51.100.7"); }); });