forked from baron/baron-sso
org chart 연동기능 추가
This commit is contained in:
@@ -114,24 +114,100 @@ test.describe("Tenants Management", () => {
|
||||
await expect(page).toHaveURL(/.*\/tenants$/, { timeout: 15000 });
|
||||
});
|
||||
|
||||
test("should clarify organization import creates org tenants and users together", async ({
|
||||
test("should export and import tenant CSV without organization/user combined import", async ({
|
||||
page,
|
||||
}) => {
|
||||
let exportRequested = false;
|
||||
let importRequested = false;
|
||||
let importBody = "";
|
||||
|
||||
await page.route("**/api/v1/admin/tenants**", async (route) => {
|
||||
const url = route.request().url();
|
||||
const method = route.request().method();
|
||||
const headers = { "Access-Control-Allow-Origin": "*" };
|
||||
|
||||
if (url.includes("/export")) {
|
||||
exportRequested = true;
|
||||
return route.fulfill({
|
||||
body: "tenant_id,name,type,parent_tenant_id,slug,memo,email_domain\n",
|
||||
contentType: "text/csv",
|
||||
headers: {
|
||||
...headers,
|
||||
"Content-Disposition": 'attachment; filename="tenants.csv"',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.includes("/import")) {
|
||||
importRequested = true;
|
||||
importBody = route.request().postData() ?? "";
|
||||
return route.fulfill({
|
||||
json: { created: 0, updated: 1, failed: 0, errors: [] },
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
if (method === "GET") {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
items: [
|
||||
{
|
||||
id: "tenant-alpha-id",
|
||||
name: "Tenant Alpha",
|
||||
slug: "tenant-alpha",
|
||||
status: "active",
|
||||
type: "COMPANY",
|
||||
domains: [],
|
||||
memberCount: 0,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
return route.continue();
|
||||
});
|
||||
|
||||
await page.goto("/tenants");
|
||||
await expect(page.locator("h2").last()).toContainText(
|
||||
/테넌트 목록|Tenants/i,
|
||||
{ timeout: 20000 },
|
||||
);
|
||||
|
||||
await page
|
||||
.locator("button")
|
||||
.filter({ hasText: /임포트|Import/i })
|
||||
.first()
|
||||
.click();
|
||||
await expect(page.getByText(/조직\/사용자 통합/)).toHaveCount(0);
|
||||
await expect(page.getByTestId("tenant-template-btn")).toBeVisible();
|
||||
await expect(page.getByTestId("tenant-export-btn")).toBeVisible();
|
||||
await expect(page.getByTestId("tenant-import-btn")).toBeVisible();
|
||||
|
||||
await expect(page.getByRole("dialog")).toContainText(
|
||||
/조직\/사용자 통합 일괄 등록|organization and user batch registration/i,
|
||||
const download = page.waitForEvent("download");
|
||||
await page.getByTestId("tenant-export-btn").click();
|
||||
await download;
|
||||
expect(exportRequested).toBe(true);
|
||||
|
||||
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,Tenant Alpha,COMPANY,,tenant-alpha-copy,Imported memo,imported.example.com\n",
|
||||
),
|
||||
});
|
||||
|
||||
await expect(page.getByRole("dialog")).toContainText("CSV 가져오기 확인");
|
||||
await expect(page.getByTestId("tenant-import-candidate")).toContainText(
|
||||
"Tenant Alpha",
|
||||
);
|
||||
await page.getByTestId("tenant-import-confirm-btn").click();
|
||||
|
||||
await expect(page.getByTestId("tenant-import-result")).toContainText(
|
||||
/갱신 1|Updated 1/i,
|
||||
);
|
||||
expect(importRequested).toBe(true);
|
||||
expect(importBody).toContain("tenant-alpha-id");
|
||||
});
|
||||
|
||||
test("should show validation error on empty name", async ({ page }) => {
|
||||
|
||||
119
adminfront/tests/tenants_live.spec.ts
Normal file
119
adminfront/tests/tenants_live.spec.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
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 headers = { ...route.request().headers() };
|
||||
delete headers.authorization;
|
||||
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-result")).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);
|
||||
});
|
||||
});
|
||||
@@ -79,14 +79,73 @@ test.describe("User Management", () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "03dbe16b-e47b-4f72-927b-782807d67a35",
|
||||
slug: "tech-planning",
|
||||
name: "기술기획",
|
||||
type: "USER_GROUP",
|
||||
parentId: "hanmac-company-id",
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: "hanmac-family-id",
|
||||
slug: "hanmac-family",
|
||||
name: "한맥가족",
|
||||
type: "COMPANY_GROUP",
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: "hanmac-company-id",
|
||||
slug: "hanmac-company",
|
||||
name: "한맥기술",
|
||||
type: "COMPANY",
|
||||
parentId: "hanmac-family-id",
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: "hanmac-team-id",
|
||||
slug: "hanmac-team",
|
||||
name: "한맥팀",
|
||||
type: "USER_GROUP",
|
||||
parentId: "hanmac-company-id",
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: "system-id",
|
||||
slug: "system",
|
||||
name: "System Tenant",
|
||||
type: "SYSTEM",
|
||||
config: {},
|
||||
},
|
||||
{
|
||||
id: "external-tenant-id",
|
||||
slug: "external-tenant",
|
||||
name: "External Tenant",
|
||||
type: "COMPANY",
|
||||
config: {},
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
total: 7,
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.match(/\/admin\/tenants$/) && method === "POST") {
|
||||
const postData = route.request().postDataJSON();
|
||||
return route.fulfill({
|
||||
status: 201,
|
||||
json: {
|
||||
id: "personal-tenant-id",
|
||||
slug: postData?.slug || "personal",
|
||||
name: postData?.name || "Personal",
|
||||
type: postData?.type || "PERSONAL",
|
||||
config: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.match(/\/admin\/tenants\/t-1$/) && method === "GET") {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
@@ -107,6 +166,21 @@ test.describe("User Management", () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
url.match(/\/admin\/tenants\/03dbe16b-e47b-4f72-927b-782807d67a35$/) &&
|
||||
method === "GET"
|
||||
) {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "03dbe16b-e47b-4f72-927b-782807d67a35",
|
||||
slug: "tech-planning",
|
||||
name: "기술기획",
|
||||
type: "USER_GROUP",
|
||||
config: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.match(/\/admin\/users\/u-1$/) && method === "GET") {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
@@ -185,6 +259,7 @@ test.describe("User Management", () => {
|
||||
name: "John Doe Updated",
|
||||
email: "john@test.com",
|
||||
loginId: "johndoe_updated",
|
||||
status: "inactive",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -197,9 +272,11 @@ test.describe("User Management", () => {
|
||||
id: "u-1",
|
||||
name: "John Doe",
|
||||
email: "john@test.com",
|
||||
phone: "010-1111-2222",
|
||||
loginId: "johndoe",
|
||||
role: "user",
|
||||
status: "active",
|
||||
createdAt: "2026-04-01T00:00:00Z",
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
@@ -285,8 +362,26 @@ test.describe("User Management", () => {
|
||||
|
||||
// Ensure the page title is loaded
|
||||
await expect(page.getByText(/사용자 추가/i).first()).toBeVisible();
|
||||
const userTypeTabs = page.getByRole("tab");
|
||||
await expect(userTypeTabs).toHaveText([
|
||||
"한맥가족 구성원",
|
||||
"외부 기업 회원",
|
||||
"개인 회원",
|
||||
]);
|
||||
await expect(page.getByRole("tab", { name: /외부 기업 회원/i })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("tab", { name: /한맥가족 구성원/i }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("tab", { name: /개인 회원/i })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("tab", { name: /한맥가족 구성원/i }),
|
||||
).toHaveAttribute("data-state", "active");
|
||||
await expect(
|
||||
page.getByLabel(/한맥 가족 구성원으로 등록/i),
|
||||
).toHaveCount(0);
|
||||
|
||||
// Select Tenant first (important for schema fields to show up)
|
||||
await page.getByRole("tab", { name: /외부 기업 회원/i }).click();
|
||||
await page.selectOption("select#tenantSlug", "test-tenant");
|
||||
|
||||
// Fill required fields
|
||||
@@ -305,6 +400,303 @@ test.describe("User Management", () => {
|
||||
await expect(page).toHaveURL(/.*\/users$/, { timeout: 10000 });
|
||||
});
|
||||
|
||||
test("should export users through the authenticated API client", async ({
|
||||
page,
|
||||
}) => {
|
||||
let authorizationHeader: string | undefined;
|
||||
|
||||
await page.route(/\/admin\/users\/export(\?.*)?$/, async (route) => {
|
||||
authorizationHeader = route.request().headers().authorization;
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"content-disposition": 'attachment; filename="users.csv"',
|
||||
},
|
||||
body: "email,name\njohn@test.com,John Doe\n",
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/users");
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent("download"),
|
||||
page.getByRole("button", { name: /내보내기|Export/i }).click(),
|
||||
]);
|
||||
|
||||
expect(download.suggestedFilename()).toBe("users.csv");
|
||||
expect(authorizationHeader).toBe("Bearer fake-token");
|
||||
});
|
||||
|
||||
test("should show contact info in one row, hide roles, and toggle user status", async ({
|
||||
page,
|
||||
}) => {
|
||||
let updatePayload: Record<string, unknown> | undefined;
|
||||
|
||||
await page.route(/\/admin\/users\/u-1$/, async (route) => {
|
||||
if (route.request().method() === "PUT") {
|
||||
updatePayload = route.request().postDataJSON();
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "u-1",
|
||||
name: "John Doe",
|
||||
email: "john@test.com",
|
||||
phone: "010-1111-2222",
|
||||
loginId: "johndoe",
|
||||
status: "inactive",
|
||||
createdAt: "2026-04-01T00:00:00Z",
|
||||
},
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
await page.goto("/users");
|
||||
const table = page.locator("table");
|
||||
await expect(
|
||||
table.getByRole("columnheader", { name: /ROLE|역할/i }),
|
||||
).toHaveCount(0);
|
||||
await expect(page.getByTestId("user-contact-u-1")).toContainText(
|
||||
"John Doe john@test.com 010-1111-2222",
|
||||
);
|
||||
|
||||
await page.getByTestId("user-status-toggle-u-1").click();
|
||||
await expect
|
||||
.poll(() => updatePayload)
|
||||
.toMatchObject({ status: "inactive" });
|
||||
});
|
||||
|
||||
test("should create a Hanmac family user with tenant appointments and no representative affiliation", async ({
|
||||
page,
|
||||
}) => {
|
||||
let createPayload: Record<string, unknown> | undefined;
|
||||
|
||||
await page.route(/\/admin\/users(\?.*)?$/, async (route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
createPayload = route.request().postDataJSON();
|
||||
return route.fulfill({
|
||||
status: 201,
|
||||
json: {
|
||||
id: "new-user-id",
|
||||
name: "Family User",
|
||||
email: "family@test.com",
|
||||
},
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
await page.goto("/users/new");
|
||||
|
||||
await expect(
|
||||
page.getByRole("tab", { name: /한맥가족 구성원/i }),
|
||||
).toHaveAttribute("data-state", "active");
|
||||
await expect(
|
||||
page.getByLabel(/한맥 가족 구성원으로 등록/i),
|
||||
).toHaveCount(0);
|
||||
await expect(page.locator("select#role")).toHaveCount(0);
|
||||
await expect(page.locator("input#department")).toHaveCount(0);
|
||||
|
||||
await expect(page.getByText(/대표 소속/i)).toHaveCount(0);
|
||||
await page.getByRole("button", { name: /^추가$/i }).click();
|
||||
await expect(page.getByTestId("appointment-row-0")).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId("appointment-tenant-owner-line-0"),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId("appointment-position-line-0")).toBeVisible();
|
||||
await page.getByRole("button", { name: /테넌트 선택/i }).click();
|
||||
|
||||
await expect(page.getByTitle(/테넌트 선택/i)).toHaveAttribute(
|
||||
"src",
|
||||
/\/login\?auto=1&returnTo=%2Fembed%2Fpicker%3Fmode%3Dsingle%26select%3Dtenant%26width%3D400%26height%3D600%26tenantId%3Dhanmac-family-id$/,
|
||||
);
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: {
|
||||
type: "orgfront:picker:confirm",
|
||||
payload: {
|
||||
mode: "single",
|
||||
selections: [
|
||||
{
|
||||
type: "tenant",
|
||||
id: "03dbe16b-e47b-4f72-927b-782807d67a35",
|
||||
name: "기술기획",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await expect(page.getByText("기술기획")).toBeVisible();
|
||||
await page.getByLabel(/조직장/i).check();
|
||||
await page.getByLabel(/^직무$/i).fill("플랫폼 운영");
|
||||
await page.getByLabel(/^직급$/i).fill("책임");
|
||||
|
||||
await page.locator('input[name="name"]').fill("Family User");
|
||||
await page.locator('input[name="email"]').fill("family@test.com");
|
||||
await page.getByRole("button", { name: /생성/i }).click();
|
||||
|
||||
await expect
|
||||
.poll(() => createPayload)
|
||||
.toMatchObject({
|
||||
metadata: {
|
||||
hanmacFamily: true,
|
||||
additionalAppointments: [
|
||||
{
|
||||
tenantId: "03dbe16b-e47b-4f72-927b-782807d67a35",
|
||||
tenantSlug: "tech-planning",
|
||||
tenantName: "기술기획",
|
||||
isOwner: true,
|
||||
jobTitle: "플랫폼 운영",
|
||||
position: "책임",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(createPayload).not.toHaveProperty("role");
|
||||
expect(createPayload).not.toHaveProperty("department");
|
||||
expect(createPayload).not.toHaveProperty("tenantSlug");
|
||||
expect(createPayload).not.toHaveProperty("companyCode");
|
||||
expect(createPayload).not.toHaveProperty("primaryTenantId");
|
||||
});
|
||||
|
||||
test("should hide Hanmac family subtree and system tenants when creating a non-family user", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/users/new");
|
||||
|
||||
await page.getByRole("tab", { name: /외부 기업 회원/i }).click();
|
||||
|
||||
const tenantOptionValues = await page
|
||||
.locator("select#tenantSlug option")
|
||||
.evaluateAll((options) =>
|
||||
options.map((option) => (option as HTMLOptionElement).value),
|
||||
);
|
||||
|
||||
expect(tenantOptionValues).toContain("test-tenant");
|
||||
expect(tenantOptionValues).toContain("external-tenant");
|
||||
expect(tenantOptionValues).not.toContain("");
|
||||
expect(tenantOptionValues).not.toContain("system");
|
||||
expect(tenantOptionValues).not.toContain("hanmac-family");
|
||||
expect(tenantOptionValues).not.toContain("hanmac-company");
|
||||
expect(tenantOptionValues).not.toContain("hanmac-team");
|
||||
expect(tenantOptionValues).not.toContain("tech-planning");
|
||||
});
|
||||
|
||||
test("should create a personal user and provision Personal tenant when missing", async ({
|
||||
page,
|
||||
}) => {
|
||||
let tenantPayload: Record<string, unknown> | undefined;
|
||||
let createPayload: Record<string, unknown> | undefined;
|
||||
|
||||
await page.route(/\/admin\/tenants$/, async (route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
tenantPayload = route.request().postDataJSON();
|
||||
return route.fulfill({
|
||||
status: 201,
|
||||
json: {
|
||||
id: "personal-tenant-id",
|
||||
slug: "personal",
|
||||
name: "Personal",
|
||||
type: "PERSONAL",
|
||||
config: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
await page.route(/\/admin\/users(\?.*)?$/, async (route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
createPayload = route.request().postDataJSON();
|
||||
return route.fulfill({
|
||||
status: 201,
|
||||
json: {
|
||||
id: "personal-user-id",
|
||||
name: "Personal User",
|
||||
email: "personal@test.com",
|
||||
},
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
await page.goto("/users/new");
|
||||
await page.getByRole("tab", { name: /개인 회원/i }).click();
|
||||
|
||||
await expect(page.getByTestId("personal-tenant-summary")).toContainText(
|
||||
/Personal/i,
|
||||
);
|
||||
await page.locator('input[name="name"]').fill("Personal User");
|
||||
await page.locator('input[name="email"]').fill("personal@test.com");
|
||||
await page.getByRole("button", { name: /생성/i }).click();
|
||||
|
||||
await expect
|
||||
.poll(() => tenantPayload)
|
||||
.toMatchObject({ name: "Personal", slug: "personal", type: "PERSONAL" });
|
||||
await expect
|
||||
.poll(() => createPayload)
|
||||
.toMatchObject({
|
||||
tenantSlug: "personal",
|
||||
metadata: { userType: "personal", hanmacFamily: false },
|
||||
});
|
||||
});
|
||||
|
||||
test("should show Hanmac family appointments layout on user detail", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route(/\/admin\/users\/u-1$/, async (route) => {
|
||||
if (route.request().method() === "GET") {
|
||||
return route.fulfill({
|
||||
json: {
|
||||
id: "u-1",
|
||||
name: "Family User",
|
||||
email: "family@test.com",
|
||||
phone: "010-1111-2222",
|
||||
loginId: "familyuser",
|
||||
role: "user",
|
||||
status: "active",
|
||||
createdAt: "2026-04-01T00:00:00Z",
|
||||
updatedAt: "2026-04-01T00:00:00Z",
|
||||
metadata: {
|
||||
hanmacFamily: true,
|
||||
additionalAppointments: [
|
||||
{
|
||||
tenantId: "03dbe16b-e47b-4f72-927b-782807d67a35",
|
||||
tenantSlug: "tech-planning",
|
||||
tenantName: "기술기획",
|
||||
isOwner: true,
|
||||
jobTitle: "플랫폼 운영",
|
||||
position: "책임",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
await page.goto("/users/u-1");
|
||||
|
||||
await expect(
|
||||
page.getByRole("tab", { name: /한맥가족 구성원/i }),
|
||||
).toHaveAttribute("data-state", "active");
|
||||
await expect(
|
||||
page.getByLabel(/한맥 가족 구성원으로 등록/i),
|
||||
).toHaveCount(0);
|
||||
await expect(page.getByTestId("detail-appointment-row-0")).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId("detail-appointment-tenant-owner-line-0"),
|
||||
).toContainText(/기술기획|조직장/);
|
||||
await expect(
|
||||
page.getByTestId("detail-appointment-position-line-0"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show conflict error when creating with an existing Login ID", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -312,6 +704,8 @@ test.describe("User Management", () => {
|
||||
|
||||
await expect(page.getByText(/사용자 추가/i).first()).toBeVisible();
|
||||
|
||||
await page.getByRole("tab", { name: /외부 기업 회원/i }).click();
|
||||
|
||||
// Select Tenant first (important for schema fields to show up)
|
||||
await page.selectOption("select#tenantSlug", "test-tenant");
|
||||
|
||||
|
||||
82
adminfront/tests/users_live.spec.ts
Normal file
82
adminfront/tests/users_live.spec.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import fs from "node:fs";
|
||||
|
||||
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 headers = { ...route.request().headers() };
|
||||
delete headers.authorization;
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user