Files
cfw-auth/tests/auth-business-scenarios.test.ts

517 lines
15 KiB
TypeScript

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<string, unknown>;
interface ApiResponse<T> {
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<JsonObject>("/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<JsonObject>(`/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<JsonObject>(
`/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<JsonObject>("/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<JsonObject>("/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<JsonObject>("/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<JsonObject>("/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<JsonObject[]>(
`/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<JsonObject>("/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<JsonObject>("/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<JsonObject>("/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<JsonObject>("/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<JsonObject>(`/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<JsonObject>("/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<JsonObject>(`/api-key/get?id=${encodeURIComponent(keyId)}`, {
cookie: API_TEST_SESSION_COOKIE,
});
expect(readDeleted.status).toBe(404);
}, 30_000);
});
function readApiTestEnv(): Record<string, string> {
const path = process.env.API_TEST_ENV_FILE ?? ".api-test.env";
if (!existsSync(path)) return {};
const values: Record<string, string> = {};
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<T = unknown>(
path: string,
{
method = "GET",
body,
cookie,
headers,
}: {
method?: string;
body?: unknown;
cookie?: string;
headers?: Record<string, string>;
} = {},
): Promise<ApiResponse<T>> {
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<JsonObject> {
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`;
}