import { Database } from "bun:sqlite"; import { beforeEach, describe, expect, mock, test } from "bun:test"; import { readFileSync } from "node:fs"; mock.module("cloudflare:workers", () => ({ env: { PUBLIC_URL: "https://forge.example.com" } })); const sent: Array<{ to: string; subject: string; url: string }> = []; mock.module("./email", () => ({ emailTemplates: { waitlistInvite: (url: string, _holdFor: string) => ({ subject: "You're off the waitlist", text: url, html: url, url, }), }, queueEmail: async (to: string, content: { subject: string; url: string }) => { sent.push({ to, subject: content.subject, url: content.url }); }, })); mock.module("./events/emit", () => ({ publicUrl: (path: string) => `https://forge.example.com${path}` })); let db = new Database(":memory:"); type Statement = { sql: string; params: unknown[] }; const exec = ({ sql, params }: Statement) => { const changes = db.query(sql).run(...(params as never[])).changes; return { meta: { changes } }; }; mock.module("./db", () => ({ stmt: (sql: string, ...params: unknown[]): Statement => ({ sql, params }), first: async (sql: string, ...params: unknown[]) => db.query(sql).get(...(params as never[])) ?? null, all: async (sql: string, ...params: unknown[]) => db.query(sql).all(...(params as never[])), run: async (sql: string, ...params: unknown[]) => exec({ sql, params }), batch: async (statements: Statement[]) => db.transaction(() => statements.map(exec))(), })); const waitlist = await import("./waitlist"); const { advanceWaitlist, approveWaitlistEntry, listWaitlist, claimSignupSeat, joinWaitlist, releaseWaitlistSpot, seatGuard, setSignupCap, } = waitlist; const HOUR = 60 * 60 * 1000; const QUEUED = 30; function addUsers(count: number) { for (let index = 0; index < count; index++) { db.query("INSERT INTO users (account_id, email) VALUES (?, ?)").run( crypto.randomUUID(), `${crypto.randomUUID()}@x`, ); } } function inviteToken(email: string): string { const url = sent.findLast((mail) => mail.to === email)?.url ?? ""; return decodeURIComponent(new URL(url).searchParams.get("invite") ?? ""); } describe("signup waitlist", () => { beforeEach(() => { db = new Database(":memory:"); db.exec("CREATE TABLE users (account_id TEXT PRIMARY KEY, email TEXT NOT NULL)"); for (const migration of ["0051_signup_waitlist.sql", "0052_waitlist_bypass.sql"]) { const sql = readFileSync(new URL(`../../migrations/${migration}`, import.meta.url), "utf8"); for (const statement of sql.replace(/^--.*$/gm, "").split(";")) { if (statement.trim()) db.run(statement); } } sent.length = 0; }); test("no cap means open signups and no waitlist", async () => { expect(await claimSignupSeat("a@x.dev", null)).toEqual({ kind: "open" }); await expect(joinWaitlist("a@x.dev")).rejects.toThrow(/open/); }); test("at the cap, signups are refused and people queue in order", async () => { addUsers(2); await setSignupCap(2); await expect(claimSignupSeat("new@x.dev", null)).rejects.toThrow(/waitlist/); expect(await joinWaitlist("first@x.dev")).toBe(1); expect(await joinWaitlist("second@x.dev")).toBe(2); expect(await joinWaitlist("third@x.dev")).toBe(3); expect(await joinWaitlist("first@x.dev")).toBe(1); expect(sent).toHaveLength(0); }); test("raising the cap invites the next people in line, and their seats are held", async () => { addUsers(2); await setSignupCap(2); for (const email of ["first@x.dev", "second@x.dev", "third@x.dev"]) await joinWaitlist(email); await setSignupCap(4); expect(sent.map((mail) => mail.to)).toEqual(["first@x.dev", "second@x.dev"]); // Both new seats are held by invites, so a stranger still can't sign up. await expect(claimSignupSeat("stranger@x.dev", null)).rejects.toThrow(/waitlist/); // Advancing again does not re-invite anyone. expect(await advanceWaitlist()).toBe(0); }); test("an invite works only for its own email, then the spot is released", async () => { addUsers(1); await setSignupCap(1); await joinWaitlist("first@x.dev"); await setSignupCap(2); const token = inviteToken("first@x.dev"); await expect(claimSignupSeat("other@x.dev", token)).rejects.toThrow(/email address the invite/); const seat = await claimSignupSeat("FIRST@x.dev", token); expect(seat).toMatchObject({ kind: "capped", cap: 2 }); expect(seatGuard(seat)?.sql).not.toContain("signup_waitlist"); addUsers(1); await releaseWaitlistSpot("first@x.dev"); expect(db.query("SELECT COUNT(*) AS n FROM signup_waitlist").get()).toEqual({ n: 0 }); }); test("an invite lapses after 48 hours and the next person is invited", async () => { addUsers(1); await setSignupCap(1); await joinWaitlist("first@x.dev"); await joinWaitlist("second@x.dev"); await setSignupCap(2); expect(sent.map((mail) => mail.to)).toEqual(["first@x.dev"]); const token = inviteToken("first@x.dev"); db.query("UPDATE signup_waitlist SET invite_expires_at = ? WHERE email = ?").run(Date.now() - HOUR, "first@x.dev"); expect(await advanceWaitlist()).toBe(1); expect(sent.map((mail) => mail.to)).toEqual(["first@x.dev", "second@x.dev"]); await expect(claimSignupSeat("first@x.dev", token)).rejects.toThrow(/expired/); expect(db.query("SELECT email FROM signup_waitlist WHERE email = 'first@x.dev'").get()).toBeNull(); }); test("the seat guard blocks an insert once seats run out", async () => { addUsers(1); await setSignupCap(2); const seat = await claimSignupSeat("late@x.dev", null); const guard = seatGuard(seat); addUsers(1); const row = db.query(`SELECT 1 AS ok WHERE ${guard?.sql}`).get(...((guard?.params ?? []) as never[])); expect(row).toBeNull(); }); test("an admin can let someone in past the cap without taking a seat from the line", async () => { addUsers(1); await setSignupCap(1); await joinWaitlist("first@x.dev"); await joinWaitlist("vip@x.dev"); const vip = db.query("SELECT id FROM signup_waitlist WHERE email = 'vip@x.dev'").get() as { id: number }; await approveWaitlistEntry(vip.id); expect(sent.map((mail) => mail.to)).toEqual(["vip@x.dev"]); const seat = await claimSignupSeat("vip@x.dev", inviteToken("vip@x.dev")); expect(seat).toEqual({ kind: "approved" }); expect(seatGuard(seat)).toBeNull(); // Raising the cap by one goes to the person at the front of the line, not to the approved one. await setSignupCap(2); expect(sent.map((mail) => mail.to)).toEqual(["vip@x.dev", "first@x.dev"]); }); test("the admin list pages through the line and filters by email and status", async () => { addUsers(1); await setSignupCap(1); for (let index = 0; index < QUEUED; index++) await joinWaitlist(`user${index}@x.dev`); await joinWaitlist("100%_odd@x.dev"); const first = await listWaitlist({ query: "", status: "all", page: 1 }); expect(first.total).toBe(31); expect(first.rows).toHaveLength(25); expect(first.rows[0]?.email).toBe("user0@x.dev"); expect((await listWaitlist({ query: "", status: "all", page: 2 })).rows).toHaveLength(6); expect((await listWaitlist({ query: "user1", status: "all", page: 1 })).total).toBe(11); expect((await listWaitlist({ query: "%_", status: "all", page: 1 })).rows.map((row) => row.email)).toEqual([ "100%_odd@x.dev", ]); await approveWaitlistEntry(first.rows[3]?.id ?? 0); expect((await listWaitlist({ query: "", status: "approved", page: 1 })).rows.map((row) => row.email)).toEqual([ "user3@x.dev", ]); expect((await listWaitlist({ query: "", status: "waiting", page: 1 })).total).toBe(30); }); });