vignette/apps/web/e2e/public-auth-turn.spec.ts
2026-06-26 14:47:00 +09:00

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);
}
}
});
});