192 lines
7.6 KiB
TypeScript
192 lines
7.6 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 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");
|
|
});
|
|
});
|