1
0
forked from baron/baron-sso

chore: consolidate local integration changes

This commit is contained in:
2026-06-09 21:03:05 +09:00
parent aa2848c3b6
commit 1341f07ef9
158 changed files with 10995 additions and 1490 deletions

View File

@@ -180,7 +180,7 @@ test.describe("보안 및 접근 제어: 시스템 관리자 vs 일반 사용자
await expect(page.locator('a[href="/api-keys"]')).toBeVisible();
await expect(page.locator('a[href="/audit-logs"]')).toBeVisible();
await expect(
page.locator('a[href="/system/projections/users"]'),
page.locator('a[href="/system/ory-ssot"]'),
).toBeVisible();
await expect(
page.locator('a[href="/system/data-integrity"]'),
@@ -209,7 +209,7 @@ test.describe("보안 및 접근 제어: 시스템 관리자 vs 일반 사용자
await expect(page.locator('a[href="/tenants"]')).not.toBeVisible();
await expect(page.locator('a[href="/api-keys"]')).not.toBeVisible();
await expect(
page.locator('a[href="/system/projections/users"]'),
page.locator('a[href="/system/ory-ssot"]'),
).not.toBeVisible();
await expect(
page.locator('a[href="/system/data-integrity"]'),

View File

@@ -121,6 +121,105 @@ test.describe("Tenants Management", () => {
expect(headerWhiteSpace.every((value) => value === "nowrap")).toBe(true);
});
test("should export currently selected organization users by tenant slug", async ({
page,
}) => {
let exportUrl = "";
await page.route("**/api/v1/admin/tenants**", async (route) => {
if (route.request().method() !== "GET") {
return route.continue();
}
const url = new URL(route.request().url());
if (url.pathname.endsWith("/admin/tenants/tenant-company")) {
return route.fulfill({
json: {
id: "tenant-company",
name: "GPDTDC",
slug: "gpdtdc",
type: "COMPANY",
status: "active",
},
headers: { "Access-Control-Allow-Origin": "*" },
});
}
return route.fulfill({
json: {
items: [
{
id: "tenant-company",
name: "GPDTDC",
slug: "gpdtdc",
type: "COMPANY",
status: "active",
memberCount: 1,
recursiveMemberCount: 1,
},
{
id: "tenant-team",
parentId: "tenant-company",
name: "기술연구팀",
slug: "gpdtdc-rnd",
type: "ORGANIZATION",
status: "active",
memberCount: 1,
recursiveMemberCount: 1,
},
],
total: 2,
limit: 1000,
offset: 0,
},
headers: { "Access-Control-Allow-Origin": "*" },
});
});
await page.route(/\/admin\/users(\?.*)?$/, async (route) => {
const url = new URL(route.request().url());
expect(url.searchParams.get("tenantSlug")).toBe("gpdtdc");
return route.fulfill({
json: {
items: [
{
id: "user-1",
name: "Member User",
email: "member@example.com",
role: "user",
status: "active",
tenantSlug: "gpdtdc",
},
],
total: 1,
},
headers: { "Access-Control-Allow-Origin": "*" },
});
});
await page.route(/\/admin\/users\/export(\?.*)?$/, async (route) => {
exportUrl = route.request().url();
return route.fulfill({
status: 200,
headers: {
"content-type": "text/csv; charset=utf-8",
"content-disposition": 'attachment; filename="tenant-users.csv"',
},
body: "email,name\nmember@example.com,Member User\n",
});
});
await page.goto("/tenants/tenant-company/organization");
await expect(page.getByText("Member User")).toBeVisible();
const [download] = await Promise.all([
page.waitForEvent("download"),
page.getByTestId("tenant-current-users-export-btn").click(),
]);
expect(download.suggestedFilename()).toBe("tenant-users.csv");
expect(exportUrl).toContain("tenantSlug=gpdtdc");
expect(exportUrl).toContain("includeIds=false");
});
test("searches tenant ids in the tree view and selects descendants", async ({
page,
}) => {
@@ -141,7 +240,8 @@ test.describe("Tenants Management", () => {
slug: "acme",
status: "active",
type: "COMPANY",
memberCount: 0,
memberCount: 3,
totalMemberCount: 9,
updatedAt: new Date().toISOString(),
},
{
@@ -151,7 +251,8 @@ test.describe("Tenants Management", () => {
status: "active",
type: "ORGANIZATION",
parentId: "company-1",
memberCount: 0,
memberCount: 4,
totalMemberCount: 6,
updatedAt: new Date().toISOString(),
},
{
@@ -161,19 +262,31 @@ test.describe("Tenants Management", () => {
status: "active",
type: "USER_GROUP",
parentId: "dept-1",
memberCount: 0,
memberCount: 2,
totalMemberCount: 2,
updatedAt: new Date().toISOString(),
},
];
let filtered = items;
if (search) {
filtered = items.filter(
const directMatches = items.filter(
(i) =>
i.name.toLowerCase().includes(search) ||
i.slug.toLowerCase().includes(search) ||
i.id.toLowerCase().includes(search),
);
const ids = new Set(directMatches.map((item) => item.id));
for (const match of directMatches) {
let parentId = match.parentId;
while (parentId) {
const parent = items.find((item) => item.id === parentId);
if (!parent) break;
ids.add(parent.id);
parentId = parent.parentId;
}
}
filtered = items.filter((item) => ids.has(item.id));
}
await route.fulfill({
@@ -192,7 +305,14 @@ test.describe("Tenants Management", () => {
await page
.getByPlaceholder(/이름 또는 슬러그, ID 검색|search/i)
.fill("team-1");
await expect(page.locator("table")).toContainText("Acme");
await expect(page.locator("table")).toContainText("Planning");
await expect(page.locator("table")).toContainText("Platform");
await expect(page.getByTestId("tenant-search-match-team-1")).toBeVisible();
await expect(page.getByTestId("tenant-search-match-company-1")).toHaveCount(
0,
);
await expect(page.getByTestId("tenant-search-match-dept-1")).toHaveCount(0);
await page.getByPlaceholder(/이름 또는 슬러그, ID 검색|search/i).fill("");
await page
@@ -226,7 +346,8 @@ test.describe("Tenants Management", () => {
slug: "acme",
status: "active",
type: "COMPANY",
memberCount: 0,
memberCount: 3,
totalMemberCount: 9,
updatedAt: new Date().toISOString(),
},
{
@@ -236,7 +357,8 @@ test.describe("Tenants Management", () => {
status: "active",
type: "ORGANIZATION",
parentId: "company-1",
memberCount: 0,
memberCount: 4,
totalMemberCount: 6,
updatedAt: new Date().toISOString(),
},
{
@@ -246,7 +368,8 @@ test.describe("Tenants Management", () => {
status: "active",
type: "USER_GROUP",
parentId: "dept-1",
memberCount: 0,
memberCount: 2,
totalMemberCount: 2,
updatedAt: new Date().toISOString(),
},
];
@@ -280,6 +403,11 @@ test.describe("Tenants Management", () => {
"aria-pressed",
"true",
);
await expect(
page
.getByTestId("tenant-internal-id-company-1")
.locator("xpath=ancestor::tr"),
).toContainText("9명");
await page.getByPlaceholder(/UUID|슬러그|slug/i).fill("team-1");
await page.keyboard.press("Enter");
@@ -743,16 +871,18 @@ test.describe("Tenants Management", () => {
let exportUrl = "";
let importRequested = false;
let importBody = "";
const openDataManagementMenu = async () => {
const openDataManagementMenu = async (
expectedTestId = "tenant-export-menu-item",
) => {
const btn = page.getByTestId("tenant-data-mgmt-btn");
const exportMenuItem = page.getByTestId("tenant-export-menu-item");
const expectedMenuItem = page.getByTestId(expectedTestId);
// Attempt to open the menu with a retry loop using toPass
await expect(async () => {
if (!(await exportMenuItem.isVisible())) {
if (!(await expectedMenuItem.isVisible())) {
await btn.click({ force: true });
}
await expect(exportMenuItem).toBeVisible({ timeout: 2000 });
await expect(expectedMenuItem).toBeVisible({ timeout: 2000 });
}).toPass({
intervals: [1000, 2000],
timeout: 10000,
@@ -847,7 +977,7 @@ test.describe("Tenants Management", () => {
await expect(page.getByText(/조직\/사용자 통합/)).toHaveCount(0);
await openDataManagementMenu();
await openDataManagementMenu("tenant-export-menu-item");
await expect(page.getByTestId("tenant-template-menu-item")).toBeVisible();
await expect(page.getByTestId("tenant-import-menu-item")).toBeVisible();
@@ -872,14 +1002,14 @@ test.describe("Tenants Management", () => {
expect(exportDownload.suggestedFilename()).toBe("tenants.csv");
expect(exportUrl).toContain("includeIds=false");
await openDataManagementMenu();
await openDataManagementMenu("tenant-export-with-ids-menu-item");
await expect(
page.getByTestId("tenant-export-with-ids-menu-item"),
).toBeVisible();
await safeDownload("tenant-export-with-ids-menu-item");
expect(exportUrl).toContain("includeIds=true");
await openDataManagementMenu();
await openDataManagementMenu("tenant-template-menu-item");
const template = await safeDownload("tenant-template-menu-item");
expect(template.suggestedFilename()).toBe("tenant-import-template.csv");

View File

@@ -315,6 +315,185 @@ test.describe("User Management", () => {
await expect(page.getByText(/저장/i).first()).toBeVisible();
});
test("should manage global custom claim permissions in user detail", async ({
page,
}) => {
let updatePayload: Record<string, unknown> | undefined;
await page.route(/\/admin\/users\/u-1$/, async (route) => {
const method = route.request().method();
if (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" },
global_custom_claims: {
contract_date: "2026-06-09",
},
global_custom_claim_types: {
contract_date: "date",
},
global_custom_claim_permissions: {
contract_date: {
readPermission: "user_and_admin",
writePermission: "admin_only",
},
},
},
},
});
}
if (method === "PUT") {
updatePayload = route.request().postDataJSON();
return route.fulfill({
json: {
id: "u-1",
name: "John Doe",
email: "john@test.com",
loginId: "johndoe",
status: "active",
metadata: updatePayload?.metadata,
},
});
}
return route.fallback();
});
await page.goto("/users/u-1");
await page
.getByRole("tab", { name: /전역 Custom Claims|Custom Claims/i })
.click();
await expect(
page.getByTestId("global-custom-claim-key-contract_date"),
).toHaveValue("contract_date");
await expect(
page.getByTestId("global-custom-claim-read-permission-contract_date"),
).toHaveValue("user_and_admin");
await expect(
page.getByTestId("global-custom-claim-write-permission-contract_date"),
).toHaveValue("admin_only");
await page
.getByTestId("global-custom-claim-write-permission-contract_date")
.selectOption("user_and_admin");
await page.screenshot({
path: "test-results/adminfront-global-custom-claim-permissions.png",
fullPage: true,
});
await page
.getByRole("button", { name: /전역 Claim 저장|Save Global Claim/i })
.click();
await expect
.poll(() => updatePayload)
.toMatchObject({
metadata: {
global_custom_claims: {
contract_date: "2026-06-09",
},
global_custom_claim_types: {
contract_date: "date",
},
global_custom_claim_permissions: {
contract_date: {
readPermission: "user_and_admin",
writePermission: "user_and_admin",
},
},
},
});
});
test("should configure global custom claim definitions", async ({ page }) => {
let updatePayload:
| {
items?: Array<Record<string, unknown>>;
}
| undefined;
await page.route(/\/admin\/global-custom-claims$/, async (route) => {
const method = route.request().method();
if (method === "GET") {
return route.fulfill({
json: {
items: [
{
key: "contract_date",
label: "Contract date",
valueType: "date",
readPermission: "user_and_admin",
writePermission: "admin_only",
description: "전체 RP에 공통 제공되는 계약일",
},
],
},
});
}
if (method === "PUT") {
updatePayload = route.request().postDataJSON();
return route.fulfill({ json: updatePayload });
}
return route.fallback();
});
await page.goto("/users/custom-claims");
await expect(page.getByText("전역 Claim 설정")).toBeVisible();
await expect(
page.getByTestId("global-claim-definition-key-contract_date"),
).toHaveValue("contract_date");
await expect(
page.getByTestId("global-claim-definition-read-permission-contract_date"),
).toHaveValue("user_and_admin");
await expect(
page.getByTestId(
"global-claim-definition-write-permission-contract_date",
),
).toHaveValue("admin_only");
await page
.getByTestId("global-claim-definition-write-permission-contract_date")
.selectOption("user_and_admin");
await page.screenshot({
path: "test-results/adminfront-global-custom-claim-definition-settings.png",
fullPage: true,
});
await page.getByRole("button", { name: /^저장$|^Save$/i }).click();
await expect
.poll(() => updatePayload)
.toMatchObject({
items: [
{
key: "contract_date",
label: "Contract date",
valueType: "date",
readPermission: "user_and_admin",
writePermission: "user_and_admin",
},
],
});
});
test("should show conflict error when updating to an existing Login ID", async ({
page,
}) => {

View File

@@ -1,4 +1,3 @@
import { readFile } from "node:fs/promises";
import { expect, test } from "@playwright/test";
test.describe("Worksmobile tenant management", () => {
@@ -32,7 +31,10 @@ test.describe("Worksmobile tenant management", () => {
page,
}) => {
const comparisonRequests: boolean[] = [];
const syncRequests: string[] = [];
const syncRequests: Array<{
userId: string;
body: Record<string, unknown>;
}> = [];
await page.route("**/api/v1/**", async (route) => {
const url = new URL(route.request().url());
@@ -218,7 +220,13 @@ test.describe("Worksmobile tenant management", () => {
isWorksmobileTenantPath("/worksmobile/users/user-missing/sync") &&
method === "POST"
) {
syncRequests.push("user-missing");
syncRequests.push({
userId: "user-missing",
body: JSON.parse(route.request().postData() ?? "{}") as Record<
string,
unknown
>,
});
return route.fulfill({
json: { id: "job-user-missing", resourceId: "user-missing" },
headers,
@@ -235,7 +243,8 @@ test.describe("Worksmobile tenant management", () => {
await expect(page.getByRole("tab", { name: "조직" })).toBeVisible();
await page.getByRole("tab", { name: "이력" }).click();
await expect(page.getByText("비밀번호 파일 히스토리")).toBeVisible();
await expect(page.getByText("비밀번호 파일 히스토리")).not.toBeVisible();
await expect(page.getByText("최근 작업")).toBeVisible();
await expect(page.getByText("domainMappings")).not.toBeVisible();
await expect(page.getByText("SCIM token")).not.toBeVisible();
await page.getByRole("tab", { name: "사용자" }).click();
@@ -246,6 +255,9 @@ test.describe("Worksmobile tenant management", () => {
"worksmobile-구성원-virtual-body",
);
await expect(userComparisonTable).toBeVisible();
await expect(page.getByTestId("worksmobile-구성원-row-count")).toHaveText(
"표시 2 / 전체 5",
);
await expect(userSyncCard).toBeVisible();
expect(
await page.evaluate(() => {
@@ -347,7 +359,17 @@ test.describe("Worksmobile tenant management", () => {
await page
.getByRole("button", { name: "선택 구성원 WORKS에 생성" })
.click();
await expect.poll(() => syncRequests).toEqual(["user-missing"]);
await expect(page.getByText("WORKS 초기 비밀번호")).toBeVisible();
await page.getByLabel("초기 비밀번호").fill("InitPass123!");
await page.getByRole("button", { name: "생성 작업 등록" }).click();
await expect
.poll(() => syncRequests)
.toEqual([
{
userId: "user-missing",
body: expect.objectContaining({ initialPassword: "InitPass123!" }),
},
]);
await page.getByRole("tab", { name: "조직" }).click();
await expect(page.getByText("조직 단건 동기화")).toBeVisible();
@@ -357,6 +379,9 @@ test.describe("Worksmobile tenant management", () => {
"worksmobile-조직/그룹-virtual-body",
);
await expect(groupComparisonTable).toBeVisible();
await expect(
page.getByTestId("worksmobile-조직/그룹-row-count"),
).toHaveText("표시 2 / 전체 2");
await expect(groupSyncCard).toBeVisible();
expect(
await page.evaluate(() => {
@@ -381,6 +406,228 @@ test.describe("Worksmobile tenant management", () => {
await expect(page.getByText("works-parent-tech")).toBeVisible();
});
test("separates selected user create and update actions", async ({
page,
}) => {
const syncRequests: Array<{
userId: string;
body: Record<string, unknown>;
}> = [];
await page.route("**/api/v1/**", async (route) => {
const url = new URL(route.request().url());
const method = route.request().method();
const headers = { "Access-Control-Allow-Origin": "*" };
const isWorksmobileTenantPath = (suffix: string) =>
url.pathname.endsWith(`/admin/tenants/hanmac-family-id${suffix}`) ||
url.pathname.endsWith(
`/admin/tenants/038326b6-954a-48a7-a85f-efd83f62b82a${suffix}`,
);
if (url.pathname.endsWith("/user/me")) {
return route.fulfill({
json: {
id: "admin-user",
name: "Admin",
role: "super_admin",
manageableTenants: [
{
id: "038326b6-954a-48a7-a85f-efd83f62b82a",
name: "한맥 가족",
slug: "hanmac-family",
type: "COMPANY_GROUP",
},
],
},
headers,
});
}
if (
url.pathname.endsWith("/admin/tenants/hanmac-family-id") &&
method === "GET"
) {
return route.fulfill({
json: {
id: "hanmac-family-id",
name: "한맥 가족",
slug: "hanmac-family",
type: "COMPANY_GROUP",
status: "active",
parentId: null,
},
headers,
});
}
if (isWorksmobileTenantPath("/worksmobile") && method === "GET") {
return route.fulfill({
json: {
tenant: {
id: "hanmac-family-id",
name: "한맥 가족",
slug: "hanmac-family",
type: "COMPANY_GROUP",
status: "active",
memberCount: 0,
createdAt: "2026-05-04T00:00:00Z",
updatedAt: "2026-05-04T00:00:00Z",
},
config: {
enabled: true,
tokenConfigured: true,
},
recentJobs: [],
},
headers,
});
}
if (
isWorksmobileTenantPath("/worksmobile/credential-batches") &&
method === "GET"
) {
return route.fulfill({ json: [], headers });
}
if (
isWorksmobileTenantPath("/worksmobile/comparison") &&
method === "GET"
) {
return route.fulfill({
json: {
users: [
{
resourceType: "USER",
baronId: "user-missing",
baronName: "김생성",
status: "missing_in_worksmobile",
},
{
resourceType: "USER",
baronId: "user-update",
baronName: "이업데이트",
baronEmail: "domain@typo.example.com",
worksmobileId: "works-user-update",
externalKey: "user-update",
worksmobileName: "이업데이트",
worksmobileEmail: "domain@example.com",
status: "needs_update",
},
],
groups: [],
},
headers,
});
}
if (
isWorksmobileTenantPath("/worksmobile/users/user-missing/sync") &&
method === "POST"
) {
syncRequests.push({
userId: "user-missing",
body: JSON.parse(route.request().postData() ?? "{}") as Record<
string,
unknown
>,
});
return route.fulfill({
json: { id: "job-user-missing", resourceId: "user-missing" },
headers,
});
}
if (
isWorksmobileTenantPath("/worksmobile/users/user-update/sync") &&
method === "POST"
) {
syncRequests.push({
userId: "user-update",
body: JSON.parse(route.request().postData() ?? "{}") as Record<
string,
unknown
>,
});
return route.fulfill({
json: { id: "job-user-update", resourceId: "user-update" },
headers,
});
}
return route.fulfill({ json: { items: [], total: 0 }, headers });
});
await page.goto("/worksmobile");
await page.getByRole("tab", { name: "사용자" }).click();
const userComparisonSection = page
.getByRole("heading", { name: "구성원" })
.locator("xpath=ancestor::div[contains(@class, 'space-y-2')][1]");
await expect(userComparisonSection.getByText("김생성")).toBeVisible();
await expect(userComparisonSection.getByText("이업데이트")).toHaveCount(2);
const statusHeader = userComparisonSection
.locator("thead th")
.filter({ hasText: "상태" })
.locator("div")
.first();
await expect
.poll(() =>
statusHeader.evaluate((element) => {
const style = window.getComputedStyle(element);
return { alignItems: style.alignItems, display: style.display };
}),
)
.toEqual({ alignItems: "center", display: "flex" });
await page
.getByRole("row", { name: /김생성/ })
.getByRole("checkbox")
.check();
await page
.getByRole("row", { name: /이업데이트/ })
.getByRole("checkbox")
.check();
await userComparisonSection
.getByRole("button", { name: "선택 구성원 WORKS에 생성" })
.click();
await expect(page.getByText("WORKS 초기 비밀번호")).toBeVisible();
await page.getByLabel("초기 비밀번호").fill("InitPass123!");
await page.getByRole("button", { name: "생성 작업 등록" }).click();
await expect
.poll(() => syncRequests)
.toEqual([
{
userId: "user-missing",
body: expect.objectContaining({ initialPassword: "InitPass123!" }),
},
]);
await page
.getByRole("row", { name: /이업데이트/ })
.getByRole("checkbox")
.check();
await userComparisonSection
.getByRole("button", { name: "선택 구성원 업데이트 적용" })
.click();
await expect
.poll(() => syncRequests)
.toEqual([
{
userId: "user-missing",
body: expect.objectContaining({ initialPassword: "InitPass123!" }),
},
{
userId: "user-update",
body: expect.not.objectContaining({
initialPassword: expect.anything(),
}),
},
]);
});
test("shows a toast when selected WORKS creation fails", async ({ page }) => {
await page.route("**/api/v1/**", async (route) => {
const url = new URL(route.request().url());
@@ -651,9 +898,7 @@ test.describe("Worksmobile tenant management", () => {
).toBeDisabled();
});
test("downloads initial password CSV and enqueues WORKS admin jobs", async ({
page,
}) => {
test("shows WORKS job history and enqueues admin jobs", async ({ page }) => {
const requests: string[] = [];
const headers = {
"Access-Control-Allow-Origin": "*",
@@ -790,24 +1035,6 @@ test.describe("Worksmobile tenant management", () => {
});
}
if (
url.pathname.endsWith(
"/admin/tenants/038326b6-954a-48a7-a85f-efd83f62b82a/worksmobile/initial-passwords.csv",
) &&
method === "GET"
) {
requests.push("download-passwords");
return route.fulfill({
body: "email,password\nuser@example.com,Secret123!\n",
contentType: "text/csv",
headers: {
...headers,
"Content-Disposition":
'attachment; filename="worksmobile-passwords.csv"',
},
});
}
if (
url.pathname.endsWith(
"/admin/tenants/038326b6-954a-48a7-a85f-efd83f62b82a/worksmobile/backfill/dry-run",
@@ -864,18 +1091,10 @@ test.describe("Worksmobile tenant management", () => {
await page.goto("/worksmobile");
await expect(page.getByText("Worksmobile 연동")).toBeVisible();
await page.getByRole("tab", { name: "이력" }).click();
const download = page.waitForEvent("download");
await page
.getByRole("button", { name: "batch-1 비밀번호 CSV 다운로드" })
.click();
const passwordCsv = await download;
expect(passwordCsv.suggestedFilename()).toBe("worksmobile-passwords.csv");
const passwordCsvPath = await passwordCsv.path();
expect(passwordCsvPath).toBeTruthy();
expect(await readFile(passwordCsvPath ?? "", "utf8")).toContain(
"user@example.com,Secret123!",
);
await expect(page.getByText("비밀번호 파일 히스토리")).not.toBeVisible();
await expect(
page.getByRole("button", { name: /비밀번호 CSV 다운로드/ }),
).toHaveCount(0);
await page.getByRole("button", { name: "Backfill Dry-run" }).click();
await expect.poll(() => requests).toContain("dry-run");
@@ -915,6 +1134,5 @@ test.describe("Worksmobile tenant management", () => {
page.once("dialog", (dialog) => dialog.accept());
await page.getByRole("button", { name: /대기중 payload 삭제/ }).click();
await expect.poll(() => requests).toContain("delete-pending");
expect(requests).toContain("download-passwords");
});
});