feat: add business scenario tests for user and organization management
This commit is contained in:
@@ -4,6 +4,7 @@ This project has two API automation layers:
|
|||||||
|
|
||||||
- `scripts/api-contract-test.sh` runs OpenAPI contract checks with Schemathesis against the schema at `OPENAPI_SCHEMA_URL`.
|
- `scripts/api-contract-test.sh` runs OpenAPI contract checks with Schemathesis against the schema at `OPENAPI_SCHEMA_URL`.
|
||||||
- `tests/auth-live-api.test.ts` runs focused Vitest live scenarios against a deployed or local Better Auth API.
|
- `tests/auth-live-api.test.ts` runs focused Vitest live scenarios against a deployed or local Better Auth API.
|
||||||
|
- `tests/auth-business-scenarios.test.ts` runs fixture-backed business workflows that create, update, and delete test data.
|
||||||
|
|
||||||
## Local Commands
|
## Local Commands
|
||||||
|
|
||||||
@@ -25,6 +26,12 @@ Run live smoke scenarios:
|
|||||||
pnpm api:test:live
|
pnpm api:test:live
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Run business workflow scenarios:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm api:test:business
|
||||||
|
```
|
||||||
|
|
||||||
Run OpenAPI contract checks:
|
Run OpenAPI contract checks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -69,8 +76,17 @@ Vitest live tests currently cover:
|
|||||||
- optional namespaced email sign-up when write tests are explicitly enabled.
|
- optional namespaced email sign-up when write tests are explicitly enabled.
|
||||||
- bootstrapped user session, admin user lookup, organization listing, and API key listing when `.api-test.env` exists.
|
- bootstrapped user session, admin user lookup, organization listing, and API key listing when `.api-test.env` exists.
|
||||||
|
|
||||||
|
Business scenario tests currently cover:
|
||||||
|
|
||||||
|
- Email sign-up requiring verification before password sign-in.
|
||||||
|
- Verified user sign-in, session read, and sign-out.
|
||||||
|
- Admin create/read/update/ban/unban/remove user lifecycle.
|
||||||
|
- Regular user rejection from admin endpoints.
|
||||||
|
- Organization active selection plus team create/list-members/update/remove workflow.
|
||||||
|
- API key create/read/server-only update rejection/delete workflow.
|
||||||
|
|
||||||
## Safety
|
## Safety
|
||||||
|
|
||||||
The OpenAPI document includes account, organization, session, API key, and admin write operations. Do not run broad fuzzing or stateful phases against production unless the target is an isolated test environment.
|
The OpenAPI document includes account, organization, session, API key, and admin write operations. Do not run broad fuzzing or stateful phases against production unless the target is an isolated test environment.
|
||||||
|
|
||||||
Write scenarios are skipped unless `ALLOW_AUTH_WRITE_TESTS=true`. Generated users use `RUN_ID` and the `example.invalid` domain, but there is no public delete-user cleanup in the default scenario because Better Auth deletion normally requires a valid session and may require verification.
|
Write scenarios are intended for the current test environment. Generated users use `RUN_ID` and the `example.invalid` domain. Business scenarios create temporary users, teams, and API keys, then remove the resources whose lifecycle is part of the scenario.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"db:generate": "auth generate --config src/auth.migration.ts --output docs/schema/better-auth-target.sql -y",
|
"db:generate": "auth generate --config src/auth.migration.ts --output docs/schema/better-auth-target.sql -y",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"api:test:live": "vitest run tests/auth-live-api.test.ts",
|
"api:test:live": "vitest run tests/auth-live-api.test.ts",
|
||||||
|
"api:test:business": "vitest run tests/auth-business-scenarios.test.ts",
|
||||||
"api:test:contract": "bash scripts/api-contract-test.sh",
|
"api:test:contract": "bash scripts/api-contract-test.sh",
|
||||||
"api:test:bootstrap": "node scripts/bootstrap-api-test-fixtures.mjs",
|
"api:test:bootstrap": "node scripts/bootstrap-api-test-fixtures.mjs",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
|
|||||||
516
tests/auth-business-scenarios.test.ts
Normal file
516
tests/auth-business-scenarios.test.ts
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
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`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user