1
0
forked from baron/baron-sso
Files
baron-sso/adminfront/tests/tenant-performance.spec.ts

404 lines
12 KiB
TypeScript

import { performance } from "node:perf_hooks";
import { expect, test } from "@playwright/test";
const tenantCount = 3500;
const userCount = 3500;
type TenantFixture = {
id: string;
name: string;
slug: string;
status: string;
type: string;
memberCount: number;
recursiveMemberCount: number;
createdAt: string;
updatedAt: string;
};
type UserFixture = {
id: string;
name: string;
email: string;
phone: string;
loginId: string;
role: string;
status: string;
tenantId: string;
tenantSlug: string;
tenantName: string;
department: string;
createdAt: string;
};
function buildTenants(): TenantFixture[] {
const baseTime = Date.UTC(2026, 0, 1, 0, 0, 0);
return Array.from({ length: tenantCount }, (_, index) => {
const sequence = index + 1;
const padded = String(sequence).padStart(4, "0");
const timestamp = new Date(baseTime + sequence * 1000).toISOString();
return {
id: `tenant-${padded}`,
name: `Tenant ${padded}`,
slug: sequence === 100 ? "full-dataset-needle-0100" : `tenant-${padded}`,
status: sequence % 17 === 0 ? "inactive" : "active",
type: sequence % 5 === 0 ? "ORGANIZATION" : "COMPANY",
memberCount: sequence % 13,
recursiveMemberCount: sequence % 29,
createdAt: timestamp,
updatedAt: timestamp,
};
});
}
function buildUsers(): UserFixture[] {
const baseTime = Date.UTC(2026, 0, 1, 0, 0, 0);
return Array.from({ length: userCount }, (_, index) => {
const sequence = index + 1;
const padded = String(sequence).padStart(4, "0");
const timestamp = new Date(baseTime + sequence * 1000).toISOString();
const email =
sequence === 100
? "full-dataset-user-needle-0100@example.com"
: `user-${padded}@example.com`;
return {
id: `user-${padded}`,
name: `User ${padded}`,
email,
phone: "010-1111-2222",
loginId: `user-${padded}`,
role: "user",
status: sequence % 19 === 0 ? "inactive" : "active",
tenantId: "tenant-main",
tenantSlug: "tenant-main",
tenantName: "Main Tenant",
department: "Platform",
createdAt: timestamp,
};
});
}
function compareTenantValues(
left: TenantFixture,
right: TenantFixture,
sortKey: string,
) {
const key = sortKey as keyof TenantFixture;
const leftValue = left[key] ?? "";
const rightValue = right[key] ?? "";
return String(leftValue).localeCompare(String(rightValue));
}
test.describe("Tenant list performance", () => {
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("**/oidc/**", async (route) => {
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
});
});
test("loads and searches the tenant list within the performance budget", async ({
page,
}, testInfo) => {
await page.setViewportSize({ width: 1440, height: 900 });
const tenants = buildTenants();
await page.route("**/api/v1/**", async (route) => {
const url = new URL(route.request().url());
const headers = { "Access-Control-Allow-Origin": "*" };
if (url.pathname.endsWith("/user/me")) {
return route.fulfill({
json: {
id: "admin-user",
name: "Admin",
role: "super_admin",
manageableTenants: [],
},
headers,
});
}
if (
url.pathname.endsWith("/admin/tenants") &&
route.request().method() === "GET"
) {
const limit = Number(url.searchParams.get("limit") ?? "500");
const cursor = Number(url.searchParams.get("cursor") ?? "0");
const search = url.searchParams.get("search")?.trim().toLowerCase();
const sort = url.searchParams.get("sort") ?? "createdAt";
const direction = url.searchParams.get("direction") ?? "desc";
let filtered = tenants;
if (search) {
filtered = tenants.filter((tenant) =>
[tenant.id, tenant.name, tenant.slug, tenant.type].some((value) =>
value.toLowerCase().includes(search),
),
);
}
const sorted = [...filtered].sort((left, right) => {
const result = compareTenantValues(left, right, sort);
return direction === "asc" ? result : -result;
});
const pageItems = sorted.slice(cursor, cursor + limit);
const nextOffset = cursor + limit;
return route.fulfill({
json: {
items: pageItems,
total: sorted.length,
limit,
offset: 0,
nextCursor:
nextOffset < sorted.length ? String(nextOffset) : undefined,
},
headers,
});
}
return route.fulfill({ json: { items: [], total: 0 }, headers });
});
const loadStarted = performance.now();
await page.goto("/tenants");
await expect(
page.getByTestId("tenant-internal-id-tenant-3500"),
).toBeVisible({ timeout: 15000 });
const loadMs = performance.now() - loadStarted;
const loadSnapshot = testInfo.outputPath("tenant-list-load.png");
await page.screenshot({ path: loadSnapshot, fullPage: true });
await expect(page.locator("tbody tr").first()).toContainText("Tenant 3500");
const searchInput = page.getByPlaceholder("이름 또는 슬러그, ID 검색");
const searchStarted = performance.now();
await searchInput.fill("full-dataset-needle-0100");
await expect(
page.getByTestId("tenant-internal-id-tenant-0100"),
).toBeVisible({ timeout: 15000 });
const searchMs = performance.now() - searchStarted;
const searchSnapshot = testInfo.outputPath("tenant-list-search.png");
await page.screenshot({ path: searchSnapshot, fullPage: true });
await expect(page.locator("tbody")).toContainText(
"full-dataset-needle-0100",
);
await expect(
page.getByTestId("tenant-internal-id-tenant-3500"),
).toHaveCount(0);
console.log(
JSON.stringify({
metric: "tenant-list-performance",
loadMs: Math.round(loadMs),
searchMs: Math.round(searchMs),
loadSnapshot,
searchSnapshot,
}),
);
const searchBudgetMs = testInfo.project.name === "firefox" ? 1000 : 500;
expect(loadMs).toBeLessThanOrEqual(1500);
expect(searchMs).toBeLessThanOrEqual(searchBudgetMs);
});
});
test.describe("User list performance", () => {
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({
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,
}),
);
});
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" } });
});
});
test("loads and searches the user list within the performance budget", async ({
page,
}, testInfo) => {
await page.setViewportSize({ width: 1440, height: 900 });
const users = buildUsers();
await page.route("**/api/v1/**", async (route) => {
const url = new URL(route.request().url());
const headers = { "Access-Control-Allow-Origin": "*" };
if (url.pathname.endsWith("/user/me")) {
return route.fulfill({
json: {
id: "admin-user",
name: "Admin",
email: "admin@test.com",
role: "super_admin",
manageableTenants: [],
},
headers,
});
}
if (
url.pathname.endsWith("/admin/tenants") &&
route.request().method() === "GET"
) {
return route.fulfill({
json: {
items: [
{
id: "tenant-main",
slug: "tenant-main",
name: "Main Tenant",
type: "COMPANY",
config: { userSchema: [] },
},
],
total: 1,
limit: 500,
offset: 0,
},
headers,
});
}
if (
url.pathname.endsWith("/admin/users") &&
route.request().method() === "GET"
) {
const limit = Number(url.searchParams.get("limit") ?? "50");
const cursor = Number(url.searchParams.get("cursor") ?? "0");
const search = url.searchParams.get("search")?.trim().toLowerCase();
const filtered = search
? users.filter((user) =>
[user.id, user.name, user.email, user.loginId].some((value) =>
value.toLowerCase().includes(search),
),
)
: users;
const sorted = [...filtered].sort((left, right) =>
right.createdAt.localeCompare(left.createdAt),
);
const pageItems = sorted.slice(cursor, cursor + limit);
const nextOffset = cursor + limit;
return route.fulfill({
json: {
items: pageItems,
total: sorted.length,
limit,
offset: 0,
nextCursor:
nextOffset < sorted.length ? String(nextOffset) : undefined,
},
headers,
});
}
return route.fulfill({ json: { items: [], total: 0 }, headers });
});
const loadStarted = performance.now();
await page.goto("/users");
await expect(page.getByTestId("user-internal-id-user-3500")).toBeVisible({
timeout: 15000,
});
const loadMs = performance.now() - loadStarted;
const loadSnapshot = testInfo.outputPath("user-list-load.png");
await page.screenshot({ path: loadSnapshot, fullPage: true });
await expect(page.getByText("User 3500")).toBeVisible();
const searchInput = page.getByPlaceholder("이름 또는 이메일 검색");
const searchStarted = performance.now();
await searchInput.fill("full-dataset-user-needle-0100");
await expect(page.getByTestId("user-internal-id-user-0100")).toBeVisible({
timeout: 15000,
});
const searchMs = performance.now() - searchStarted;
const searchSnapshot = testInfo.outputPath("user-list-search.png");
await page.screenshot({ path: searchSnapshot, fullPage: true });
await expect(
page.getByText("full-dataset-user-needle-0100@example.com"),
).toBeVisible();
await expect(page.getByTestId("user-internal-id-user-3500")).toHaveCount(0);
console.log(
JSON.stringify({
metric: "user-list-performance",
loadMs: Math.round(loadMs),
searchMs: Math.round(searchMs),
loadSnapshot,
searchSnapshot,
}),
);
expect(loadMs).toBeLessThanOrEqual(1500);
expect(searchMs).toBeLessThanOrEqual(500);
});
});