forked from baron/baron-sso
384 lines
12 KiB
TypeScript
384 lines
12 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import type { TenantSummary } from "../../../lib/adminApi";
|
|
import {
|
|
buildTenantImportParentOptionGroups,
|
|
buildTenantImportPreview,
|
|
inferTenantImportRootParentSlug,
|
|
parseTenantCSV,
|
|
serializeTenantImportCSV,
|
|
} from "./tenantCsvImport";
|
|
|
|
const tenants: TenantSummary[] = [
|
|
{
|
|
id: "tenant-1",
|
|
type: "COMPANY",
|
|
name: "Hanmac Technology",
|
|
slug: "hanmac",
|
|
description: "",
|
|
status: "active",
|
|
domains: ["hanmac.example.com"],
|
|
memberCount: 0,
|
|
createdAt: "",
|
|
updatedAt: "",
|
|
},
|
|
{
|
|
id: "tenant-2",
|
|
type: "COMPANY",
|
|
name: "Saman Engineering",
|
|
slug: "saman",
|
|
description: "",
|
|
status: "active",
|
|
domains: [],
|
|
memberCount: 0,
|
|
createdAt: "",
|
|
updatedAt: "",
|
|
},
|
|
{
|
|
id: "tenant-3",
|
|
type: "COMPANY_GROUP",
|
|
name: "Hanmac Family",
|
|
slug: "hanmac-family",
|
|
description: "",
|
|
status: "active",
|
|
domains: [],
|
|
memberCount: 0,
|
|
createdAt: "",
|
|
updatedAt: "",
|
|
},
|
|
{
|
|
id: "tenant-4",
|
|
type: "ORGANIZATION",
|
|
name: "기획부",
|
|
slug: "planning",
|
|
description: "",
|
|
status: "active",
|
|
domains: [],
|
|
memberCount: 0,
|
|
createdAt: "",
|
|
updatedAt: "",
|
|
},
|
|
];
|
|
|
|
describe("tenantCsvImport", () => {
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
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,worksmobile_sync\n,Hanmac Tech,COMPANY,,hanmac-tech,Memo,hanmac-tech.example.com,internal,센터,no\n",
|
|
);
|
|
|
|
expect(rows).toEqual([
|
|
{
|
|
rowNumber: 2,
|
|
tenantId: "",
|
|
name: "Hanmac Tech",
|
|
type: "COMPANY",
|
|
parentTenantId: "",
|
|
parentTenantSlug: "",
|
|
slug: "hanmac-tech",
|
|
memo: "Memo",
|
|
emailDomain: "hanmac-tech.example.com",
|
|
visibility: "internal",
|
|
orgUnitType: "센터",
|
|
worksmobileSync: "no",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("puts tenant_id-less rows with exact or similar matches first", () => {
|
|
const rows = parseTenantCSV(
|
|
"tenant_id,name,type,parent_tenant_id,slug,memo,email_domain\n,New Tenant,COMPANY,,new-tenant,,\n,Hanmac Tech,COMPANY,,hanmac-tech,,\n,Saman Engineering,COMPANY,,saman-copy,,\n",
|
|
);
|
|
|
|
const preview = buildTenantImportPreview(rows, tenants);
|
|
|
|
expect(preview.map((row) => row.row.name)).toEqual([
|
|
"Saman Engineering",
|
|
"Hanmac Tech",
|
|
"New Tenant",
|
|
]);
|
|
expect(preview[0].candidates[0]).toMatchObject({
|
|
tenantId: "tenant-2",
|
|
reason: "exact_name",
|
|
});
|
|
expect(preview[1].candidates[0]).toMatchObject({
|
|
tenantId: "tenant-1",
|
|
reason: "similar_name",
|
|
});
|
|
expect(preview[2].candidates).toEqual([]);
|
|
});
|
|
|
|
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,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, {
|
|
2: "tenant-1",
|
|
});
|
|
|
|
expect(csv.split("\n")[0]).toBe(
|
|
"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,팀,no",
|
|
);
|
|
});
|
|
|
|
it("serializes create resolutions by resetting external tenant id and conflicting slug", () => {
|
|
const rows = parseTenantCSV(
|
|
"tenant_id,name,type,parent_tenant_id,slug,memo,email_domain\nlocal-tenant-id,Hanmac Technology,COMPANY,,hanmac,Memo,hanmac.example.com\n",
|
|
);
|
|
const preview = buildTenantImportPreview(rows, tenants);
|
|
|
|
expect(preview[0].conflicts).toEqual(
|
|
expect.arrayContaining(["external_tenant_id", "slug_exists"]),
|
|
);
|
|
|
|
const csv = serializeTenantImportCSV(preview, {
|
|
2: {
|
|
mode: "create",
|
|
tenantId: "staging-new-tenant-id",
|
|
slug: "hanmac-imported",
|
|
},
|
|
});
|
|
|
|
expect(csv).toContain(
|
|
"staging-new-tenant-id,Hanmac Technology,COMPANY,,,hanmac-imported,Memo,hanmac.example.com",
|
|
);
|
|
expect(csv).not.toContain("local-tenant-id");
|
|
});
|
|
|
|
it("preserves source tenant_id when a create resolution does not override it", () => {
|
|
const exportedTenantId = "11111111-2222-4333-8444-555555555555";
|
|
const rows = parseTenantCSV(
|
|
`tenant_id,name,type,parent_tenant_id,parent_tenant_slug,slug,memo,email_domain
|
|
${exportedTenantId},Tenant With UUID,COMPANY,,,tenant-with-uuid,Memo,tenant-with-uuid.example.com
|
|
`,
|
|
);
|
|
const preview = buildTenantImportPreview(rows, tenants);
|
|
const csv = serializeTenantImportCSV(preview, {
|
|
2: {
|
|
mode: "create",
|
|
slug: "tenant-with-uuid",
|
|
},
|
|
});
|
|
|
|
expect(csv).toContain(
|
|
`${exportedTenantId},Tenant With UUID,COMPANY,,,tenant-with-uuid,Memo,tenant-with-uuid.example.com`,
|
|
);
|
|
});
|
|
|
|
it("remaps child parent_tenant_id from source ids to selected staging ids", () => {
|
|
const rows = parseTenantCSV(
|
|
[
|
|
"tenant_id,name,type,parent_tenant_id,slug,memo,email_domain",
|
|
"local-parent-id,Parent Tenant,COMPANY,,parent-local,,",
|
|
"local-child-id,Child Tenant,ORGANIZATION,local-parent-id,child-local,,",
|
|
].join("\n"),
|
|
);
|
|
const preview = buildTenantImportPreview(rows, tenants);
|
|
const csv = serializeTenantImportCSV(preview, {
|
|
2: {
|
|
mode: "create",
|
|
tenantId: "staging-parent-id",
|
|
slug: "parent-staging",
|
|
},
|
|
3: {
|
|
mode: "create",
|
|
tenantId: "staging-child-id",
|
|
slug: "child-staging",
|
|
},
|
|
});
|
|
|
|
expect(csv).toContain(
|
|
"staging-parent-id,Parent Tenant,COMPANY,,,parent-staging,,",
|
|
);
|
|
expect(csv).toContain(
|
|
"staging-child-id,Child Tenant,ORGANIZATION,staging-parent-id,,child-staging,,",
|
|
);
|
|
expect(csv).not.toContain("local-parent-id");
|
|
expect(csv).not.toContain("local-child-id");
|
|
});
|
|
|
|
it("parses parent_tenant_slug and remaps it to selected staging ids", () => {
|
|
const rows = parseTenantCSV(
|
|
[
|
|
"name,type,parent_tenant_slug,slug,memo,email_domain",
|
|
"Parent Tenant,COMPANY,,parent-slug,,",
|
|
"Child Tenant,ORGANIZATION,parent-slug,child-slug,,",
|
|
].join("\n"),
|
|
);
|
|
const preview = buildTenantImportPreview(rows, tenants);
|
|
const csv = serializeTenantImportCSV(preview, {
|
|
2: {
|
|
mode: "create",
|
|
tenantId: "staging-parent-id",
|
|
slug: "parent-slug",
|
|
},
|
|
3: {
|
|
mode: "create",
|
|
tenantId: "staging-child-id",
|
|
slug: "child-slug",
|
|
},
|
|
});
|
|
|
|
expect(rows[1].parentTenantSlug).toBe("parent-slug");
|
|
expect(csv).toContain(
|
|
"staging-child-id,Child Tenant,ORGANIZATION,staging-parent-id,parent-slug,child-slug,,",
|
|
);
|
|
});
|
|
|
|
it("keeps parent_tenant_slug in the serialized CSV as a fallback for hierarchy import", () => {
|
|
const rows = parseTenantCSV(
|
|
[
|
|
"name,type,parent_tenant_slug,slug,memo,email_domain",
|
|
"Parent Tenant,COMPANY,,parent-slug,,",
|
|
"Child Tenant,ORGANIZATION,parent-slug,child-slug,,",
|
|
].join("\n"),
|
|
);
|
|
const preview = buildTenantImportPreview(rows, tenants);
|
|
const csv = serializeTenantImportCSV(preview, {
|
|
2: {
|
|
mode: "create",
|
|
tenantId: "staging-parent-id",
|
|
slug: "parent-slug",
|
|
},
|
|
3: {
|
|
mode: "create",
|
|
tenantId: "staging-child-id",
|
|
slug: "child-slug",
|
|
},
|
|
});
|
|
|
|
expect(csv.split("\n")[0]).toBe(
|
|
"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,,,,,yes",
|
|
);
|
|
});
|
|
|
|
it("parses Naver Works organization CSV columns into tenant import rows", () => {
|
|
const rows = parseTenantCSV(
|
|
[
|
|
'"조직명","멤버 수","조직장","조직 다국어명","설명","메일링 리스트","상위 조직"',
|
|
'"기술개발센터","1","","","","tdc@samaneng.com",""',
|
|
'"기획부","1","","","","planning@samaneng.com","기술개발센터(tdc@samaneng.com)"',
|
|
'"업무팀","0","","","","t_226wn@samaneng.com","기획부(planning@samaneng.com)"',
|
|
].join("\n"),
|
|
{ rootParentSlug: "saman" },
|
|
);
|
|
|
|
expect(rows).toMatchObject([
|
|
{
|
|
name: "기술개발센터",
|
|
type: "ORGANIZATION",
|
|
slug: "tdc",
|
|
parentTenantSlug: "saman",
|
|
},
|
|
{
|
|
name: "기획부",
|
|
type: "ORGANIZATION",
|
|
slug: "planning",
|
|
parentTenantSlug: "tdc",
|
|
},
|
|
{
|
|
name: "업무팀",
|
|
type: "ORGANIZATION",
|
|
slug: "t-226wn",
|
|
parentTenantSlug: "planning",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("infers root parent slug from an organization CSV file prefix that matches an existing slug", () => {
|
|
expect(inferTenantImportRootParentSlug("saman_org.csv", tenants)).toBe(
|
|
"saman",
|
|
);
|
|
expect(
|
|
inferTenantImportRootParentSlug("/tmp/hanmac-family_org.csv", tenants),
|
|
).toBe("hanmac-family");
|
|
expect(
|
|
inferTenantImportRootParentSlug("saman_org_slugged.csv", tenants),
|
|
).toBe("saman");
|
|
expect(inferTenantImportRootParentSlug("unknown_org.csv", tenants)).toBe(
|
|
"",
|
|
);
|
|
expect(inferTenantImportRootParentSlug("tenant-import.csv", tenants)).toBe(
|
|
"",
|
|
);
|
|
});
|
|
|
|
it("groups existing parent candidates by company group, company, and organization", () => {
|
|
const groups = buildTenantImportParentOptionGroups(tenants);
|
|
|
|
expect(groups.map((group) => group.type)).toEqual([
|
|
"COMPANY_GROUP",
|
|
"COMPANY",
|
|
"ORGANIZATION",
|
|
]);
|
|
expect(
|
|
groups.map((group) => group.tenants.map((tenant) => tenant.id)),
|
|
).toEqual([["tenant-3"], ["tenant-1", "tenant-2"], ["tenant-4"]]);
|
|
});
|
|
|
|
it("keeps generated ids stable and follows edited parent slugs for child rows", () => {
|
|
const randomUUID = vi
|
|
.fn()
|
|
.mockReturnValueOnce("parent-generated-id")
|
|
.mockReturnValueOnce("child-generated-id");
|
|
vi.stubGlobal("crypto", { randomUUID });
|
|
|
|
const rows = parseTenantCSV(
|
|
[
|
|
"name,type,parent_tenant_slug,slug,memo,email_domain",
|
|
"기술개발센터,ORGANIZATION,saman,t-536fc,,",
|
|
"일반구조물 div,ORGANIZATION,t-536fc,t-568cz,,",
|
|
].join("\n"),
|
|
);
|
|
const preview = buildTenantImportPreview(rows, tenants);
|
|
const csv = serializeTenantImportCSV(preview, {
|
|
2: { mode: "create", slug: "tech-center" },
|
|
3: { mode: "create", slug: "structure-div" },
|
|
});
|
|
|
|
expect(csv).toContain(
|
|
"parent-generated-id,기술개발센터,ORGANIZATION,,saman,tech-center,,",
|
|
);
|
|
expect(csv).toContain(
|
|
"child-generated-id,일반구조물 div,ORGANIZATION,parent-generated-id,tech-center,structure-div,,",
|
|
);
|
|
});
|
|
|
|
it("serializes explicit parent tenant selections from the import preview", () => {
|
|
const rows = parseTenantCSV(
|
|
[
|
|
"name,type,parent_tenant_slug,slug,memo,email_domain",
|
|
"기술개발센터,ORGANIZATION,saman,t-536fc,,",
|
|
"일반구조물 div,ORGANIZATION,t-536fc,t-568cz,,",
|
|
].join("\n"),
|
|
);
|
|
const preview = buildTenantImportPreview(rows, tenants);
|
|
const csv = serializeTenantImportCSV(preview, {
|
|
2: {
|
|
mode: "create",
|
|
slug: "tech-center",
|
|
parentTenantId: "tenant-2",
|
|
parentTenantSlug: "",
|
|
},
|
|
3: {
|
|
mode: "create",
|
|
slug: "structure-div",
|
|
parentTenantSlug: "tech-center",
|
|
},
|
|
});
|
|
|
|
expect(csv).toContain("기술개발센터,ORGANIZATION,tenant-2,,tech-center,,");
|
|
expect(csv).toContain(",일반구조물 div,ORGANIZATION,");
|
|
expect(csv).toContain(",tech-center,structure-div,,");
|
|
});
|
|
});
|