/** * End-to-end tests for Admin procedures (superuser operations) * * These tests use a real PostgreSQL database to test: * * Users: * - admin.users.list - list all users * - admin.users.get - get user by email * - admin.users.create - create passwordless user with optional org * - admin.users.update - update user properties (e.g., isSuperuser) * - admin.users.confirmEmail - confirm a user's email * * Organizations: * - admin.orgs.list - list all organizations * - admin.orgs.get - get organization by slug * - admin.orgs.create - create organization with owner * - admin.orgs.update - update organization properties * - admin.orgs.delete - delete organization and related records * - admin.orgs.listSites - list sites for organization * - admin.orgs.addSite - add site to organization * - admin.orgs.removeSite - remove site from organization * * Auth: * - admin.auth.completeLogin - complete pending login request */ import type { Database } from "@reviq/db-schema"; import type { Kysely } from "kysely"; import type { APIContext } from "../../context.js"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { call } from "@orpc/server"; import { createLoggingEmailClient } from "@reviq/emails"; import { createTestUser, describeE2E, getSharedDb, initTestDb, TEST_RP, truncateAllTables, uniqueTestId, withTestTransaction, } from "@reviq/test-helpers"; import { router } from "../../router.js"; import { COOKIE_NAMES } from "../../utils/cookies.js"; import { hashToken } from "../../utils/crypto.js"; /** Session expiry duration: 24 hours in milliseconds */ const SESSION_EXPIRY_MS = 24 * 60 * 60 * 1000; /** Login request expiry: 15 minutes in milliseconds */ const LOGIN_REQUEST_EXPIRY_MS = 15 * 60 * 1000; /** * Create an API context with optional authentication */ function createAPIContext( db: Kysely, options?: { sessionToken?: string; }, ): APIContext { const reqHeaders = new Headers(); const cookies: string[] = []; if (options?.sessionToken) { cookies.push(`${COOKIE_NAMES.SESSION_TOKEN}=${options.sessionToken}`); } if (cookies.length > 0) { reqHeaders.set("cookie", cookies.join("; ")); } return { db, origin: TEST_RP.origin, allowedOrigins: [...TEST_RP.allowedOrigins], rpName: TEST_RP.rpName, reqHeaders, resHeaders: new Headers(), email: { client: createLoggingEmailClient(), fromAddress: "test@example.com", baseUrl: TEST_RP.origin, }, }; } /** * Create a real session in the database and return the token */ async function createSession( db: Kysely, userId: number, ): Promise<{ token: string; sessionId: number }> { const token = `test-session-${uniqueTestId()}`; const tokenHashValue = await hashToken(token); const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS); const result = await db .insertInto("sessions") .values({ user_id: userId, token_hash: tokenHashValue, ip_address: "127.0.0.1", user_agent: "test-agent", expires_at: expiresAt, trusted_mode: false, }) .returning("id") .executeTakeFirstOrThrow(); return { token, sessionId: Number(result.id) }; } /** * Create an organization in the database */ async function createOrg( db: Kysely, options?: { slug?: string; displayName?: string; logoUrl?: string; }, ): Promise<{ id: number; slug: string }> { const slug = options?.slug ?? `org-${uniqueTestId()}`; const result = await db .insertInto("orgs") .values({ slug, display_name: options?.displayName ?? "Test Organization", logo_url: options?.logoUrl ?? null, }) .returning(["id", "slug"]) .executeTakeFirstOrThrow(); return result; } /** * Add a user as a member of an organization */ async function addOrgMember( db: Kysely, orgId: number, userId: number, role: "owner" | "admin" | "member" = "member", ): Promise { await db .insertInto("org_members") .values({ org_id: orgId, user_id: userId, role, }) .execute(); } /** * Create a site for an organization */ async function createSite( db: Kysely, orgId: number, domain: string, ): Promise<{ id: number; domain: string }> { const result = await db .insertInto("org_sites") .values({ org_id: orgId, domain, }) .returning(["id", "domain"]) .executeTakeFirstOrThrow(); return result; } /** * Create a login request in the database */ async function createLoginRequest( db: Kysely, userId: number, email: string, options?: { completedAt?: Date | null; expiresAt?: Date; }, ): Promise<{ id: number; token: string }> { const token = `login-${uniqueTestId()}`; const expiresAt = options?.expiresAt ?? new Date(Date.now() + LOGIN_REQUEST_EXPIRY_MS); const result = await db .insertInto("login_requests") .values({ user_id: userId, email, token, device_fingerprint: "test-fingerprint", expires_at: expiresAt, completed_at: options?.completedAt ?? null, }) .returning("id") .executeTakeFirstOrThrow(); return { id: Number(result.id), token }; } /** * Create an organization invite */ async function createOrgInvite( db: Kysely, orgId: number, email: string, invitedBy: number, ): Promise<{ id: number }> { const token = `invite-${uniqueTestId()}`; const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days const result = await db .insertInto("org_invites") .values({ org_id: orgId, email, role: "member", invited_by: invitedBy, token, expires_at: expiresAt, }) .returning("id") .executeTakeFirstOrThrow(); return { id: result.id }; } describeE2E("admin", () => { beforeAll(async () => { await initTestDb(); // Ensure clean slate in case other test files left data behind await truncateAllTables(getSharedDb()); }); // ===== Authorization Tests ===== describe("admin authorization", () => { test("rejects non-superuser for admin.users.list", async () => { await withTestTransaction(getSharedDb(), async (db) => { const user = await createTestUser(db, { email: "regular@example.com", isSuperuser: false, }); const { token: sessionToken } = await createSession(db, user.id); const context = createAPIContext(db, { sessionToken }); await expect( call(router.admin.users.list, undefined, { context }), ).rejects.toThrow("Superuser access required"); }); }); test("rejects unauthenticated request for admin.orgs.list", async () => { await withTestTransaction(getSharedDb(), async (db) => { const context = createAPIContext(db); await expect( call(router.admin.orgs.list, undefined, { context }), ).rejects.toThrow("No session or API key"); }); }); }); // ===== admin.users.list ===== describe("admin.users.list", () => { test("returns all users", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createTestUser(db, { email: "user1@example.com" }); await createTestUser(db, { email: "user2@example.com" }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const users = await call(router.admin.users.list, undefined, { context, }); expect(users.length).toBe(3); const emails = users.map((u) => u.email).sort(); expect(emails).toContain("admin@example.com"); expect(emails).toContain("user1@example.com"); expect(emails).toContain("user2@example.com"); }); }); test("returns users with correct fields", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, displayName: "Admin User", fullName: "Admin Full Name", emailVerifiedAt: new Date(), }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const users = await call(router.admin.users.list, undefined, { context, }); const adminUser = users.find((u) => u.email === "admin@example.com"); expect(adminUser).toBeDefined(); expect(adminUser?.displayName).toBe("Admin User"); expect(adminUser?.fullName).toBe("Admin Full Name"); expect(adminUser?.emailVerified).toBe(true); expect(adminUser?.isSuperuser).toBe(true); expect(adminUser?.needsSetup).toBe(false); }); }); test("returns empty array when no users", async () => { await withTestTransaction(getSharedDb(), async (db) => { // Create only the admin user const admin = await createTestUser(db, { email: "onlyadmin@example.com", isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const users = await call(router.admin.users.list, undefined, { context, }); // Only the admin user exists expect(users.length).toBe(1); expect(users[0]?.email).toBe("onlyadmin@example.com"); }); }); }); // ===== admin.users.get ===== describe("admin.users.get", () => { test("returns user by email", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createTestUser(db, { email: "target@example.com", displayName: "Target User", fullName: "Target Full", }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const user = await call( router.admin.users.get, { email: "target@example.com" }, { context }, ); expect(user.email).toBe("target@example.com"); expect(user.displayName).toBe("Target User"); expect(user.fullName).toBe("Target Full"); }); }); test("normalizes email to lowercase", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createTestUser(db, { email: "test@example.com" }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const user = await call( router.admin.users.get, { email: "TEST@EXAMPLE.COM" }, { context }, ); expect(user.email).toBe("test@example.com"); }); }); test("throws NOT_FOUND for non-existent user", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.users.get, { email: "nonexistent@example.com" }, { context }, ), ).rejects.toThrow("User not found"); }); }); test("returns correct hasPassword and needsSetup flags", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); // User without display name (needs setup) await createTestUser(db, { email: "nosetup@example.com", displayName: undefined, }); // Set display_name to null await db .updateTable("users") .set({ display_name: null }) .where("email", "=", "nosetup@example.com") .execute(); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const user = await call( router.admin.users.get, { email: "nosetup@example.com" }, { context }, ); expect(user.needsSetup).toBe(true); expect(user.hasPassword).toBe(false); }); }); }); // ===== admin.users.create ===== // NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally describe("admin.users.create", () => { afterAll(async () => { // Clean up all test data await truncateAllTables(getSharedDb()); }); test("creates passwordless user", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const result = await call( router.admin.users.create, { email: `newuser-${uniqueId}@example.com` }, { context }, ); expect(result.success).toBe(true); // Verify user was created const user = await db .selectFrom("users") .where("email", "=", `newuser-${uniqueId}@example.com`) .selectAll() .executeTakeFirst(); expect(user).toBeDefined(); expect(user?.password_hash).toBeNull(); }); test("creates user with name", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await call( router.admin.users.create, { email: `named-${uniqueId}@example.com`, name: "Named User" }, { context }, ); const user = await db .selectFrom("users") .where("email", "=", `named-${uniqueId}@example.com`) .selectAll() .executeTakeFirst(); expect(user?.display_name).toBe("Named User"); }); test("creates user and adds to organization as member", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); const org = await createOrg(db, { slug: `test-org-${uniqueId}` }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await call( router.admin.users.create, { email: `orguser-${uniqueId}@example.com`, orgSlug: `test-org-${uniqueId}`, }, { context }, ); // Verify org membership const membership = await db .selectFrom("org_members") .innerJoin("users", "users.id", "org_members.user_id") .where("users.email", "=", `orguser-${uniqueId}@example.com`) .where("org_members.org_id", "=", org.id) .selectAll() .executeTakeFirst(); expect(membership).toBeDefined(); expect(membership?.role).toBe("member"); }); test("creates user and adds to organization with custom role", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); const org = await createOrg(db, { slug: `test-org-${uniqueId}` }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await call( router.admin.users.create, { email: `adminuser-${uniqueId}@example.com`, orgSlug: `test-org-${uniqueId}`, orgRole: "admin", }, { context }, ); const membership = await db .selectFrom("org_members") .innerJoin("users", "users.id", "org_members.user_id") .where("users.email", "=", `adminuser-${uniqueId}@example.com`) .where("org_members.org_id", "=", org.id) .selectAll() .executeTakeFirst(); expect(membership?.role).toBe("admin"); }); test("normalizes email to lowercase", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await call( router.admin.users.create, { email: `UPPERCASE-${uniqueId}@EXAMPLE.COM` }, { context }, ); const user = await db .selectFrom("users") .where("email", "=", `uppercase-${uniqueId}@example.com`) .selectAll() .executeTakeFirst(); expect(user).toBeDefined(); }); test("throws CONFLICT for duplicate email", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); await createTestUser(db, { email: `existing-${uniqueId}@example.com` }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.users.create, { email: `existing-${uniqueId}@example.com` }, { context }, ), ).rejects.toThrow("User with this email already exists"); }); test("throws NOT_FOUND for non-existent org", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.users.create, { email: `newuser-${uniqueId}@example.com`, orgSlug: "nonexistent-org", }, { context }, ), ).rejects.toThrow("Organization not found"); }); }); // ===== admin.users.update ===== describe("admin.users.update", () => { test("grants superuser status", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createTestUser(db, { email: "regular@example.com", isSuperuser: false, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await call( router.admin.users.update, { email: "regular@example.com", isSuperuser: true }, { context }, ); const user = await db .selectFrom("users") .where("email", "=", "regular@example.com") .select(["is_superuser"]) .executeTakeFirstOrThrow(); expect(user.is_superuser).toBe(true); }); }); test("revokes superuser status from another user", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createTestUser(db, { email: "otheradmin@example.com", isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await call( router.admin.users.update, { email: "otheradmin@example.com", isSuperuser: false }, { context }, ); const user = await db .selectFrom("users") .where("email", "=", "otheradmin@example.com") .select(["is_superuser"]) .executeTakeFirstOrThrow(); expect(user.is_superuser).toBe(false); }); }); test("prevents self-demotion", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.users.update, { email: "admin@example.com", isSuperuser: false }, { context }, ), ).rejects.toThrow("Cannot remove your own superuser status"); }); }); test("normalizes email to lowercase", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createTestUser(db, { email: "target@example.com" }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await call( router.admin.users.update, { email: "TARGET@EXAMPLE.COM", isSuperuser: true }, { context }, ); const user = await db .selectFrom("users") .where("email", "=", "target@example.com") .select(["is_superuser"]) .executeTakeFirstOrThrow(); expect(user.is_superuser).toBe(true); }); }); test("throws NOT_FOUND for non-existent user", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.users.update, { email: "nonexistent@example.com", isSuperuser: true }, { context }, ), ).rejects.toThrow("User not found"); }); }); test("returns success for no-op update (no fields to update)", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createTestUser(db, { email: "target@example.com" }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const result = await call( router.admin.users.update, { email: "target@example.com" }, { context }, ); expect(result.success).toBe(true); }); }); test("throws NOT_FOUND for no-op update on non-existent user", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.users.update, { email: "nonexistent@example.com" }, { context }, ), ).rejects.toThrow("User not found"); }); }); }); // ===== admin.users.confirmEmail ===== describe("admin.users.confirmEmail", () => { test("confirms user email", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createTestUser(db, { email: "unverified@example.com", emailVerifiedAt: undefined, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await call( router.admin.users.confirmEmail, { email: "unverified@example.com" }, { context }, ); const user = await db .selectFrom("users") .where("email", "=", "unverified@example.com") .select(["email_verified_at"]) .executeTakeFirstOrThrow(); expect(user.email_verified_at).not.toBeNull(); }); }); test("normalizes email to lowercase", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createTestUser(db, { email: "test@example.com" }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await call( router.admin.users.confirmEmail, { email: "TEST@EXAMPLE.COM" }, { context }, ); const user = await db .selectFrom("users") .where("email", "=", "test@example.com") .select(["email_verified_at"]) .executeTakeFirstOrThrow(); expect(user.email_verified_at).not.toBeNull(); }); }); test("throws NOT_FOUND for non-existent user", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.users.confirmEmail, { email: "nonexistent@example.com" }, { context }, ), ).rejects.toThrow("User not found"); }); }); test("succeeds for already verified user (idempotent)", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createTestUser(db, { email: "verified@example.com", emailVerifiedAt: new Date(), }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); // Should not throw const result = await call( router.admin.users.confirmEmail, { email: "verified@example.com" }, { context }, ); expect(result.success).toBe(true); }); }); }); // ===== admin.orgs.list ===== describe("admin.orgs.list", () => { test("returns all organizations", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createOrg(db, { slug: "org-one", displayName: "Org One" }); await createOrg(db, { slug: "org-two", displayName: "Org Two" }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const orgs = await call(router.admin.orgs.list, undefined, { context }); expect(orgs.length).toBe(2); const slugs = orgs.map((o) => o.slug).sort(); expect(slugs).toEqual(["org-one", "org-two"]); }); }); test("returns organizations with correct fields", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createOrg(db, { slug: "test-org", displayName: "Test Org", logoUrl: "https://example.com/logo.png", }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const orgs = await call(router.admin.orgs.list, undefined, { context }); const org = orgs.find((o) => o.slug === "test-org"); expect(org).toBeDefined(); expect(org?.displayName).toBe("Test Org"); expect(org?.logoUrl).toBe("https://example.com/logo.png"); expect(org?.createdAt).toBeInstanceOf(Date); }); }); test("returns empty array when no organizations", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const orgs = await call(router.admin.orgs.list, undefined, { context }); expect(orgs).toHaveLength(0); }); }); }); // ===== admin.orgs.get ===== describe("admin.orgs.get", () => { test("returns organization by slug", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createOrg(db, { slug: "target-org", displayName: "Target Organization", logoUrl: "https://example.com/logo.png", }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const org = await call( router.admin.orgs.get, { slug: "target-org" }, { context }, ); expect(org.slug).toBe("target-org"); expect(org.displayName).toBe("Target Organization"); expect(org.logoUrl).toBe("https://example.com/logo.png"); }); }); test("throws NOT_FOUND for non-existent organization", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call(router.admin.orgs.get, { slug: "nonexistent" }, { context }), ).rejects.toThrow("Organization not found"); }); }); }); // ===== admin.orgs.create ===== // NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally describe("admin.orgs.create", () => { afterAll(async () => { await truncateAllTables(getSharedDb()); }); test("creates organization with owner", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const result = await call( router.admin.orgs.create, { slug: `new-org-${uniqueId}`, displayName: "New Organization", ownerEmail: `owner-${uniqueId}@example.com`, }, { context }, ); expect(result.slug).toBe(`new-org-${uniqueId}`); // Verify org was created const org = await db .selectFrom("orgs") .where("slug", "=", `new-org-${uniqueId}`) .selectAll() .executeTakeFirst(); expect(org).toBeDefined(); expect(org?.display_name).toBe("New Organization"); // Verify owner membership if (org) { const membership = await db .selectFrom("org_members") .where("org_id", "=", org.id) .where("user_id", "=", owner.id) .selectAll() .executeTakeFirst(); expect(membership).toBeDefined(); expect(membership?.role).toBe("owner"); } }); test("normalizes owner email to lowercase", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); await createTestUser(db, { email: `owner-${uniqueId}@example.com` }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const result = await call( router.admin.orgs.create, { slug: `new-org-${uniqueId}`, displayName: "New Organization", ownerEmail: `OWNER-${uniqueId}@EXAMPLE.COM`, }, { context }, ); expect(result.slug).toBe(`new-org-${uniqueId}`); }); test("throws NOT_FOUND for non-existent owner", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.orgs.create, { slug: `new-org-${uniqueId}`, displayName: "New Organization", ownerEmail: "nonexistent@example.com", }, { context }, ), ).rejects.toThrow("User not found"); }); test("throws CONFLICT for duplicate slug", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com`, }); await createOrg(db, { slug: `existing-org-${uniqueId}` }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.orgs.create, { slug: `existing-org-${uniqueId}`, displayName: "New Organization", ownerEmail: owner.email, }, { context }, ), ).rejects.toThrow("Organization with this slug already exists"); }); }); // ===== admin.orgs.update ===== describe("admin.orgs.update", () => { test("updates display name", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createOrg(db, { slug: "test-org", displayName: "Old Name" }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await call( router.admin.orgs.update, { slug: "test-org", displayName: "New Name" }, { context }, ); const org = await db .selectFrom("orgs") .where("slug", "=", "test-org") .select(["display_name"]) .executeTakeFirstOrThrow(); expect(org.display_name).toBe("New Name"); }); }); test("updates logo URL", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createOrg(db, { slug: "test-org" }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await call( router.admin.orgs.update, { slug: "test-org", logoUrl: "https://example.com/newlogo.png" }, { context }, ); const org = await db .selectFrom("orgs") .where("slug", "=", "test-org") .select(["logo_url"]) .executeTakeFirstOrThrow(); expect(org.logo_url).toBe("https://example.com/newlogo.png"); }); }); test("clears logo URL with empty string", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createOrg(db, { slug: "test-org", logoUrl: "https://example.com/logo.png", }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await call( router.admin.orgs.update, { slug: "test-org", logoUrl: "" }, { context }, ); const org = await db .selectFrom("orgs") .where("slug", "=", "test-org") .select(["logo_url"]) .executeTakeFirstOrThrow(); expect(org.logo_url).toBeNull(); }); }); test("updates multiple fields at once", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createOrg(db, { slug: "test-org", displayName: "Old", logoUrl: undefined, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await call( router.admin.orgs.update, { slug: "test-org", displayName: "New Name", logoUrl: "https://example.com/logo.png", }, { context }, ); const org = await db .selectFrom("orgs") .where("slug", "=", "test-org") .select(["display_name", "logo_url"]) .executeTakeFirstOrThrow(); expect(org.display_name).toBe("New Name"); expect(org.logo_url).toBe("https://example.com/logo.png"); }); }); test("returns success for no-op update", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createOrg(db, { slug: "test-org" }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const result = await call( router.admin.orgs.update, { slug: "test-org" }, { context }, ); expect(result.success).toBe(true); }); }); test("throws NOT_FOUND for no-op on non-existent org", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call(router.admin.orgs.update, { slug: "nonexistent" }, { context }), ).rejects.toThrow("Organization not found"); }); }); test("throws NOT_FOUND for non-existent organization", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.orgs.update, { slug: "nonexistent", displayName: "Test" }, { context }, ), ).rejects.toThrow("Organization not found"); }); }); }); // ===== admin.orgs.delete ===== // NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally describe("admin.orgs.delete", () => { afterAll(async () => { await truncateAllTables(getSharedDb()); }); test("deletes organization and related records", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); const member = await createTestUser(db, { email: `member-${uniqueId}@example.com`, }); const org = await createOrg(db, { slug: `delete-me-${uniqueId}` }); // Create related records await addOrgMember(db, org.id, member.id, "owner"); await createSite(db, org.id, `example-${uniqueId}.com`); await createOrgInvite( db, org.id, `invite-${uniqueId}@example.com`, admin.id, ); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const result = await call( router.admin.orgs.delete, { slug: `delete-me-${uniqueId}` }, { context }, ); expect(result.success).toBe(true); // Verify org is deleted const deletedOrg = await db .selectFrom("orgs") .where("slug", "=", `delete-me-${uniqueId}`) .selectAll() .executeTakeFirst(); expect(deletedOrg).toBeUndefined(); // Verify related records are deleted const members = await db .selectFrom("org_members") .where("org_id", "=", org.id) .selectAll() .execute(); expect(members).toHaveLength(0); const sites = await db .selectFrom("org_sites") .where("org_id", "=", org.id) .selectAll() .execute(); expect(sites).toHaveLength(0); const invites = await db .selectFrom("org_invites") .where("org_id", "=", org.id) .selectAll() .execute(); expect(invites).toHaveLength(0); }); test("throws NOT_FOUND for non-existent organization", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call(router.admin.orgs.delete, { slug: "nonexistent" }, { context }), ).rejects.toThrow("Organization not found"); }); }); // ===== admin.orgs.listSites ===== describe("admin.orgs.listSites", () => { test("returns sites for organization", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const org = await createOrg(db, { slug: "test-org" }); await createSite(db, org.id, "example.com"); await createSite(db, org.id, "test.com"); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const sites = await call( router.admin.orgs.listSites, { slug: "test-org" }, { context }, ); expect(sites.length).toBe(2); const domains = sites.map((s) => s.domain).sort(); expect(domains).toEqual(["example.com", "test.com"]); }); }); test("returns empty array when no sites", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createOrg(db, { slug: "empty-org" }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const sites = await call( router.admin.orgs.listSites, { slug: "empty-org" }, { context }, ); expect(sites).toHaveLength(0); }); }); test("returns sites with correct fields", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const org = await createOrg(db, { slug: "test-org" }); await createSite(db, org.id, "example.com"); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const sites = await call( router.admin.orgs.listSites, { slug: "test-org" }, { context }, ); expect(sites[0]?.id).toBeDefined(); expect(sites[0]?.domain).toBe("example.com"); expect(sites[0]?.createdAt).toBeInstanceOf(Date); }); }); test("throws NOT_FOUND for non-existent organization", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.orgs.listSites, { slug: "nonexistent" }, { context }, ), ).rejects.toThrow("Organization not found"); }); }); }); // ===== admin.orgs.addSite ===== // NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally describe("admin.orgs.addSite", () => { afterAll(async () => { await truncateAllTables(getSharedDb()); }); test("adds site to organization", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); const org = await createOrg(db, { slug: `test-org-${uniqueId}` }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const result = await call( router.admin.orgs.addSite, { slug: `test-org-${uniqueId}`, domain: `newsite-${uniqueId}.com` }, { context }, ); expect(result.success).toBe(true); // Verify site was created const site = await db .selectFrom("org_sites") .where("org_id", "=", org.id) .where("domain", "=", `newsite-${uniqueId}.com`) .selectAll() .executeTakeFirst(); expect(site).toBeDefined(); }); test("throws NOT_FOUND for non-existent organization", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.orgs.addSite, { slug: "nonexistent", domain: `test-${uniqueId}.com` }, { context }, ), ).rejects.toThrow("Organization not found"); }); test("throws CONFLICT for duplicate domain", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); const org = await createOrg(db, { slug: `test-org-${uniqueId}` }); await createSite(db, org.id, `existing-${uniqueId}.com`); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.orgs.addSite, { slug: `test-org-${uniqueId}`, domain: `existing-${uniqueId}.com` }, { context }, ), ).rejects.toThrow("Site with this domain already exists"); }); test("throws CONFLICT for domain in another organization", async () => { const db = getSharedDb(); const uniqueId = uniqueTestId(); const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com`, isSuperuser: true, }); const org1 = await createOrg(db, { slug: `org-one-${uniqueId}` }); await createOrg(db, { slug: `org-two-${uniqueId}` }); await createSite(db, org1.id, `shared-${uniqueId}.com`); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.orgs.addSite, { slug: `org-two-${uniqueId}`, domain: `shared-${uniqueId}.com` }, { context }, ), ).rejects.toThrow("Site with this domain already exists"); }); }); // ===== admin.orgs.removeSite ===== describe("admin.orgs.removeSite", () => { test("removes site from organization", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const org = await createOrg(db, { slug: "test-org" }); await createSite(db, org.id, "remove-me.com"); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const result = await call( router.admin.orgs.removeSite, { slug: "test-org", domain: "remove-me.com" }, { context }, ); expect(result.success).toBe(true); // Verify site was removed const site = await db .selectFrom("org_sites") .where("org_id", "=", org.id) .where("domain", "=", "remove-me.com") .selectAll() .executeTakeFirst(); expect(site).toBeUndefined(); }); }); test("throws NOT_FOUND for non-existent organization", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.orgs.removeSite, { slug: "nonexistent", domain: "test.com" }, { context }, ), ).rejects.toThrow("Organization not found"); }); }); test("throws NOT_FOUND for non-existent site", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); await createOrg(db, { slug: "test-org" }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.orgs.removeSite, { slug: "test-org", domain: "nonexistent.com" }, { context }, ), ).rejects.toThrow("Site not found"); }); }); test("throws NOT_FOUND for site in another organization", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const org1 = await createOrg(db, { slug: "org-one" }); await createOrg(db, { slug: "org-two" }); await createSite(db, org1.id, "org1-site.com"); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.orgs.removeSite, { slug: "org-two", domain: "org1-site.com" }, { context }, ), ).rejects.toThrow("Site not found"); }); }); }); // ===== admin.auth.completeLogin ===== describe("admin.auth.completeLogin", () => { test("completes pending login request", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const user = await createTestUser(db, { email: "user@example.com" }); const loginRequest = await createLoginRequest( db, user.id, "user@example.com", ); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); const result = await call( router.admin.auth.completeLogin, { email: "user@example.com" }, { context }, ); expect(result.success).toBe(true); // Verify login request was completed const request = await db .selectFrom("login_requests") .where("id", "=", loginRequest.id.toString()) .select(["completed_at"]) .executeTakeFirstOrThrow(); expect(request.completed_at).not.toBeNull(); }); }); test("normalizes email to lowercase", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const user = await createTestUser(db, { email: "user@example.com" }); const loginRequest = await createLoginRequest( db, user.id, "user@example.com", ); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await call( router.admin.auth.completeLogin, { email: "USER@EXAMPLE.COM" }, { context }, ); const request = await db .selectFrom("login_requests") .where("id", "=", loginRequest.id.toString()) .select(["completed_at"]) .executeTakeFirstOrThrow(); expect(request.completed_at).not.toBeNull(); }); }); test("throws NOT_FOUND for no login request", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.auth.completeLogin, { email: "noRequest@example.com" }, { context }, ), ).rejects.toThrow("No login request found"); }); }); test("throws BAD_REQUEST for already completed request", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const user = await createTestUser(db, { email: "user@example.com" }); await createLoginRequest(db, user.id, "user@example.com", { completedAt: new Date(), }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.auth.completeLogin, { email: "user@example.com" }, { context }, ), ).rejects.toThrow("Login request already completed"); }); }); test("throws BAD_REQUEST for expired request", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const user = await createTestUser(db, { email: "user@example.com" }); await createLoginRequest(db, user.id, "user@example.com", { expiresAt: new Date(Date.now() - 1000), // Expired }); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await expect( call( router.admin.auth.completeLogin, { email: "user@example.com" }, { context }, ), ).rejects.toThrow("Login request expired"); }); }); test("completes most recent login request", async () => { await withTestTransaction(getSharedDb(), async (db) => { const admin = await createTestUser(db, { email: "admin@example.com", isSuperuser: true, }); const user = await createTestUser(db, { email: "user@example.com" }); // Create two login requests await createLoginRequest(db, user.id, "user@example.com"); await createLoginRequest(db, user.id, "user@example.com"); const { token: sessionToken } = await createSession(db, admin.id); const context = createAPIContext(db, { sessionToken }); await call( router.admin.auth.completeLogin, { email: "user@example.com" }, { context }, ); // Check that exactly one login request was completed // (Note: both requests have the same created_at in transaction, so ORDER BY is non-deterministic) const allRequests = await db .selectFrom("login_requests") .where("email", "=", "user@example.com") .select(["id", "completed_at"]) .execute(); expect(allRequests.length).toBe(2); const completedCount = allRequests.filter( (r) => r.completed_at !== null, ).length; expect(completedCount).toBe(1); const uncompletedCount = allRequests.filter( (r) => r.completed_at === null, ).length; expect(uncompletedCount).toBe(1); }); }); }); }); // Close describeE2E("admin")