1
0
forked from baron/baron-sso

ci: add code check badges and coverage reports

This commit is contained in:
2026-05-29 12:05:43 +09:00
parent c489c7c38f
commit a830242947
164 changed files with 9059 additions and 2012 deletions

View File

@@ -1,4 +1,10 @@
import { expect, test, type Locator, type Page, type Route } from '@playwright/test';
import {
expect,
type Locator,
type Page,
type Route,
test,
} from "@playwright/test";
type RequestCapture = {
loginBody?: Record<string, unknown>;
@@ -7,13 +13,14 @@ type RequestCapture = {
clientLogs: string[];
};
const resetNewPasswordName = /^(새 비밀번호|ui\.userfront\.reset\.new_password)$/;
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' });
const button = page.getByRole("button", { name: "Enable accessibility" });
if (await button.count()) {
await button.first().evaluate((node) => {
(node as HTMLElement).click();
@@ -22,7 +29,7 @@ async function enableFlutterAccessibility(page: Page): Promise<void> {
return;
}
await page.waitForTimeout(300);
const placeholder = page.locator('flt-semantics-placeholder').first();
const placeholder = page.locator("flt-semantics-placeholder").first();
if (await placeholder.count()) {
await placeholder.evaluate((node) => {
(node as HTMLElement).click();
@@ -98,7 +105,7 @@ async function clickPasswordTab(page: Page): Promise<void> {
}
const coords = coordsFor(page);
await page.waitForTimeout(900);
const pane = page.locator('flt-glass-pane');
const pane = page.locator("flt-glass-pane");
await pane.click({
position: { x: coords.signinPasswordTabX, y: coords.signinTabY },
force: true,
@@ -111,12 +118,17 @@ async function clickPasswordTab(page: Page): Promise<void> {
await page.waitForTimeout(200);
}
async function fillAt(page: Page, x: number, y: number, value: string): Promise<void> {
const pane = page.locator('flt-glass-pane');
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.press("Control+A");
await page.keyboard.press("Backspace");
await page.keyboard.type(value);
}
@@ -127,8 +139,8 @@ async function typeIntoAccessibleField(
): 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.press("Control+A");
await page.keyboard.press("Backspace");
await page.keyboard.type(value);
}
@@ -139,7 +151,7 @@ async function fillPasswordLoginForm(
): Promise<void> {
if (isMobileProject(page)) {
await enableFlutterAccessibility(page);
const inputs = page.getByRole('textbox');
const inputs = page.getByRole("textbox");
await inputs.nth(0).fill(loginId);
await inputs.nth(1).fill(password);
return;
@@ -152,32 +164,47 @@ async function fillPasswordLoginForm(
async function submitPasswordLogin(page: Page): Promise<void> {
if (isMobileProject(page)) {
await enableFlutterAccessibility(page);
await page.getByRole('button', { name: '로그인' }).click({ force: true });
await page.getByRole("button", { name: "로그인" }).click({ force: true });
return;
}
await page.keyboard.press('Enter');
await page.keyboard.press("Enter");
}
async function fillResetPasswordForm(page: Page, password: string): Promise<void> {
async function fillResetPasswordForm(
page: Page,
password: string,
): Promise<void> {
await enableFlutterAccessibility(page);
const newPasswordInput = page.getByRole('textbox', {
const newPasswordInput = page.getByRole("textbox", {
name: resetNewPasswordName,
});
const confirmPasswordInput = page.getByRole('textbox', {
const confirmPasswordInput = page.getByRole("textbox", {
name: resetConfirmPasswordName,
});
if ((await newPasswordInput.count()) > 0 && (await confirmPasswordInput.count()) > 0) {
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);
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.resetNewPasswordX,
coords.resetNewPasswordY,
password,
);
await fillAt(
page,
coords.resetConfirmPasswordX,
@@ -188,7 +215,9 @@ async function fillResetPasswordForm(page: Page, password: string): Promise<void
async function submitResetPassword(page: Page): Promise<void> {
await enableFlutterAccessibility(page);
const submitButton = page.getByRole('button', { name: resetSubmitButtonName });
const submitButton = page.getByRole("button", {
name: resetSubmitButtonName,
});
if ((await submitButton.count()) > 0) {
await submitButton.click({ force: true });
return;
@@ -197,32 +226,35 @@ async function submitResetPassword(page: Page): Promise<void> {
return;
}
const coords = coordsFor(page);
await page.locator('flt-glass-pane').click({
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) => {
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')) {
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!') {
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',
contentType: "application/json",
body: JSON.stringify({
sessionJwt: 'e30.e30.e30',
provider: 'ory',
sessionJwt: "e30.e30.e30",
provider: "ory",
}),
});
return;
@@ -230,16 +262,16 @@ async function mockAuthApis(page: Page, capture: RequestCapture): Promise<void>
await route.fulfill({
status: 401,
contentType: 'application/json',
body: JSON.stringify({ error: 'password_or_email_mismatch' }),
contentType: "application/json",
body: JSON.stringify({ error: "password_or_email_mismatch" }),
});
return;
}
if (path.endsWith('/api/v1/auth/password/policy')) {
if (path.endsWith("/api/v1/auth/password/policy")) {
await route.fulfill({
status: 200,
contentType: 'application/json',
contentType: "application/json",
body: JSON.stringify({
minLength: 12,
minCharacterTypes: 3,
@@ -252,21 +284,21 @@ async function mockAuthApis(page: Page, capture: RequestCapture): Promise<void>
return;
}
if (path.endsWith('/api/v1/auth/password/reset/complete')) {
if (path.endsWith("/api/v1/auth/password/reset/complete")) {
capture.resetBody = (route.request().postDataJSON() ?? {}) as Record<
string,
unknown
>;
capture.resetToken = requestUrl.searchParams.get('token');
capture.resetToken = requestUrl.searchParams.get("token");
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ status: 'ok' }),
contentType: "application/json",
body: JSON.stringify({ status: "ok" }),
});
return;
}
if (path.endsWith('/api/v1/client-log')) {
if (path.endsWith("/api/v1/client-log")) {
const payload = (route.request().postDataJSON() ?? {}) as {
message?: string;
};
@@ -275,108 +307,112 @@ async function mockAuthApis(page: Page, capture: RequestCapture): Promise<void>
}
await route.fulfill({
status: 200,
contentType: 'application/json',
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 ')) {
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' }),
contentType: "application/json",
body: JSON.stringify({ error: "unauthorized" }),
});
return;
}
await route.fulfill({
status: 200,
contentType: 'application/json',
contentType: "application/json",
body: JSON.stringify({
id: 'e2e-user',
email: 'e2e@example.com',
name: 'E2E User',
phone: '+821012341234',
department: 'QA',
affiliationType: 'employee',
companyCode: 'BARON',
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',
id: "tenant-1",
name: "Baron",
slug: "baron",
description: "E2E tenant",
},
}),
});
return;
}
if (path.endsWith('/api/v1/user/rp/linked')) {
if (path.endsWith("/api/v1/user/rp/linked")) {
await route.fulfill({
status: 200,
contentType: 'application/json',
contentType: "application/json",
body: JSON.stringify({ items: [] }),
});
return;
}
if (path.endsWith('/api/v1/audit/auth/timeline')) {
if (path.endsWith("/api/v1/audit/auth/timeline")) {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [], next_cursor: '' }),
contentType: "application/json",
body: JSON.stringify({ items: [], next_cursor: "" }),
});
return;
}
await route.fulfill({
status: 200,
contentType: 'application/json',
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.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.',
"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 page.goto("/ko/signin");
await clickPasswordTab(page);
await fillPasswordLoginForm(page, 'e2e@example.com', 'ValidPass1!');
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!');
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'),
window.localStorage.getItem("baron_auth_token"),
);
expect(storedToken).toBe('e30.e30.e30');
expect(storedToken).toBe("e30.e30.e30");
});
test('비밀번호 로그인 실패 시 에러 코드를 사용자에게 표시한다', async ({ page }) => {
test("비밀번호 로그인 실패 시 에러 코드를 사용자에게 표시한다", async ({
page,
}) => {
test.skip(
isMobileProject(page),
'Mobile webapp keeps dedicated auth-routing coverage; password form canvas automation is covered on desktop.',
"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 page.goto("/ko/signin");
await clickPasswordTab(page);
await fillPasswordLoginForm(page, 'e2e@example.com', 'WrongPass1!');
await fillPasswordLoginForm(page, "e2e@example.com", "WrongPass1!");
await submitPasswordLogin(page);
await expect(page).toHaveURL(/\/ko\/signin$/);
@@ -384,36 +420,37 @@ test.describe('UserFront WASM password login and reset', () => {
.poll(
() =>
capture.clientLogs.some((message) =>
message.includes('password_or_email_mismatch'),
message.includes("password_or_email_mismatch"),
),
{ timeout: 10000 },
)
.toBe(true);
});
test('reset-password에서 변경 성공 시 signin으로 이동한다', async ({ page }) => {
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.url().includes("/api/v1/auth/password/policy") &&
response.status() === 200,
);
await page.goto('/ko/reset-password?token=reset-token-e2e');
await page.goto("/ko/reset-password?token=reset-token-e2e");
await policyLoaded;
await page.waitForTimeout(900);
await fillResetPasswordForm(page, 'ValidPass1!A');
await fillResetPasswordForm(page, "ValidPass1!A");
await submitResetPassword(page);
await expect
.poll(
() => capture.resetBody?.newPassword as string | undefined,
{ timeout: 10000 },
)
.toBe('ValidPass1!A');
.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');
expect(capture.resetToken).toBe("reset-token-e2e");
expect(capture.resetBody?.newPassword).toBe("ValidPass1!A");
});
});