feat: add bootstrap script for API test fixtures and enhance live API tests
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
.wrangler/
|
.wrangler/
|
||||||
.dev.vars
|
.dev.vars
|
||||||
|
.api-test.env
|
||||||
coverage/
|
coverage/
|
||||||
reports/
|
reports/
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -13,10 +13,16 @@ Run repository tests without network dependencies:
|
|||||||
pnpm test
|
pnpm test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Create or refresh writable live-test fixtures:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm api:test:bootstrap
|
||||||
|
```
|
||||||
|
|
||||||
Run live smoke scenarios:
|
Run live smoke scenarios:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
API_BASE_URL=https://cfw-auth.bowong.cc/api/auth pnpm test tests/auth-live-api.test.ts
|
pnpm api:test:live
|
||||||
```
|
```
|
||||||
|
|
||||||
Run OpenAPI contract checks:
|
Run OpenAPI contract checks:
|
||||||
@@ -41,6 +47,15 @@ API_BASE_URL=https://cfw-auth.bowong.cc/api/auth MAX_EXAMPLES=100 pnpm api:test:
|
|||||||
- `ALLOW_AUTH_WRITE_TESTS=true`: enables Vitest scenarios that create authentication data.
|
- `ALLOW_AUTH_WRITE_TESTS=true`: enables Vitest scenarios that create authentication data.
|
||||||
- `MAX_EXAMPLES`, `TEST_SEED`, `REQUEST_TIMEOUT`, `SCHEMATHESIS_PHASES`, `SCHEMATHESIS_CHECKS`, `REPORT_DIR`: tune Schemathesis execution.
|
- `MAX_EXAMPLES`, `TEST_SEED`, `REQUEST_TIMEOUT`, `SCHEMATHESIS_PHASES`, `SCHEMATHESIS_CHECKS`, `REPORT_DIR`: tune Schemathesis execution.
|
||||||
|
|
||||||
|
`pnpm api:test:bootstrap` writes `.api-test.env`, which is ignored by git. It contains generated passwords, session cookies, API keys, and fixture IDs for later live tests:
|
||||||
|
|
||||||
|
- `API_TEST_USER_ID`, `API_TEST_USER_EMAIL`, `API_TEST_SESSION_COOKIE`
|
||||||
|
- `API_TEST_ADMIN_ID`, `API_TEST_ADMIN_EMAIL`, `API_TEST_ADMIN_SESSION_COOKIE`
|
||||||
|
- `API_TEST_ORGANIZATION_ID`, `API_TEST_ORGANIZATION_SLUG`
|
||||||
|
- `API_TEST_API_KEY_ID`, `API_TEST_API_KEY`
|
||||||
|
|
||||||
|
The bootstrap script creates users through the public auth API, then uses Wrangler D1 access to mark only those namespaced test users as email-verified and to assign the test admin role. This keeps administrator bootstrap outside the public auth API.
|
||||||
|
|
||||||
## Coverage
|
## Coverage
|
||||||
|
|
||||||
OpenAPI automation covers static schema linting and schema-driven request/response checks. The default Schemathesis phases are `examples,coverage` to avoid broad fuzzing against production by accident.
|
OpenAPI automation covers static schema linting and schema-driven request/response checks. The default Schemathesis phases are `examples,coverage` to avoid broad fuzzing against production by accident.
|
||||||
@@ -52,6 +67,7 @@ Vitest live tests currently cover:
|
|||||||
- anonymous `GET /get-session` behavior.
|
- anonymous `GET /get-session` behavior.
|
||||||
- invalid email sign-in rejection.
|
- invalid email sign-in rejection.
|
||||||
- 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.
|
||||||
|
|
||||||
## Safety
|
## Safety
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"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: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",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"ready": "pnpm db:check && pnpm typecheck && pnpm test"
|
"ready": "pnpm db:check && pnpm typecheck && pnpm test"
|
||||||
},
|
},
|
||||||
|
|||||||
315
scripts/bootstrap-api-test-fixtures.mjs
Normal file
315
scripts/bootstrap-api-test-fixtures.mjs
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { randomBytes } from "node:crypto";
|
||||||
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
|
||||||
|
const API_BASE_URL = trimTrailingSlash(
|
||||||
|
process.env.API_BASE_URL ?? "https://cfw-auth.bowong.cc/api/auth",
|
||||||
|
);
|
||||||
|
const TRUSTED_ORIGIN = process.env.API_TEST_ORIGIN ?? "http://localhost:8787";
|
||||||
|
const RUN_ID = process.env.RUN_ID ?? `api-test-${new Date().toISOString().slice(0, 10)}`;
|
||||||
|
const ENV_FILE = process.env.API_TEST_ENV_FILE ?? ".api-test.env";
|
||||||
|
const D1_DATABASE = process.env.API_TEST_D1_DATABASE ?? "cfw-auth";
|
||||||
|
const USE_REMOTE_D1 = process.env.API_TEST_USE_REMOTE_D1 !== "false";
|
||||||
|
|
||||||
|
const existingEnv = readEnvFile(ENV_FILE);
|
||||||
|
const password = existingEnv.API_TEST_PASSWORD ?? makePassword();
|
||||||
|
const adminPassword = existingEnv.API_TEST_ADMIN_PASSWORD ?? makePassword();
|
||||||
|
const userEmail = existingEnv.API_TEST_USER_EMAIL ?? `${slug(RUN_ID)}-user@example.invalid`;
|
||||||
|
const adminEmail = existingEnv.API_TEST_ADMIN_EMAIL ?? `${slug(RUN_ID)}-admin@example.invalid`;
|
||||||
|
const organizationSlug =
|
||||||
|
existingEnv.API_TEST_ORGANIZATION_SLUG ?? `${slug(RUN_ID)}-org`.slice(0, 48);
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
API_BASE_URL,
|
||||||
|
API_TEST_ORIGIN: TRUSTED_ORIGIN,
|
||||||
|
RUN_ID,
|
||||||
|
API_TEST_USER_EMAIL: userEmail,
|
||||||
|
API_TEST_PASSWORD: password,
|
||||||
|
API_TEST_ADMIN_EMAIL: adminEmail,
|
||||||
|
API_TEST_ADMIN_PASSWORD: adminPassword,
|
||||||
|
API_TEST_ORGANIZATION_SLUG: organizationSlug,
|
||||||
|
};
|
||||||
|
|
||||||
|
await ensureSignedUp({
|
||||||
|
email: userEmail,
|
||||||
|
password,
|
||||||
|
name: `API Test User ${RUN_ID}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await ensureSignedUp({
|
||||||
|
email: adminEmail,
|
||||||
|
password: adminPassword,
|
||||||
|
name: `API Test Admin ${RUN_ID}`,
|
||||||
|
});
|
||||||
|
const databaseUser = getUserByEmail(userEmail);
|
||||||
|
const databaseAdmin = getUserByEmail(adminEmail);
|
||||||
|
if (!databaseUser || !databaseAdmin) {
|
||||||
|
throw new Error("Created test users were not found in D1.");
|
||||||
|
}
|
||||||
|
|
||||||
|
markVerifiedAndAdmin({
|
||||||
|
userId: databaseUser.id,
|
||||||
|
adminId: databaseAdmin.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userSession = await signIn({ email: userEmail, password });
|
||||||
|
state.API_TEST_SESSION_COOKIE = userSession.cookie;
|
||||||
|
state.API_TOKEN = userSession.token ?? "";
|
||||||
|
state.API_TEST_USER_ID = userSession.user.id;
|
||||||
|
|
||||||
|
const adminSession = await signIn({ email: adminEmail, password: adminPassword });
|
||||||
|
state.API_TEST_ADMIN_SESSION_COOKIE = adminSession.cookie;
|
||||||
|
state.API_TEST_ADMIN_TOKEN = adminSession.token ?? "";
|
||||||
|
state.API_TEST_ADMIN_ID = adminSession.user.id;
|
||||||
|
|
||||||
|
const organization = await ensureOrganization({
|
||||||
|
cookie: userSession.cookie,
|
||||||
|
name: `API Test Org ${RUN_ID}`,
|
||||||
|
slug: organizationSlug,
|
||||||
|
});
|
||||||
|
if (organization?.id) {
|
||||||
|
state.API_TEST_ORGANIZATION_ID = organization.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = await createApiKey({ cookie: userSession.cookie, runId: RUN_ID });
|
||||||
|
if (apiKey?.id) {
|
||||||
|
state.API_TEST_API_KEY_ID = apiKey.id;
|
||||||
|
}
|
||||||
|
if (apiKey?.key) {
|
||||||
|
state.API_TEST_API_KEY = apiKey.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeEnvFile(ENV_FILE, state);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
envFile: ENV_FILE,
|
||||||
|
apiBaseUrl: API_BASE_URL,
|
||||||
|
runId: RUN_ID,
|
||||||
|
user: { id: state.API_TEST_USER_ID, email: userEmail },
|
||||||
|
admin: { id: state.API_TEST_ADMIN_ID, email: adminEmail },
|
||||||
|
organization: state.API_TEST_ORGANIZATION_ID
|
||||||
|
? { id: state.API_TEST_ORGANIZATION_ID, slug: organizationSlug }
|
||||||
|
: null,
|
||||||
|
apiKey: state.API_TEST_API_KEY_ID ? { id: state.API_TEST_API_KEY_ID } : null,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
function trimTrailingSlash(value) {
|
||||||
|
return value.replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function slug(value) {
|
||||||
|
return value
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "")
|
||||||
|
.slice(0, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePassword() {
|
||||||
|
return `ApiTest-${randomBytes(12).toString("hex")}-Aa1!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readEnvFile(path) {
|
||||||
|
if (!existsSync(path)) return {};
|
||||||
|
const out = {};
|
||||||
|
for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
||||||
|
if (!line || line.trimStart().startsWith("#")) continue;
|
||||||
|
const index = line.indexOf("=");
|
||||||
|
if (index === -1) continue;
|
||||||
|
const key = line.slice(0, index).trim();
|
||||||
|
const raw = line.slice(index + 1).trim();
|
||||||
|
out[key] = raw.replace(/^"|"$/g, "").replace(/\\"/g, '"');
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeEnvFile(path, values) {
|
||||||
|
const lines = [
|
||||||
|
"# Generated by scripts/bootstrap-api-test-fixtures.mjs",
|
||||||
|
"# Contains test credentials. Do not commit.",
|
||||||
|
];
|
||||||
|
for (const [key, value] of Object.entries(values)) {
|
||||||
|
lines.push(`${key}=${quoteEnv(String(value ?? ""))}`);
|
||||||
|
}
|
||||||
|
writeFileSync(path, `${lines.join("\n")}\n`, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function quoteEnv(value) {
|
||||||
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureSignedUp({ email, password, name }) {
|
||||||
|
const response = await request("/sign-up/email", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
name,
|
||||||
|
rememberMe: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) return response.body;
|
||||||
|
if (response.status === 422 || response.status === 400) {
|
||||||
|
const user = getUserByEmail(email);
|
||||||
|
if (user) return { user };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Failed to create or load ${email}: ${response.status} ${JSON.stringify(response.body)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signIn({ email, password }) {
|
||||||
|
const response = await request("/sign-in/email", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
rememberMe: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`Failed to sign in ${email}: ${response.status} ${JSON.stringify(response.body)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookie = sessionCookieFromSetCookie(response.setCookie);
|
||||||
|
if (!cookie) throw new Error(`Sign-in for ${email} did not return a session cookie.`);
|
||||||
|
const session = await request("/get-session", { cookie });
|
||||||
|
if (session.status !== 200 || !session.body?.user?.id) {
|
||||||
|
throw new Error(`Failed to read session for ${email}: ${session.status}`);
|
||||||
|
}
|
||||||
|
return { cookie, token: response.body?.token, user: session.body.user };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureOrganization({ cookie, name, slug }) {
|
||||||
|
const response = await request("/organization/create", {
|
||||||
|
method: "POST",
|
||||||
|
cookie,
|
||||||
|
body: {
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
keepCurrentActiveOrganization: true,
|
||||||
|
metadata: { runId: RUN_ID },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) return response.body;
|
||||||
|
if (response.status === 400 || response.status === 422) {
|
||||||
|
const existing = getOrganizationBySlug(slug);
|
||||||
|
if (existing) return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(`Organization fixture not created: ${response.status} ${JSON.stringify(response.body)}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createApiKey({ cookie, runId }) {
|
||||||
|
if (existingEnv.API_TEST_API_KEY && existingEnv.API_TEST_API_KEY_ID) {
|
||||||
|
return { id: existingEnv.API_TEST_API_KEY_ID, key: existingEnv.API_TEST_API_KEY };
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request("/api-key/create", {
|
||||||
|
method: "POST",
|
||||||
|
cookie,
|
||||||
|
body: {
|
||||||
|
name: `api-test-${runId}`,
|
||||||
|
expiresIn: null,
|
||||||
|
remaining: null,
|
||||||
|
metadata: { runId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) return response.body;
|
||||||
|
console.warn(`API key fixture not created: ${response.status} ${JSON.stringify(response.body)}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request(path, { method = "GET", body, cookie } = {}) {
|
||||||
|
const headers = {
|
||||||
|
accept: "application/json",
|
||||||
|
origin: TRUSTED_ORIGIN,
|
||||||
|
};
|
||||||
|
if (body !== undefined) headers["content-type"] = "application/json";
|
||||||
|
if (cookie) headers.cookie = cookie;
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body === undefined ? undefined : JSON.stringify(body),
|
||||||
|
redirect: "manual",
|
||||||
|
});
|
||||||
|
const text = await response.text();
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
body: text ? JSON.parse(text) : null,
|
||||||
|
setCookie: response.headers.getSetCookie?.() ?? splitSetCookie(response.headers.get("set-cookie")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitSetCookie(value) {
|
||||||
|
if (!value) return [];
|
||||||
|
return value.split(/,(?=\s*[^;,]+=)/g);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sessionCookieFromSetCookie(setCookieHeaders) {
|
||||||
|
return setCookieHeaders
|
||||||
|
.map((entry) => entry.split(";")[0])
|
||||||
|
.filter((entry) => /better-auth\.session|session_token|session/.test(entry))
|
||||||
|
.join("; ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function markVerifiedAndAdmin({ userId, adminId }) {
|
||||||
|
const userIdSql = sqlString(userId);
|
||||||
|
const adminIdSql = sqlString(adminId);
|
||||||
|
const sql = [
|
||||||
|
`update "user" set "emailVerified" = 1 where "id" in (${userIdSql}, ${adminIdSql});`,
|
||||||
|
`update "user" set "role" = 'admin' where "id" = ${adminIdSql};`,
|
||||||
|
].join(" ");
|
||||||
|
runD1(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserByEmail(email) {
|
||||||
|
const rows = runD1(
|
||||||
|
`select "id", "name", "email", "emailVerified", "role" from "user" where "email" = ${sqlString(
|
||||||
|
email.toLowerCase(),
|
||||||
|
)} limit 1;`,
|
||||||
|
);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrganizationBySlug(organizationSlugValue) {
|
||||||
|
const rows = runD1(
|
||||||
|
`select "id", "name", "slug", "createdAt" from "organization" where "slug" = ${sqlString(
|
||||||
|
organizationSlugValue,
|
||||||
|
)} limit 1;`,
|
||||||
|
);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runD1(command) {
|
||||||
|
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);
|
||||||
|
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
||||||
|
if (!first?.success) {
|
||||||
|
throw new Error(`D1 command failed: ${JSON.stringify(parsed)}`);
|
||||||
|
}
|
||||||
|
return first.results ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function sqlString(value) {
|
||||||
|
return `'${String(value).replace(/'/g, "''")}'`;
|
||||||
|
}
|
||||||
@@ -1,12 +1,24 @@
|
|||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
const API_BASE_URL = process.env.API_BASE_URL?.replace(/\/+$/, "");
|
const apiTestEnv = readApiTestEnv();
|
||||||
const API_TOKEN = process.env.API_TOKEN;
|
const API_BASE_URL = (process.env.API_BASE_URL ?? apiTestEnv.API_BASE_URL)?.replace(/\/+$/, "");
|
||||||
const ALLOW_AUTH_WRITE_TESTS = process.env.ALLOW_AUTH_WRITE_TESTS === "true";
|
const API_TOKEN = process.env.API_TOKEN ?? apiTestEnv.API_TOKEN;
|
||||||
const RUN_ID = process.env.RUN_ID ?? `api-test-${Date.now()}`;
|
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 API_TEST_API_KEY_ID = process.env.API_TEST_API_KEY_ID ?? apiTestEnv.API_TEST_API_KEY_ID;
|
||||||
|
const ALLOW_AUTH_WRITE_TESTS =
|
||||||
|
process.env.ALLOW_AUTH_WRITE_TESTS === "true" || apiTestEnv.ALLOW_AUTH_WRITE_TESTS === "true";
|
||||||
|
const RUN_ID = process.env.RUN_ID ?? apiTestEnv.RUN_ID ?? `api-test-${Date.now()}`;
|
||||||
|
|
||||||
const liveDescribe = API_BASE_URL ? describe : describe.skip;
|
const liveDescribe = API_BASE_URL ? describe : describe.skip;
|
||||||
const writeDescribe = API_BASE_URL && ALLOW_AUTH_WRITE_TESTS ? describe : describe.skip;
|
const writeDescribe = API_BASE_URL && ALLOW_AUTH_WRITE_TESTS ? describe : describe.skip;
|
||||||
|
const fixtureDescribe =
|
||||||
|
API_BASE_URL && API_TEST_SESSION_COOKIE && API_TEST_ADMIN_SESSION_COOKIE ? describe : describe.skip;
|
||||||
|
|
||||||
interface OpenAPIDocument {
|
interface OpenAPIDocument {
|
||||||
openapi: string;
|
openapi: string;
|
||||||
@@ -46,6 +58,25 @@ async function requestJson<T = unknown>(
|
|||||||
return { status: response.status, headers: response.headers, body };
|
return { status: response.status, headers: response.headers, body };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
function expectJsonResponse(headers: Headers): void {
|
function expectJsonResponse(headers: Headers): void {
|
||||||
expect(headers.get("content-type") ?? "").toContain("application/json");
|
expect(headers.get("content-type") ?? "").toContain("application/json");
|
||||||
}
|
}
|
||||||
@@ -136,3 +167,71 @@ writeDescribe("cfw-auth live API write scenarios", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fixtureDescribe("cfw-auth live API fixture-backed scenarios", () => {
|
||||||
|
it("reads the bootstrapped user session", async () => {
|
||||||
|
const response = await requestJson<{ session: JsonObject; user: JsonObject }>("/get-session", {
|
||||||
|
headers: {
|
||||||
|
cookie: API_TEST_SESSION_COOKIE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.user).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: API_TEST_USER_ID,
|
||||||
|
emailVerified: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets the bootstrapped admin read the test user", async () => {
|
||||||
|
const response = await requestJson<JsonObject>(
|
||||||
|
`/admin/get-user?id=${encodeURIComponent(API_TEST_USER_ID ?? "")}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
cookie: API_TEST_ADMIN_SESSION_COOKIE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: API_TEST_USER_ID,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lists the bootstrapped organization and API key for the test user", async () => {
|
||||||
|
const organizations = await requestJson<JsonObject[]>("/organization/list", {
|
||||||
|
headers: {
|
||||||
|
cookie: API_TEST_SESSION_COOKIE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(organizations.status).toBe(200);
|
||||||
|
expect(organizations.body).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: API_TEST_ORGANIZATION_ID,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const apiKeys = await requestJson<{ apiKeys: JsonObject[] }>("/api-key/list", {
|
||||||
|
headers: {
|
||||||
|
cookie: API_TEST_SESSION_COOKIE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(apiKeys.status).toBe(200);
|
||||||
|
expect(apiKeys.body.apiKeys).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: API_TEST_API_KEY_ID,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user