vignette/apps/web/e2e/auth.spec.ts
2026-06-28 12:18:20 +09:00

255 lines
10 KiB
TypeScript

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