import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { beforeAll, describe, expect, it } from "vitest"; const apiTestEnv = readApiTestEnv(); const API_BASE_URL = (process.env.API_BASE_URL ?? apiTestEnv.API_BASE_URL)?.replace(/\/+$/, ""); const API_TEST_ORIGIN = process.env.API_TEST_ORIGIN ?? apiTestEnv.API_TEST_ORIGIN ?? "http://localhost:8787"; const API_TEST_SESSION_COOKIE = process.env.API_TEST_SESSION_COOKIE ?? apiTestEnv.API_TEST_SESSION_COOKIE; const API_TEST_ADMIN_SESSION_COOKIE = process.env.API_TEST_ADMIN_SESSION_COOKIE ?? apiTestEnv.API_TEST_ADMIN_SESSION_COOKIE; const API_TEST_USER_ID = process.env.API_TEST_USER_ID ?? apiTestEnv.API_TEST_USER_ID; const API_TEST_ORGANIZATION_ID = process.env.API_TEST_ORGANIZATION_ID ?? apiTestEnv.API_TEST_ORGANIZATION_ID; const RUN_ID = process.env.RUN_ID ?? apiTestEnv.RUN_ID ?? `api-test-${Date.now()}`; const D1_DATABASE = process.env.API_TEST_D1_DATABASE ?? "cfw-auth"; const USE_REMOTE_D1 = process.env.API_TEST_USE_REMOTE_D1 !== "false"; const scenarioDescribe = API_BASE_URL && API_TEST_SESSION_COOKIE && API_TEST_ADMIN_SESSION_COOKIE ? describe : describe.skip; type JsonObject = Record; interface ApiResponse { status: number; headers: Headers; body: T; setCookie: string[]; } beforeAll(async () => { if (!API_BASE_URL || !API_TEST_SESSION_COOKIE || !API_TEST_ADMIN_SESSION_COOKIE) { return; } const userSession = await requestJson<{ user?: JsonObject }>("/get-session", { cookie: API_TEST_SESSION_COOKIE, }); const adminSession = await requestJson<{ user?: JsonObject }>("/get-session", { cookie: API_TEST_ADMIN_SESSION_COOKIE, }); expect(userSession.status).toBe(200); expect(adminSession.status).toBe(200); }); scenarioDescribe("cfw-auth account and session business scenarios", () => { it("enforces email verification before allowing password sign-in", async () => { const email = uniqueEmail("unverified"); const password = "ScenarioPassword123!"; const signUp = await requestJson<{ user: JsonObject }>("/sign-up/email", { method: "POST", body: { name: "Unverified Scenario User", email, password, rememberMe: true, }, }); expect(signUp.status).toBe(200); expect(signUp.body.user).toEqual( expect.objectContaining({ email, emailVerified: false, }), ); const signIn = await requestJson("/sign-in/email", { method: "POST", body: { email, password, rememberMe: true, }, }); expect(signIn.status).toBe(403); expect(signIn.body).toEqual( expect.objectContaining({ code: "EMAIL_NOT_VERIFIED", }), ); }, 30_000); it("signs in a verified user, reads the session, and signs out", async () => { const email = uniqueEmail("verified"); const password = "ScenarioPassword123!"; const created = await createVerifiedUser({ email, password, name: "Verified Scenario User" }); const signIn = await requestJson<{ token?: string; user: JsonObject }>("/sign-in/email", { method: "POST", body: { email, password, rememberMe: true, }, }); expect(signIn.status).toBe(200); expect(signIn.body.user).toEqual( expect.objectContaining({ id: created.id, email, }), ); const cookie = sessionCookieFromSetCookie(signIn.setCookie); expect(cookie).toContain("better-auth.session"); const session = await requestJson<{ session: JsonObject; user: JsonObject }>("/get-session", { cookie, }); expect(session.status).toBe(200); expect(session.body.user).toEqual( expect.objectContaining({ id: created.id, }), ); const signOut = await requestJson<{ success: boolean }>("/sign-out", { method: "POST", cookie, body: {}, }); expect(signOut.status).toBe(200); expect(signOut.body).toEqual({ success: true }); }, 30_000); }); scenarioDescribe("cfw-auth admin user management business scenarios", () => { it("creates, reads, updates, bans, unbans, and removes a user", async () => { const email = uniqueEmail("admin-managed"); const create = await requestJson<{ user: JsonObject }>("/admin/create-user", { method: "POST", cookie: API_TEST_ADMIN_SESSION_COOKIE, body: { email, password: "ManagedPassword123!", name: "Managed Scenario User", role: "user", }, }); expect(create.status).toBe(200); const userId = String(create.body.user.id); expect(create.body.user).toEqual(expect.objectContaining({ email, role: "user" })); const get = await requestJson(`/admin/get-user?id=${encodeURIComponent(userId)}`, { cookie: API_TEST_ADMIN_SESSION_COOKIE, }); expect(get.status).toBe(200); expect(get.body).toEqual(expect.objectContaining({ id: userId, email })); const update = await requestJson<{ user: JsonObject }>("/admin/update-user", { method: "POST", cookie: API_TEST_ADMIN_SESSION_COOKIE, body: { userId, data: { name: "Managed Scenario User Updated", }, }, }); expect(update.status).toBe(200); expect(update.body).toEqual( expect.objectContaining({ id: userId, name: "Managed Scenario User Updated", }), ); const ban = await requestJson<{ user: JsonObject }>("/admin/ban-user", { method: "POST", cookie: API_TEST_ADMIN_SESSION_COOKIE, body: { userId, banReason: "business scenario", banExpiresIn: 60, }, }); expect(ban.status).toBe(200); expect(ban.body.user).toEqual( expect.objectContaining({ id: userId, banned: true, banReason: "business scenario", }), ); const unban = await requestJson<{ user: JsonObject }>("/admin/unban-user", { method: "POST", cookie: API_TEST_ADMIN_SESSION_COOKIE, body: { userId }, }); expect(unban.status).toBe(200); expect(unban.body.user).toEqual( expect.objectContaining({ id: userId, banned: false, banReason: null, }), ); const remove = await requestJson<{ success: boolean }>("/admin/remove-user", { method: "POST", cookie: API_TEST_ADMIN_SESSION_COOKIE, body: { userId }, }); expect(remove.status).toBe(200); expect(remove.body).toEqual({ success: true }); const getRemoved = await requestJson( `/admin/get-user?id=${encodeURIComponent(userId)}`, { cookie: API_TEST_ADMIN_SESSION_COOKIE, }, ); expect(getRemoved.status).toBe(404); }, 30_000); it("rejects admin operations from a regular user session", async () => { const response = await requestJson("/admin/list-users?limit=1", { cookie: API_TEST_SESSION_COOKIE, }); expect(response.status).toBe(403); expect(response.body).toEqual( expect.objectContaining({ message: expect.any(String), }), ); }); }); scenarioDescribe("cfw-auth organization and team business scenarios", () => { it("creates, updates, and removes a team inside the bootstrapped organization", async () => { const teamName = `Scenario Team ${Date.now()}`; const setActive = await requestJson("/organization/set-active", { method: "POST", cookie: API_TEST_SESSION_COOKIE, body: { organizationId: API_TEST_ORGANIZATION_ID, }, }); expect(setActive.status).toBe(200); expect(setActive.body).toEqual(expect.objectContaining({ id: API_TEST_ORGANIZATION_ID })); const create = await requestJson("/organization/create-team", { method: "POST", cookie: API_TEST_SESSION_COOKIE, body: { name: teamName, organizationId: API_TEST_ORGANIZATION_ID, }, }); expect(create.status).toBe(200); const teamId = String(create.body.id); expect(create.body).toEqual( expect.objectContaining({ name: teamName, organizationId: API_TEST_ORGANIZATION_ID, }), ); const addMember = await requestJson("/organization/add-team-member", { method: "POST", cookie: API_TEST_SESSION_COOKIE, body: { teamId, userId: API_TEST_USER_ID, organizationId: API_TEST_ORGANIZATION_ID, }, }); expect(addMember.status).toBe(200); expect(addMember.body).toEqual(expect.objectContaining({ teamId, userId: API_TEST_USER_ID })); const listMembers = await requestJson( `/organization/list-team-members?teamId=${encodeURIComponent( teamId, )}&organizationId=${encodeURIComponent(API_TEST_ORGANIZATION_ID ?? "")}`, { cookie: API_TEST_SESSION_COOKIE, }, ); expect(listMembers.status).toBe(200); expect(listMembers.body).toEqual( expect.arrayContaining([expect.objectContaining({ teamId, userId: API_TEST_USER_ID })]), ); const update = await requestJson("/organization/update-team", { method: "POST", cookie: API_TEST_SESSION_COOKIE, body: { teamId, organizationId: API_TEST_ORGANIZATION_ID, data: { name: `${teamName} Updated`, }, }, }); expect(update.status).toBe(200); expect(update.body).toEqual(expect.objectContaining({ id: teamId, name: `${teamName} Updated` })); const removeMember = await requestJson("/organization/remove-team-member", { method: "POST", cookie: API_TEST_SESSION_COOKIE, body: { teamId, userId: API_TEST_USER_ID, organizationId: API_TEST_ORGANIZATION_ID, }, }); expect(removeMember.status).toBe(200); const removeTeam = await requestJson("/organization/remove-team", { method: "POST", cookie: API_TEST_SESSION_COOKIE, body: { teamId, organizationId: API_TEST_ORGANIZATION_ID, }, }); expect(removeTeam.status).toBe(200); }, 30_000); }); scenarioDescribe("cfw-auth API key business scenarios", () => { it("creates, reads, rejects server-only client updates, and deletes an API key", async () => { const name = `scenario-key-${Date.now()}`; const create = await requestJson("/api-key/create", { method: "POST", cookie: API_TEST_SESSION_COOKIE, body: { name, expiresIn: null, remaining: null, metadata: { runId: RUN_ID, }, }, }); expect(create.status).toBe(200); const keyId = String(create.body.id); expect(create.body).toEqual( expect.objectContaining({ id: expect.any(String), key: expect.stringMatching(/^cfw_/), name, enabled: true, }), ); const read = await requestJson(`/api-key/get?id=${encodeURIComponent(keyId)}`, { cookie: API_TEST_SESSION_COOKIE, }); expect(read.status).toBe(200); expect(read.body).toEqual(expect.objectContaining({ id: keyId, name })); expect(read.body).not.toHaveProperty("key"); const serverOnlyUpdate = await requestJson("/api-key/update", { method: "POST", cookie: API_TEST_SESSION_COOKIE, body: { keyId, expiresIn: null, permissions: null, enabled: true, metadata: { updated: true, }, }, }); expect(serverOnlyUpdate.status).toBe(400); expect(serverOnlyUpdate.body).toEqual( expect.objectContaining({ code: "SERVER_ONLY_PROPERTY", }), ); const remove = await requestJson<{ success: boolean }>("/api-key/delete", { method: "POST", cookie: API_TEST_SESSION_COOKIE, body: { keyId, }, }); expect(remove.status).toBe(200); expect(remove.body).toEqual({ success: true }); const readDeleted = await requestJson(`/api-key/get?id=${encodeURIComponent(keyId)}`, { cookie: API_TEST_SESSION_COOKIE, }); expect(readDeleted.status).toBe(404); }, 30_000); }); function readApiTestEnv(): Record { const path = process.env.API_TEST_ENV_FILE ?? ".api-test.env"; if (!existsSync(path)) return {}; const values: Record = {}; for (const line of readFileSync(path, "utf8").split(/\r?\n/)) { if (!line || line.trimStart().startsWith("#")) continue; const index = line.indexOf("="); if (index === -1) continue; values[line.slice(0, index).trim()] = line .slice(index + 1) .trim() .replace(/^"|"$/g, "") .replace(/\\"/g, '"') .replace(/\\\\/g, "\\"); } return values; } async function requestJson( path: string, { method = "GET", body, cookie, headers, }: { method?: string; body?: unknown; cookie?: string; headers?: Record; } = {}, ): Promise> { if (!API_BASE_URL) { throw new Error("Set API_BASE_URL before running business scenario tests."); } const requestHeaders = new Headers(headers); requestHeaders.set("accept", "application/json"); requestHeaders.set("origin", API_TEST_ORIGIN); if (body !== undefined) requestHeaders.set("content-type", "application/json"); if (cookie) requestHeaders.set("cookie", cookie); const response = await fetch(new URL(`${API_BASE_URL}${path}`), { method, headers: requestHeaders, body: body === undefined ? undefined : JSON.stringify(body), }); const text = await response.text(); return { status: response.status, headers: response.headers, body: text ? (JSON.parse(text) as T) : (undefined as T), setCookie: response.headers.getSetCookie?.() ?? splitSetCookie(response.headers.get("set-cookie")), }; } async function createVerifiedUser({ email, password, name, }: { email: string; password: string; name: string; }): Promise { const create = await requestJson<{ user: JsonObject }>("/admin/create-user", { method: "POST", cookie: API_TEST_ADMIN_SESSION_COOKIE, body: { email, password, name, role: "user", }, }); expect(create.status).toBe(200); markEmailVerified(String(create.body.user.id)); return create.body.user; } function markEmailVerified(userId: string): void { const sql = `update "user" set "emailVerified" = 1 where "id" = ${sqlString(userId)};`; runD1(sql); } function runD1(command: string): void { const args = ["wrangler", "d1", "execute", D1_DATABASE, "--command", command, "--json"]; if (USE_REMOTE_D1) args.push("--remote"); const raw = execFileSync("pnpm", ["exec", ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); const parsed = JSON.parse(raw) as Array<{ success?: boolean }>; const first = Array.isArray(parsed) ? parsed[0] : parsed; if (!first?.success) throw new Error(`D1 command failed: ${raw}`); } function sqlString(value: string): string { return `'${value.replace(/'/g, "''")}'`; } function sessionCookieFromSetCookie(setCookieHeaders: string[]): string { return setCookieHeaders .map((entry) => entry.split(";")[0]) .filter((entry) => /better-auth\.session|session_token|session/.test(entry)) .join("; "); } function splitSetCookie(value: string | null): string[] { if (!value) return []; return value.split(/,(?=\s*[^;,]+=)/g); } function uniqueEmail(label: string): string { return `${RUN_ID}-${label}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.invalid`; }