mirror of
https://github.com/Sea-Haven-Industries/seahaven-door-unlock-api.git
synced 2026-09-30 22:13:13 +00:00
Some checks failed
Deploy / deploy (push) Has been cancelled
* feat(3cx): sync office department BLFs via XAPI Keep unlock and lockdown keys in the templates and write colleague plus shared-parking BLFs per extension so phones skip their own line. * fix(3cx): preserve parking BLF IDs and fail the job on PATCH errors
210 lines
7.8 KiB
TypeScript
210 lines
7.8 KiB
TypeScript
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("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);
|
|
});
|
|
|
|
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);
|
|
});
|