첫 커밋: 로컬 프로젝트 업로드
This commit is contained in:
234
baron-sso/adminfront/tests/audit.spec.ts
Normal file
234
baron-sso/adminfront/tests/audit.spec.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Audit Logs Management", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
page.on("console", (msg) => console.log(`[PAGE] ${msg.text()}`));
|
||||
|
||||
await page.addInitScript(() => {
|
||||
const authority = `${window.location.origin}/oidc`;
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
id_token: "fake-id-token",
|
||||
access_token: "fake-token",
|
||||
token_type: "Bearer",
|
||||
scope: "openid profile email",
|
||||
profile: {
|
||||
sub: "admin-user",
|
||||
name: "Admin",
|
||||
email: "admin@test.com",
|
||||
role: "super_admin",
|
||||
},
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
};
|
||||
window.localStorage.setItem(key, JSON.stringify(authData));
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("oidc.state", "dummy");
|
||||
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
const url = route.request().url();
|
||||
if (url.includes("/.well-known/openid-configuration")) {
|
||||
const origin = new URL(url).origin;
|
||||
return route.fulfill({
|
||||
json: {
|
||||
issuer: `${origin}/oidc`,
|
||||
authorization_endpoint: `${origin}/oidc/auth`,
|
||||
token_endpoint: `${origin}/oidc/token`,
|
||||
userinfo_endpoint: `${origin}/oidc/userinfo`,
|
||||
jwks_uri: `${origin}/oidc/jwks`,
|
||||
},
|
||||
});
|
||||
}
|
||||
const origin = new URL(url).origin;
|
||||
await route.fulfill({ json: { issuer: `${origin}/oidc` } });
|
||||
});
|
||||
|
||||
await page.route("**/v1/audit*", async (route) => {
|
||||
const url = route.request().url();
|
||||
const urlObj = new URL(url);
|
||||
const cursor = urlObj.searchParams.get("cursor");
|
||||
const search = urlObj.searchParams.get("search")?.toLowerCase();
|
||||
const status = urlObj.searchParams.get("status");
|
||||
const offset = cursor ? 20 : 0;
|
||||
|
||||
let allMockLogs = generateMockLogs(40, 0);
|
||||
if (status && status !== "all") {
|
||||
allMockLogs = allMockLogs.filter((l) => l.status === status);
|
||||
}
|
||||
if (search) {
|
||||
allMockLogs = allMockLogs.filter(
|
||||
(l) =>
|
||||
l.user_id.toLowerCase().includes(search) ||
|
||||
l.details.toLowerCase().includes(search),
|
||||
);
|
||||
}
|
||||
|
||||
const paginatedItems = allMockLogs.slice(offset, offset + 20);
|
||||
|
||||
console.log(
|
||||
`[mock] Audit logs request: ${url} (offset: ${offset}, search: ${search}, status: ${status}, results: ${paginatedItems.length})`,
|
||||
);
|
||||
|
||||
return route.fulfill({
|
||||
json: {
|
||||
items: paginatedItems,
|
||||
next_cursor: allMockLogs.length > offset + 20 ? "fake-cursor" : null,
|
||||
total: allMockLogs.length,
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/user/me", async (route) => {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const generateMockLogs = (count: number, offset = 0) => {
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const id = offset + i;
|
||||
return {
|
||||
event_id: `evt-${id}`,
|
||||
timestamp: new Date(Date.now() - id * 10000).toISOString(),
|
||||
user_id: id % 2 === 0 ? "user-even" : "user-odd",
|
||||
event_type: "API_REQUEST",
|
||||
status: id % 5 === 0 ? "failure" : "success",
|
||||
ip_address: "192.168.1.1",
|
||||
user_agent: "Playwright",
|
||||
details: JSON.stringify({
|
||||
action: id % 3 === 0 ? "CREATE_TENANT" : "ROTATE_SECRET",
|
||||
method: "POST",
|
||||
path: `/v1/admin/tenants`,
|
||||
}),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
test("should load initial logs and display correctly", async ({ page }) => {
|
||||
console.log("[test] Navigating to /audit-logs");
|
||||
await page.goto("/audit-logs");
|
||||
|
||||
// Check header - this should be visible immediately now
|
||||
await expect(page.getByText(/감사 로그|Audit Logs/i).first()).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Ensure we are not stuck in a global loading state (AppLayout)
|
||||
await expect(page.locator(".animate-spin")).not.toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Check for audit page specific error
|
||||
const errorEl = page.getByTestId("audit-error");
|
||||
if (await errorEl.isVisible()) {
|
||||
const errorText = await errorEl.innerText();
|
||||
throw new Error(`Audit log page showed error: ${errorText}`);
|
||||
}
|
||||
|
||||
// Wait for loading to finish
|
||||
await expect(page.getByTestId("audit-loading")).not.toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
// Wait for the table row to appear
|
||||
await expect(page.locator("tbody tr")).toHaveCount(20, { timeout: 15000 });
|
||||
|
||||
// Check specific data visible in the row
|
||||
await expect(page.locator("tbody")).toContainText("user-even");
|
||||
await expect(page.locator("tbody")).toContainText("CREATE_TENANT");
|
||||
});
|
||||
|
||||
test("should load more logs on scroll (infinite scroll)", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/audit-logs");
|
||||
await expect(page.locator(".animate-spin")).not.toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(page.getByTestId("audit-loading")).not.toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
await expect(page.locator("tbody tr")).toHaveCount(20, { timeout: 15000 });
|
||||
|
||||
const loadMoreBtn = page.getByRole("button", {
|
||||
name: /더 보기|Load more/i,
|
||||
});
|
||||
await expect(loadMoreBtn).toBeVisible({ timeout: 10000 });
|
||||
await expect(loadMoreBtn).toBeEnabled();
|
||||
|
||||
await loadMoreBtn.click();
|
||||
|
||||
// Wait for the next page to load (should reach 40)
|
||||
await expect(page.locator("tbody tr")).toHaveCount(40, { timeout: 15000 });
|
||||
await expect(page.locator("tbody")).toContainText("user-even");
|
||||
});
|
||||
|
||||
test("should filter logs by Action and User ID locally", async ({ page }) => {
|
||||
await page.goto("/audit-logs");
|
||||
await expect(page.locator(".animate-spin")).not.toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(page.getByTestId("audit-loading")).not.toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
await expect(page.locator("tbody tr")).toHaveCount(20, { timeout: 15000 });
|
||||
|
||||
// Search by User ID
|
||||
const userIdInput = page.getByTestId("audit-search-user-id");
|
||||
await userIdInput.fill("user-even");
|
||||
|
||||
// Wait for deferred value to apply
|
||||
await expect(page.locator("tbody tr")).toHaveCount(20, { timeout: 15000 });
|
||||
await expect(page.locator("tbody")).not.toContainText("user-odd");
|
||||
|
||||
// Clear User ID
|
||||
await userIdInput.clear();
|
||||
await expect(page.locator("tbody tr")).toHaveCount(20, { timeout: 15000 });
|
||||
|
||||
// Search by Action
|
||||
const actionInput = page.getByTestId("audit-search-action");
|
||||
await actionInput.fill("ROTATE_SECRET");
|
||||
|
||||
// Check that we see ROTATE_SECRET across all 40 logs (40 - 14 = 26)
|
||||
// Wait for the mock to respond and render
|
||||
await expect(page.locator("tbody tr")).toHaveCount(20, { timeout: 15000 });
|
||||
await expect(page.locator("tbody")).not.toContainText("CREATE_TENANT");
|
||||
});
|
||||
|
||||
test("should filter logs by Status", async ({ page }) => {
|
||||
await page.goto("/audit-logs");
|
||||
await expect(page.locator(".animate-spin")).not.toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(page.getByTestId("audit-loading")).not.toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
await expect(page.locator("tbody tr")).toHaveCount(20, { timeout: 15000 });
|
||||
|
||||
// Select "Failure" status
|
||||
await page.getByTestId("audit-filter-status").selectOption("failure");
|
||||
|
||||
// Total 8 failures in 40 logs
|
||||
await expect(page.locator("tbody tr")).toHaveCount(8, { timeout: 15000 });
|
||||
|
||||
// Select "Success" status
|
||||
await page.getByTestId("audit-filter-status").selectOption("success");
|
||||
|
||||
// Total 32 successes in 40 logs, but page limit is 20
|
||||
await expect(page.locator("tbody tr")).toHaveCount(20, { timeout: 15000 });
|
||||
});
|
||||
});
|
||||
144
baron-sso/adminfront/tests/auth.spec.ts
Normal file
144
baron-sso/adminfront/tests/auth.spec.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Authentication", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// 1. Force state
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
access_token: "fake-token",
|
||||
token_type: "Bearer",
|
||||
profile: {
|
||||
sub: "admin-user",
|
||||
name: "Admin",
|
||||
email: "admin@test.com",
|
||||
role: "super_admin",
|
||||
},
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
};
|
||||
window.localStorage.setItem(key, JSON.stringify(authData));
|
||||
});
|
||||
|
||||
// 2. High-priority Mocks
|
||||
await page.route("**/api/v1/user/me", async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
const url = route.request().url();
|
||||
if (url.includes(".well-known/openid-configuration")) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
issuer: "http://localhost:5000/oidc",
|
||||
authorization_endpoint: "http://localhost:5000/oidc/auth",
|
||||
token_endpoint: "http://localhost:5000/oidc/token",
|
||||
jwks_uri: "http://localhost:5000/oidc/jwks",
|
||||
userinfo_endpoint: "http://localhost:5000/oidc/userinfo",
|
||||
end_session_endpoint: "http://localhost:5000/oidc/session/end",
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} else if (url.includes("/jwks")) {
|
||||
await route.fulfill({
|
||||
json: { keys: [] },
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} else {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: "ok",
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Catch-all for others
|
||||
await page.route(/.*\/api\/v1\/.*/, async (route) => {
|
||||
if (route.request().method() === "GET") {
|
||||
await route.fulfill({ json: { items: [], total: 0 } });
|
||||
} else {
|
||||
await route.fulfill({ status: 200, json: {} });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("should redirect unauthorized users to login page", async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.clear();
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = false;
|
||||
});
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveURL(/.*\/login.*/, { timeout: 15000 });
|
||||
});
|
||||
|
||||
test("should render an actionable SSO button on login route with returnTo", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.clear();
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = false;
|
||||
});
|
||||
|
||||
await page.goto("/login?returnTo=%2F");
|
||||
|
||||
const loginButton = page.getByRole("button", {
|
||||
name: /SSO 계정으로 로그인/i,
|
||||
});
|
||||
await expect(loginButton).toBeVisible();
|
||||
await expect(loginButton).toBeEnabled();
|
||||
await expect(loginButton).not.toContainText("로그인 진행 중");
|
||||
});
|
||||
|
||||
test("should allow access to dashboard when authenticated", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
// strict mode violation 피하기 위해 .last() 사용하거나 더 구체적인 셀렉터 사용
|
||||
await expect(page.locator("h1").last()).toContainText(
|
||||
/Admin Control|운영 도구/i,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
});
|
||||
|
||||
test("should link org chart navigation through the auto login entry", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("link", { name: "조직도" })).toHaveAttribute(
|
||||
"href",
|
||||
/\/login\?auto=1&returnTo=%2Fchart%3FincludeInternal%3Dtrue$/,
|
||||
);
|
||||
});
|
||||
|
||||
test("should logout and redirect to login page", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
page.on("dialog", (dialog) => dialog.accept());
|
||||
const logoutBtn = page
|
||||
.locator("button")
|
||||
.filter({ hasText: /Logout|로그아웃/i })
|
||||
.first();
|
||||
await logoutBtn.waitFor({ state: "visible" });
|
||||
await logoutBtn.click();
|
||||
await expect(page).toHaveURL(/.*\/login.*/, { timeout: 15000 });
|
||||
});
|
||||
});
|
||||
457
baron-sso/adminfront/tests/bulk_actions.spec.ts
Normal file
457
baron-sso/adminfront/tests/bulk_actions.spec.ts
Normal file
@@ -0,0 +1,457 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Bulk Actions and Tree Search", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
access_token: "fake-token",
|
||||
token_type: "Bearer",
|
||||
profile: { sub: "admin", role: "super_admin", name: "Admin" },
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
};
|
||||
window.localStorage.setItem(key, JSON.stringify(authData));
|
||||
});
|
||||
|
||||
// Capture ALL API calls to our mock host
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const url = route.request().url();
|
||||
const headers = { "Access-Control-Allow-Origin": "*" };
|
||||
|
||||
if (url.includes("/user/me")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin",
|
||||
role: "super_admin",
|
||||
name: "Admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
if (
|
||||
url.includes("/admin/users/bulk") &&
|
||||
route.request().method() === "PUT"
|
||||
) {
|
||||
return route.fulfill({
|
||||
json: { results: [{ id: "u-1", success: true }] },
|
||||
headers,
|
||||
});
|
||||
}
|
||||
if (url.includes("/admin/users")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
items: [
|
||||
{
|
||||
id: "u-1",
|
||||
name: "User One",
|
||||
email: "u1@test.com",
|
||||
status: "active",
|
||||
role: "user",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: "u-2",
|
||||
name: "User Two",
|
||||
email: "u2@test.com",
|
||||
status: "active",
|
||||
role: "user",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
if (url.includes("/organization")) {
|
||||
return route.fulfill({
|
||||
json: [
|
||||
{ id: "g-1", name: "Engineering", slug: "eng", tenantId: "t-1" },
|
||||
{ id: "g-2", name: "Sales", slug: "sales", tenantId: "t-1" },
|
||||
],
|
||||
headers,
|
||||
});
|
||||
}
|
||||
if (url.includes("/admin/tenants/t-1")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "t-1",
|
||||
name: "Main Tenant",
|
||||
slug: "main",
|
||||
status: "active",
|
||||
type: "COMPANY",
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
if (url.includes("/admin/tenants")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
items: [
|
||||
{ id: "t-1", name: "Main Tenant", slug: "main", type: "COMPANY" },
|
||||
{
|
||||
id: "g-1",
|
||||
name: "Engineering",
|
||||
slug: "eng",
|
||||
parentId: "t-1",
|
||||
type: "USER_GROUP",
|
||||
},
|
||||
{
|
||||
id: "g-2",
|
||||
name: "Sales",
|
||||
slug: "sales",
|
||||
parentId: "t-1",
|
||||
type: "USER_GROUP",
|
||||
},
|
||||
],
|
||||
total: 3,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
return route.fulfill({ json: { items: [], total: 0 }, headers });
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
});
|
||||
|
||||
test("should show bulk action bar when users are selected", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/users");
|
||||
// 로딩바 대기 대신 실제 데이터 텍스트 대기
|
||||
const table = page.locator("table");
|
||||
await expect(table).toContainText("User One", { timeout: 20000 });
|
||||
|
||||
// 첫 번째 데이터의 체크박스 선택
|
||||
const userCheckbox = page.locator('table input[type="checkbox"]').nth(1);
|
||||
await userCheckbox.click();
|
||||
|
||||
// 일괄 작업 바 확인
|
||||
const selectionBar = page.getByTestId("bulk-action-bar");
|
||||
await expect(selectionBar).toBeVisible({ timeout: 15000 });
|
||||
|
||||
await expect(page.getByTestId("bulk-status-select")).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(page.getByTestId("bulk-apply-btn")).toBeVisible();
|
||||
|
||||
// 전체 선택
|
||||
await page.locator('table input[type="checkbox"]').first().click();
|
||||
await expect(selectionBar).toBeVisible();
|
||||
|
||||
// 선택 해제 버튼
|
||||
const closeBtn = page.getByTestId("bulk-close-btn");
|
||||
await closeBtn.click();
|
||||
|
||||
await expect(selectionBar).not.toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("should apply selected bulk status to selected users", async ({
|
||||
page,
|
||||
}) => {
|
||||
let capturedPayload: unknown = null;
|
||||
await page.route("**/api/v1/admin/users/bulk", async (route) => {
|
||||
if (route.request().method() === "PUT") {
|
||||
capturedPayload = route.request().postDataJSON();
|
||||
return route.fulfill({
|
||||
json: { results: [{ id: "u-1", success: true }] },
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
await page.goto("/users");
|
||||
await expect(page.locator("table")).toContainText("User One", {
|
||||
timeout: 20000,
|
||||
});
|
||||
|
||||
await page.locator('table input[type="checkbox"]').nth(1).click();
|
||||
const selectionBar = page.getByTestId("bulk-action-bar");
|
||||
await expect(selectionBar).toBeVisible({ timeout: 15000 });
|
||||
|
||||
await page.getByTestId("bulk-status-select").click();
|
||||
await page.getByRole("option", { name: /입사대기|Preboarding/i }).click();
|
||||
await page.getByTestId("bulk-apply-btn").click();
|
||||
|
||||
await expect
|
||||
.poll(() => capturedPayload)
|
||||
.toEqual({
|
||||
userIds: ["u-1"],
|
||||
status: "preboarding",
|
||||
});
|
||||
await expect(selectionBar).not.toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("should let super admins apply selected admin permission to selected users", async ({
|
||||
page,
|
||||
}) => {
|
||||
let capturedPayload: unknown = null;
|
||||
await page.route("**/api/v1/admin/users/bulk", async (route) => {
|
||||
if (route.request().method() === "PUT") {
|
||||
capturedPayload = route.request().postDataJSON();
|
||||
return route.fulfill({
|
||||
json: { results: [{ id: "u-1", success: true }] },
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
await page.goto("/users");
|
||||
await expect(page.locator("table")).toContainText("User One", {
|
||||
timeout: 20000,
|
||||
});
|
||||
|
||||
await page.locator('table input[type="checkbox"]').nth(1).click();
|
||||
const selectionBar = page.getByTestId("bulk-action-bar");
|
||||
await expect(selectionBar).toBeVisible({ timeout: 15000 });
|
||||
|
||||
await page.getByTestId("bulk-permission-select").click();
|
||||
await page
|
||||
.getByRole("option", { name: /시스템 관리자|Super Admin/i })
|
||||
.click();
|
||||
await page.getByTestId("bulk-apply-btn").click();
|
||||
|
||||
await expect
|
||||
.poll(() => capturedPayload)
|
||||
.toEqual({
|
||||
userIds: ["u-1"],
|
||||
role: "super_admin",
|
||||
});
|
||||
await expect(selectionBar).not.toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("should only expose super admin grant and revoke options in bulk permission select", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/users");
|
||||
await expect(page.locator("table")).toContainText("User One", {
|
||||
timeout: 20000,
|
||||
});
|
||||
|
||||
await page.locator('table input[type="checkbox"]').nth(1).click();
|
||||
await expect(page.getByTestId("bulk-action-bar")).toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
await page.getByTestId("bulk-permission-select").click();
|
||||
await expect(
|
||||
page.getByRole("option", { name: /시스템 관리자|Super Admin/i }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("option", { name: /일반 사용자|User/i }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("option", { name: /테넌트 관리자|Tenant Admin/i }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole("option", {
|
||||
name: /서비스 관리자|RP Admin|Service Admin/i,
|
||||
}),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("should let super admins revoke selected super admin permission", async ({
|
||||
page,
|
||||
}) => {
|
||||
let capturedPayload: unknown = null;
|
||||
await page.route("**/api/v1/admin/users/bulk", async (route) => {
|
||||
if (route.request().method() === "PUT") {
|
||||
capturedPayload = route.request().postDataJSON();
|
||||
return route.fulfill({
|
||||
json: { results: [{ id: "u-1", success: true }] },
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
await page.goto("/users");
|
||||
await expect(page.locator("table")).toContainText("User One", {
|
||||
timeout: 20000,
|
||||
});
|
||||
|
||||
await page.locator('table input[type="checkbox"]').nth(1).click();
|
||||
await expect(page.getByTestId("bulk-action-bar")).toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
await page.getByTestId("bulk-permission-select").click();
|
||||
await page.getByRole("option", { name: /일반 사용자|User/i }).click();
|
||||
await page.getByTestId("bulk-apply-btn").click();
|
||||
|
||||
await expect
|
||||
.poll(() => capturedPayload)
|
||||
.toEqual({
|
||||
userIds: ["u-1"],
|
||||
role: "user",
|
||||
});
|
||||
});
|
||||
|
||||
test("should not render role field on user detail page", async ({ page }) => {
|
||||
await page.unroute("**/api/v1/**");
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const url = route.request().url();
|
||||
const headers = { "Access-Control-Allow-Origin": "*" };
|
||||
|
||||
if (url.includes("/user/me")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin",
|
||||
role: "super_admin",
|
||||
name: "Admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
if (url.includes("/auth/password/policy")) {
|
||||
return route.fulfill({ json: { minLength: 12 }, headers });
|
||||
}
|
||||
if (url.includes("/admin/users/u-1/rp-history")) {
|
||||
return route.fulfill({ json: [], headers });
|
||||
}
|
||||
if (url.includes("/admin/users/u-1")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "u-1",
|
||||
name: "User One",
|
||||
email: "u1@test.com",
|
||||
phone: "",
|
||||
status: "active",
|
||||
role: "user",
|
||||
tenantSlug: "main",
|
||||
createdAt: new Date().toISOString(),
|
||||
metadata: {},
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
if (url.includes("/admin/tenants")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
items: [
|
||||
{ id: "t-1", name: "Main Tenant", slug: "main", type: "COMPANY" },
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
return route.fulfill({ json: { items: [], total: 0 }, headers });
|
||||
});
|
||||
|
||||
await page.goto("/users/u-1");
|
||||
await expect(page.getByRole("heading", { name: "User One" })).toBeVisible({
|
||||
timeout: 20000,
|
||||
});
|
||||
await expect(page.locator("#role")).toHaveCount(0);
|
||||
await expect(page.getByLabel("역할")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("should let canonical super admin aliases promote selected users", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.unroute("**/api/v1/**");
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const url = route.request().url();
|
||||
const headers = { "Access-Control-Allow-Origin": "*" };
|
||||
|
||||
if (url.includes("/user/me")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin",
|
||||
role: "super-admin",
|
||||
name: "Admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
if (url.includes("/admin/users")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
items: [
|
||||
{
|
||||
id: "u-1",
|
||||
name: "User One",
|
||||
email: "u1@test.com",
|
||||
status: "active",
|
||||
role: "user",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
if (url.includes("/admin/tenants")) {
|
||||
return route.fulfill({
|
||||
json: { items: [], total: 0 },
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
return route.fulfill({ json: { items: [], total: 0 }, headers });
|
||||
});
|
||||
|
||||
await page.goto("/users");
|
||||
await expect(page.locator("table")).toContainText("User One", {
|
||||
timeout: 20000,
|
||||
});
|
||||
|
||||
await page.locator('table input[type="checkbox"]').nth(1).click();
|
||||
const selectionBar = page.getByTestId("bulk-action-bar");
|
||||
await expect(selectionBar).toBeVisible({ timeout: 15000 });
|
||||
await expect(page.getByTestId("bulk-permission-select")).toBeVisible();
|
||||
await expect(page.getByTestId("bulk-apply-btn")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should filter and highlight nodes in organization tree", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/tenants/t-1");
|
||||
// 테넌트 이름이 제목으로 나올 때까지 대기
|
||||
await expect(page.locator("h2").last()).toContainText(
|
||||
/Main Tenant|상세|Profile/i,
|
||||
{ timeout: 20000 },
|
||||
);
|
||||
|
||||
const subTenantLink = page
|
||||
.locator("a, button")
|
||||
.filter({ hasText: /조직 관리|Organization|Sub-tenant/i })
|
||||
.first();
|
||||
await subTenantLink.click();
|
||||
|
||||
// 트리 검색 입력창 대기 (더 유연한 셀렉터)
|
||||
const searchInput = page
|
||||
.locator('input[placeholder*="검색"], input[placeholder*="Search"]')
|
||||
.first();
|
||||
await expect(searchInput).toBeVisible({ timeout: 15000 });
|
||||
|
||||
await searchInput.fill("Eng");
|
||||
|
||||
const engNode = page
|
||||
.locator('button, [role="button"]')
|
||||
.filter({ hasText: "Engineering" })
|
||||
.first();
|
||||
await expect(engNode).toBeVisible();
|
||||
await expect(engNode).toHaveClass(/bg-primary\/5/);
|
||||
});
|
||||
});
|
||||
235
baron-sso/adminfront/tests/data_integrity.spec.ts
Normal file
235
baron-sso/adminfront/tests/data_integrity.spec.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Data integrity management", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
let orphanLoginIDDeleted = false;
|
||||
let integrityReportRequests = 0;
|
||||
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
window.localStorage.setItem("RoleSwitcher-Collapsed", "true");
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const clientId = "adminfront";
|
||||
const key = `oidc.user:${authority}:${clientId}`;
|
||||
window.localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify({
|
||||
access_token: "fake-token",
|
||||
token_type: "Bearer",
|
||||
profile: {
|
||||
sub: "admin-user",
|
||||
name: "Admin",
|
||||
email: "admin@test.com",
|
||||
role: "super_admin",
|
||||
},
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/user/me", async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
email: "admin@test.com",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/admin/integrity", async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
status: "fail",
|
||||
checkedAt: "2026-05-14T00:00:00Z",
|
||||
summary: {
|
||||
totalChecks: 2,
|
||||
passed: 1,
|
||||
warnings: 0,
|
||||
failures: 1,
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
key: "tenant_integrity",
|
||||
label: "테넌트 정합성",
|
||||
status: "fail",
|
||||
checks: [
|
||||
{
|
||||
key: "duplicate_tenant_slugs",
|
||||
label: "중복 테넌트 slug",
|
||||
description:
|
||||
"삭제되지 않은 tenant의 slug를 대소문자 무시 기준으로 검사합니다.",
|
||||
status: "fail",
|
||||
severity: "error",
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await page.route(/.*\/api\/v1\/.*/, async (route) => {
|
||||
const url = route.request().url();
|
||||
if (url.includes("/api/v1/user/me")) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
email: "admin@test.com",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (url.includes("/api/v1/admin/integrity/orphan-user-login-ids")) {
|
||||
if (route.request().method() === "DELETE") {
|
||||
orphanLoginIDDeleted = true;
|
||||
await route.fulfill({
|
||||
json: {
|
||||
deletedCount: 1,
|
||||
deleted: [
|
||||
{
|
||||
id: "login-id-1",
|
||||
userId: "user-1",
|
||||
tenantId: "tenant-1",
|
||||
fieldKey: "emp_id",
|
||||
loginId: "EMP001",
|
||||
reasons: ["deleted_tenant"],
|
||||
},
|
||||
],
|
||||
skippedIds: [],
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
await route.fulfill({
|
||||
json: orphanLoginIDDeleted
|
||||
? { items: [], total: 0 }
|
||||
: {
|
||||
items: [
|
||||
{
|
||||
id: "login-id-1",
|
||||
userId: "user-1",
|
||||
userEmail: "missing@example.com",
|
||||
tenantId: "tenant-1",
|
||||
tenantSlug: "deleted-tenant",
|
||||
fieldKey: "emp_id",
|
||||
loginId: "EMP001",
|
||||
reasons: ["deleted_tenant"],
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (url.includes("/api/v1/admin/integrity")) {
|
||||
integrityReportRequests += 1;
|
||||
if (integrityReportRequests > 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
}
|
||||
await route.fulfill({
|
||||
json: {
|
||||
status: "fail",
|
||||
checkedAt: "2026-05-14T00:00:00Z",
|
||||
summary: {
|
||||
totalChecks: 2,
|
||||
passed: 1,
|
||||
warnings: 0,
|
||||
failures: 1,
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
key: "tenant_integrity",
|
||||
label: "테넌트 정합성",
|
||||
status: "fail",
|
||||
checks: [
|
||||
{
|
||||
key: "duplicate_tenant_slugs",
|
||||
label: "중복 테넌트 slug",
|
||||
description:
|
||||
"삭제되지 않은 tenant의 slug를 대소문자 무시 기준으로 검사합니다.",
|
||||
status: "fail",
|
||||
severity: "error",
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (route.request().method() === "GET") {
|
||||
await route.fulfill({ json: { items: [], total: 0 } });
|
||||
} else {
|
||||
await route.fulfill({ status: 200, json: {} });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("shows the super-admin integrity report", async ({ page }) => {
|
||||
await page.goto("/system/data-integrity");
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "데이터 정합성 검증" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("테넌트 정합성")).toBeVisible();
|
||||
await expect(page.getByText("중복 테넌트 slug")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "다시 검사" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows manual recheck progress and completion", async ({ page }) => {
|
||||
await page.goto("/system/data-integrity");
|
||||
|
||||
await expect(page.getByText("중복 테넌트 slug")).toBeVisible();
|
||||
await page.getByRole("button", { name: "다시 검사" }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: "검사 중" })).toBeDisabled();
|
||||
await expect(page.getByText("정합성 검사를 실행 중입니다.")).toBeVisible();
|
||||
await expect(page.getByText("검사가 완료되었습니다.")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "다시 검사" })).toBeEnabled();
|
||||
});
|
||||
|
||||
test("deletes selected orphan login ID targets after confirmation", async ({
|
||||
page,
|
||||
}) => {
|
||||
page.on("dialog", async (dialog) => {
|
||||
await dialog.accept();
|
||||
});
|
||||
|
||||
await page.goto("/system/data-integrity");
|
||||
|
||||
await expect(page.getByText("EMP001")).toBeVisible();
|
||||
await expect(page.getByText("삭제된 테넌트")).toBeVisible();
|
||||
await page.getByRole("checkbox", { name: "EMP001 선택" }).check();
|
||||
await page.getByRole("button", { name: "선택 삭제" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText("1개의 유령 로그인 ID를 삭제했습니다."),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("삭제할 유령 로그인 ID가 없습니다."),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows the latest integrity summary on the overview page", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByText("정합성 최종 검증")).toBeVisible();
|
||||
await expect(page.getByText("실패 1건")).toBeVisible();
|
||||
await expect(page.getByText("테넌트 정합성")).toBeVisible();
|
||||
});
|
||||
});
|
||||
131
baron-sso/adminfront/tests/owners.spec.ts
Normal file
131
baron-sso/adminfront/tests/owners.spec.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Tenant Owners Management", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
access_token: "fake-token",
|
||||
token_type: "Bearer",
|
||||
profile: {
|
||||
sub: "admin-user",
|
||||
name: "Admin User",
|
||||
email: "admin@example.com",
|
||||
role: "super_admin",
|
||||
},
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
};
|
||||
window.localStorage.setItem(key, JSON.stringify(authData));
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
|
||||
await page.route(/.*\/api\/v1\/.*/, async (route) => {
|
||||
const url = route.request().url();
|
||||
if (url.includes("/user/me")) {
|
||||
console.log("Mocking ME");
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin User",
|
||||
email: "admin@example.com",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (url.includes("/owners")) {
|
||||
return route.fulfill({
|
||||
json: [
|
||||
{ id: "owner-1", name: "Owner One", email: "owner1@example.com" },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (url.includes("/admins")) {
|
||||
return route.fulfill({ json: [] });
|
||||
}
|
||||
if (url.includes("/admin/tenants/tenant-1")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "tenant-1",
|
||||
name: "Test Tenant",
|
||||
slug: "test-tenant",
|
||||
status: "active",
|
||||
type: "COMPANY",
|
||||
},
|
||||
});
|
||||
}
|
||||
if (url.includes("/admin/users") && route.request().method() === "GET") {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
items: [
|
||||
{ id: "user-2", name: "User Two", email: "user2@example.com" },
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (route.request().method() === "GET") {
|
||||
return route.fulfill({ json: { items: [], total: 0 } });
|
||||
}
|
||||
return route.fulfill({ status: 200, json: {} });
|
||||
});
|
||||
});
|
||||
|
||||
test("should list tenant owners", async ({ page }) => {
|
||||
await page.goto("/tenants/tenant-1/permissions");
|
||||
|
||||
await expect(page.locator(".animate-spin").first()).not.toBeVisible();
|
||||
|
||||
await expect(page.getByText(/테넌트 소유자|Tenant Owners/)).toBeVisible();
|
||||
await expect(page.locator("table").first()).toContainText("Owner One");
|
||||
await expect(page.locator("table").first()).toContainText(
|
||||
"owner1@example.com",
|
||||
);
|
||||
});
|
||||
|
||||
test("should add a new owner", async ({ page }) => {
|
||||
// Specific override for this test
|
||||
await page.route(
|
||||
"**/api/v1/admin/tenants/tenant-1/owners",
|
||||
async (route) => {
|
||||
if (route.request().method() === "GET") {
|
||||
await route.fulfill({ json: [] });
|
||||
} else {
|
||||
await route.fulfill({ status: 200, json: {} });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await page.goto("/tenants/tenant-1/permissions");
|
||||
|
||||
await expect(page.locator(".animate-spin").first()).not.toBeVisible();
|
||||
|
||||
await page.click(
|
||||
'button:has-text("소유자 추가"), button:has-text("Add Owner")',
|
||||
);
|
||||
await page.fill(
|
||||
'input[placeholder*="사용자 검색"], input[placeholder*="Search users"]',
|
||||
"User Two",
|
||||
);
|
||||
|
||||
const addButton = page
|
||||
.locator("role=dialog")
|
||||
.getByRole("button", { name: /추가|Add/ });
|
||||
await addButton.click();
|
||||
|
||||
await expect(page.getByText("소유자가 추가되었습니다.")).toBeVisible();
|
||||
await expect(page.locator("table").first()).toContainText("User Two");
|
||||
await page.waitForTimeout(1200);
|
||||
await expect(page.locator("table").first()).toContainText("User Two");
|
||||
});
|
||||
});
|
||||
294
baron-sso/adminfront/tests/security_roles.spec.ts
Normal file
294
baron-sso/adminfront/tests/security_roles.spec.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("보안 및 접근 제어: 시스템 관리자 vs 일반 사용자", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
page.on("console", (msg) => console.log(`[PAGE] ${msg.text()}`));
|
||||
});
|
||||
|
||||
const setupAuth = async (
|
||||
page,
|
||||
role: string,
|
||||
profileOverrides: Record<string, unknown> = {},
|
||||
) => {
|
||||
// 1. Inject initial state and mock tokens
|
||||
await page.addInitScript(
|
||||
({ role }) => {
|
||||
const authority = `${window.location.origin}/oidc`;
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
id_token: "fake-id-token",
|
||||
access_token: "fake-token",
|
||||
token_type: "Bearer",
|
||||
scope: "openid profile email",
|
||||
profile: {
|
||||
sub: "test-user",
|
||||
name: "테스트 사용자",
|
||||
email: "test@example.com",
|
||||
role: role,
|
||||
},
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
};
|
||||
window.localStorage.setItem(key, JSON.stringify(authData));
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("oidc.state", "dummy");
|
||||
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
},
|
||||
{ role },
|
||||
);
|
||||
|
||||
// 2. OIDC Configuration Mocking
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
const url = route.request().url();
|
||||
if (url.includes("/.well-known/openid-configuration")) {
|
||||
const origin = new URL(url).origin;
|
||||
await route.fulfill({
|
||||
json: {
|
||||
issuer: `${origin}/oidc`,
|
||||
authorization_endpoint: `${origin}/oidc/auth`,
|
||||
token_endpoint: `${origin}/oidc/token`,
|
||||
jwks_uri: `${origin}/oidc/jwks`,
|
||||
userinfo_endpoint: `${origin}/oidc/userinfo`,
|
||||
end_session_endpoint: `${origin}/oidc/session/end`,
|
||||
response_types_supported: ["code", "id_token"],
|
||||
subject_types_supported: ["public"],
|
||||
id_token_signing_alg_values_supported: ["RS256"],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} else {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
json: {},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 3. API Mocking
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const url = route.request().url();
|
||||
if (url.includes("/user/me")) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: "test-user",
|
||||
name: "테스트 사용자",
|
||||
email: "test@example.com",
|
||||
role: role,
|
||||
manageableTenants: [],
|
||||
...profileOverrides,
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} else if (url.includes("/tenants")) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
items: [
|
||||
{
|
||||
id: "t1",
|
||||
name: "테넌트 1",
|
||||
slug: "t1",
|
||||
status: "active",
|
||||
type: "COMPANY",
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} else if (url.match(/\/admin\/users\/u1$/)) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
id: "u1",
|
||||
name: "사용자 1",
|
||||
email: "u1@example.com",
|
||||
role: "user",
|
||||
status: "active",
|
||||
tenantId: "t1",
|
||||
tenantSlug: "t1",
|
||||
tenant: {
|
||||
id: "t1",
|
||||
name: "테넌트 1",
|
||||
slug: "t1",
|
||||
status: "active",
|
||||
type: "COMPANY",
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} else if (url.includes("/rp-history")) {
|
||||
await route.fulfill({
|
||||
json: [],
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} else if (url.includes("/users")) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
items: [
|
||||
{
|
||||
id: "u1",
|
||||
name: "사용자 1",
|
||||
email: "u1@example.com",
|
||||
role: "user",
|
||||
status: "active",
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} else if (url.includes("/admin/integrity")) {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
status: "pass",
|
||||
checkedAt: new Date().toISOString(),
|
||||
summary: { failures: 0, warnings: 0, pass: 10 },
|
||||
sections: [],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} else if (url.includes("/password-policy")) {
|
||||
await route.fulfill({
|
||||
json: { minLength: 8, minCharacterTypes: 2 },
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} else {
|
||||
await route.fulfill({
|
||||
json: { items: [], total: 0 },
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
test.describe("시스템 관리자 (Super Admin) 권한", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupAuth(page, "super_admin");
|
||||
await page.goto("/");
|
||||
await expect(page.locator("aside")).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("모든 행정 메뉴가 보여야 함", async ({ page }) => {
|
||||
await expect(page.locator('a[href="/tenants"]')).toBeVisible();
|
||||
await expect(page.locator('a[href="/api-keys"]')).toBeVisible();
|
||||
await expect(page.locator('a[href="/audit-logs"]')).toBeVisible();
|
||||
await expect(page.locator('a[href="/system/ory-ssot"]')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('a[href="/system/data-integrity"]'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("테넌트 관리 기능에 접근 가능해야 함", async ({ page }) => {
|
||||
await page.goto("/tenants");
|
||||
// "테넌트 추가" 버튼 확인
|
||||
await expect(
|
||||
page.getByRole("link", { name: /테넌트 추가/i }),
|
||||
).toBeVisible();
|
||||
// "데이터 관리" 드롭다운 확인
|
||||
await expect(page.getByTestId("tenant-data-mgmt-btn")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("일반 사용자 (Tenant Member) 제한", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupAuth(page, "user");
|
||||
await page.goto("/");
|
||||
await expect(page.locator("aside")).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("관리자 전용 메뉴가 숨겨져야 함", async ({ page }) => {
|
||||
await expect(page.locator('a[href="/tenants"]')).not.toBeVisible();
|
||||
await expect(page.locator('a[href="/api-keys"]')).not.toBeVisible();
|
||||
await expect(
|
||||
page.locator('a[href="/system/ory-ssot"]'),
|
||||
).not.toBeVisible();
|
||||
await expect(
|
||||
page.locator('a[href="/system/data-integrity"]'),
|
||||
).not.toBeVisible();
|
||||
|
||||
// 일반 사용자가 볼 수 있는 메뉴 확인
|
||||
await expect(page.locator('a[href="/audit-logs"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test("테넌트 목록 페이지 접근 시 차단되어야 함", async ({ page }) => {
|
||||
await page.goto("/tenants");
|
||||
// AppLayout.tsx에서 profileRole !== 'super_admin'일 때 보여주는 메시지 확인
|
||||
await expect(
|
||||
page.getByText(
|
||||
/접근 권한이 없습니다|이 작업을 수행할 권한이 없습니다/i,
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("다른 사용자 상세 페이지 직접 접근 시 차단되어야 함", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/users/other-user");
|
||||
await expect(
|
||||
page.getByText(/이 작업을 수행할 권한이 없습니다/i),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("사용자 생성 페이지 직접 접근 시 차단되어야 함", async ({ page }) => {
|
||||
await page.goto("/users/new");
|
||||
await expect(
|
||||
page.getByText(/이 작업을 수행할 권한이 없습니다/i),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("테넌트 관리자 권한", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupAuth(page, "tenant_admin", {
|
||||
tenantId: "t1",
|
||||
tenantSlug: "t1",
|
||||
manageableTenants: [
|
||||
{
|
||||
id: "t1",
|
||||
name: "테넌트 1",
|
||||
slug: "t1",
|
||||
status: "active",
|
||||
type: "COMPANY",
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.goto("/");
|
||||
await expect(page.locator("aside")).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("사용자 관리 목록에 접근 가능해야 함", async ({ page }) => {
|
||||
await page.goto("/users");
|
||||
|
||||
await expect(
|
||||
page.getByTestId("page-title").filter({ hasText: /사용자 관리/i }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("사용자 1")).toBeVisible();
|
||||
});
|
||||
|
||||
test("사용자 생성 화면에 접근 가능해야 함", async ({ page }) => {
|
||||
await page.goto("/users/new");
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "사용자 추가" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("관리 대상 테넌트 사용자 상세에 접근 가능해야 함", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/users/u1");
|
||||
|
||||
await expect(page.getByText("사용자 1")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(/이 작업을 수행할 권한이 없습니다/i),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
80
baron-sso/adminfront/tests/shell_layout.spec.ts
Normal file
80
baron-sso/adminfront/tests/shell_layout.spec.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Admin shell layout", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
window.localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify({
|
||||
access_token: "fake-token",
|
||||
token_type: "Bearer",
|
||||
profile: { sub: "admin-user", name: "Admin", role: "super_admin" },
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const url = route.request().url();
|
||||
const headers = { "Access-Control-Allow-Origin": "*" };
|
||||
|
||||
if (url.includes("/user/me")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/admin/tenants")) {
|
||||
return route.fulfill({
|
||||
json: { items: [], total: 0, limit: 1000, offset: 0 },
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
return route.fulfill({ json: { items: [], total: 0 }, headers });
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps navigation in the left sidebar without covering content", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 900, height: 700 });
|
||||
await page.goto("/tenants");
|
||||
|
||||
const sidebar = page.locator("aside").first();
|
||||
const main = page.locator("main").first();
|
||||
|
||||
await expect(sidebar).toBeVisible();
|
||||
await expect(main).toBeVisible();
|
||||
|
||||
const sidebarBox = await sidebar.boundingBox();
|
||||
const mainBox = await main.boundingBox();
|
||||
|
||||
expect(sidebarBox).not.toBeNull();
|
||||
expect(mainBox).not.toBeNull();
|
||||
expect(sidebarBox?.x).toBeLessThanOrEqual(1);
|
||||
expect(sidebarBox?.width).toBeLessThanOrEqual(260);
|
||||
expect(mainBox?.x).toBeGreaterThanOrEqual(
|
||||
(sidebarBox?.x ?? 0) + (sidebarBox?.width ?? 0) - 1,
|
||||
);
|
||||
});
|
||||
});
|
||||
158
baron-sso/adminfront/tests/tenant_domains.spec.ts
Normal file
158
baron-sso/adminfront/tests/tenant_domains.spec.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Tenant Allowed Domains", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
window.localStorage.setItem("RoleSwitcher-Collapsed", "true");
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
window.localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify({
|
||||
access_token: "fake-token",
|
||||
token_type: "Bearer",
|
||||
profile: { sub: "admin-user", name: "Admin", role: "super_admin" },
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
});
|
||||
|
||||
test("adds samaneng.com to the current tenant after duplicate warning confirmation", async ({
|
||||
page,
|
||||
}) => {
|
||||
let savedPayload:
|
||||
| {
|
||||
domains?: string[];
|
||||
forceDomainConflicts?: string[];
|
||||
}
|
||||
| undefined;
|
||||
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const url = route.request().url();
|
||||
const method = route.request().method();
|
||||
const headers = { "Access-Control-Allow-Origin": "*" };
|
||||
|
||||
if (url.includes("/user/me")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/admin/tenants/current") && method === "GET") {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "current",
|
||||
name: "현재 테넌트",
|
||||
slug: "current",
|
||||
type: "COMPANY",
|
||||
description: "",
|
||||
status: "active",
|
||||
domains: [],
|
||||
memberCount: 0,
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/admin/tenants/current") && method === "PUT") {
|
||||
savedPayload = route.request().postDataJSON();
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "current",
|
||||
name: "현재 테넌트",
|
||||
slug: "current",
|
||||
type: "COMPANY",
|
||||
description: "",
|
||||
status: "active",
|
||||
domains: savedPayload?.domains ?? [],
|
||||
memberCount: 0,
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/admin/tenants")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
items: [
|
||||
{
|
||||
id: "current",
|
||||
name: "현재 테넌트",
|
||||
slug: "current",
|
||||
type: "COMPANY",
|
||||
description: "",
|
||||
status: "active",
|
||||
domains: [],
|
||||
memberCount: 0,
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
},
|
||||
{
|
||||
id: "existing",
|
||||
name: "한맥가족",
|
||||
slug: "hanmac-family",
|
||||
type: "COMPANY",
|
||||
description: "",
|
||||
status: "active",
|
||||
domains: ["samaneng.com"],
|
||||
memberCount: 0,
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
return route.fulfill({ json: {}, headers });
|
||||
});
|
||||
|
||||
await page.goto("/tenants/current");
|
||||
await page.locator("#tenant-domains").fill("samaneng.com");
|
||||
await page.keyboard.press("Space");
|
||||
|
||||
await expect(
|
||||
page.getByText(
|
||||
"samaneng.com 도메인은 한맥가족 테넌트에 이미 설정되어 있습니다. 그래도 현재 테넌트에도 추가하시겠습니까?",
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "계속 진행" }).click();
|
||||
await expect(page.getByText("samaneng.com")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "저장" }).click();
|
||||
|
||||
await expect
|
||||
.poll(() => savedPayload)
|
||||
.toMatchObject({
|
||||
domains: ["samaneng.com"],
|
||||
forceDomainConflicts: ["samaneng.com"],
|
||||
});
|
||||
});
|
||||
});
|
||||
124
baron-sso/adminfront/tests/tenant_schema.spec.ts
Normal file
124
baron-sso/adminfront/tests/tenant_schema.spec.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Tenant Schema Management", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
window.localStorage.setItem("RoleSwitcher-Collapsed", "true");
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
access_token: "fake-token",
|
||||
token_type: "Bearer",
|
||||
profile: { sub: "admin-user", name: "Admin", role: "super_admin" },
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
};
|
||||
window.localStorage.setItem(key, JSON.stringify(authData));
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
});
|
||||
|
||||
test("should force indexed when schema field is used as login ID", async ({
|
||||
page,
|
||||
}) => {
|
||||
let savedConfig: Record<string, unknown> | undefined;
|
||||
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const url = route.request().url();
|
||||
const method = route.request().method();
|
||||
const headers = { "Access-Control-Allow-Origin": "*" };
|
||||
|
||||
if (url.includes("/user/me")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/admin/tenants/t-1") && method === "GET") {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "t-1",
|
||||
name: "Test Tenant",
|
||||
slug: "test-tenant",
|
||||
type: "COMPANY",
|
||||
status: "active",
|
||||
config: { userSchema: [] },
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/admin/tenants/t-1") && method === "PUT") {
|
||||
const payload = route.request().postDataJSON() as {
|
||||
config?: Record<string, unknown>;
|
||||
};
|
||||
savedConfig = payload.config;
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "t-1",
|
||||
name: "Test Tenant",
|
||||
slug: "test-tenant",
|
||||
type: "COMPANY",
|
||||
status: "active",
|
||||
config: savedConfig,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
return route.fulfill({ json: { items: [], total: 0 }, headers });
|
||||
});
|
||||
|
||||
await page.goto("/tenants/t-1/schema");
|
||||
await expect(
|
||||
page.getByText(/사용자 스키마 확장|User Schema Extension/),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: /필드 추가/ }).click();
|
||||
await page
|
||||
.getByPlaceholder(/예: employee_id|e\.g\. employee_id/)
|
||||
.fill("emp_no");
|
||||
await page.getByPlaceholder("예: 사번").fill("사번");
|
||||
|
||||
const indexedCheckbox = page.getByLabel(/검색 인덱스 필요|조회 인덱스/);
|
||||
await expect(indexedCheckbox).not.toBeChecked();
|
||||
await expect(indexedCheckbox).toBeEnabled();
|
||||
|
||||
await page.getByLabel("로그인 ID로 사용").check();
|
||||
await expect(indexedCheckbox).toBeChecked();
|
||||
await expect(indexedCheckbox).toBeDisabled();
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: /변경사항 저장|스키마 저장|Save/ })
|
||||
.click();
|
||||
|
||||
await expect
|
||||
.poll(() => savedConfig)
|
||||
.toMatchObject({
|
||||
userSchema: [
|
||||
{
|
||||
key: "emp_no",
|
||||
label: "사번",
|
||||
type: "text",
|
||||
indexed: true,
|
||||
isLoginId: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
113
baron-sso/adminfront/tests/tenant_seed_protection.spec.ts
Normal file
113
baron-sso/adminfront/tests/tenant_seed_protection.spec.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
const tenants = [
|
||||
{
|
||||
id: "seed-hanmac",
|
||||
name: "한맥가족",
|
||||
slug: "hanmac-family",
|
||||
type: "COMPANY_GROUP",
|
||||
description: "한맥가족 기본 루트 테넌트",
|
||||
status: "active",
|
||||
domains: [],
|
||||
memberCount: 0,
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
},
|
||||
{
|
||||
id: "normal-tenant",
|
||||
name: "일반 테넌트",
|
||||
slug: "normal-tenant",
|
||||
type: "COMPANY",
|
||||
description: "",
|
||||
status: "active",
|
||||
domains: [],
|
||||
memberCount: 0,
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
},
|
||||
];
|
||||
|
||||
test.describe("Seed tenant protection", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
window.localStorage.setItem("RoleSwitcher-Collapsed", "true");
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
window.localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify({
|
||||
access_token: "fake-token",
|
||||
token_type: "Bearer",
|
||||
profile: { sub: "admin-user", name: "Admin", role: "super_admin" },
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const url = route.request().url();
|
||||
const headers = { "Access-Control-Allow-Origin": "*" };
|
||||
|
||||
if (url.includes("/user/me")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/admin/tenants/seed-hanmac")) {
|
||||
return route.fulfill({ json: tenants[0], headers });
|
||||
}
|
||||
|
||||
if (url.includes("/admin/tenants")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
items: tenants,
|
||||
total: tenants.length,
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
return route.fulfill({ json: {}, headers });
|
||||
});
|
||||
});
|
||||
|
||||
test("removes selection and disables delete action for seed tenants in the list", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/tenants");
|
||||
|
||||
const seedRow = page.getByRole("row", { name: /한맥가족/ });
|
||||
await expect(seedRow.getByRole("checkbox")).toHaveCount(0);
|
||||
await expect(seedRow.getByText("초기 설정")).toBeVisible();
|
||||
|
||||
const normalRow = page.getByRole("row", { name: /일반 테넌트/ });
|
||||
await expect(normalRow.getByRole("checkbox")).toBeEnabled();
|
||||
});
|
||||
|
||||
test("disables delete action on seed tenant profile", async ({ page }) => {
|
||||
await page.goto("/tenants/seed-hanmac");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "한맥가족" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "삭제" })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
1578
baron-sso/adminfront/tests/tenants.spec.ts
Normal file
1578
baron-sso/adminfront/tests/tenants.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
120
baron-sso/adminfront/tests/tenants_live.spec.ts
Normal file
120
baron-sso/adminfront/tests/tenants_live.spec.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
const liveE2E = process.env.LIVE_BACKEND_E2E === "1";
|
||||
const oidcAuthority = "https://sso.hmac.kr/oidc";
|
||||
const clientId = "adminfront";
|
||||
|
||||
test.describe("Tenants CSV live E2E", () => {
|
||||
test.skip(!liveE2E, "Set LIVE_BACKEND_E2E=1 to run against a live backend.");
|
||||
|
||||
test.beforeEach(async ({ page, baseURL }) => {
|
||||
await page.addInitScript(
|
||||
({ authority, client_id }) => {
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("X-Mock-Role-Enabled", "true");
|
||||
window.localStorage.setItem("X-Mock-Role", "super_admin");
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
window.localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify({
|
||||
access_token: "live-e2e-placeholder-token",
|
||||
token_type: "Bearer",
|
||||
profile: {
|
||||
sub: "live-e2e-admin",
|
||||
name: "Live E2E Admin",
|
||||
role: "super_admin",
|
||||
},
|
||||
expires_at: Math.floor(Date.now() / 1000) + 3600,
|
||||
}),
|
||||
);
|
||||
},
|
||||
{ authority: oidcAuthority, client_id: clientId },
|
||||
);
|
||||
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
const liveUrl = `${baseURL}${requestUrl.pathname}${requestUrl.search}`;
|
||||
const { authorization: _authorization, ...headers } = route
|
||||
.request()
|
||||
.headers();
|
||||
headers["x-test-role"] = "super_admin";
|
||||
const response = await route.fetch({ url: liveUrl, headers });
|
||||
await route.fulfill({ response });
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
issuer: oidcAuthority,
|
||||
authorization_endpoint: `${oidcAuthority}/auth`,
|
||||
token_endpoint: `${oidcAuthority}/token`,
|
||||
jwks_uri: `${oidcAuthority}/jwks`,
|
||||
userinfo_endpoint: `${oidcAuthority}/userinfo`,
|
||||
end_session_endpoint: `${oidcAuthority}/session/end`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("exports and imports tenant CSV through the admin UI", async ({
|
||||
page,
|
||||
baseURL,
|
||||
}) => {
|
||||
const slug = `csv-live-${Date.now()}`;
|
||||
let tenantId = "";
|
||||
|
||||
await page.goto("/tenants");
|
||||
await expect(page.locator("h2").last()).toContainText(
|
||||
/테넌트 목록|Tenants/i,
|
||||
);
|
||||
await expect(page.getByTestId("tenant-export-btn")).toBeVisible();
|
||||
await expect(page.getByTestId("tenant-import-btn")).toBeVisible();
|
||||
|
||||
const download = page.waitForEvent("download");
|
||||
await page.getByTestId("tenant-export-btn").click();
|
||||
const exported = await download;
|
||||
expect(exported.suggestedFilename()).toContain("tenants");
|
||||
|
||||
await page.getByTestId("tenant-import-input").setInputFiles({
|
||||
name: "tenants.csv",
|
||||
mimeType: "text/csv",
|
||||
buffer: Buffer.from(
|
||||
`tenant_id,name,type,parent_tenant_id,slug,memo,email_domain\n,Live CSV Tenant,COMPANY,,${slug},Live E2E import,${slug}.example.com\n`,
|
||||
),
|
||||
});
|
||||
|
||||
await expect(page.getByRole("dialog")).toContainText("CSV 가져오기 확인");
|
||||
await page.getByTestId("tenant-import-confirm-btn").click();
|
||||
|
||||
await expect(page.getByTestId("tenant-import-summary")).toContainText(
|
||||
/생성 1|Created 1/i,
|
||||
);
|
||||
|
||||
const listResponse = await page.request.get(
|
||||
`${baseURL}/api/v1/admin/tenants?limit=1000&offset=0`,
|
||||
{ headers: { "X-Test-Role": "super_admin" } },
|
||||
);
|
||||
expect(listResponse.ok()).toBeTruthy();
|
||||
const list = await listResponse.json();
|
||||
const imported = list.items.find(
|
||||
(tenant: { slug: string }) => tenant.slug === slug,
|
||||
);
|
||||
expect(imported).toMatchObject({
|
||||
name: "Live CSV Tenant",
|
||||
slug,
|
||||
description: "Live E2E import",
|
||||
domains: [`${slug}.example.com`],
|
||||
});
|
||||
tenantId = imported.id;
|
||||
|
||||
const deleteResponse = await page.request.delete(
|
||||
`${baseURL}/api/v1/admin/tenants/${tenantId}`,
|
||||
{ headers: { "X-Test-Role": "super_admin" } },
|
||||
);
|
||||
expect(deleteResponse.status()).toBe(204);
|
||||
});
|
||||
});
|
||||
1229
baron-sso/adminfront/tests/users.spec.ts
Normal file
1229
baron-sso/adminfront/tests/users.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
459
baron-sso/adminfront/tests/users_bulk.spec.ts
Normal file
459
baron-sso/adminfront/tests/users_bulk.spec.ts
Normal file
@@ -0,0 +1,459 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Users Bulk Upload", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
access_token: "fake-token",
|
||||
token_type: "Bearer",
|
||||
profile: { sub: "admin-user", name: "Admin", role: "super_admin" },
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
};
|
||||
window.localStorage.setItem(key, JSON.stringify(authData));
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const url = route.request().url();
|
||||
const headers = { "Access-Control-Allow-Origin": "*" };
|
||||
|
||||
if (url.includes("/user/me")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
if (url.includes("/admin/users")) {
|
||||
if (!url.includes("/bulk")) {
|
||||
return route.fulfill({
|
||||
json: { items: [], total: 0, limit: 50, offset: 0 },
|
||||
headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (url.includes("/admin/tenants")) {
|
||||
return route.fulfill({
|
||||
json: { items: [], total: 0, limit: 100, offset: 0 },
|
||||
headers,
|
||||
});
|
||||
}
|
||||
return route.fulfill({ json: { items: [], total: 0 }, headers });
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
});
|
||||
|
||||
test("should open bulk upload modal and show preview", async ({ page }) => {
|
||||
await page.goto("/users");
|
||||
// 헤더 타이틀이 뜰 때까지 대기
|
||||
await expect(page.getByTestId("page-title")).toContainText(
|
||||
/사용자|Users/i,
|
||||
{ timeout: 20000 },
|
||||
);
|
||||
|
||||
await page.getByTestId("user-data-mgmt-btn").click();
|
||||
await page
|
||||
.getByRole("menuitem", { name: /일괄 임포트|일괄 등록|Bulk Import/i })
|
||||
.click();
|
||||
|
||||
await expect(page.getByTestId("bulk-upload-title")).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
const downloadBtn = page
|
||||
.locator("button")
|
||||
.filter({ hasText: /템플릿 다운로드|템플릿 받기|Download Template/ })
|
||||
.first();
|
||||
await expect(downloadBtn).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show success results after mock upload", async ({ page }) => {
|
||||
await page.route("**/api/v1/admin/users/bulk", async (route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
results: [
|
||||
{ email: "success@test.com", success: true, userId: "u-1" },
|
||||
{
|
||||
email: "fail@test.com",
|
||||
success: false,
|
||||
message: "Invalid format",
|
||||
},
|
||||
],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto("/users");
|
||||
await expect(page.getByTestId("page-title")).toContainText(
|
||||
/사용자|Users/i,
|
||||
{ timeout: 20000 },
|
||||
);
|
||||
|
||||
await page.getByTestId("user-data-mgmt-btn").click();
|
||||
await page
|
||||
.getByRole("menuitem", { name: /일괄 임포트|일괄 등록|Bulk Import/i })
|
||||
.click();
|
||||
|
||||
const uploadBtn = page.getByTestId("bulk-start-btn");
|
||||
await expect(uploadBtn).toBeDisabled();
|
||||
});
|
||||
|
||||
test("should create missing tenant before user bulk import", async ({
|
||||
page,
|
||||
}) => {
|
||||
const requests: string[] = [];
|
||||
let bulkPayload = "";
|
||||
|
||||
await page.route("**/api/v1/admin/tenants**", async (route) => {
|
||||
const method = route.request().method();
|
||||
requests.push(`${method} ${route.request().url()}`);
|
||||
|
||||
if (method === "GET") {
|
||||
return route.fulfill({
|
||||
json: { items: [], total: 0, limit: 100, offset: 0 },
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
|
||||
if (method === "POST") {
|
||||
return route.fulfill({
|
||||
status: 201,
|
||||
json: {
|
||||
id: "staging-missing-tenant-id",
|
||||
name: "Missing Tenant",
|
||||
slug: "missing-slug",
|
||||
type: "COMPANY",
|
||||
description: "Imported memo",
|
||||
status: "active",
|
||||
domains: ["missing.example.com"],
|
||||
memberCount: 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
|
||||
return route.continue();
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/admin/users/bulk", async (route) => {
|
||||
bulkPayload = route.request().postData() ?? "";
|
||||
return route.fulfill({
|
||||
json: {
|
||||
results: [{ email: "new@test.com", success: true, userId: "u-1" }],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/users");
|
||||
await expect(page.getByTestId("page-title")).toContainText(
|
||||
/사용자|Users/i,
|
||||
{ timeout: 20000 },
|
||||
);
|
||||
|
||||
await page.getByTestId("user-data-mgmt-btn").click();
|
||||
await page
|
||||
.getByRole("menuitem", { name: /일괄 임포트|일괄 등록|Bulk Import/i })
|
||||
.click();
|
||||
await page.locator('input[type="file"]').setInputFiles({
|
||||
name: "users.csv",
|
||||
mimeType: "text/csv",
|
||||
buffer: Buffer.from(
|
||||
"email,name,tenant_id,tenant_slug,tenant_name,tenant_type,tenant_memo,email_domain\nnew@test.com,New User,local-tenant-id,missing-slug,Missing Tenant,COMPANY,Imported memo,missing.example.com\n",
|
||||
),
|
||||
});
|
||||
|
||||
await expect(
|
||||
page.getByTestId("user-import-tenant-resolution"),
|
||||
).toContainText(/신규 생성|Create new/i);
|
||||
await page.getByTestId("bulk-start-btn").click();
|
||||
|
||||
await expect(page.getByText("new@test.com")).toBeVisible();
|
||||
expect(requests.some((request) => request.startsWith("POST "))).toBe(true);
|
||||
expect(bulkPayload).toContain('"tenantId":"staging-missing-tenant-id"');
|
||||
expect(bulkPayload).toContain('"tenantSlug":"missing-slug"');
|
||||
expect(bulkPayload).toContain('"emailDomain":"missing.example.com"');
|
||||
});
|
||||
|
||||
test("should include one nullable additional appointment from numbered CSV columns", async ({
|
||||
page,
|
||||
}) => {
|
||||
let bulkPayload = "";
|
||||
|
||||
await page.unroute("**/api/v1/**");
|
||||
await page.route("**/api/v1/user/me", async (route) => {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
await page.route(/\/api\/v1\/admin\/users(?:\?|$)/, async (route) => {
|
||||
return route.fulfill({
|
||||
json: { items: [], total: 0, limit: 50, offset: 0 },
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
await page.route(/\/api\/v1\/admin\/tenants(?:\?|$)/, async (route) => {
|
||||
if (route.request().method() === "GET") {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
items: [
|
||||
{
|
||||
id: "tenant-primary-id",
|
||||
name: "Primary Tenant",
|
||||
slug: "primary-tenant",
|
||||
status: "active",
|
||||
type: "COMPANY",
|
||||
memberCount: 0,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: "tenant-second-id",
|
||||
name: "Second Tenant",
|
||||
slug: "second-tenant",
|
||||
status: "active",
|
||||
type: "COMPANY",
|
||||
memberCount: 0,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
return route.fulfill({
|
||||
status: 201,
|
||||
json: {
|
||||
id: "tenant-created-id",
|
||||
name: "Primary Tenant",
|
||||
slug: "primary-tenant",
|
||||
status: "active",
|
||||
type: "COMPANY",
|
||||
memberCount: 0,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/admin/users/bulk", async (route) => {
|
||||
bulkPayload = route.request().postData() ?? "";
|
||||
return route.fulfill({
|
||||
json: {
|
||||
results: [{ email: "dual@test.com", success: true, userId: "u-1" }],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/users");
|
||||
await expect(page.getByTestId("page-title")).toContainText(
|
||||
/사용자|Users/i,
|
||||
{ timeout: 20000 },
|
||||
);
|
||||
|
||||
await page.getByTestId("user-data-mgmt-btn").click();
|
||||
await page
|
||||
.getByRole("menuitem", { name: /일괄 임포트|일괄 등록|Bulk Import/i })
|
||||
.click();
|
||||
await page.locator('input[type="file"]').setInputFiles({
|
||||
name: "users.csv",
|
||||
mimeType: "text/csv",
|
||||
buffer: Buffer.from(
|
||||
[
|
||||
"email,name,phone,role,tenant_slug,department,grade,position,jobTitle,employee_id,tenant_slug1,department1,grade1,position1,jobTitle1,employee_id1,sub_email",
|
||||
"dual@test.com,Dual User,010-0000-0000,user,primary-tenant,개발팀,책임,팀장,Backend,EMP001,second-tenant,센터,수석,,Architecture,EMP002,dual.alias@hanmaceng.co.kr",
|
||||
].join("\n"),
|
||||
),
|
||||
});
|
||||
|
||||
await page.getByTestId("bulk-start-btn").click();
|
||||
await expect(page.getByText("dual@test.com")).toBeVisible();
|
||||
|
||||
const payload = JSON.parse(bulkPayload);
|
||||
expect(payload.users[0].tenantSlug).toBe("primary-tenant");
|
||||
expect(payload.users[0].metadata.employee_id).toBe("EMP001");
|
||||
expect(payload.users[0].metadata.sub_email).toEqual([
|
||||
"dual.alias@hanmaceng.co.kr",
|
||||
]);
|
||||
expect(payload.users[0].metadata.secondary_emails).toEqual([
|
||||
"dual.alias@hanmaceng.co.kr",
|
||||
]);
|
||||
expect(payload.users[0].metadata.aliasEmails).toEqual([
|
||||
"dual.alias@hanmaceng.co.kr",
|
||||
]);
|
||||
expect(payload.users[0].additionalAppointments).toEqual([
|
||||
{
|
||||
tenantSlug: "second-tenant",
|
||||
department: "센터",
|
||||
grade: "수석",
|
||||
jobTitle: "Architecture",
|
||||
metadata: {
|
||||
employee_id: "EMP002",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("should show GPDTDC as Baron representative without changing WorksMobile primary affiliation", async ({
|
||||
page,
|
||||
}) => {
|
||||
let bulkPayload = "";
|
||||
|
||||
await page.unroute("**/api/v1/**");
|
||||
await page.route("**/api/v1/user/me", async (route) => {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
await page.route(/\/api\/v1\/admin\/users(?:\?|$)/, async (route) => {
|
||||
return route.fulfill({
|
||||
json: { items: [], total: 0, limit: 50, offset: 0 },
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
await page.route(/\/api\/v1\/admin\/tenants(?:\?|$)/, async (route) => {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
items: [
|
||||
{
|
||||
id: "family-id",
|
||||
name: "한맥가족사",
|
||||
slug: "hanmac-family",
|
||||
status: "active",
|
||||
type: "COMPANY",
|
||||
memberCount: 0,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: "gpdtdc-id",
|
||||
name: "총괄기획&기술개발센터",
|
||||
slug: "gpdtdc",
|
||||
parentId: "family-id",
|
||||
status: "active",
|
||||
type: "COMPANY",
|
||||
memberCount: 0,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: "tdc-id",
|
||||
name: "기술개발센터",
|
||||
slug: "tdc",
|
||||
parentId: "gpdtdc-id",
|
||||
status: "active",
|
||||
type: "ORGANIZATION",
|
||||
memberCount: 0,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: "rnd-saman-id",
|
||||
name: "삼안기술개발센터",
|
||||
slug: "rnd-saman",
|
||||
status: "active",
|
||||
type: "ORGANIZATION",
|
||||
memberCount: 0,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 4,
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/admin/users/bulk", async (route) => {
|
||||
bulkPayload = route.request().postData() ?? "";
|
||||
return route.fulfill({
|
||||
json: {
|
||||
results: [
|
||||
{ email: "gpdtdc-dual@test.com", success: true, userId: "u-1" },
|
||||
],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/users");
|
||||
await expect(page.getByTestId("page-title")).toContainText(
|
||||
/사용자|Users/i,
|
||||
{ timeout: 20000 },
|
||||
);
|
||||
|
||||
await page.getByTestId("user-data-mgmt-btn").click();
|
||||
await page
|
||||
.getByRole("menuitem", { name: /일괄 임포트|일괄 등록|Bulk Import/i })
|
||||
.click();
|
||||
await page.locator('input[type="file"]').setInputFiles({
|
||||
name: "users.csv",
|
||||
mimeType: "text/csv",
|
||||
buffer: Buffer.from(
|
||||
[
|
||||
"email,name,phone,role,tenant_slug,department,grade,position,jobTitle,employee_id,tenant_slug1,department1,grade1,position1,jobTitle1,employee_id1",
|
||||
"gpdtdc-dual@test.com,GPDTDC Dual User,010-0000-0000,user,rnd-saman,삼안기술연구소,책임,,,SAMAN001,tdc,,책임연구원,,,B24051",
|
||||
].join("\n"),
|
||||
),
|
||||
});
|
||||
|
||||
await page.getByTestId("bulk-start-btn").click();
|
||||
await expect(page.getByText("gpdtdc-dual@test.com")).toBeVisible();
|
||||
|
||||
const payload = JSON.parse(bulkPayload);
|
||||
expect(payload.users[0].tenantSlug).toBe("gpdtdc");
|
||||
expect(payload.users[0].additionalAppointments).toEqual([
|
||||
expect.objectContaining({
|
||||
tenantSlug: "rnd-saman",
|
||||
isPrimary: true,
|
||||
department: "삼안기술연구소",
|
||||
grade: "책임",
|
||||
metadata: {
|
||||
employee_id: "SAMAN001",
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
tenantSlug: "tdc",
|
||||
isPrimary: false,
|
||||
grade: "책임연구원",
|
||||
metadata: {
|
||||
employee_id: "B24051",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
124
baron-sso/adminfront/tests/users_bulk_secondary.spec.ts
Normal file
124
baron-sso/adminfront/tests/users_bulk_secondary.spec.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
type BulkUsersRequest = {
|
||||
users: Array<{
|
||||
metadata: {
|
||||
sub_email?: string[];
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
test.describe("Users Bulk Upload Secondary Emails", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const authData = {
|
||||
access_token: "fake-token",
|
||||
token_type: "Bearer",
|
||||
profile: { sub: "admin-user", name: "Admin", role: "super_admin" },
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
};
|
||||
window.localStorage.setItem(
|
||||
"oidc.user:http://localhost:5000/oidc:adminfront",
|
||||
JSON.stringify(authData),
|
||||
);
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/user/me", async (route) => {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/admin/tenants*", async (route) => {
|
||||
return route.fulfill({
|
||||
json: { items: [], total: 0, limit: 100, offset: 0 },
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/admin/users*", async (route) => {
|
||||
if (route.request().url().includes("/bulk")) {
|
||||
return route.continue();
|
||||
}
|
||||
return route.fulfill({
|
||||
json: { items: [], total: 0, limit: 50, offset: 0 },
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
});
|
||||
|
||||
test("should parse secondary_emails and send to backend", async ({
|
||||
page,
|
||||
}) => {
|
||||
let bulkPayload: BulkUsersRequest | null = null;
|
||||
|
||||
await page.route("**/api/v1/admin/users/bulk", async (route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
bulkPayload = route.request().postDataJSON() as BulkUsersRequest;
|
||||
return route.fulfill({
|
||||
json: {
|
||||
results: [
|
||||
{ email: "test@example.com", success: true, userId: "u-1" },
|
||||
],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
}
|
||||
return route.continue();
|
||||
});
|
||||
|
||||
await page.goto("/users");
|
||||
await expect(page.getByTestId("page-title")).toContainText(
|
||||
/사용자|Users/i,
|
||||
{ timeout: 20000 },
|
||||
);
|
||||
|
||||
await page.getByTestId("user-data-mgmt-btn").click();
|
||||
await page
|
||||
.getByRole("menuitem", { name: /일괄 임포트|일괄 등록|Bulk Import/i })
|
||||
.click();
|
||||
|
||||
// Create a mock CSV with secondary_emails
|
||||
const csvContent = `email,sub_email,name,phone,role,tenant_slug\ntest@example.com,sub1@test.com;sub2@test.com,홍길동,010-1234-5678,user,tenant-slug`;
|
||||
|
||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||
await page.getByText(/파일 선택|Change file|Select file/i).click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
|
||||
await fileChooser.setFiles({
|
||||
name: "users_with_secondary.csv",
|
||||
mimeType: "text/csv",
|
||||
buffer: Buffer.from(csvContent),
|
||||
});
|
||||
|
||||
await expect(page.getByText(/파싱 중/)).not.toBeVisible();
|
||||
await expect(page.getByTestId("bulk-start-btn")).toBeEnabled();
|
||||
|
||||
await page.getByTestId("bulk-start-btn").click();
|
||||
|
||||
await expect(page.getByText(/성공|Success/i).first()).toBeVisible();
|
||||
|
||||
expect(bulkPayload).not.toBeNull();
|
||||
expect(bulkPayload.users).toHaveLength(1);
|
||||
|
||||
// The most important check - does it parse to the metadata
|
||||
expect(bulkPayload?.users[0].metadata.sub_email).toContain("sub1@test.com");
|
||||
expect(bulkPayload?.users[0].metadata.sub_email).toContain("sub2@test.com");
|
||||
});
|
||||
});
|
||||
139
baron-sso/adminfront/tests/users_bulk_uuid.spec.ts
Normal file
139
baron-sso/adminfront/tests/users_bulk_uuid.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("Users Bulk Upload UUID Import Policy", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
access_token: "fake-token",
|
||||
token_type: "Bearer",
|
||||
profile: { sub: "admin-user", name: "Admin", role: "super_admin" },
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
};
|
||||
window.localStorage.setItem(key, JSON.stringify(authData));
|
||||
});
|
||||
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const url = route.request().url();
|
||||
const headers = { "Access-Control-Allow-Origin": "*" };
|
||||
|
||||
if (url.includes("/user/me")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
role: "super_admin",
|
||||
manageableTenants: [],
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
if (url.includes("/admin/users") && !url.includes("/bulk")) {
|
||||
return route.fulfill({
|
||||
json: { items: [], total: 0, limit: 50, offset: 0 },
|
||||
headers,
|
||||
});
|
||||
}
|
||||
if (url.includes("/admin/tenants")) {
|
||||
return route.fulfill({
|
||||
json: { items: [], total: 0, limit: 100, offset: 0 },
|
||||
headers,
|
||||
});
|
||||
}
|
||||
return route.fulfill({ json: { items: [], total: 0 }, headers });
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
});
|
||||
|
||||
test("should not include exported user_id or uuid in bulk upload payload", async ({
|
||||
page,
|
||||
}) => {
|
||||
let bulkPayload = "";
|
||||
const testUuid = "550e8400-e29b-41d4-a716-446655440000";
|
||||
|
||||
await page.route("**/api/v1/admin/users/bulk", async (route) => {
|
||||
bulkPayload = route.request().postData() ?? "";
|
||||
return route.fulfill({
|
||||
json: {
|
||||
results: [
|
||||
{ email: "uuid@test.com", success: true, userId: "kratos-id" },
|
||||
],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/users");
|
||||
await page.getByTestId("user-data-mgmt-btn").click();
|
||||
await page
|
||||
.getByRole("menuitem", { name: /일괄 임포트|Bulk Import/i })
|
||||
.click();
|
||||
|
||||
await page.locator('input[type="file"]').setInputFiles({
|
||||
name: "users_uuid.csv",
|
||||
mimeType: "text/csv",
|
||||
buffer: Buffer.from(
|
||||
`user_id,email,name,uuid\n${testUuid},uuid@test.com,UUID User,${testUuid}\n`,
|
||||
),
|
||||
});
|
||||
|
||||
await page.getByTestId("bulk-start-btn").click();
|
||||
await expect(page.getByText("uuid@test.com")).toBeVisible();
|
||||
|
||||
const payload = JSON.parse(bulkPayload);
|
||||
expect(payload.users[0]).not.toHaveProperty("userId");
|
||||
expect(payload.users[0]).not.toHaveProperty("uuid");
|
||||
expect(payload.users[0]).not.toHaveProperty("id");
|
||||
});
|
||||
|
||||
test("should treat id column as login id, not UUID import", async ({
|
||||
page,
|
||||
}) => {
|
||||
let bulkPayload = "";
|
||||
const testUuid = "550e8400-e29b-41d4-a716-446655440001";
|
||||
|
||||
await page.route("**/api/v1/admin/users/bulk", async (route) => {
|
||||
bulkPayload = route.request().postData() ?? "";
|
||||
return route.fulfill({
|
||||
json: {
|
||||
results: [
|
||||
{ email: "id-uuid@test.com", success: true, userId: "kratos-id" },
|
||||
],
|
||||
},
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/users");
|
||||
await page.getByTestId("user-data-mgmt-btn").click();
|
||||
await page
|
||||
.getByRole("menuitem", { name: /일괄 임포트|Bulk Import/i })
|
||||
.click();
|
||||
|
||||
await page.locator('input[type="file"]').setInputFiles({
|
||||
name: "users_id.csv",
|
||||
mimeType: "text/csv",
|
||||
buffer: Buffer.from(
|
||||
`email,name,id\nid-uuid@test.com,ID UUID User,${testUuid}\n`,
|
||||
),
|
||||
});
|
||||
|
||||
await page.getByTestId("bulk-start-btn").click();
|
||||
|
||||
const payload = JSON.parse(bulkPayload);
|
||||
expect(payload.users[0]).not.toHaveProperty("uuid");
|
||||
expect(payload.users[0].loginId).toBe(testUuid);
|
||||
expect(payload.users[0].metadata.naverworks_id).toBe(testUuid);
|
||||
});
|
||||
});
|
||||
85
baron-sso/adminfront/tests/users_live.spec.ts
Normal file
85
baron-sso/adminfront/tests/users_live.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import fs from "node:fs";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
const liveE2E = process.env.LIVE_BACKEND_E2E === "1";
|
||||
const oidcAuthority = "https://sso.hmac.kr/oidc";
|
||||
const clientId = "adminfront";
|
||||
|
||||
test.describe("Users CSV live E2E", () => {
|
||||
test.skip(!liveE2E, "Set LIVE_BACKEND_E2E=1 to run against a live backend.");
|
||||
|
||||
test.beforeEach(async ({ page, baseURL }) => {
|
||||
await page.addInitScript(
|
||||
({ authority, client_id }) => {
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
window.localStorage.setItem("X-Mock-Role-Enabled", "true");
|
||||
window.localStorage.setItem("X-Mock-Role", "super_admin");
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
window.localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify({
|
||||
access_token: "live-e2e-placeholder-token",
|
||||
token_type: "Bearer",
|
||||
profile: {
|
||||
sub: "live-e2e-admin",
|
||||
name: "Live E2E Admin",
|
||||
role: "super_admin",
|
||||
},
|
||||
expires_at: Math.floor(Date.now() / 1000) + 3600,
|
||||
}),
|
||||
);
|
||||
},
|
||||
{ authority: oidcAuthority, client_id: clientId },
|
||||
);
|
||||
|
||||
await page.route("**/api/v1/**", async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
const liveUrl = `${baseURL}${requestUrl.pathname}${requestUrl.search}`;
|
||||
const { authorization: _authorization, ...headers } = route
|
||||
.request()
|
||||
.headers();
|
||||
headers["x-test-role"] = "super_admin";
|
||||
const response = await route.fetch({ url: liveUrl, headers });
|
||||
await route.fulfill({ response });
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
issuer: oidcAuthority,
|
||||
authorization_endpoint: `${oidcAuthority}/auth`,
|
||||
token_endpoint: `${oidcAuthority}/token`,
|
||||
jwks_uri: `${oidcAuthority}/jwks`,
|
||||
userinfo_endpoint: `${oidcAuthority}/userinfo`,
|
||||
end_session_endpoint: `${oidcAuthority}/session/end`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("exports user CSV through the authenticated admin UI path", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/users");
|
||||
await expect(page.getByTestId("page-title")).toContainText(/사용자|Users/i);
|
||||
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
await page.getByRole("button", { name: /내보내기|Export/i }).click();
|
||||
const download = await downloadPromise;
|
||||
|
||||
expect(download.suggestedFilename()).toContain("users");
|
||||
const path = await download.path();
|
||||
expect(path).toBeTruthy();
|
||||
|
||||
const csv = fs.readFileSync(path as string, "utf8");
|
||||
expect(csv).toContain(
|
||||
"ID,Email,Name,Phone,Status,Tenant,Position,JobTitle,CreatedAt",
|
||||
);
|
||||
expect(csv).not.toContain("Role");
|
||||
expect(csv).not.toContain("Department");
|
||||
});
|
||||
});
|
||||
233
baron-sso/adminfront/tests/users_schema.spec.ts
Normal file
233
baron-sso/adminfront/tests/users_schema.spec.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("User Schema Dynamic Form", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
const authority = "http://localhost:5000/oidc";
|
||||
const client_id = "adminfront";
|
||||
const key = `oidc.user:${authority}:${client_id}`;
|
||||
const authData = {
|
||||
id_token: "fake-id-token",
|
||||
access_token: "fake-token",
|
||||
token_type: "Bearer",
|
||||
scope: "openid profile email",
|
||||
profile: {
|
||||
sub: "admin-user",
|
||||
name: "Admin",
|
||||
email: "admin@test.com",
|
||||
role: "super_admin",
|
||||
},
|
||||
expires_at: Math.floor(Date.now() / 1000) + 36000,
|
||||
};
|
||||
window.localStorage.setItem(key, JSON.stringify(authData));
|
||||
window.localStorage.setItem("admin_session", "fake-token");
|
||||
window.localStorage.setItem("locale", "ko");
|
||||
|
||||
// Mock oidc state to prevent redirection if the library checks it
|
||||
window.localStorage.setItem("oidc.state", "dummy");
|
||||
|
||||
(
|
||||
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
|
||||
)._IS_TEST_MODE = true;
|
||||
});
|
||||
|
||||
await page.route("**/oidc/**", async (route) => {
|
||||
if (route.request().url().includes("/.well-known/openid-configuration")) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
issuer: "http://localhost:5000/oidc",
|
||||
authorization_endpoint: "http://localhost:5000/oidc/auth",
|
||||
token_endpoint: "http://localhost:5000/oidc/token",
|
||||
userinfo_endpoint: "http://localhost:5000/oidc/userinfo",
|
||||
jwks_uri: "http://localhost:5000/oidc/jwks",
|
||||
},
|
||||
});
|
||||
}
|
||||
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
|
||||
});
|
||||
|
||||
await page.route(/.*\/api\/v1\/.*/, async (route) => {
|
||||
const url = route.request().url();
|
||||
if (url.includes("/user/me")) {
|
||||
console.log("Mocking /user/me");
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "admin-user",
|
||||
name: "Admin",
|
||||
email: "admin@test.com",
|
||||
role: "super_admin",
|
||||
manageableTenants: [
|
||||
{ id: "t-1", name: "Test Tenant", slug: "test-tenant" },
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.match(/\/admin\/tenants\/t-1$/)) {
|
||||
console.log("Mocking /admin/tenants/t-1");
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "t-1",
|
||||
name: "Test Tenant",
|
||||
slug: "test-tenant",
|
||||
config: {
|
||||
userSchema: [
|
||||
{
|
||||
key: "emp_id",
|
||||
label: "Employee ID",
|
||||
required: true,
|
||||
validation: "^E[0-9]{3}$",
|
||||
},
|
||||
{
|
||||
key: "loginId",
|
||||
label: "Login ID",
|
||||
required: true,
|
||||
isLoginId: true,
|
||||
},
|
||||
{
|
||||
key: "salary",
|
||||
label: "Salary",
|
||||
adminOnly: true,
|
||||
type: "number",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.match(/\/admin\/users\/u-1$/)) {
|
||||
console.log("Mocking /admin/users/u-1");
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "u-1",
|
||||
name: "John Doe",
|
||||
email: "john@test.com",
|
||||
tenantSlug: "test-tenant",
|
||||
tenant: { id: "t-1", name: "Test Tenant", slug: "test-tenant" },
|
||||
joinedTenants: [
|
||||
{ id: "t-1", name: "Test Tenant", slug: "test-tenant" },
|
||||
],
|
||||
metadata: {
|
||||
"t-1": { emp_id: "E123", salary: 1000, loginId: "johndoe" },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/password/policy")) {
|
||||
console.log("Mocking /password/policy");
|
||||
return route.fulfill({
|
||||
json: {
|
||||
minLength: 12,
|
||||
lowercase: true,
|
||||
uppercase: true,
|
||||
number: true,
|
||||
nonAlphanumeric: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/rp-history")) {
|
||||
console.log("Mocking /rp-history");
|
||||
return route.fulfill({
|
||||
json: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (url.match(/\/admin\/tenants(\?.*)?$/)) {
|
||||
console.log("Mocking /admin/tenants");
|
||||
return route.fulfill({
|
||||
json: {
|
||||
items: [
|
||||
{
|
||||
id: "t-1",
|
||||
slug: "test-tenant",
|
||||
name: "Test Tenant",
|
||||
config: {
|
||||
userSchema: [
|
||||
{
|
||||
key: "emp_id",
|
||||
label: "Employee ID",
|
||||
required: true,
|
||||
validation: "^E[0-9]{3}$",
|
||||
},
|
||||
{
|
||||
key: "loginId",
|
||||
label: "Login ID",
|
||||
required: true,
|
||||
isLoginId: true,
|
||||
},
|
||||
{
|
||||
key: "salary",
|
||||
label: "Salary",
|
||||
adminOnly: true,
|
||||
type: "number",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Mocking default empty list for:", url);
|
||||
return route.fulfill({ json: { items: [], total: 0 } });
|
||||
});
|
||||
});
|
||||
|
||||
test("should render custom fields from schema in user detail", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/users/u-1");
|
||||
|
||||
// "테넌트 프로필" 탭 클릭
|
||||
await page
|
||||
.getByRole("tab", { name: /테넌트 프로필|Tenants|Tenant Profile/i })
|
||||
.click();
|
||||
|
||||
// 섹션 헤더 확인
|
||||
const header = page
|
||||
.getByText(/테넌트별 프로필 관리|Per-tenant Profile/i)
|
||||
.first();
|
||||
await header.waitFor({ state: "visible" });
|
||||
|
||||
// 커스텀 필드 레이블 확인
|
||||
await expect(page.getByText("Employee ID")).toBeVisible();
|
||||
|
||||
// input 값 확인 (id에 t-1.emp_id가 포함됨)
|
||||
const empIdInput = page.locator('input[id*="emp_id"]');
|
||||
await expect(empIdInput).toHaveValue("E123");
|
||||
|
||||
const salaryInput = page.locator('input[id*="salary"]');
|
||||
await expect(salaryInput).toHaveValue("1000");
|
||||
|
||||
await expect(page.getByText(/Admin Only/i).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show regex validation error for custom field", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/users/u-1");
|
||||
|
||||
// "테넌트 프로필" 탭 클릭
|
||||
await page
|
||||
.getByRole("tab", { name: /테넌트 프로필|Tenants|Tenant Profile/i })
|
||||
.click();
|
||||
|
||||
const empIdInput = page.locator('input[id*="emp_id"]');
|
||||
await empIdInput.waitFor({ state: "visible" });
|
||||
await empIdInput.fill("invalid");
|
||||
|
||||
// Press Enter to trigger form submission and validation
|
||||
await empIdInput.press("Enter");
|
||||
|
||||
// 에러 메시지 확인
|
||||
const errorMsg = page
|
||||
.getByText(/형식이 올바르지 않습니다|Invalid format/i)
|
||||
.first();
|
||||
await expect(errorMsg).toBeVisible();
|
||||
});
|
||||
});
|
||||
1138
baron-sso/adminfront/tests/worksmobile.spec.ts
Normal file
1138
baron-sso/adminfront/tests/worksmobile.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user