1
0
forked from baron/baron-sso

feat: improve Worksmobile tenant sync handling

This commit is contained in:
2026-06-02 18:05:36 +09:00
parent d6d39ca300
commit d32ca69eee
58 changed files with 4035 additions and 1400 deletions

View File

@@ -4,6 +4,7 @@ import {
mergeTenantOrgConfig,
ORG_UNIT_TYPE_OPTIONS,
readTenantOrgConfig,
removeTenantOrgConfig,
shouldAllowHanmacOrgConfig,
} from "./orgConfig";
@@ -49,17 +50,69 @@ describe("tenant org config", () => {
it("reads and writes tenant visibility and org unit type", () => {
expect(
readTenantOrgConfig({ visibility: "private", orgUnitType: "팀" }),
).toEqual({ orgUnitType: "팀", visibility: "private" });
).toEqual({
orgUnitType: "팀",
visibility: "private",
worksmobileExcluded: false,
});
expect(
readTenantOrgConfig({ visibility: "internal", orgUnitType: "센터" }),
).toEqual({ orgUnitType: "센터", visibility: "internal" });
).toEqual({
orgUnitType: "센터",
visibility: "internal",
worksmobileExcluded: false,
});
expect(
mergeTenantOrgConfig(
{ userSchema: [], visibility: "private", orgUnitType: "팀" },
{ orgUnitType: "", visibility: "internal" },
{
orgUnitType: "",
visibility: "internal",
worksmobileExcluded: false,
},
),
).toEqual({ userSchema: [], visibility: "internal" });
).toEqual({
userSchema: [],
visibility: "internal",
worksmobileExcluded: false,
});
});
it("reads, writes, and removes the Worksmobile exclusion flag", () => {
expect(
readTenantOrgConfig({ worksmobileExcluded: true }),
).toMatchObject({
worksmobileExcluded: true,
});
expect(
readTenantOrgConfig({ worksmobileExcluded: "true" }),
).toMatchObject({
worksmobileExcluded: true,
});
expect(
mergeTenantOrgConfig(
{ userSchema: [], worksmobileExcluded: false },
{
orgUnitType: "팀",
visibility: "private",
worksmobileExcluded: true,
},
),
).toEqual({
userSchema: [],
orgUnitType: "팀",
visibility: "private",
worksmobileExcluded: true,
});
expect(
removeTenantOrgConfig({
userSchema: [],
orgUnitType: "팀",
visibility: "private",
worksmobileExcluded: true,
}),
).toEqual({ userSchema: [] });
});
it("includes task-force and executive-direct org unit types", () => {

View File

@@ -14,6 +14,13 @@ export const ORG_UNIT_TYPE_OPTIONS = [
"임원직속",
] as const;
export const USER_GROUP_ORG_UNIT_TYPE_OPTIONS = [
"팀",
"TF",
"TF팀",
"셀",
] as const;
export const TENANT_VISIBILITY_OPTIONS = [
{ label: "공개", value: "public" },
{ label: "내부", value: "internal" },
@@ -26,6 +33,7 @@ export type TenantVisibility =
export type TenantOrgConfig = {
orgUnitType: string;
visibility: TenantVisibility;
worksmobileExcluded: boolean;
};
const ORG_UNIT_TYPE_SET = new Set<string>(ORG_UNIT_TYPE_OPTIONS);
@@ -55,17 +63,29 @@ export function shouldAllowHanmacOrgConfig(
return false;
}
export function getOrgUnitTypeOptionsForTenantType(type: string) {
return type === "USER_GROUP"
? USER_GROUP_ORG_UNIT_TYPE_OPTIONS
: ORG_UNIT_TYPE_OPTIONS;
}
export function readTenantOrgConfig(
config: Record<string, unknown> | undefined,
): TenantOrgConfig {
const rawVisibility = String(config?.visibility ?? "public").toLowerCase();
const rawOrgUnitType = String(config?.orgUnitType ?? "");
const rawWorksmobileExcluded = config?.worksmobileExcluded;
return {
orgUnitType: ORG_UNIT_TYPE_SET.has(rawOrgUnitType) ? rawOrgUnitType : "",
visibility: TENANT_VISIBILITY_SET.has(rawVisibility)
? (rawVisibility as TenantVisibility)
: "public",
worksmobileExcluded:
rawWorksmobileExcluded === true ||
String(rawWorksmobileExcluded ?? "")
.trim()
.toLowerCase() === "true",
};
}
@@ -76,6 +96,7 @@ export function mergeTenantOrgConfig(
const { orgUnitType: _orgUnitType, ...rest } = config ?? {};
const merged = { ...rest };
merged.visibility = next.visibility;
merged.worksmobileExcluded = next.worksmobileExcluded;
if (next.orgUnitType) {
merged.orgUnitType = next.orgUnitType;
@@ -90,6 +111,7 @@ export function removeTenantOrgConfig(
const {
orgUnitType: _orgUnitType,
visibility: _visibility,
worksmobileExcluded: _worksmobileExcluded,
...rest
} = config ?? {};
return rest;

View File

@@ -66,7 +66,7 @@ describe("tenantCsvImport", () => {
it("parses tenant CSV rows with the supported import columns", () => {
const rows = parseTenantCSV(
"tenant_id,name,type,parent_tenant_id,slug,memo,email_domain,visibility,org_unit_type\n,Hanmac Tech,COMPANY,,hanmac-tech,Memo,hanmac-tech.example.com,internal,센터\n",
"tenant_id,name,type,parent_tenant_id,slug,memo,email_domain,visibility,org_unit_type,worksmobile_sync\n,Hanmac Tech,COMPANY,,hanmac-tech,Memo,hanmac-tech.example.com,internal,센터,no\n",
);
expect(rows).toEqual([
@@ -82,6 +82,7 @@ describe("tenantCsvImport", () => {
emailDomain: "hanmac-tech.example.com",
visibility: "internal",
orgUnitType: "센터",
worksmobileSync: "no",
},
]);
});
@@ -111,7 +112,7 @@ describe("tenantCsvImport", () => {
it("serializes selected matches by filling tenant_id before upload", () => {
const rows = parseTenantCSV(
"tenant_id,name,type,parent_tenant_id,slug,memo,email_domain,visibility,org_unit_type\n,Hanmac Tech,COMPANY,,hanmac-tech,Memo,hanmac-tech.example.com,private,팀\n",
"tenant_id,name,type,parent_tenant_id,slug,memo,email_domain,visibility,org_unit_type,worksmobile_sync\n,Hanmac Tech,COMPANY,,hanmac-tech,Memo,hanmac-tech.example.com,private,팀,no\n",
);
const preview = buildTenantImportPreview(rows, tenants);
const csv = serializeTenantImportCSV(preview, {
@@ -119,10 +120,10 @@ describe("tenantCsvImport", () => {
});
expect(csv.split("\n")[0]).toBe(
"tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type",
"tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type,worksmobile_sync",
);
expect(csv).toContain(
"tenant-1,Hanmac Tech,COMPANY,,,hanmac-tech,Memo,hanmac-tech.example.com,private,팀",
"tenant-1,Hanmac Tech,COMPANY,,,hanmac-tech,Memo,hanmac-tech.example.com,private,팀,no",
);
});
@@ -253,10 +254,10 @@ ${exportedTenantId},Tenant With UUID,COMPANY,,,tenant-with-uuid,Memo,tenant-with
});
expect(csv.split("\n")[0]).toBe(
"tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type",
"tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type,worksmobile_sync",
);
expect(csv).toContain(
"staging-child-id,Child Tenant,ORGANIZATION,staging-parent-id,parent-slug,child-slug,,",
"staging-child-id,Child Tenant,ORGANIZATION,staging-parent-id,parent-slug,child-slug,,,,,yes",
);
});

View File

@@ -12,6 +12,7 @@ export type TenantCSVRow = {
emailDomain: string;
visibility: string;
orgUnitType: string;
worksmobileSync: string;
};
export type TenantCSVParseOptions = {
@@ -80,6 +81,7 @@ const importHeaders = [
"email_domain",
"visibility",
"org_unit_type",
"worksmobile_sync",
];
const headerAliases: Record<string, TenantCSVSourceKey> = {
@@ -116,6 +118,11 @@ const headerAliases: Record<string, TenantCSVSourceKey> = {
organization_type: "orgUnitType",
orgtype: "orgUnitType",
org_type: "orgUnitType",
worksmobile: "worksmobileSync",
worksmobilesync: "worksmobileSync",
worksmobile_sync: "worksmobileSync",
works_sync: "worksmobileSync",
works: "worksmobileSync",
};
export function parseTenantCSV(
@@ -175,6 +182,7 @@ export function parseTenantCSV(
emailDomain: value("emailDomain"),
visibility: value("visibility"),
orgUnitType: value("orgUnitType"),
worksmobileSync: normalizeWorksmobileSync(value("worksmobileSync")),
};
});
}
@@ -305,6 +313,7 @@ export function serializeTenantImportCSV(
preview.row.emailDomain,
preview.row.visibility,
preview.row.orgUnitType,
preview.row.worksmobileSync || "yes",
]);
}
return `${lines.map(formatCSVRecord).join("\n")}\n`;
@@ -528,6 +537,30 @@ function normalizeHeader(value: string) {
return value.trim().toLowerCase().replaceAll(" ", "_");
}
function normalizeWorksmobileSync(value: string) {
const normalized = value.trim().toLowerCase();
if (
[
"no",
"n",
"false",
"0",
"off",
"none",
"excluded",
"exclude",
"not_sync",
"not-synced",
"미연동",
"연동안함",
"제외",
].includes(normalized)
) {
return "no";
}
return "yes";
}
function slugFromMailingList(value: string) {
if (!value) return "";
return normalizeTenantSlug(value.split("@")[0] ?? value);