seahaven-door-unlock-api/lambda/blf-sync/blf-sync.test.ts

223 lines
8.1 KiB
TypeScript
Raw Normal View History

import assert from "node:assert/strict";
import { test } from "node:test";
import { blfsEqual, parseBlfs, serializeBlfs } from "./blf-xml";
import { addAlwaysIncludedColleagues, isEligibleColleague, primaryDepartmentName, splitPeerName } from "./department";
import { mergeDepartmentBlfs } from "./merge";
import { resolveTemplateId, SLOT_CONTRACT } from "./slot-contract";
const colleagues = [
{ id: 29, number: "100", firstName: "Adam", lastName: "Moussa" },
{ id: 33, number: "111", firstName: "Alyssa", lastName: "Ficarra" },
{ id: 38, number: "114", firstName: "Ashley", lastName: "Fedner" },
{ id: 66, number: "115", firstName: "Derrick", lastName: "Smith" },
{ id: 68, number: "113", firstName: "Sarah", lastName: "May" },
{ id: 69, number: "116", firstName: "Cindy", lastName: "Vallecillo" },
];
test("resolveTemplateId matches longest Sea Haven template first", () => {
assert.equal(
resolveTemplateId("yealinkT54W-door-unlock-with-sp.ph.xml"),
"t54w-door-unlock-with-sp"
);
assert.equal(
resolveTemplateId("yealinkT57W-door-unlock-with-sp.ph.xml"),
"t57w-door-unlock-with-sp"
);
assert.equal(resolveTemplateId("yealinkT54W-door-unlock.ph.xml"), "t54w-door-unlock");
assert.equal(resolveTemplateId("yealinkT58W-door-unlock.ph.xml"), "t58w-door-unlock");
assert.equal(resolveTemplateId("yealinkT54W.ph.xml"), undefined);
assert.equal(resolveTemplateId("yealinkT57W.ph.xml"), undefined);
});
test("T57W+SP slot contract matches T54W+SP", () => {
assert.deepEqual(
SLOT_CONTRACT["t57w-door-unlock-with-sp"],
SLOT_CONTRACT["t54w-door-unlock-with-sp"]
);
});
test("parseBlfs reads Line self-close and extension BLFs", () => {
const xml =
'<PhoneDevice><BLFS><BLF ID="-1" BLFNo="1" BLFType="Line" BLFTypeID="6" /><BLF ID="91" BLFNo="6" BLFType="BLF" BLFTypeID="0">112</BLF></BLFS></PhoneDevice>';
assert.deepEqual(parseBlfs(xml), [
{ id: "-1", blfNo: 1, blfType: "Line", blfTypeId: "6", value: "" },
{ id: "91", blfNo: 6, blfType: "BLF", blfTypeId: "0", value: "112" },
]);
});
test("merge skips self and sorts by extension", () => {
const result = mergeDepartmentBlfs({
currentXml: "<PhoneDevice><BLFS/></PhoneDevice>",
selfExtension: "114",
colleagues,
contract: SLOT_CONTRACT["t54w-door-unlock-with-sp"],
});
const managed = parseBlfs(result.xml).filter((e) => e.blfNo >= 6);
assert.deepEqual(
managed.map((e) => e.value),
["100", "111", "113", "115", "116"]
);
assert.equal(result.overflow, 0);
assert.equal(result.placed, 5);
assert.equal(managed.some((e) => e.value === "114"), false);
});
test("merge never writes reserved T54W+SP unlock key 2", () => {
const current =
'<PhoneDevice><BLFS><BLF ID="33" BLFNo="1" BLFType="BLF" BLFTypeID="0">111</BLF><BLF ID="91" BLFNo="2" BLFType="BLF" BLFTypeID="0">112</BLF><BLF ID="38" BLFNo="3" BLFType="BLF" BLFTypeID="0">114</BLF></BLFS></PhoneDevice>';
const result = mergeDepartmentBlfs({
currentXml: current,
selfExtension: "113",
colleagues,
contract: SLOT_CONTRACT["t54w-door-unlock-with-sp"],
});
const entries = parseBlfs(result.xml);
assert.equal(entries.some((e) => e.blfNo === 2), false);
assert.equal(entries.find((e) => e.blfNo === 1)?.blfType, "Line");
});
test("merge writes SP1-SP3 on T54W+SP keys 3-5", () => {
const result = mergeDepartmentBlfs({
currentXml: "<PhoneDevice><BLFS/></PhoneDevice>",
selfExtension: "111",
colleagues,
contract: SLOT_CONTRACT["t54w-door-unlock-with-sp"],
});
const parking = parseBlfs(result.xml).filter((e) => e.blfNo >= 3 && e.blfNo <= 5);
assert.deepEqual(
parking.map((e) => ({ blfNo: e.blfNo, type: e.blfType, value: e.value })),
[
{ blfNo: 3, type: "SharedParking", value: "SP1" },
{ blfNo: 4, type: "SharedParking", value: "SP2" },
{ blfNo: 5, type: "SharedParking", value: "SP3" },
]
);
});
test("merge preserves personal BLFs outside the managed range", () => {
const current =
'<PhoneDevice><BLFS><BLF ID="-1" BLFNo="1" BLFType="Line" BLFTypeID="6" /><BLF ID="9" BLFNo="16" BLFType="BLF" BLFTypeID="0">215</BLF></BLFS></PhoneDevice>';
const result = mergeDepartmentBlfs({
currentXml: current,
selfExtension: "116",
colleagues,
contract: SLOT_CONTRACT["t54w-door-unlock-with-sp"],
});
const personal = parseBlfs(result.xml).find((e) => e.blfNo === 16);
assert.equal(personal?.value, "215");
assert.equal(personal?.id, "9");
});
test("merge caps overflow at the managed slot count", () => {
const many = Array.from({ length: 12 }, (_, i) => ({
id: 200 + i,
number: String(300 + i),
firstName: "User",
lastName: `Z${String(i).padStart(2, "0")}`,
}));
const result = mergeDepartmentBlfs({
currentXml: "<PhoneDevice><BLFS/></PhoneDevice>",
selfExtension: "111",
colleagues: many,
contract: SLOT_CONTRACT["t54w-door-unlock"],
});
assert.equal(result.placed, 10);
assert.equal(result.overflow, 2);
const managed = parseBlfs(result.xml).filter((e) => e.blfNo >= 3 && e.blfNo <= 12);
assert.equal(managed.length, 10);
assert.equal(managed[0]?.blfNo, 3);
assert.equal(managed[9]?.blfNo, 12);
});
test("merge is a no-op when XML already matches", () => {
const first = mergeDepartmentBlfs({
currentXml: "<PhoneDevice><BLFS/></PhoneDevice>",
selfExtension: "116",
colleagues,
contract: SLOT_CONTRACT["t54w-door-unlock-with-sp"],
});
const second = mergeDepartmentBlfs({
currentXml: first.xml,
selfExtension: "116",
colleagues,
contract: SLOT_CONTRACT["t54w-door-unlock-with-sp"],
});
assert.equal(first.changed, true);
assert.equal(second.changed, false);
assert.equal(blfsEqual(first.xml, second.xml), true);
});
test("merge keeps assigned shared-parking IDs so later runs are a no-op", () => {
const first = mergeDepartmentBlfs({
currentXml: "<PhoneDevice><BLFS/></PhoneDevice>",
selfExtension: "116",
colleagues,
contract: SLOT_CONTRACT["t54w-door-unlock-with-sp"],
});
const assigned = serializeBlfs(
parseBlfs(first.xml).map((entry) =>
entry.blfType === "SharedParking" || entry.blfType === "Line"
? { ...entry, id: String(900 + entry.blfNo) }
: entry
)
);
const second = mergeDepartmentBlfs({
currentXml: assigned,
selfExtension: "116",
colleagues,
contract: SLOT_CONTRACT["t54w-door-unlock-with-sp"],
});
assert.equal(second.changed, false);
const parking = parseBlfs(second.xml).filter((e) => e.blfNo >= 3 && e.blfNo <= 5);
assert.deepEqual(
parking.map((e) => e.id),
["903", "904", "905"]
);
});
test("serializeBlfs normalizes empty lists", () => {
assert.equal(serializeBlfs([]), "<PhoneDevice><BLFS></BLFS></PhoneDevice>");
assert.equal(blfsEqual("<PhoneDevice><BLFS/></PhoneDevice>", serializeBlfs([])), true);
});
test("primaryDepartmentName prefers Office over DEFAULT", () => {
assert.equal(
primaryDepartmentName({
Id: 1,
Number: "111",
Groups: [{ Id: 28, Name: "DEFAULT" }, { Id: 142, Name: "Office" }],
}),
"Office"
);
assert.equal(
primaryDepartmentName({
Id: 2,
Number: "201",
Groups: [{ Id: 28, Name: "DEFAULT" }],
}),
undefined
);
});
test("addAlwaysIncludedColleagues injects ext 100 when missing", () => {
const with100 = addAlwaysIncludedColleagues(colleagues, [
{ id: 29, number: "100", firstName: "Adam", lastName: "Moussa" },
]);
assert.equal(with100.some((c) => c.number === "100"), true);
const again = addAlwaysIncludedColleagues(with100, [
{ id: 29, number: "100", firstName: "Adam", lastName: "Moussa" },
]);
assert.equal(again.filter((c) => c.number === "100").length, 1);
});
test("splitPeerName uses last token as last name", () => {
assert.deepEqual(splitPeerName("Adam Moussa"), { firstName: "Adam", lastName: "Moussa" });
});
test("isEligibleColleague skips queues and voicemail", () => {
assert.equal(isEligibleColleague({ Id: 1, Number: "111", FirstName: "A", LastName: "B" }), true);
assert.equal(isEligibleColleague({ Id: 2, Number: "801", FirstName: "After", LastName: "Hours" }), false);
assert.equal(isEligibleColleague({ Id: 3, Number: "201", FirstName: "Voicemail" }), false);
assert.equal(isEligibleColleague({ Id: 4, Number: "scheduler" }), false);
});