첫 커밋: 로컬 프로젝트 업로드
This commit is contained in:
456
baron-sso/userfront-e2e/tests/password-and-reset.spec.ts
Normal file
456
baron-sso/userfront-e2e/tests/password-and-reset.spec.ts
Normal file
@@ -0,0 +1,456 @@
|
||||
import {
|
||||
expect,
|
||||
type Locator,
|
||||
type Page,
|
||||
type Route,
|
||||
test,
|
||||
} from "@playwright/test";
|
||||
|
||||
type RequestCapture = {
|
||||
loginBody?: Record<string, unknown>;
|
||||
resetBody?: Record<string, unknown>;
|
||||
resetToken?: string | null;
|
||||
clientLogs: string[];
|
||||
};
|
||||
|
||||
const resetNewPasswordName =
|
||||
/^(새 비밀번호|ui\.userfront\.reset\.new_password)$/;
|
||||
const resetConfirmPasswordName =
|
||||
/^(새 비밀번호 확인|ui\.userfront\.reset\.confirm_password)$/;
|
||||
const resetSubmitButtonName = /^(비밀번호 변경|ui\.userfront\.reset\.submit)$/;
|
||||
|
||||
async function enableFlutterAccessibility(page: Page): Promise<void> {
|
||||
const button = page.getByRole("button", { name: "Enable accessibility" });
|
||||
if (await button.count()) {
|
||||
await button.first().evaluate((node) => {
|
||||
(node as HTMLElement).click();
|
||||
});
|
||||
await page.waitForTimeout(200);
|
||||
return;
|
||||
}
|
||||
await page.waitForTimeout(300);
|
||||
const placeholder = page.locator("flt-semantics-placeholder").first();
|
||||
if (await placeholder.count()) {
|
||||
await placeholder.evaluate((node) => {
|
||||
(node as HTMLElement).click();
|
||||
});
|
||||
await page.waitForTimeout(800);
|
||||
}
|
||||
}
|
||||
|
||||
type ScreenCoords = {
|
||||
signinPasswordTabX: number;
|
||||
signinTabY: number;
|
||||
signinLoginIdX: number;
|
||||
signinLoginIdY: number;
|
||||
signinPasswordX: number;
|
||||
signinPasswordY: number;
|
||||
signinSubmitX: number;
|
||||
signinSubmitY: number;
|
||||
resetNewPasswordX: number;
|
||||
resetNewPasswordY: number;
|
||||
resetConfirmPasswordX: number;
|
||||
resetConfirmPasswordY: number;
|
||||
resetSubmitX: number;
|
||||
resetSubmitY: number;
|
||||
};
|
||||
|
||||
const desktopCoords: ScreenCoords = {
|
||||
signinPasswordTabX: 522,
|
||||
signinTabY: 158,
|
||||
signinLoginIdX: 640,
|
||||
signinLoginIdY: 245,
|
||||
signinPasswordX: 640,
|
||||
signinPasswordY: 311,
|
||||
signinSubmitX: 640,
|
||||
signinSubmitY: 381,
|
||||
resetNewPasswordX: 640,
|
||||
resetNewPasswordY: 382,
|
||||
resetConfirmPasswordX: 640,
|
||||
resetConfirmPasswordY: 464,
|
||||
resetSubmitX: 640,
|
||||
resetSubmitY: 534,
|
||||
};
|
||||
|
||||
const mobileCoords: ScreenCoords = {
|
||||
signinPasswordTabX: 90,
|
||||
signinTabY: 158,
|
||||
signinLoginIdX: 206,
|
||||
signinLoginIdY: 268,
|
||||
signinPasswordX: 206,
|
||||
signinPasswordY: 334,
|
||||
signinSubmitX: 206,
|
||||
signinSubmitY: 399,
|
||||
resetNewPasswordX: 206,
|
||||
resetNewPasswordY: 382,
|
||||
resetConfirmPasswordX: 206,
|
||||
resetConfirmPasswordY: 464,
|
||||
resetSubmitX: 206,
|
||||
resetSubmitY: 534,
|
||||
};
|
||||
|
||||
function coordsFor(page: Page): ScreenCoords {
|
||||
const viewport = page.viewportSize();
|
||||
return (viewport?.width ?? 1280) <= 500 ? mobileCoords : desktopCoords;
|
||||
}
|
||||
|
||||
function isMobileProject(page: Page): boolean {
|
||||
const viewport = page.viewportSize();
|
||||
return (viewport?.width ?? 1280) <= 500;
|
||||
}
|
||||
|
||||
async function clickPasswordTab(page: Page): Promise<void> {
|
||||
if (isMobileProject(page)) {
|
||||
return;
|
||||
}
|
||||
const coords = coordsFor(page);
|
||||
await page.waitForTimeout(900);
|
||||
const pane = page.locator("flt-glass-pane");
|
||||
await pane.click({
|
||||
position: { x: coords.signinPasswordTabX, y: coords.signinTabY },
|
||||
force: true,
|
||||
});
|
||||
await page.waitForTimeout(120);
|
||||
await pane.click({
|
||||
position: { x: coords.signinPasswordTabX, y: coords.signinTabY },
|
||||
force: true,
|
||||
});
|
||||
await page.waitForTimeout(200);
|
||||
}
|
||||
|
||||
async function fillAt(
|
||||
page: Page,
|
||||
x: number,
|
||||
y: number,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
const pane = page.locator("flt-glass-pane");
|
||||
await pane.click({ position: { x, y }, force: true });
|
||||
await page.waitForTimeout(100);
|
||||
await page.keyboard.press("Control+A");
|
||||
await page.keyboard.press("Backspace");
|
||||
await page.keyboard.type(value);
|
||||
}
|
||||
|
||||
async function typeIntoAccessibleField(
|
||||
page: Page,
|
||||
field: Locator,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
await field.click({ force: true });
|
||||
await page.waitForTimeout(100);
|
||||
await page.keyboard.press("Control+A");
|
||||
await page.keyboard.press("Backspace");
|
||||
await page.keyboard.type(value);
|
||||
}
|
||||
|
||||
async function fillPasswordLoginForm(
|
||||
page: Page,
|
||||
loginId: string,
|
||||
password: string,
|
||||
): Promise<void> {
|
||||
if (isMobileProject(page)) {
|
||||
await enableFlutterAccessibility(page);
|
||||
const inputs = page.getByRole("textbox");
|
||||
await inputs.nth(0).fill(loginId);
|
||||
await inputs.nth(1).fill(password);
|
||||
return;
|
||||
}
|
||||
const coords = coordsFor(page);
|
||||
await fillAt(page, coords.signinLoginIdX, coords.signinLoginIdY, loginId);
|
||||
await fillAt(page, coords.signinPasswordX, coords.signinPasswordY, password);
|
||||
}
|
||||
|
||||
async function submitPasswordLogin(page: Page): Promise<void> {
|
||||
if (isMobileProject(page)) {
|
||||
await enableFlutterAccessibility(page);
|
||||
await page.getByRole("button", { name: "로그인" }).click({ force: true });
|
||||
return;
|
||||
}
|
||||
await page.keyboard.press("Enter");
|
||||
}
|
||||
|
||||
async function fillResetPasswordForm(
|
||||
page: Page,
|
||||
password: string,
|
||||
): Promise<void> {
|
||||
await enableFlutterAccessibility(page);
|
||||
const newPasswordInput = page.getByRole("textbox", {
|
||||
name: resetNewPasswordName,
|
||||
});
|
||||
const confirmPasswordInput = page.getByRole("textbox", {
|
||||
name: resetConfirmPasswordName,
|
||||
});
|
||||
if (
|
||||
(await newPasswordInput.count()) > 0 &&
|
||||
(await confirmPasswordInput.count()) > 0
|
||||
) {
|
||||
await typeIntoAccessibleField(page, newPasswordInput, password);
|
||||
await typeIntoAccessibleField(page, confirmPasswordInput, password);
|
||||
return;
|
||||
}
|
||||
if (isMobileProject(page)) {
|
||||
await page
|
||||
.getByRole("textbox", { name: resetNewPasswordName })
|
||||
.fill(password);
|
||||
await page
|
||||
.getByRole("textbox", { name: resetConfirmPasswordName })
|
||||
.fill(password);
|
||||
return;
|
||||
}
|
||||
const coords = coordsFor(page);
|
||||
await fillAt(
|
||||
page,
|
||||
coords.resetNewPasswordX,
|
||||
coords.resetNewPasswordY,
|
||||
password,
|
||||
);
|
||||
await fillAt(
|
||||
page,
|
||||
coords.resetConfirmPasswordX,
|
||||
coords.resetConfirmPasswordY,
|
||||
password,
|
||||
);
|
||||
}
|
||||
|
||||
async function submitResetPassword(page: Page): Promise<void> {
|
||||
await enableFlutterAccessibility(page);
|
||||
const submitButton = page.getByRole("button", {
|
||||
name: resetSubmitButtonName,
|
||||
});
|
||||
if ((await submitButton.count()) > 0) {
|
||||
await submitButton.click({ force: true });
|
||||
return;
|
||||
}
|
||||
if (isMobileProject(page)) {
|
||||
return;
|
||||
}
|
||||
const coords = coordsFor(page);
|
||||
await page.locator("flt-glass-pane").click({
|
||||
position: { x: coords.resetSubmitX, y: coords.resetSubmitY },
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function mockAuthApis(
|
||||
page: Page,
|
||||
capture: RequestCapture,
|
||||
): Promise<void> {
|
||||
await page.route("**/api/v1/**", async (route: Route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
const path = requestUrl.pathname;
|
||||
|
||||
if (path.endsWith("/api/v1/auth/password/login")) {
|
||||
capture.loginBody = (route.request().postDataJSON() ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
const loginId = String(capture.loginBody.loginId ?? "");
|
||||
const password = String(capture.loginBody.password ?? "");
|
||||
if (loginId === "e2e@example.com" && password === "ValidPass1!") {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
sessionJwt: "e30.e30.e30",
|
||||
provider: "ory",
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ error: "password_or_email_mismatch" }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith("/api/v1/auth/password/policy")) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
minLength: 12,
|
||||
minCharacterTypes: 3,
|
||||
lowercase: true,
|
||||
uppercase: true,
|
||||
number: true,
|
||||
nonAlphanumeric: true,
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith("/api/v1/auth/password/reset/complete")) {
|
||||
capture.resetBody = (route.request().postDataJSON() ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
capture.resetToken = requestUrl.searchParams.get("token");
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ status: "ok" }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith("/api/v1/client-log")) {
|
||||
const payload = (route.request().postDataJSON() ?? {}) as {
|
||||
message?: string;
|
||||
};
|
||||
if (payload.message != null) {
|
||||
capture.clientLogs.push(payload.message);
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ ok: true }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith("/api/v1/user/me")) {
|
||||
const authHeader = route.request().headers().authorization ?? "";
|
||||
if (!authHeader.startsWith("Bearer ")) {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ error: "unauthorized" }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
id: "e2e-user",
|
||||
email: "e2e@example.com",
|
||||
name: "E2E User",
|
||||
phone: "+821012341234",
|
||||
department: "QA",
|
||||
affiliationType: "employee",
|
||||
companyCode: "BARON",
|
||||
tenant: {
|
||||
id: "tenant-1",
|
||||
name: "Baron",
|
||||
slug: "baron",
|
||||
description: "E2E tenant",
|
||||
},
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith("/api/v1/user/rp/linked")) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ items: [] }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith("/api/v1/audit/auth/timeline")) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ items: [], next_cursor: "" }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("UserFront WASM password login and reset", () => {
|
||||
test.skip(({ isMobile }) => isMobile, "Desktop only (hardcoded coordinates)");
|
||||
test("비밀번호 로그인 성공 시 dashboard로 이동하고 토큰을 저장한다", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(
|
||||
isMobileProject(page),
|
||||
"Mobile webapp keeps dedicated auth-routing coverage; password form canvas automation is covered on desktop.",
|
||||
);
|
||||
const capture: RequestCapture = { clientLogs: [] };
|
||||
await mockAuthApis(page, capture);
|
||||
|
||||
await page.goto("/ko/signin");
|
||||
await clickPasswordTab(page);
|
||||
await fillPasswordLoginForm(page, "e2e@example.com", "ValidPass1!");
|
||||
await submitPasswordLogin(page);
|
||||
|
||||
await expect(page).toHaveURL(/\/ko\/dashboard$/);
|
||||
|
||||
expect(capture.loginBody?.loginId).toBe("e2e@example.com");
|
||||
expect(capture.loginBody?.password).toBe("ValidPass1!");
|
||||
|
||||
const storedToken = await page.evaluate(() =>
|
||||
window.localStorage.getItem("baron_auth_token"),
|
||||
);
|
||||
expect(storedToken).toBe("e30.e30.e30");
|
||||
});
|
||||
|
||||
test("비밀번호 로그인 실패 시 에러 코드를 사용자에게 표시한다", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(
|
||||
isMobileProject(page),
|
||||
"Mobile webapp keeps dedicated auth-routing coverage; password form canvas automation is covered on desktop.",
|
||||
);
|
||||
const capture: RequestCapture = { clientLogs: [] };
|
||||
await mockAuthApis(page, capture);
|
||||
|
||||
await page.goto("/ko/signin");
|
||||
await clickPasswordTab(page);
|
||||
await fillPasswordLoginForm(page, "e2e@example.com", "WrongPass1!");
|
||||
await submitPasswordLogin(page);
|
||||
|
||||
await expect(page).toHaveURL(/\/ko\/signin$/);
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
capture.clientLogs.some((message) =>
|
||||
message.includes("password_or_email_mismatch"),
|
||||
),
|
||||
{ timeout: 10000 },
|
||||
)
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test("reset-password에서 변경 성공 시 signin으로 이동한다", async ({
|
||||
page,
|
||||
}) => {
|
||||
const capture: RequestCapture = { clientLogs: [] };
|
||||
await mockAuthApis(page, capture);
|
||||
|
||||
const policyLoaded = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes("/api/v1/auth/password/policy") &&
|
||||
response.status() === 200,
|
||||
);
|
||||
await page.goto("/ko/reset-password?token=reset-token-e2e");
|
||||
await policyLoaded;
|
||||
await page.waitForTimeout(900);
|
||||
await fillResetPasswordForm(page, "ValidPass1!A");
|
||||
await submitResetPassword(page);
|
||||
|
||||
await expect
|
||||
.poll(() => capture.resetBody?.newPassword as string | undefined, {
|
||||
timeout: 10000,
|
||||
})
|
||||
.toBe("ValidPass1!A");
|
||||
await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/, { timeout: 10_000 });
|
||||
expect(capture.resetToken).toBe("reset-token-e2e");
|
||||
expect(capture.resetBody?.newPassword).toBe("ValidPass1!A");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user