forked from baron/baron-sso
org chart 연동기능 추가
This commit is contained in:
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user