signup-allowlist.integration.test.ts62 lines · main
1/**
2 * Invite-only gate building blocks (control plane).
3 *
4 * Guards the regression behind Phase 2's invite-only fix: the gate is driven
5 * by `isEmailAllowed` + `getOpenSignupsFlag`, which the Better Auth
6 * `user.create.before` hook (lib/auth.ts) consults as the SINGLE signup gate
7 * (after we set `disableSignUp:false` so the hook actually runs). This test
8 * exercises the allowlist service directly + the open/closed gate decision.
9 *
10 * Integration test (real control Postgres, no mock.module — avoids the
11 * process-global mock leak). Gated on BRIVEN_DATA_PLANE_URL — the repo's
12 * "integration mode is on" signal — NOT BRIVEN_DATABASE_URL, which
13 * test-preload.ts always sets to a dead URL so route tests can construct
14 * getDb(). The test:integration run provides both real URLs together.
15 */
16import { afterAll, describe, expect, test } from 'bun:test';
17import { eq } from 'drizzle-orm';
18
19import { getDb } from '../db/client.js';
20import { signupAllowlist } from '../db/schema.js';
21import { addToAllowlist, isEmailAllowed, removeFromAllowlist } from './signup-allowlist.js';
22
23const HAS_DB = Boolean(process.env.BRIVEN_DATA_PLANE_URL);
24const SUFFIX = Date.now().toString(36);
25const ALLOWED = `allow-${SUFFIX}@example.com`;
26const UNKNOWN = `nobody-${SUFFIX}@example.com`;
27
28describe.skipIf(!HAS_DB)('signup allowlist — invite gate building blocks', () => {
29 afterAll(async () => {
30 const db = getDb();
31 await db.delete(signupAllowlist).where(eq(signupAllowlist.email, ALLOWED.toLowerCase()));
32 });
33
34 test('unknown email is NOT allowed', async () => {
35 expect(await isEmailAllowed(UNKNOWN)).toBe(false);
36 });
37
38 test('added email is allowed; lookup is case- + whitespace-insensitive', async () => {
39 await addToAllowlist({ email: `${ALLOWED.toUpperCase()} `, invitedBy: null });
40 expect(await isEmailAllowed(ALLOWED)).toBe(true);
41 expect(await isEmailAllowed(` ${ALLOWED.toUpperCase()}`)).toBe(true);
42 });
43
44 test('duplicate add is rejected (one invite per email)', async () => {
45 await expect(addToAllowlist({ email: ALLOWED, invitedBy: null })).rejects.toThrow(
46 /already on the allowlist/i,
47 );
48 });
49
50 test('invalid email is rejected', async () => {
51 await expect(addToAllowlist({ email: 'not-an-email', invitedBy: null })).rejects.toThrow(
52 /valid email/i,
53 );
54 });
55
56 test('removeFromAllowlist revokes the invite', async () => {
57 expect(await removeFromAllowlist(ALLOWED)).toBe(true);
58 expect(await isEmailAllowed(ALLOWED)).toBe(false);
59 // removing a non-present email is a no-op, not an error
60 expect(await removeFromAllowlist(UNKNOWN)).toBe(false);
61 });
62});