1
0
forked from baron/baron-sso

userfront e2e 전체 테스트

This commit is contained in:
2026-05-29 08:19:34 +09:00
parent dc16958804
commit da01f63c54
22 changed files with 1439 additions and 103 deletions

View File

@@ -128,4 +128,23 @@ nullable@test.com,Nullable User,010-1111-1111,user,primary-tenant,개발팀,책
});
expect(result[1].additionalAppointments).toBeUndefined();
});
it("should preserve sub_email as secondary email metadata without replacing primary email", () => {
const csv = `email,name,tenant_slug,employee_id,sub_email
primary@samaneng.com,Primary User,rnd-saman,EMP001,secondary@hanmaceng.co.kr`;
const result = parseUserCSV(csv);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
email: "primary@samaneng.com",
tenantSlug: "rnd-saman",
metadata: {
employee_id: "EMP001",
sub_email: "secondary@hanmaceng.co.kr",
secondary_emails: ["secondary@hanmaceng.co.kr"],
aliasEmails: ["secondary@hanmaceng.co.kr"],
},
});
});
});

View File

@@ -12,11 +12,13 @@ export function parseUserCSV(text: string): BulkUserItem[] {
for (let i = 1; i < records.length; i++) {
const values = records[i].map((v) => v.trim());
if (values.every((value) => value === "")) continue;
const item: Partial<BulkUserItem> & { metadata: Record<string, string> } = {
const item: Partial<BulkUserItem> & {
metadata: Record<string, unknown>;
} = {
metadata: {},
};
const additionalAppointment: BulkUserAppointment & {
metadata: Record<string, string>;
metadata: Record<string, unknown>;
} = {
metadata: {},
};
@@ -94,6 +96,8 @@ export function parseUserCSV(text: string): BulkUserItem[] {
item.jobTitle = value;
} else if (header === "employee_id") {
item.metadata.employee_id = value;
} else if (header === "secondary_emails") {
applySecondaryEmailMetadata(item, value);
} else if (header === "tenant_slug1") {
additionalAppointment.tenantSlug = value;
} else if (header === "department1") {
@@ -117,6 +121,7 @@ export function parseUserCSV(text: string): BulkUserItem[] {
item.metadata.personal_email = value;
} else if (header === "subemail") {
item.metadata.naverworks_sub_email = value;
addWorksmobileAliasEmails(item, splitEmailTokens(value).slice(1));
item.email = firstEmailToken(value) || item.email;
} else if (header === "nickname") {
item.metadata.naverworks_nickname = value;
@@ -185,7 +190,7 @@ export function parseUserCSV(text: string): BulkUserItem[] {
}
function cleanAdditionalAppointment(
appointment: BulkUserAppointment & { metadata: Record<string, string> },
appointment: BulkUserAppointment & { metadata: Record<string, unknown> },
) {
const metadata =
Object.keys(appointment.metadata).length > 0
@@ -210,7 +215,31 @@ function cleanAdditionalAppointment(
}
function normalizeHeader(header: string) {
return header
const raw = header.trim().replace(/^\uFEFF/, "");
const lower = raw.toLowerCase();
const separatorNormalized = lower.replace(/-+/g, "_").replace(/_+/g, "_");
const compactKorean = raw.replace(/\s+/g, "");
if (
[
"sub_email",
"secondary_email",
"secondary_emails",
"additional_email",
"additional_emails",
"alias_email",
"alias_emails",
"worksmobile_alias_email",
"worksmobile_alias_emails",
].includes(separatorNormalized) ||
["보조이메일", "보조메일", "추가이메일", "추가메일"].includes(
compactKorean,
)
) {
return "secondary_emails";
}
return raw
.trim()
.toLowerCase()
.replace(/^\uFEFF/, "")
@@ -264,12 +293,69 @@ function parseCSVRecords(text: string) {
}
function firstEmailToken(value: string) {
return (
value
.split(/[;,]/)
.map((token) => token.trim())
.find((token) => token.includes("@")) ?? ""
);
return splitEmailTokens(value)[0] ?? "";
}
function splitEmailTokens(value: string) {
return value
.split(/[;,\n\r\t]/)
.map((token) => token.trim())
.filter((token) => token.includes("@"));
}
function metadataString(value: unknown) {
return typeof value === "string" ? value : "";
}
function metadataEmailList(value: unknown) {
if (Array.isArray(value)) {
return value
.map((item) => (typeof item === "string" ? item.trim() : ""))
.filter(Boolean);
}
if (typeof value === "string") {
return splitEmailTokens(value);
}
return [];
}
function uniqueEmails(values: string[]) {
const seen = new Set<string>();
const result: string[] = [];
for (const value of values) {
const normalized = value.trim().toLowerCase();
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
result.push(normalized);
}
return result;
}
function addWorksmobileAliasEmails(
item: Partial<BulkUserItem> & { metadata: Record<string, unknown> },
emails: string[],
) {
const aliases = uniqueEmails([
...metadataEmailList(item.metadata.aliasEmails),
...emails,
]);
if (aliases.length > 0) {
item.metadata.aliasEmails = aliases;
item.metadata.worksmobileAliasEmails = aliases;
}
}
function applySecondaryEmailMetadata(
item: Partial<BulkUserItem> & { metadata: Record<string, unknown> },
value: string,
) {
const emails = splitEmailTokens(value);
item.metadata.sub_email = value;
item.metadata.secondary_emails = uniqueEmails([
...metadataEmailList(item.metadata.secondary_emails),
...emails,
]);
addWorksmobileAliasEmails(item, emails);
}
function splitOrganizationPath(value: string) {
@@ -280,28 +366,32 @@ function splitOrganizationPath(value: string) {
}
function applyNaverWorksFallbacks(
item: Partial<BulkUserItem> & { metadata: Record<string, string> },
item: Partial<BulkUserItem> & { metadata: Record<string, unknown> },
) {
if (!item.name) {
const firstName = item.metadata.naverworks_first_name ?? "";
const lastName = item.metadata.naverworks_last_name ?? "";
const firstName = metadataString(item.metadata.naverworks_first_name);
const lastName = metadataString(item.metadata.naverworks_last_name);
item.name = [firstName, lastName].filter(Boolean).join(" ").trim();
if (!item.name && item.metadata.naverworks_nickname) {
item.name = item.metadata.naverworks_nickname;
const nickname = metadataString(item.metadata.naverworks_nickname);
if (!item.name && nickname) {
item.name = nickname;
}
}
if (!item.email) {
item.email = item.metadata.personal_email;
item.email = metadataString(item.metadata.personal_email);
}
if (!item.phone) {
const countryCode = item.metadata.naverworks_mobile_country_code ?? "";
const number = item.metadata.naverworks_mobile_numbers ?? "";
const countryCode = metadataString(
item.metadata.naverworks_mobile_country_code,
);
const number = metadataString(item.metadata.naverworks_mobile_numbers);
item.phone = `${countryCode}${number}`.replace(/\s/g, "");
}
if (!item.grade && item.metadata.naverworks_level) {
item.grade = item.metadata.naverworks_level;
const level = metadataString(item.metadata.naverworks_level);
if (!item.grade && level) {
item.grade = level;
}
}

View File

@@ -101,4 +101,60 @@ describe("applyGeneralPlanningOfficePriority", () => {
employee_id: "EMP001",
});
});
it("uses GPDTDC as the Baron representative while keeping the first affiliation primary for WorksMobile", () => {
const user: BulkUserItem = {
email: "gpdtdc-dual@test.com",
name: "GPDTDC Dual User",
tenantSlug: "rnd-saman",
department: "삼안기술연구소",
grade: "책임",
metadata: {
employee_id: "SAMAN001",
},
additionalAppointments: [
{
tenantSlug: "tdc",
tenantName: "기술개발센터",
grade: "책임연구원",
metadata: {
employee_id: "B24051",
},
},
],
};
const result = applyGeneralPlanningOfficePriority(user, [
tenant("family", "한맥가족사", "hanmac-family"),
tenant("gpdtdc", "총괄기획&기술개발센터", "gpdtdc", "family"),
tenant("tdc", "기술개발센터", "tdc", "gpdtdc"),
tenant("saman", "삼안", "rnd-saman"),
]);
expect(result.tenantSlug).toBe("gpdtdc");
expect(result.tenantImport).toMatchObject({
slug: "gpdtdc",
name: "총괄기획&기술개발센터",
});
expect(result.metadata.employee_id).toBe("SAMAN001");
expect(result.additionalAppointments).toEqual([
expect.objectContaining({
tenantSlug: "rnd-saman",
isPrimary: true,
department: "삼안기술연구소",
grade: "책임",
metadata: {
employee_id: "SAMAN001",
},
}),
expect.objectContaining({
tenantSlug: "tdc",
isPrimary: false,
grade: "책임연구원",
metadata: {
employee_id: "B24051",
},
}),
]);
});
});

View File

@@ -1,9 +1,18 @@
import type { BulkUserItem, TenantSummary } from "../../../lib/adminApi";
import type {
BulkUserAppointment,
BulkUserItem,
TenantSummary,
} from "../../../lib/adminApi";
export function applyGeneralPlanningOfficePriority(
user: BulkUserItem,
tenants: TenantSummary[],
): BulkUserItem {
const gpdtdcRepresentative = applyGPDTDCRepresentativeTenant(user, tenants);
if (gpdtdcRepresentative) {
return gpdtdcRepresentative;
}
const firstAdditional = user.additionalAppointments?.[0];
const secondarySlug = firstAdditional?.tenantSlug;
@@ -67,6 +76,82 @@ export function applyGeneralPlanningOfficePriority(
};
}
function applyGPDTDCRepresentativeTenant(
user: BulkUserItem,
tenants: TenantSummary[],
): BulkUserItem | undefined {
const root = findGPDTDCRootTenant(tenants);
if (!root) return undefined;
const primarySlug = user.tenantSlug || "";
const hasPrimaryUnderRoot = isUnderTenant(primarySlug, root, tenants);
const hasAppointmentUnderRoot = (user.additionalAppointments ?? []).some(
(appointment) => isUnderTenant(appointment.tenantSlug || "", root, tenants),
);
if (!hasPrimaryUnderRoot && !hasAppointmentUnderRoot) return undefined;
if (primarySlug === root.slug) return undefined;
const worksmobileAppointments: BulkUserAppointment[] = [];
const seen = new Set<string>();
const addAppointment = (
appointment: BulkUserAppointment,
fallbackKey: string,
) => {
const key = appointment.tenantSlug || appointment.tenantId || fallbackKey;
if (!key || key === root.slug || seen.has(key)) return;
seen.add(key);
worksmobileAppointments.push(appointment);
};
addAppointment(buildPrimaryAppointment(user), "primary");
for (const appointment of user.additionalAppointments ?? []) {
addAppointment(
{ ...appointment, isPrimary: false },
appointment.tenantSlug || "",
);
}
return {
...user,
tenantSlug: root.slug,
tenantImport: {
...(user.tenantImport ?? {}),
sourceTenantId: undefined,
slug: root.slug,
name: root.name,
},
additionalAppointments:
worksmobileAppointments.length > 0 ? worksmobileAppointments : undefined,
};
}
function buildPrimaryAppointment(user: BulkUserItem): BulkUserAppointment {
return {
...(user.tenantId ? { tenantId: user.tenantId } : {}),
...(user.tenantSlug ? { tenantSlug: user.tenantSlug } : {}),
isPrimary: true,
isOwner: false,
...(user.department ? { department: user.department } : {}),
...(user.grade ? { grade: user.grade } : {}),
...(user.position ? { position: user.position } : {}),
...(user.jobTitle ? { jobTitle: user.jobTitle } : {}),
metadata: { ...user.metadata },
};
}
function findGPDTDCRootTenant(tenants: TenantSummary[]) {
return tenants.find((tenant) => {
const slug = tenant.slug.trim().toLowerCase();
const name = tenant.name.replace(/\s+/g, "").toLowerCase();
return (
slug === "gpdtdc" ||
name === "gpdtdc" ||
name.includes("총괄기획&기술개발센터") ||
name.includes("총괄기획기술개발센터")
);
});
}
function isUnderGeneralPlanningOffice(
tenantSlug: string,
tenants: TenantSummary[],
@@ -80,6 +165,20 @@ function isUnderGeneralPlanningOffice(
return false;
}
function isUnderTenant(
tenantSlug: string,
root: TenantSummary,
tenants: TenantSummary[],
): boolean {
let current = tenants.find((tenant) => tenant.slug === tenantSlug);
while (current) {
if (current.id === root.id || current.slug === root.slug) return true;
if (!current.parentId) break;
current = tenants.find((tenant) => tenant.id === current?.parentId);
}
return false;
}
function stringValue(value: unknown) {
return typeof value === "string" ? value : undefined;
}