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, completes onboarding, and redirects to learner home", async ({ page, }, testInfo) => { await page.goto("/login"); await expect(page.locator(".lg-dev")).toBeVisible(); const email = `onboarding.${testInfo.workerIndex}.${Date.now()}@hs.ac.kr`; const login = await page.request.post("/api/auth/dev-login", { data: { email, role: "learner", display_name: "온보딩 미완료 학습자", }, }); expect(login.ok(), await login.text()).toBeTruthy(); await page.goto("/"); await page.waitForURL(/\/onboarding(?:$|[/?#])/); await expect(page.getByRole("heading", { name: "가입 정보를 입력합니다." })).toBeVisible(); await expect(page.locator(".vg-topbar")).toHaveCount(0); await expect(page.locator(".vg-nav")).toHaveCount(0); await expect(page.locator(".ob-card")).toHaveCount(0); await expect(page.locator(".ob-docs")).toHaveCount(0); await expect(page.getByText("온보딩 정보를 확인하고 있습니다.")).toHaveCount(0); await expect(page.locator(".ob-legal")).toContainText("서비스 이용약관"); await expect(page.locator(".ob-legal")).toContainText("개인정보 처리방침"); for (const blockedPath of [ "/learn", "/settings", "/admin", "/dev/avatar-preview", "/login", ]) { await page.goto(blockedPath); await page.waitForURL(/\/onboarding(?:$|[/?#])/); await expect(page.getByRole("heading", { name: "가입 정보를 입력합니다." })).toBeVisible(); await expect(page.locator(".vg-nav")).toHaveCount(0); } await page.getByLabel("닉네임").fill("비넷 학습자"); await page.getByLabel("자기소개").fill("상담 시뮬레이션에서 라포 형성을 집중 연습합니다."); await page.getByLabel("아바타 이미지").setInputFiles({ name: "avatar.png", mimeType: "image/png", buffer: Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAFgwJ/lTQvYwAAAABJRU5ErkJggg==", "base64", ), }); await expect(page.getByText("다른 이미지 선택")).toBeVisible(); await page.getByLabel("이름").fill("로컬 테스트 학습자"); await page.getByLabel("소속").fill("한신대학교"); await page.getByLabel("학과/부서").fill("상담심리학과"); await page.getByLabel("학년/직위").fill("3학년"); await page.getByLabel("연락처").fill("010-1234-5678"); await page.getByLabel("주소/수령지").fill("경기도 오산시 한신대학교"); await page.getByLabel("서비스 이용약관에 동의합니다.").check(); await page.getByLabel("개인정보 수집 및 이용에 동의합니다.").check(); 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); const meBody = (await me.json()) as { onboarding_completed_at?: number | null; nickname?: string; self_introduction?: string; avatar_url?: string; }; expect(meBody.onboarding_completed_at).toBeTruthy(); expect(meBody.nickname).toBe("비넷 학습자"); expect(meBody.self_introduction).toContain("라포 형성"); expect(meBody.avatar_url).toContain("/uploads/profile-avatars/"); 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"); }); });