import { expect, test } from "@playwright/test"; import { useRealApi } from "./support"; const apiBase = process.env.E2E_API_BASE ?? "http://127.0.0.1:8000"; const publicBase = process.env.E2E_PUBLIC_BASE_URL ?? "https://vignette.chanpaca.net"; const publicApiBase = process.env.E2E_PUBLIC_API_BASE ?? "https://api-vignette.chanpaca.net"; function isLocalHostname(hostname: string): boolean { return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"; } test.describe("auth domain policy", () => { test.beforeEach(async ({ page }) => { await useRealApi(page); }); test("requires authentication for the persona catalog", async ({ page }) => { const response = await page.request.get("/api/personas"); expect(response.status(), await response.text()).toBe(401); }); test("allows only the configured school and operator email domains", async ({ page, }, testInfo) => { const slug = `${testInfo.project.name}.${testInfo.workerIndex}.${testInfo.retry}.${Date.now()}`; const allowed = [ `domain-learner.${slug}@hs.ac.kr`, `domain-admin.${slug}@twentyoz.kr`, ]; for (const email of allowed) { const response = await page.request.post("/api/auth/dev-login", { data: { email: email.toLowerCase(), role: email.endsWith("@twentyoz.kr") ? "admin" : "learner", display_name: "Domain Policy E2E", }, }); expect(response.ok(), await response.text()).toBeTruthy(); } const denied = await page.request.post("/api/auth/dev-login", { data: { email: `domain-denied.${slug}@example.com`.toLowerCase(), role: "learner", display_name: "Denied Domain", }, }); const deniedText = await denied.text(); expect(denied.status(), deniedText).toBe(403); expect(deniedText).toContain("email domain is not allowed"); }); test("shows Google OAuth readiness without exposing a raw JSON error page", async ({ page, }) => { const configResponse = await page.request.get("/api/auth/config"); expect(configResponse.ok(), await configResponse.text()).toBeTruthy(); const config = (await configResponse.json()) as { google_oauth_configured: boolean; allowed_email_domains: string[]; redirect_uri: string; dev_login_enabled: boolean; }; await page.goto("/login"); const googleButtons = page.locator(".lg-obtn"); await expect(googleButtons).toHaveCount(2); await expect(page.locator(".lg-policy b")).toContainText(config.allowed_email_domains); const redirectHost = new URL(config.redirect_uri).hostname; const devOAuthUnavailable = config.dev_login_enabled && !isLocalHostname(redirectHost); if (config.dev_login_enabled) { await expect(page.locator(".lg-dev")).toBeVisible(); } else { await expect(page.locator(".lg-dev")).toHaveCount(0); } if (config.google_oauth_configured && !devOAuthUnavailable) { await expect(googleButtons.first()).toBeEnabled(); await expect(page.locator(".lg-config")).toHaveCount(0); } else { await expect(googleButtons.first()).toBeDisabled(); await expect(googleButtons.nth(1)).toBeDisabled(); await expect(page.locator(".lg-config")).toBeVisible(); await expect(page).toHaveURL(/\/login$/); await expect(page.locator("body")).not.toContainText("Google OAuth is not configured"); if (devOAuthUnavailable) { await expect(page.locator(".lg-config")).toContainText("로컬 테스트 계정으로 로그인"); await page.goto("/api/auth/login?provider=google&next=%2Flearn"); await expect(page).toHaveURL(/\/login\?oauth=local_oauth_unavailable$/); await expect(page.locator(".lg-error")).toContainText("로컬 개발 주소에서는 Google OAuth"); return; } await page.goto("/api/auth/login?provider=google&next=%2Flearn"); await expect(page).toHaveURL(/\/login\?oauth=not_configured$/); await expect(page.locator(".lg-error")).toContainText("Google 로그인이 아직 연결되지 않았습니다"); await expect(page.locator("body")).not.toContainText("Google OAuth is not configured"); } }); test("shows the concrete OAuth failure reason on the login screen", async ({ page }) => { await page.goto("/login?oauth=provider_error"); const error = page.locator(".lg-error"); await expect(error).toContainText("Google이 인증 코드를 발급하지 못했습니다"); await expect(error).toContainText("오류 코드: provider_error"); }); test("logs in locally with the server dev session and redirects to learner home", async ({ page, }) => { await page.goto("/login"); await expect(page.locator(".lg-dev")).toBeVisible(); await Promise.all([ page.waitForURL(/\/learn(?:$|[/?#])/), page.getByRole("button", { name: "로컬 테스트 계정으로 계속" }).click(), ]); const me = await page.request.get("/api/auth/me"); expect(me.status(), await me.text()).toBe(200); await expect(page.getByRole("heading", { name: "오늘의 회기를 준비합니다." })).toBeVisible(); }); test("keeps dev login closed for the public API origin", async ({ request }) => { const publicHeaders = { origin: "https://vignette.chanpaca.net", "x-forwarded-host": "api-vignette.chanpaca.net", }; const configResponse = await request.get(`${apiBase}/auth/config`, { headers: publicHeaders, }); expect(configResponse.ok(), await configResponse.text()).toBeTruthy(); const config = (await configResponse.json()) as { dev_login_enabled: boolean }; expect(config.dev_login_enabled).toBe(false); const response = await request.post(`${apiBase}/auth/dev-login`, { headers: publicHeaders, data: { email: "public-dev-login-probe@hs.ac.kr", role: "learner", display_name: "Public Probe", }, }); expect(response.status(), await response.text()).toBe(404); }); test("keeps the public login screen on real Google OAuth only", async ({ page, request }) => { const configResponse = await request.get(`${publicApiBase}/auth/config`, { headers: { origin: publicBase, "x-forwarded-host": "api-vignette.chanpaca.net", }, }); expect(configResponse.ok(), await configResponse.text()).toBeTruthy(); const config = (await configResponse.json()) as { google_oauth_configured: boolean; allowed_email_domains: string[]; dev_login_enabled: boolean; }; expect(config).toMatchObject({ google_oauth_configured: true, dev_login_enabled: false, }); expect(config.allowed_email_domains).toEqual(expect.arrayContaining(["hs.ac.kr", "twentyoz.kr"])); await page.goto(`${publicBase}/login`, { waitUntil: "domcontentloaded" }); await page.waitForLoadState("networkidle", { timeout: 15_000 }).catch(() => undefined); await expect(page.locator(".lg-obtn")).toHaveCount(2); await expect(page.locator(".lg-dev")).toHaveCount(0); await expect(page.getByText("로컬 테스트")).toHaveCount(0); await expect(page.getByText("로컬 테스트 계정으로 계속")).toHaveCount(0); await expect(page.locator("body")).not.toContainText("Google OAuth is not configured"); await expect(page.locator(".lg-obtn").first()).toBeEnabled(); await Promise.all([ page.waitForURL(/accounts\.google\.com/, { timeout: 20_000 }), page.locator(".lg-obtn").first().click(), ]); expect(page.url()).toContain("client_id="); expect(page.url()).toContain("redirect_uri=https%3A%2F%2Fapi-vignette.chanpaca.net%2Fauth%2Fcallback"); }); });