GitHub(nextgroup2706/ken_nogi)は今後使わず自社Gitea運用に切替え。 NodeSrvは旧リポジトリの履歴を破棄しファイルのみ統合(Dokploy用サービスアカウントは 別途mygit-admin/NodeSrv.gitに履歴あり)。notepmエクスポート(12GB)とPleasanter インストーラzip(208MB)はサイズが大きいため.gitignoreで除外。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
506 lines
28 KiB
JavaScript
506 lines
28 KiB
JavaScript
const { test } = require("node:test");
|
||
const assert = require("node:assert");
|
||
const {
|
||
planMerge, applyMerge, i18nName, orgUnitRefs, orgUnitsToText, pleasanterUserToFields,
|
||
ensureChoiceMasterId,
|
||
} = require("../src/commands/merge-master");
|
||
|
||
function makeUser(overrides = {}) {
|
||
return {
|
||
userId: "u1",
|
||
email: "taro.yamada@next-hd.co.jp",
|
||
privateEmail: "taro.private@example.com",
|
||
aliasEmails: ["taro.alias@next-hd.co.jp"],
|
||
userName: {
|
||
lastName: "山田",
|
||
firstName: "太郎",
|
||
phoneticLastName: "ヤマダ",
|
||
phoneticFirstName: "タロウ",
|
||
},
|
||
i18nNames: [{ language: "en_US", lastName: "Yamada", firstName: "Taro" }],
|
||
nickName: "タロー",
|
||
employeeNumber: "400123",
|
||
telephone: "03-1234-5678",
|
||
cellPhone: "+81 090-1234-5678",
|
||
location: "東京本社",
|
||
birthday: "1990-04-01",
|
||
hiredDate: "2015-04-01",
|
||
organizations: [{ levelName: "正社員", orgUnits: [{ orgUnitId: "ou-1", orgUnitName: "情報システム部", primary: true, positionName: "課長" }] }],
|
||
userTypeName: "一般",
|
||
isSuspended: false,
|
||
isAdministrator: false,
|
||
leaveOfAbsence: { isLeaveOfAbsence: false },
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
test("planMerge creates a new record when the employeeId is not in the existing master", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser()], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const plan = planMerge(snapshot, []);
|
||
|
||
assert.strictEqual(plan.creates.length, 1);
|
||
assert.strictEqual(plan.updates.length, 0);
|
||
assert.strictEqual(plan.retirements.length, 0);
|
||
|
||
const fields = plan.creates[0].fields;
|
||
// タブ1: 基本情報
|
||
assert.strictEqual(fields.ClassA, "u1");
|
||
assert.strictEqual(fields.ClassB, "taro.yamada@next-hd.co.jp");
|
||
assert.strictEqual(fields.Class001, "山田");
|
||
assert.strictEqual(fields.Class002, "太郎");
|
||
assert.strictEqual(fields.Class003, "ヤマダ");
|
||
assert.strictEqual(fields.Class004, "タロウ");
|
||
assert.strictEqual(fields.Class005, "Yamada");
|
||
assert.strictEqual(fields.Class006, "Taro");
|
||
assert.strictEqual(fields.Date007, "1990-04-01");
|
||
assert.strictEqual(fields.DateA, "2026-08-08T00:00:00.000Z");
|
||
// タブ2: プリザンター管理項目はplanMergeの時点では書かない(Class035はapplyMergeがグループ解決後に追加する)
|
||
assert.strictEqual(fields.Class035, undefined);
|
||
assert.strictEqual(fields.Class011, undefined);
|
||
assert.strictEqual(fields.Body, undefined);
|
||
// planMergeはuserをそのまま持ち越す(applyMergeでのグループ解決に使う)
|
||
assert.strictEqual(plan.creates[0].user.userId, "u1");
|
||
// タブ3: LINEWORKS固有情報
|
||
assert.strictEqual(fields.Class051, "taro.private@example.com");
|
||
assert.strictEqual(fields.Class052, "タロー");
|
||
assert.strictEqual(fields.Class053, "400123");
|
||
assert.strictEqual(fields.Class054, "03-1234-5678");
|
||
assert.strictEqual(fields.Class055, "+81 090-1234-5678");
|
||
assert.strictEqual(fields.Class056, "東京本社");
|
||
// Class057/058/059(役職・職級・利用権限タイプ)は外部マスタのResultId解決が必要なため、
|
||
// planMerge時点では存在せずapplyMergeが解決する(下記のapplyMergeテスト参照)
|
||
assert.strictEqual(fields.Class057, undefined);
|
||
assert.strictEqual(fields.Class058, undefined);
|
||
assert.strictEqual(fields.Class059, undefined);
|
||
assert.strictEqual(fields.Date060, "2015-04-01");
|
||
assert.strictEqual(fields.Description061, "taro.alias@next-hd.co.jp");
|
||
assert.strictEqual(fields.Description062, "ou-1=情報システム部");
|
||
assert.strictEqual(fields.Check062, true);
|
||
assert.strictEqual(fields.Check063, false);
|
||
assert.strictEqual(fields.Check064, false);
|
||
assert.strictEqual(fields.Check065, false);
|
||
});
|
||
|
||
test("planMerge updates an existing record matched by employeeId (ClassA)", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser({ organizations: [{ levelName: "正社員", orgUnits: [{ orgUnitName: "情報システム部", primary: true, positionName: "部長" }] }] })], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const existing = [{ ResultId: 10, ClassA: "u1", ClassB: "taro.yamada@next-hd.co.jp", Check062: true }];
|
||
|
||
const plan = planMerge(snapshot, existing);
|
||
|
||
assert.strictEqual(plan.creates.length, 0);
|
||
assert.strictEqual(plan.updates.length, 1);
|
||
assert.strictEqual(plan.updates[0].itemId, 10);
|
||
assert.strictEqual(plan.updates[0].fields.Class057, undefined);
|
||
});
|
||
|
||
test("pleasanterUserToFields maps a Pleasanter Users API record onto tab-2 fields, including the AllowXxx/LoginExpiration properties found only in live responses", () => {
|
||
const pUser = {
|
||
UserId: 42, LoginId: "hayato", Name: "中野 隼人", UserCode: "", Gender: "", Language: "ja",
|
||
TimeZone: "Tokyo Standard Time", DeptId: 7, Manager: 3, TenantManager: false, Theme: "",
|
||
Body: "説明文", PasswordExpirationTime: "2023-08-31T12:00:00", Disabled: false, Lockout: false,
|
||
AllowCreationAtTopSite: true, AllowGroupAdministration: false, AllowGroupCreation: true,
|
||
AllowApi: false, AllowMovingFromTopSite: true, SecretKey: "abc123",
|
||
LoginExpirationLimit: "2030-01-01T00:00:00", LoginExpirationPeriod: 90,
|
||
};
|
||
assert.deepStrictEqual(pleasanterUserToFields(pUser), {
|
||
Class011: "42", Class012: "hayato", Class013: "中野 隼人", Class014: "", Class015: "", Class016: "ja",
|
||
Class017: "Tokyo Standard Time", Class018: "7", Class019: "3", Check020: false, Class021: "",
|
||
Description022: "説明文", Class023: "2023-08-31T12:00:00",
|
||
Check024: true, Check025: false, Check026: true, Check027: false, Check028: true,
|
||
Check029: false, Check030: false, Check031: true,
|
||
Class032: "2030-01-01T00:00:00", Class033: "90", Class036: "",
|
||
});
|
||
});
|
||
|
||
test("pleasanterUserToFields maps MailAddresses[0] to Class036 (PLメールアドレス), empty string when absent", () => {
|
||
const withMail = pleasanterUserToFields({ UserId: 1, LoginId: "x", Name: "x", MailAddresses: ["taro@nexthd.jp", "taro2@nexthd.jp"] });
|
||
assert.strictEqual(withMail.Class036, "taro@nexthd.jp");
|
||
|
||
const withoutMail = pleasanterUserToFields({ UserId: 1, LoginId: "x", Name: "x" });
|
||
assert.strictEqual(withoutMail.Class036, "");
|
||
});
|
||
|
||
test("pleasanterUserToFields treats DeptId=0, Manager=0, and the 1899 sentinel dates as unset", () => {
|
||
const pUser = {
|
||
UserId: 1, LoginId: "admin", Name: "管理者", DeptId: 0, Manager: 0,
|
||
PasswordExpirationTime: "1899-12-30T00:00:00", LoginExpirationLimit: "1899-12-30T00:00:00",
|
||
};
|
||
const fields = pleasanterUserToFields(pUser);
|
||
assert.strictEqual(fields.Class018, "");
|
||
assert.strictEqual(fields.Class019, "");
|
||
assert.strictEqual(fields.Class023, "");
|
||
assert.strictEqual(fields.Class032, "");
|
||
});
|
||
|
||
test("planMerge mirrors tab-2 fields from a Pleasanter user matched by email, and leaves them unset when no match exists", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser()], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const pleasanterUsers = [{ UserId: 99, LoginId: "taro", Name: "山田 太郎", MailAddresses: ["taro.yamada@next-hd.co.jp"], DeptId: 3 }];
|
||
|
||
const withMatch = planMerge(snapshot, [], pleasanterUsers);
|
||
assert.strictEqual(withMatch.creates[0].fields.Class011, "99");
|
||
assert.strictEqual(withMatch.creates[0].fields.Class018, "3");
|
||
|
||
const withoutMatch = planMerge(snapshot, [], []);
|
||
assert.strictEqual(withoutMatch.creates[0].fields.Class011, undefined);
|
||
});
|
||
|
||
test("planMerge matches a Pleasanter user by email local-part even when the domain differs (next-hd.co.jp vs nexthd.jp)", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser()], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const pleasanterUsers = [{ UserId: 36, LoginId: "ntkco12216", Name: "山田 太郎", MailAddresses: ["taro.yamada@nexthd.jp"] }];
|
||
|
||
const plan = planMerge(snapshot, [], pleasanterUsers);
|
||
|
||
assert.strictEqual(plan.needsReview.length, 0);
|
||
assert.strictEqual(plan.creates[0].fields.Class011, "36");
|
||
});
|
||
|
||
test("planMerge does not report needsReview when the same Pleasanter user simply has two mail addresses sharing a local-part", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser()], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const pleasanterUsers = [{ UserId: 132, LoginId: "shiori", Name: "大内 詩織", MailAddresses: ["taro.yamada@next-hd.co.jp", "taro.yamada@nexthd.jp"] }];
|
||
|
||
const plan = planMerge(snapshot, [], pleasanterUsers);
|
||
|
||
assert.strictEqual(plan.needsReview.length, 0);
|
||
assert.strictEqual(plan.creates[0].fields.Class011, "132");
|
||
});
|
||
|
||
test("planMerge reports needsReview when an email local-part has multiple Pleasanter UserIds and no override is set, without touching tab-2", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser()], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const pleasanterUsers = [
|
||
{ UserId: 36, LoginId: "legacy", Name: "山田 太郎", MailAddresses: ["taro.yamada@next-hd.co.jp"] },
|
||
{ UserId: 593, LoginId: "current", Name: "山田 太郎", MailAddresses: ["taro.yamada@nexthd.jp"] },
|
||
];
|
||
|
||
const plan = planMerge(snapshot, [], pleasanterUsers);
|
||
|
||
assert.strictEqual(plan.needsReview.length, 1);
|
||
assert.strictEqual(plan.needsReview[0].email, "taro.yamada");
|
||
assert.deepStrictEqual(plan.needsReview[0].candidateUserIds.sort(), [36, 593]);
|
||
assert.strictEqual(plan.creates[0].fields.Class011, undefined);
|
||
});
|
||
|
||
test("planMerge resolves a duplicate email local-part via emailOverrides without reporting needsReview", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser()], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const pleasanterUsers = [
|
||
{ UserId: 36, LoginId: "legacy", Name: "山田 太郎", MailAddresses: ["taro.yamada@next-hd.co.jp"] },
|
||
{ UserId: 593, LoginId: "current", Name: "山田 太郎", MailAddresses: ["taro.yamada@nexthd.jp"] },
|
||
];
|
||
|
||
const plan = planMerge(snapshot, [], pleasanterUsers, { "taro.yamada": 593 });
|
||
|
||
assert.strictEqual(plan.needsReview.length, 0);
|
||
assert.strictEqual(plan.creates[0].fields.Class011, "593");
|
||
});
|
||
|
||
test("planMerge also creates a record for a Pleasanter user who has no matching LINEWORKS account (retired/disabled etc.), with tab-2 only and Check062=false", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const pleasanterUsers = [{ UserId: 77, LoginId: "retired", Name: "退職 太郎", MailAddresses: ["tairo.taishoku@nexthd.jp"], Disabled: true }];
|
||
|
||
const plan = planMerge(snapshot, [], pleasanterUsers);
|
||
|
||
assert.strictEqual(plan.creates.length, 1);
|
||
const fields = plan.creates[0].fields;
|
||
assert.strictEqual(fields.ClassA, undefined); // LINEWORKS社員IDは無い
|
||
assert.strictEqual(fields.ClassB, "tairo.taishoku@nexthd.jp");
|
||
assert.strictEqual(fields.Check062, false); // LINEWORKS在籍フラグはfalse固定
|
||
assert.strictEqual(fields.Class011, "77");
|
||
assert.strictEqual(fields.Check029, true); // Disabled
|
||
assert.deepStrictEqual(plan.creates[0].user, {}); // applyMergeのグループ/役職解決ヘルパーに安全に渡せる空オブジェクト
|
||
});
|
||
|
||
test("planMerge updates an existing Pleasanter-only master record matched by Class011 instead of creating a duplicate", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const pleasanterUsers = [{ UserId: 77, LoginId: "retired", Name: "退職 太郎改名", MailAddresses: ["tairo.taishoku@nexthd.jp"] }];
|
||
const existing = [{ ResultId: 50, ClassA: "", ClassB: "tairo.taishoku@nexthd.jp", Class011: "77", Check062: false }];
|
||
|
||
const plan = planMerge(snapshot, existing, pleasanterUsers);
|
||
|
||
assert.strictEqual(plan.creates.length, 0);
|
||
assert.strictEqual(plan.updates.length, 1);
|
||
assert.strictEqual(plan.updates[0].itemId, 50);
|
||
assert.strictEqual(plan.updates[0].fields.Class013, "退職 太郎改名");
|
||
});
|
||
|
||
test("planMerge does not let multiple Pleasanter UserIds sharing the same email local-part steal each other's master record (regression: kenichiro.nogi had 4 duplicate accounts)", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const pleasanterUsers = [
|
||
{ UserId: 266, LoginId: "user0001", Name: "A", MailAddresses: ["kenichiro.nogi@nexthd.jp"] },
|
||
{ UserId: 493, LoginId: "user0002", Name: "B", MailAddresses: ["kenichiro.nogi@nexthd.jp"] },
|
||
];
|
||
// 既存: 266は既にマスタに存在(前回作成済み)。493はまだマスタに存在しない
|
||
const existing = [{ ResultId: 1354, ClassA: "", ClassB: "kenichiro.nogi@nexthd.jp", Class011: "266", Check062: false }];
|
||
|
||
const plan = planMerge(snapshot, existing, pleasanterUsers);
|
||
|
||
// 493は266のレコード(1354)を奪わず、独立した新規レコードとして作成されるべき
|
||
assert.strictEqual(plan.updates.length, 1);
|
||
assert.strictEqual(plan.updates[0].itemId, 1354);
|
||
assert.strictEqual(plan.updates[0].fields.Class011, "266");
|
||
assert.strictEqual(plan.creates.length, 1);
|
||
assert.strictEqual(plan.creates[0].fields.Class011, "493");
|
||
});
|
||
|
||
test("planMerge does not duplicate a user who exists in both LINEWORKS and Pleasanter (already handled by the main loop)", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser()], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const pleasanterUsers = [{ UserId: 99, LoginId: "taro", Name: "山田 太郎", MailAddresses: ["taro.yamada@next-hd.co.jp"] }];
|
||
|
||
const plan = planMerge(snapshot, [], pleasanterUsers);
|
||
|
||
assert.strictEqual(plan.creates.length, 1); // LINEWORKSユーザー分のみ。プリザンター専用ループでの二重作成なし
|
||
});
|
||
|
||
test("planMerge registers a Pleasanter-only user who has no MailAddresses at all (e.g. test/legacy/system accounts)", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const pleasanterUsers = [{ UserId: 461, LoginId: "test003", Name: "テストユーザ" }]; // MailAddressesキー自体が無い
|
||
|
||
const plan = planMerge(snapshot, [], pleasanterUsers);
|
||
|
||
assert.strictEqual(plan.creates.length, 1);
|
||
assert.strictEqual(plan.creates[0].fields.Class011, "461");
|
||
assert.strictEqual(plan.creates[0].fields.ClassB, undefined); // メールアドレスが無いので突合キーは書かない
|
||
assert.strictEqual(plan.creates[0].fields.Check062, false);
|
||
});
|
||
|
||
test("planMerge matches an existing Pleasanter-only master record by Class011 (Pleasanter UserId) when re-run", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const pleasanterUsers = [{ UserId: 461, LoginId: "test003", Name: "テストユーザ改名" }];
|
||
const existing = [{ ResultId: 60, ClassA: "", ClassB: "", Class011: "461", Check062: false }];
|
||
|
||
const plan = planMerge(snapshot, existing, pleasanterUsers);
|
||
|
||
assert.strictEqual(plan.creates.length, 0);
|
||
assert.strictEqual(plan.updates.length, 1);
|
||
assert.strictEqual(plan.updates[0].itemId, 60);
|
||
assert.strictEqual(plan.updates[0].fields.Class013, "テストユーザ改名");
|
||
});
|
||
|
||
test("planMerge resolves tab-2 via the existing record's already-linked Class011 (Pleasanter UserId) even when that user's email was cleared and no longer matches by local-part, without creating a duplicate", () => {
|
||
// 再現ケース(2026-08-11実機発覚): UserId=36は既にResultId=10のClass011として紐付け済みだったが、
|
||
// プリザンター管理画面でこのユーザーのメールアドレスを空にしたため、メールローカル部突合が
|
||
// 効かなくなり、needsReview行き(タブ2未更新)→2巡目のプリザンター専用ループでも
|
||
// LINEWORKS現役社員として除外され、結果マスタに重複レコードが作成される事故が起きた。
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser()], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const pleasanterUsers = [{ UserId: 36, LoginId: "ntkco12216", Name: "山田 太郎", MailAddresses: [] }];
|
||
const existing = [{ ResultId: 10, ClassA: "u1", ClassB: "taro.yamada@next-hd.co.jp", Check062: true, Class011: "36" }];
|
||
|
||
const plan = planMerge(snapshot, existing, pleasanterUsers);
|
||
|
||
assert.strictEqual(plan.needsReview.length, 0);
|
||
assert.strictEqual(plan.updates.length, 1);
|
||
assert.strictEqual(plan.updates[0].itemId, 10);
|
||
assert.strictEqual(plan.updates[0].fields.Class011, "36");
|
||
assert.strictEqual(plan.updates[0].fields.Class012, "ntkco12216");
|
||
assert.strictEqual(plan.creates.length, 0);
|
||
});
|
||
|
||
test("planMerge does not let a duplicate Pleasanter account (same local-part, not chosen by overrides) overwrite the active LINEWORKS employee's master record", () => {
|
||
// 再現ケース: 「u1」はLINEWORKS社員。プリザンター側に同じローカル部を持つアカウントが2つあり、
|
||
// overridesでUserId=36が採用済み(LINEWORKSループでmirroredPleasanterUserIdsに入る)。
|
||
// 採用されなかったUserId=266は「プリザンター専用ループ」に流れるが、既存マスタのLINEWORKS社員
|
||
// レコード(ClassA="u1"、ClassB="taro.yamada@next-hd.co.jp")を誤って上書きしてはならない
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser()], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const pleasanterUsers = [
|
||
{ UserId: 36, LoginId: "current", Name: "山田 太郎", MailAddresses: ["taro.yamada@next-hd.co.jp"] },
|
||
{ UserId: 266, LoginId: "legacy", Name: "山田 太郎(旧)", MailAddresses: ["taro.yamada@nexthd.jp"] },
|
||
];
|
||
// 既存マスタ: u1は既にLINEWORKS社員として在籍中(Check062=true)、UserId=36の情報が反映済み
|
||
const existing = [{ ResultId: 10, ClassA: "u1", ClassB: "taro.yamada@next-hd.co.jp", Check062: true, Class011: "36" }];
|
||
|
||
const plan = planMerge(snapshot, existing, pleasanterUsers, { "taro.yamada": 36 });
|
||
|
||
// UserId=266は既存のu1レコードを上書きせず、新規レコードとして作成されるべき
|
||
assert.strictEqual(plan.updates.length, 1);
|
||
assert.strictEqual(plan.updates[0].itemId, 10);
|
||
assert.strictEqual(plan.updates[0].fields.Check062, true); // LINEWORKS社員側はuserToFieldsがCheck062=trueを設定(在籍中)
|
||
assert.strictEqual(plan.updates[0].fields.Class011, "36");
|
||
|
||
assert.strictEqual(plan.creates.length, 1);
|
||
assert.strictEqual(plan.creates[0].fields.Class011, "266");
|
||
assert.strictEqual(plan.creates[0].fields.Check062, false);
|
||
});
|
||
|
||
test("planMerge explicitly clears Date007/Date060 with NULL_DATE when LINEWORKS no longer has a value but the master had one", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser({ birthday: null, hiredDate: null })], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const existing = [{ ResultId: 10, ClassA: "u1", ClassB: "taro.yamada@next-hd.co.jp", Check062: true, Date007: "1990-04-01T00:00:00", Date060: "2015-04-01T00:00:00" }];
|
||
|
||
const plan = planMerge(snapshot, existing);
|
||
|
||
assert.strictEqual(plan.updates[0].fields.Date007, "1899/12/31");
|
||
assert.strictEqual(plan.updates[0].fields.Date060, "1899/12/31");
|
||
});
|
||
|
||
test("planMerge does not send a clear-date value when the master date is already unset", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser({ birthday: null, hiredDate: null })], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const existing = [{ ResultId: 10, ClassA: "u1", ClassB: "taro.yamada@next-hd.co.jp", Check062: true, Date007: "1899-12-30T00:00:00", Date060: "" }];
|
||
|
||
const plan = planMerge(snapshot, existing);
|
||
|
||
assert.strictEqual(plan.updates[0].fields.Date007, undefined);
|
||
assert.strictEqual(plan.updates[0].fields.Date060, undefined);
|
||
});
|
||
|
||
test("planMerge matches an existing record by email local-part on first-time import", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser()], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const existing = [{ ResultId: 20, ClassA: "", ClassB: "taro.yamada@example.com", Check062: true }];
|
||
|
||
const plan = planMerge(snapshot, existing);
|
||
|
||
assert.strictEqual(plan.updates.length, 1);
|
||
assert.strictEqual(plan.updates[0].itemId, 20);
|
||
});
|
||
|
||
test("planMerge marks a previously-active record as retired when it disappears from the LINEWORKS snapshot", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const existing = [{ ResultId: 30, ClassA: "gone-user", ClassB: "gone@next-hd.co.jp", Check062: true }];
|
||
|
||
const plan = planMerge(snapshot, existing);
|
||
|
||
assert.strictEqual(plan.retirements.length, 1);
|
||
assert.strictEqual(plan.retirements[0].itemId, 30);
|
||
assert.strictEqual(plan.retirements[0].fields.Check062, false);
|
||
assert.strictEqual(plan.retirements[0].fields.ClassZ, "無"); // 2026-08-09修正: ClassZもCheck062と同時に更新する
|
||
});
|
||
|
||
test("planMerge does not re-retire a record that is already inactive", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const existing = [{ ResultId: 40, ClassA: "already-gone", Check062: false }];
|
||
|
||
const plan = planMerge(snapshot, existing);
|
||
|
||
assert.strictEqual(plan.retirements.length, 0);
|
||
});
|
||
|
||
test("i18nName falls back to empty strings when there is no en_US entry", () => {
|
||
assert.deepStrictEqual(i18nName({ i18nNames: [] }), { lastName: "", firstName: "" });
|
||
assert.deepStrictEqual(i18nName({ i18nNames: [{ language: "zh_CN", lastName: "山", firstName: "田" }] }), { lastName: "", firstName: "" });
|
||
});
|
||
|
||
test("orgUnitRefs extracts {orgUnitId, orgUnitName} for every org unit across all organizations (multi-membership supported)", () => {
|
||
const user = makeUser({
|
||
organizations: [{
|
||
levelName: "正社員",
|
||
orgUnits: [
|
||
{ orgUnitId: "ou-1", orgUnitName: "情報システム部", primary: true, positionName: "課長" },
|
||
{ orgUnitId: "ou-2", orgUnitName: "管理本部", primary: false, positionName: "課長" },
|
||
],
|
||
}],
|
||
});
|
||
|
||
assert.deepStrictEqual(orgUnitRefs(user), [
|
||
{ orgUnitId: "ou-1", orgUnitName: "情報システム部" },
|
||
{ orgUnitId: "ou-2", orgUnitName: "管理本部" },
|
||
]);
|
||
});
|
||
|
||
test("orgUnitRefs drops entries missing orgUnitId or orgUnitName", () => {
|
||
const user = makeUser({
|
||
organizations: [{ orgUnits: [{ orgUnitId: "ou-1", orgUnitName: "" }, { orgUnitId: "", orgUnitName: "名前だけ" }] }],
|
||
});
|
||
assert.deepStrictEqual(orgUnitRefs(user), []);
|
||
});
|
||
|
||
test("orgUnitsToText formats refs as semicolon-separated \"orgUnitId=orgUnitName\" pairs (same pattern as aliasEmails)", () => {
|
||
assert.strictEqual(
|
||
orgUnitsToText([{ orgUnitId: "ou-1", orgUnitName: "情報システム部" }, { orgUnitId: "ou-2", orgUnitName: "管理本部" }]),
|
||
"ou-1=情報システム部;ou-2=管理本部"
|
||
);
|
||
assert.strictEqual(orgUnitsToText([]), "");
|
||
});
|
||
|
||
test("applyMerge resolves position/level/userType (get-or-create) against the choice masters, without touching Pleasanter Groups", async () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser()], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const existing = [{ ResultId: 10, ClassA: "u1", ClassB: "taro.yamada@next-hd.co.jp", Check062: true }];
|
||
const plan = planMerge(snapshot, existing);
|
||
|
||
const calls = [];
|
||
const fetchImpl = async (url, opts) => {
|
||
const urlStr = String(url);
|
||
const body = opts.body ? JSON.parse(opts.body) : {};
|
||
calls.push({ url: urlStr, body });
|
||
if (urlStr.endsWith("/api/groups/get") || urlStr.endsWith("/api/groups/create")) {
|
||
throw new Error("Pleasanter Groups API must not be called any more (org-unit info now lives in Description062)");
|
||
}
|
||
if (urlStr.match(/\/api\/items\/3\/get$/)) {
|
||
return { ok: true, json: async () => ({ StatusCode: 200, Response: { Data: [{ ResultId: 100, Title: "課長" }], TotalCount: 1 } }) };
|
||
}
|
||
if (urlStr.match(/\/api\/items\/5\/get$/)) {
|
||
return { ok: true, json: async () => ({ StatusCode: 200, Response: { Data: [{ ResultId: 200, Title: "正社員" }], TotalCount: 1 } }) };
|
||
}
|
||
if (urlStr.match(/\/api\/items\/6\/get$/)) {
|
||
return { ok: true, json: async () => ({ StatusCode: 200, Response: { Data: [], TotalCount: 0 } }) }; // 利用権限タイプ「一般」は未登録→create
|
||
}
|
||
if (urlStr.match(/\/api\/items\/6\/create$/)) {
|
||
return { ok: true, json: async () => ({ Id: 300, StatusCode: 200 }) };
|
||
}
|
||
if (urlStr.match(/\/api\/items\/\d+\/update$/)) {
|
||
return { ok: true, json: async () => ({ Id: 10, StatusCode: 200 }) };
|
||
}
|
||
throw new Error(`unexpected fetch: ${urlStr}`);
|
||
};
|
||
|
||
const result = await applyMerge(plan, {
|
||
baseUrl: "https://example.test/", apiKey: "k", siteId: 1,
|
||
positionSiteId: 3, levelSiteId: 5, userTypeSiteId: 6, fetchImpl,
|
||
});
|
||
|
||
assert.strictEqual(result.updated, 1);
|
||
const updateCall = calls.find((c) => c.url.match(/\/api\/items\/10\/update$/));
|
||
// updateSiteItemはfieldsをHash形式(ClassHash等/DescriptionHash)へネストして送信する(pleasanterClient.js参照)
|
||
assert.strictEqual(updateCall.body.ClassHash.Class057, "100");
|
||
assert.strictEqual(updateCall.body.ClassHash.Class058, "200");
|
||
assert.strictEqual(updateCall.body.ClassHash.Class059, "300");
|
||
assert.strictEqual(updateCall.body.DescriptionHash.Description062, "ou-1=情報システム部");
|
||
});
|
||
|
||
test("ensureChoiceMasterId reuses a cached ResultId and creates a new choice-master record when the title is missing", async () => {
|
||
const titleToId = new Map([["課長", 100]]);
|
||
const createCalls = [];
|
||
const fetchImpl = async (url, opts) => {
|
||
createCalls.push(JSON.parse(opts.body).Title);
|
||
return { ok: true, json: async () => ({ Id: 300, StatusCode: 200 }) };
|
||
};
|
||
|
||
const cachedId = await ensureChoiceMasterId("課長", titleToId, { baseUrl: "https://example.test/", apiKey: "k", siteId: 3, fetchImpl });
|
||
const createdId = await ensureChoiceMasterId("新役職", titleToId, { baseUrl: "https://example.test/", apiKey: "k", siteId: 3, fetchImpl });
|
||
const emptyId = await ensureChoiceMasterId("", titleToId, { baseUrl: "https://example.test/", apiKey: "k", siteId: 3, fetchImpl });
|
||
|
||
assert.strictEqual(cachedId, "100");
|
||
assert.strictEqual(createdId, "300");
|
||
assert.strictEqual(emptyId, "");
|
||
assert.deepStrictEqual(createCalls, ["新役職"]); // キャッシュ済みの「課長」は作成しない
|
||
assert.strictEqual(titleToId.get("新役職"), 300); // キャッシュに追加される
|
||
});
|
||
|
||
test("planMerge sets ClassZ=有 (LINEWORKS exists) always, and ClassY (Pleasanter exists) based on whether a Pleasanter account was mirrored", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser()], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
|
||
const withPleasanterAccount = planMerge(snapshot, [], [{ UserId: 99, LoginId: "taro", Name: "山田 太郎", MailAddresses: ["taro.yamada@next-hd.co.jp"] }]);
|
||
assert.strictEqual(withPleasanterAccount.creates[0].fields.ClassZ, "有");
|
||
assert.strictEqual(withPleasanterAccount.creates[0].fields.ClassY, "有");
|
||
|
||
const withoutPleasanterAccount = planMerge(snapshot, [], []);
|
||
assert.strictEqual(withoutPleasanterAccount.creates[0].fields.ClassZ, "有");
|
||
assert.strictEqual(withoutPleasanterAccount.creates[0].fields.ClassY, "無");
|
||
});
|
||
|
||
test("planMerge sets ClassZ=無 (LINEWORKS does not exist) and ClassY=有 for a Pleasanter-only user", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const pleasanterUsers = [{ UserId: 77, LoginId: "retired", Name: "退職 太郎", MailAddresses: ["tairo.taishoku@nexthd.jp"] }];
|
||
|
||
const plan = planMerge(snapshot, [], pleasanterUsers);
|
||
|
||
assert.strictEqual(plan.creates[0].fields.ClassZ, "無");
|
||
assert.strictEqual(plan.creates[0].fields.ClassY, "有");
|
||
});
|
||
|
||
test("planMerge always overrides Class013 (name) with lastName + halfwidth-space + firstName for a LINEWORKS employee, ignoring the Pleasanter Name value", () => {
|
||
const snapshot = { fetchedAt: "2026-08-08T00:00:00.000Z", users: [makeUser()], orgUnits: [], positions: [], levels: [], userTypes: [] };
|
||
const pleasanterUsers = [{ UserId: 99, LoginId: "taro", Name: "プリザンター側の別名", MailAddresses: ["taro.yamada@next-hd.co.jp"] }];
|
||
|
||
const withPleasanterAccount = planMerge(snapshot, [], pleasanterUsers);
|
||
assert.strictEqual(withPleasanterAccount.creates[0].fields.Class013, "山田 太郎");
|
||
|
||
const withoutPleasanterAccount = planMerge(snapshot, [], []);
|
||
assert.strictEqual(withoutPleasanterAccount.creates[0].fields.Class013, "山田 太郎");
|
||
});
|