238 lines
7.4 KiB
TypeScript
238 lines
7.4 KiB
TypeScript
import { existsSync, readFileSync } from "node:fs";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const apiTestEnv = readApiTestEnv();
|
|
const API_BASE_URL = (process.env.API_BASE_URL ?? apiTestEnv.API_BASE_URL)?.replace(/\/+$/, "");
|
|
const API_TOKEN = process.env.API_TOKEN ?? apiTestEnv.API_TOKEN;
|
|
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 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 {
|
|
openapi: string;
|
|
info: {
|
|
title: string;
|
|
};
|
|
servers?: Array<{ url: string }>;
|
|
paths: Record<string, Record<string, unknown>>;
|
|
}
|
|
|
|
type JsonObject = Record<string, unknown>;
|
|
|
|
async function requestJson<T = unknown>(
|
|
path: string,
|
|
options: RequestInit = {},
|
|
): Promise<{ status: number; headers: Headers; body: T }> {
|
|
if (!API_BASE_URL) {
|
|
throw new Error("Set API_BASE_URL before running live API tests.");
|
|
}
|
|
|
|
const headers = new Headers(options.headers);
|
|
headers.set("accept", "application/json");
|
|
if (options.body && !headers.has("content-type")) {
|
|
headers.set("content-type", "application/json");
|
|
}
|
|
if (API_TOKEN) {
|
|
headers.set("authorization", `Bearer ${API_TOKEN}`);
|
|
}
|
|
|
|
const response = await fetch(new URL(`${API_BASE_URL}${path}`), {
|
|
...options,
|
|
headers,
|
|
});
|
|
const text = await response.text();
|
|
const body = text ? (JSON.parse(text) as T) : (undefined as T);
|
|
|
|
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 {
|
|
expect(headers.get("content-type") ?? "").toContain("application/json");
|
|
}
|
|
|
|
liveDescribe("cfw-auth live API contract smoke tests", () => {
|
|
it("serves the Better Auth OpenAPI document", async () => {
|
|
const response = await requestJson<OpenAPIDocument>("/open-api/generate-schema");
|
|
|
|
expect(response.status).toBe(200);
|
|
expectJsonResponse(response.headers);
|
|
expect(response.body).toMatchObject({
|
|
openapi: expect.stringMatching(/^3\./),
|
|
info: {
|
|
title: "Better Auth",
|
|
},
|
|
});
|
|
expect(response.body.paths).toEqual(
|
|
expect.objectContaining({
|
|
"/get-session": expect.any(Object),
|
|
"/ok": expect.any(Object),
|
|
"/sign-in/email": expect.any(Object),
|
|
"/sign-up/email": expect.any(Object),
|
|
}),
|
|
);
|
|
expect(Object.keys(response.body.paths).length).toBeGreaterThan(20);
|
|
});
|
|
|
|
it("reports API readiness from the documented ok endpoint", async () => {
|
|
const response = await requestJson<{ ok: boolean }>("/ok");
|
|
|
|
expect(response.status).toBe(200);
|
|
expectJsonResponse(response.headers);
|
|
expect(response.body).toEqual({ ok: true });
|
|
});
|
|
|
|
it("returns an explicit anonymous session state without credentials", async () => {
|
|
const response = await requestJson<null | JsonObject>("/get-session");
|
|
|
|
expect(response.status).toBe(200);
|
|
expectJsonResponse(response.headers);
|
|
expect(response.body).toBeNull();
|
|
});
|
|
|
|
it("rejects invalid email sign-in without creating account data", async () => {
|
|
const response = await requestJson<JsonObject>("/sign-in/email", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
email: `missing-${RUN_ID}@example.invalid`,
|
|
password: "not-the-right-password",
|
|
}),
|
|
});
|
|
|
|
expect([400, 401, 403]).toContain(response.status);
|
|
expectJsonResponse(response.headers);
|
|
expect(response.body).toEqual(
|
|
expect.objectContaining({
|
|
message: expect.any(String),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
writeDescribe("cfw-auth live API write scenarios", () => {
|
|
it("can sign up a namespaced email user when write tests are explicitly enabled", async () => {
|
|
const email = `${RUN_ID}@example.invalid`;
|
|
const response = await requestJson<{ token?: string | null; user?: JsonObject }>(
|
|
"/sign-up/email",
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
name: `API Test ${RUN_ID}`,
|
|
email,
|
|
password: "TestPassword123!",
|
|
rememberMe: false,
|
|
}),
|
|
},
|
|
);
|
|
|
|
expect(response.status).toBe(200);
|
|
expectJsonResponse(response.headers);
|
|
expect(response.body.user).toEqual(
|
|
expect.objectContaining({
|
|
id: expect.any(String),
|
|
email,
|
|
name: `API Test ${RUN_ID}`,
|
|
emailVerified: expect.any(Boolean),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
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,
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
});
|