1
0
forked from baron/baron-sso

테넌트 목록 조회 cursor기반으로 재구성. 사용자 metadata 미사용 필드 제거

This commit is contained in:
2026-05-13 18:05:51 +09:00
parent a4d707d4d8
commit 5e7b7b878c
85 changed files with 4808 additions and 734 deletions

View File

@@ -37,6 +37,15 @@ test.describe("Bulk Actions and Tree Search", () => {
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: {
@@ -149,6 +158,41 @@ test.describe("Bulk Actions and Tree Search", () => {
await expect(selectionBar).not.toBeVisible({ timeout: 10000 });
});
test("should let super admins promote selected users to super admin", 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-promote-super-admin-btn").click();
await expect
.poll(() => capturedPayload)
.toEqual({
userIds: ["u-1"],
role: "super_admin",
});
await expect(selectionBar).not.toBeVisible({ timeout: 10000 });
});
test("should filter and highlight nodes in organization tree", async ({
page,
}) => {

View File

@@ -105,6 +105,197 @@ test.describe("Tenants Management", () => {
expect(headerWhiteSpace.every((value) => value === "nowrap")).toBe(true);
});
test("should virtualize large tenant lists and load next pages automatically", async ({
page,
}) => {
await page.setViewportSize({ width: 900, height: 700 });
let requestCount = 0;
await page.route("**/api/v1/admin/tenants**", async (route) => {
if (route.request().method() !== "GET") {
return route.continue();
}
const url = new URL(route.request().url());
const cursor = url.searchParams.get("cursor");
requestCount += 1;
if (!cursor) {
return route.fulfill({
json: {
items: Array.from({ length: 500 }, (_, index) => ({
id: `tenant-${String(index + 1).padStart(3, "0")}`,
name: `Tenant ${String(index + 1).padStart(3, "0")}`,
slug: `tenant-${String(index + 1).padStart(3, "0")}`,
status: "active",
type: "COMPANY",
memberCount: 0,
updatedAt: new Date().toISOString(),
})),
total: 501,
limit: 500,
offset: 0,
nextCursor: "next-page",
},
headers: { "Access-Control-Allow-Origin": "*" },
});
}
return route.fulfill({
json: {
items: [
{
id: "tenant-501",
name: "Tenant 501",
slug: "tenant-501",
status: "active",
type: "COMPANY",
memberCount: 0,
updatedAt: new Date().toISOString(),
},
],
total: 501,
limit: 500,
offset: 0,
},
headers: { "Access-Control-Allow-Origin": "*" },
});
});
await page.goto("/tenants");
await expect(page.getByText("총 501개 테넌트")).toBeVisible();
await expect(page.getByRole("button", { name: "더 불러오기" })).toHaveCount(
0,
);
await expect
.poll(async () => page.locator("tbody tr").count())
.toBeLessThan(80);
const tableScroller = page.getByTestId("tenant-table-scroll");
await tableScroller.evaluate((element) => {
element.scrollTop = element.scrollHeight;
element.dispatchEvent(new Event("scroll", { bubbles: true }));
});
await expect.poll(() => requestCount).toBe(2);
await tableScroller.evaluate((element) => {
element.scrollTop = element.scrollHeight;
element.dispatchEvent(new Event("scroll", { bubbles: true }));
});
await expect(page.getByText("Tenant 501")).toBeVisible();
expect(requestCount).toBe(2);
});
test("should hide Hanmac family subtree from external tenant admins", async ({
page,
}) => {
await page.route(/.*\/api\/v1\/user\/me$/, async (route) => {
return route.fulfill({
json: {
id: "external-admin",
name: "External Admin",
role: "tenant_admin",
tenantId: "external-tenant-id",
tenantSlug: "external-tenant",
tenant: {
id: "external-tenant-id",
slug: "external-tenant",
name: "External Tenant",
type: "COMPANY",
},
manageableTenants: [
{
id: "external-tenant-id",
slug: "external-tenant",
name: "External Tenant",
type: "COMPANY",
},
{
id: "external-team-id",
slug: "external-team",
name: "External Team",
type: "USER_GROUP",
parentId: "external-tenant-id",
},
],
},
});
});
await page.route("**/api/v1/admin/tenants**", async (route) => {
if (route.request().method() !== "GET") {
await route.continue();
return;
}
await route.fulfill({
json: {
items: [
{
id: "hanmac-family-id",
slug: "hanmac-family",
name: "한맥가족",
status: "active",
type: "COMPANY_GROUP",
memberCount: 0,
},
{
id: "hanmac-company-id",
slug: "hanmac-company",
name: "한맥기술",
status: "active",
type: "COMPANY",
parentId: "hanmac-family-id",
memberCount: 0,
},
{
id: "hanmac-team-id",
slug: "hanmac-team",
name: "한맥팀",
status: "active",
type: "USER_GROUP",
parentId: "hanmac-company-id",
memberCount: 0,
},
{
id: "external-tenant-id",
slug: "external-tenant",
name: "External Tenant",
status: "active",
type: "COMPANY",
memberCount: 0,
},
{
id: "external-team-id",
slug: "external-team",
name: "External Team",
status: "active",
type: "USER_GROUP",
parentId: "external-tenant-id",
memberCount: 0,
},
],
total: 5,
limit: 1000,
offset: 0,
},
headers: { "Access-Control-Allow-Origin": "*" },
});
});
await page.goto("/tenants");
await expect(page.locator("h2").last()).toContainText(
/테넌트 목록|Tenants/i,
{ timeout: 20000 },
);
await expect(page.locator("table")).toContainText("External Tenant");
await expect(page.locator("table")).toContainText("External Team");
await expect(page.locator("table")).not.toContainText("한맥가족");
await expect(page.locator("table")).not.toContainText("한맥기술");
await expect(page.locator("table")).not.toContainText("한맥팀");
});
test("should create a new tenant", async ({ page }) => {
await page.goto("/tenants/new");
await expect(page.locator("h2").last()).toContainText(/추가|Create/i, {

View File

@@ -362,8 +362,8 @@ 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([
const categoryTabs = page.getByRole("tab");
await expect(categoryTabs).toHaveText([
"한맥가족 구성원",
"외부 기업 회원",
"개인 회원",
@@ -579,7 +579,6 @@ test.describe("User Management", () => {
.poll(() => createPayload)
.toMatchObject({
metadata: {
hanmacFamily: true,
additionalAppointments: [
{
tenantId: "03dbe16b-e47b-4f72-927b-782807d67a35",
@@ -623,7 +622,7 @@ test.describe("User Management", () => {
expect(tenantOptionValues).not.toContain("tech-planning");
});
test("should create a personal user and provision Personal tenant when missing", async ({
test("should create a personal user with the fixed global Personal tenant when it is not listed", async ({
page,
}) => {
let tenantPayload: Record<string, unknown> | undefined;
@@ -671,14 +670,14 @@ test.describe("User Management", () => {
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" });
expect(tenantPayload).toBeUndefined();
await expect
.poll(() => createPayload)
.toMatchObject({
tenantSlug: "personal",
metadata: { userType: "personal", hanmacFamily: false },
metadata: {
personalTenantId: "9607eb7b-04d2-42ab-80fe-780fe21c7e8f",
},
});
});
@@ -699,7 +698,6 @@ test.describe("User Management", () => {
createdAt: "2026-04-01T00:00:00Z",
updatedAt: "2026-04-01T00:00:00Z",
metadata: {
hanmacFamily: true,
additionalAppointments: [
{
tenantId: "03dbe16b-e47b-4f72-927b-782807d67a35",
@@ -763,7 +761,6 @@ test.describe("User Management", () => {
createdAt: "2026-04-01T00:00:00Z",
updatedAt: "2026-04-01T00:00:00Z",
metadata: {
hanmacFamily: true,
additionalAppointments: [
{
tenantId: "03dbe16b-e47b-4f72-927b-782807d67a35",