160 lines
5.4 KiB
TypeScript
160 lines
5.4 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
const publicApiBase = process.env.E2E_PUBLIC_API_BASE ?? "https://api-vignette.chanpaca.net";
|
|
|
|
interface PublicHealthResponse {
|
|
status: string;
|
|
environment: string;
|
|
db: boolean;
|
|
engine: boolean;
|
|
}
|
|
|
|
async function browserFetchJson<T>(
|
|
page: import("@playwright/test").Page,
|
|
path: string,
|
|
init: RequestInit = {},
|
|
) {
|
|
return page.evaluate(
|
|
async ({ apiBase, apiPath, requestInit }) => {
|
|
const response = await fetch(`${apiBase}${apiPath}`, {
|
|
credentials: "include",
|
|
...requestInit,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
...(requestInit.headers ?? {}),
|
|
},
|
|
});
|
|
const text = await response.text();
|
|
let json: unknown = null;
|
|
try {
|
|
json = text ? JSON.parse(text) : null;
|
|
} catch {
|
|
json = null;
|
|
}
|
|
return {
|
|
ok: response.ok,
|
|
status: response.status,
|
|
text,
|
|
json,
|
|
};
|
|
},
|
|
{
|
|
apiBase: publicApiBase,
|
|
apiPath: path,
|
|
requestInit: {
|
|
...init,
|
|
body: typeof init.body === "string" ? init.body : undefined,
|
|
},
|
|
},
|
|
) as Promise<{ ok: boolean; status: number; text: string; json: T | null }>;
|
|
}
|
|
|
|
test.describe("public Google OAuth turn smoke", () => {
|
|
test("uses production-safe public API configuration @public-auth", async ({
|
|
request,
|
|
playwright,
|
|
}) => {
|
|
const healthResponse = await request.get(`${publicApiBase}/health`);
|
|
expect(healthResponse.ok(), await healthResponse.text()).toBeTruthy();
|
|
const health = (await healthResponse.json()) as PublicHealthResponse;
|
|
|
|
expect(health.environment, JSON.stringify(health)).not.toBe("dev");
|
|
expect(health.db, JSON.stringify(health)).toBe(true);
|
|
expect(health.engine, JSON.stringify(health)).toBe(true);
|
|
|
|
const configResponse = await request.get(`${publicApiBase}/auth/config`, {
|
|
headers: {
|
|
origin: "https://vignette.chanpaca.net",
|
|
"x-forwarded-host": "api-vignette.chanpaca.net",
|
|
},
|
|
});
|
|
expect(configResponse.ok(), await configResponse.text()).toBeTruthy();
|
|
await expect(await configResponse.json()).toMatchObject({
|
|
google_oauth_configured: true,
|
|
allowed_email_domains: expect.arrayContaining(["hs.ac.kr", "twentyoz.kr"]),
|
|
dev_login_enabled: false,
|
|
});
|
|
|
|
const unauthRequest = await playwright.request.newContext();
|
|
try {
|
|
const personasResponse = await unauthRequest.get(`${publicApiBase}/personas`);
|
|
expect(personasResponse.status(), await personasResponse.text()).toBe(401);
|
|
} finally {
|
|
await unauthRequest.dispose();
|
|
}
|
|
});
|
|
|
|
test("starts a real public session and completes one /turn @public-auth", async ({ page }) => {
|
|
if (!process.env.E2E_PUBLIC_STORAGE_STATE) {
|
|
throw new Error(
|
|
"Set E2E_PUBLIC_STORAGE_STATE to a Playwright storageState file captured after Google login.",
|
|
);
|
|
}
|
|
|
|
await page.goto("/learn", { waitUntil: "domcontentloaded" });
|
|
await page.waitForLoadState("networkidle", { timeout: 15_000 }).catch(() => undefined);
|
|
await expect(page).toHaveURL(/\/learn(?:$|[/?#])/);
|
|
await expect(page.getByRole("button", { name: "새 회기 시작" })).toBeVisible({
|
|
timeout: 15_000,
|
|
});
|
|
|
|
const me = await browserFetchJson<{ email?: string; role?: string }>(page, "/auth/me");
|
|
expect(me.status, me.text).toBe(200);
|
|
expect(me.json).toMatchObject({ role: "learner" });
|
|
|
|
const personas = await browserFetchJson<
|
|
Array<{ code: string; source: string; degraded: boolean }>
|
|
>(page, "/personas");
|
|
expect(personas.status, personas.text).toBe(200);
|
|
const persona = personas.json?.find(
|
|
(item) => item.source === "database" && !item.degraded,
|
|
);
|
|
expect(persona, personas.text).toBeTruthy();
|
|
|
|
let startedSessionId: string | null = null;
|
|
const started = await browserFetchJson<{
|
|
session_id: string;
|
|
degraded: boolean;
|
|
}>(page, "/sessions", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
persona_code: persona!.code,
|
|
theory_mode: "humanistic",
|
|
}),
|
|
});
|
|
expect(started.status, started.text).toBe(201);
|
|
expect(started.json).toMatchObject({ degraded: false });
|
|
expect(started.json?.session_id).toEqual(expect.any(String));
|
|
startedSessionId = started.json!.session_id;
|
|
|
|
try {
|
|
const turn = await browserFetchJson<{
|
|
client_reply?: string;
|
|
stage?: string;
|
|
turn_seq?: number;
|
|
}>(page, `/sessions/${startedSessionId}/turn`, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
text: "처음 오신 자리라 긴장될 수 있어요. 지금 가장 이야기하고 싶은 것부터 천천히 말해도 괜찮습니다.",
|
|
}),
|
|
});
|
|
expect(turn.status, turn.text).toBe(200);
|
|
expect(turn.json?.client_reply).toEqual(expect.any(String));
|
|
expect(turn.json!.client_reply!.length).toBeGreaterThan(0);
|
|
|
|
const detail = await browserFetchJson<{
|
|
session_id: string;
|
|
turns: Array<{ speaker: string; text: string }>;
|
|
}>(page, `/sessions/${startedSessionId}`);
|
|
expect(detail.status, detail.text).toBe(200);
|
|
expect(detail.json?.turns.length).toBeGreaterThanOrEqual(2);
|
|
} finally {
|
|
if (startedSessionId) {
|
|
await browserFetchJson(page, `/sessions/${startedSessionId}/end`, {
|
|
method: "POST",
|
|
body: JSON.stringify({}),
|
|
}).catch(() => undefined);
|
|
}
|
|
}
|
|
});
|
|
});
|