Skip to content
File

Blob: tests/worker/admin-invites-create.test.ts

typescript103 lines
1import { env, exports } from "cloudflare:workers";
2import { eq, sql } from "drizzle-orm";
3import { beforeAll, beforeEach, describe, expect, it } from "vitest";
4 
5const SELF = exports.default;
6 
7import { makeAuth } from "@/worker/auth";
8import { makeDb } from "@/worker/db";
9import { invites, users } from "@/worker/db/schema";
10 
11import { ISSUER, signInForCookie, testHeaders } from "./helpers";
12 
13// Admin invite creation refuses minting when (a) a `users` row already
14// exists for the email โ€” tessera is invite-only, an existing user means
15// the address signed up via another channel, so a new invite would
16// generate a doomed URL โ€” and (b) an invite for that email already
17// exists, consumed or not. The DB-level UNIQUE(email) on the invites
18// table is the authoritative race guard; these specs cover the
19// pre-flight 409 paths.
20describe("/api/admin/invites POST โ€” pre-flight rejections", () => {
21 const adminCred = {
22 email: "invite-create-admin@example.com",
23 password: "correct-horse-battery-staple",
24 name: "Invite Create Admin",
25 };
26 let cookie: string;
27 
28 beforeAll(async () => {
29 const auth = makeAuth(env, { baseURL: ISSUER, issuer: ISSUER });
30 await auth.api.signUpEmail({ body: adminCred, asResponse: false });
31 await env.DB.prepare("UPDATE users SET role = ? WHERE email = ?").bind("admin", adminCred.email).run();
32 cookie = await signInForCookie(adminCred.email, adminCred.password, "10.74.1.1");
33 });
34 
35 beforeEach(async () => {
36 const db = makeDb(env);
37 await db.delete(invites).where(sql`email LIKE 'create-test-%@example.com'`);
38 await db.delete(users).where(sql`email LIKE 'create-test-%@example.com'`);
39 });
40 
41 const mint = (email: string): Promise<Response> =>
42 SELF.fetch(`${ISSUER}/api/admin/invites`, {
43 method: "POST",
44 headers: testHeaders({ cookie }),
45 body: JSON.stringify({ email }),
46 });
47 
48 it("first mint for a fresh email succeeds (200)", async () => {
49 const res = await mint("create-test-fresh@example.com");
50 expect(res.status).toBe(200);
51 const body = (await res.json()) as { email: string; inviteUrl: string };
52 expect(body.email).toBe("create-test-fresh@example.com");
53 expect(body.inviteUrl).toBeTruthy();
54 });
55 
56 it("second mint for the same email returns 409 invite_exists", async () => {
57 const first = await mint("create-test-dup@example.com");
58 expect(first.status).toBe(200);
59 
60 const second = await mint("create-test-dup@example.com");
61 expect(second.status).toBe(409);
62 const body = (await second.json()) as { error: string };
63 expect(body.error).toBe("invite_exists");
64 
65 // Only one row should exist.
66 const db = makeDb(env);
67 const rows = await db.select().from(invites).where(eq(invites.email, "create-test-dup@example.com"));
68 expect(rows).toHaveLength(1);
69 });
70 
71 it("mint for an email already attached to a users row returns 409 user_exists", async () => {
72 const db = makeDb(env);
73 await db.insert(users).values({
74 id: "usr-create-test-existing",
75 name: "Existing",
76 email: "create-test-occupied@example.com",
77 emailVerified: true,
78 });
79 
80 const res = await mint("create-test-occupied@example.com");
81 expect(res.status).toBe(409);
82 const body = (await res.json()) as { error: string };
83 expect(body.error).toBe("user_exists");
84 
85 // No invite row should have been written.
86 const rows = await db.select().from(invites).where(eq(invites.email, "create-test-occupied@example.com"));
87 expect(rows).toHaveLength(0);
88 });
89 
90 it("mint normalizes email to lowercase before pre-flight + insert", async () => {
91 const first = await mint("Create-Test-Case@Example.com");
92 expect(first.status).toBe(200);
93 
94 // Same address differing only in case must be rejected by the
95 // invite_exists path because both checks compare against the
96 // already-lowercased stored value.
97 const second = await mint("create-test-case@example.com");
98 expect(second.status).toBe(409);
99 const body = (await second.json()) as { error: string };
100 expect(body.error).toBe("invite_exists");
101 });
102});