import { expect, test } from "@playwright/test"; test.describe("User 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 = { 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) => { 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(); const method = route.request().method(); if (url.includes("/user/me")) { return route.fulfill({ json: { id: "admin-user", name: "Admin", email: "admin@test.com", role: "super_admin", manageableTenants: [], }, }); } if (url.match(/\/admin\/tenants(\?.*)?$/) && method === "GET") { return route.fulfill({ json: { items: [ { id: "t-1", slug: "test-tenant", name: "Test Tenant", config: { userSchema: [ { key: "loginId", label: "Login ID", type: "text", isLoginId: true, }, ], }, }, ], total: 1, limit: 100, offset: 0, }, }); } if (url.match(/\/admin\/tenants\/t-1$/) && method === "GET") { return route.fulfill({ json: { id: "t-1", slug: "test-tenant", name: "Test Tenant", config: { userSchema: [ { key: "loginId", label: "Login ID", type: "text", isLoginId: true, }, ], }, }, }); } if (url.match(/\/admin\/users\/u-1$/) && method === "GET") { return route.fulfill({ json: { id: "u-1", name: "John Doe", email: "john@test.com", loginId: "johndoe", tenantSlug: "test-tenant", tenant: { id: "t-1", name: "Test Tenant", slug: "test-tenant" }, role: "user", status: "active", metadata: { "t-1": { loginId: "johndoe" } }, }, }); } if (url.includes("/password/policy")) { return route.fulfill({ json: { minLength: 12, lowercase: true, uppercase: true, number: true, nonAlphanumeric: true, }, }); } if (url.includes("/rp-history")) { return route.fulfill({ json: [], }); } if (url.match(/\/admin\/users(\?.*)?$/) && method === "POST") { // Parse request payload to simulate validation checks const postData = route.request().postDataJSON(); if (postData && postData.metadata?.loginId === "existing_user") { // Simulate a backend conflict error (409) for an existing loginId return route.fulfill({ status: 409, json: { error: "이미 존재하는 로그인 ID 입니다.", }, }); } // Mock successful user creation return route.fulfill({ json: { id: "new-user-id", name: "New User", email: "newuser@test.com", loginId: postData?.metadata?.loginId || "newuser123", }, }); } if (url.match(/\/admin\/users\/u-1$/) && method === "PUT") { const postData = route.request().postData(); console.log("PUT /admin/users/u-1 payload:", postData); // Force 409 error for this specific conflict string if (postData?.includes("johndoe_conflict")) { return route.fulfill({ status: 409, json: { error: "이미 존재하는 로그인 ID 입니다.", }, }); } // Mock successful user update return route.fulfill({ json: { id: "u-1", name: "John Doe Updated", email: "john@test.com", loginId: "johndoe_updated", }, }); } if (url.match(/\/admin\/users(\?.*)?$/) && method === "GET") { return route.fulfill({ json: { items: [ { id: "u-1", name: "John Doe", email: "john@test.com", loginId: "johndoe", role: "user", status: "active", }, ], total: 1, limit: 50, offset: 0, }, }); } return route.fulfill({ json: { items: [], total: 0 } }); }); }); test("should successfully edit a user's Login ID", async ({ page }) => { await page.goto("/users/u-1"); // "테넌트 프로필" 탭 클릭 await page .getByRole("tab", { name: /테넌트 프로필|Tenants|Tenant Profile/i }) .click(); // Wait for the form to load with the existing login ID const loginIdInput = page.locator( 'input[name*="metadata"][name*="loginId"]', ); await expect(loginIdInput).toBeVisible(); await expect(loginIdInput).toHaveValue("johndoe"); // Change the Login ID await loginIdInput.fill("johndoe_updated"); // Submit the form using Enter key await loginIdInput.press("Enter"); // Check for success message await expect(page.getByText(/저장/i).first()).toBeVisible(); }); test("should show conflict error when updating to an existing Login ID", async ({ page, }) => { // Intercept ANY PUT request to this user and return 409 await page.route(/\/admin\/users\/u-1/, async (route) => { if (route.request().method() === "PUT") { return route.fulfill({ status: 409, contentType: "application/json", body: JSON.stringify({ error: "이미 존재하는 로그인 ID 입니다." }), }); } return route.fallback(); }); await page.goto("/users/u-1"); // "테넌트 프로필" 탭 클릭 await page .getByRole("tab", { name: /테넌트 프로필|Tenants|Tenant Profile/i }) .click(); const loginIdInput = page.locator( 'input[name*="metadata"][name*="loginId"]', ); await expect(loginIdInput).toBeVisible(); await expect(loginIdInput).toHaveValue("johndoe"); // Use a value similar to the successful edit test await loginIdInput.fill("johndoe_conflict"); // Submit the form using Enter key await loginIdInput.press("Enter"); // Check for the specific error await expect( page.getByText(/이미 존재하는 로그인 ID 입니다/i).first(), ).toBeVisible(); }); test("should successfully create a new user with a Login ID", async ({ page, }) => { await page.goto("/users/new"); // Ensure the page title is loaded await expect(page.getByText(/사용자 추가/i).first()).toBeVisible(); // Select Tenant first (important for schema fields to show up) await page.selectOption("select#tenantSlug", "test-tenant"); // Fill required fields await page.locator('input[name="name"]').fill("New User"); await page.locator('input[name="email"]').fill("newuser@test.com"); // Fill Login ID const loginIdInput = page.locator('input[id*="metadata"][id*="loginId"]'); await loginIdInput.fill("newuser123"); // Submit the form const createButton = page.getByRole("button", { name: /생성/i }); await createButton.click(); // Assuming successful creation redirects back to the user list await expect(page).toHaveURL(/.*\/users$/, { timeout: 10000 }); }); test("should show conflict error when creating with an existing Login ID", async ({ page, }) => { await page.goto("/users/new"); await expect(page.getByText(/사용자 추가/i).first()).toBeVisible(); // Select Tenant first (important for schema fields to show up) await page.selectOption("select#tenantSlug", "test-tenant"); // Fill required fields await page.locator('input[name="name"]').fill("New User"); await page.locator('input[name="email"]').fill("newuser@test.com"); // Fill Login ID that triggers the mock conflict error const loginIdInput = page.locator('input[id*="metadata"][id*="loginId"]'); await loginIdInput.fill("existing_user"); // Submit the form const createButton = page.getByRole("button", { name: /생성/i }); await createButton.click(); // Check for the specific conflict error message from the backend mock await expect( page.getByText(/이미 존재하는 로그인 ID 입니다/i), ).toBeVisible(); }); });