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