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,,"); }); });