Files
cfw-auth/scripts/bootstrap-api-test-fixtures.mjs

344 lines
10 KiB
JavaScript

#!/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,
});
let 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) {
userSession = {
...userSession,
cookie: await setActiveOrganization({
cookie: userSession.cookie,
organizationId: organization.id,
}),
};
state.API_TEST_SESSION_COOKIE = userSession.cookie;
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 setActiveOrganization({ cookie, organizationId }) {
const response = await request("/organization/set-active", {
method: "POST",
cookie,
body: { organizationId },
});
if (response.status !== 200) {
throw new Error(
`Failed to set active organization ${organizationId}: ${response.status} ${JSON.stringify(response.body)}`,
);
}
return sessionCookieFromSetCookie(response.setCookie) || cookie;
}
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) {
const cookies = new Map();
for (const entry of setCookieHeaders
.map((entry) => entry.split(";")[0])
.filter((entry) => /better-auth\.session|session_token|session/.test(entry))) {
const index = entry.indexOf("=");
if (index !== -1) cookies.set(entry.slice(0, index), entry);
}
return [...cookies.values()].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, "''")}'`;
}