@dszp/netsapiens-lib 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -11
- package/dist/attribution.d.ts.map +1 -1
- package/dist/attribution.js +65 -28
- package/dist/attribution.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts +190 -6
- package/dist/inventory.d.ts.map +1 -1
- package/dist/inventory.js +206 -12
- package/dist/inventory.js.map +1 -1
- package/dist/model.d.ts +13 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/nsClient.d.ts +8 -2
- package/dist/nsClient.d.ts.map +1 -1
- package/dist/nsClient.js +6 -1
- package/dist/nsClient.js.map +1 -1
- package/dist/resolver.d.ts +18 -0
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +40 -19
- package/dist/resolver.js.map +1 -1
- package/package.json +1 -1
- package/src/attribution.selftest.ts +75 -0
- package/src/attribution.ts +60 -18
- package/src/index.ts +1 -1
- package/src/inventory.selftest.ts +279 -5
- package/src/inventory.ts +356 -19
- package/src/model.ts +13 -0
- package/src/nsClient.ts +14 -3
- package/src/resolver.selftest.ts +22 -1
- package/src/resolver.ts +42 -19
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Offline test for the domain inventory counter. pnpm test:inventory */
|
|
2
|
-
import { countDomainInventory, listDomainInventory, itemsFor, itemLabel, destinationOf, usersByExt } from './inventory.js';
|
|
2
|
+
import { countDomainInventory, emergencyDigits, legacyEmergencyNumber, listDomainInventory, itemsFor, itemLabel, destinationOf, resolveEmergency, usersByExt, DEFAULT_DEVICE_SUFFIXES } from './inventory.js';
|
|
3
3
|
import type { Rec, Snapshot } from './model.js';
|
|
4
4
|
|
|
5
5
|
let pass = 0, fail = 0;
|
|
@@ -98,9 +98,9 @@ ok(sysDev.devices.total === 0, 'a system user device is not counted');
|
|
|
98
98
|
const e101 = d.extensions.find((x) => x.ext === '101')!;
|
|
99
99
|
ok(
|
|
100
100
|
JSON.stringify(e101.devices) === JSON.stringify([
|
|
101
|
-
{ name: '101a', model: 'Yealink T54W', teams: false },
|
|
102
|
-
{ name: '101b', model: 'Yealink T31P', teams: false },
|
|
103
|
-
{ name: '101c', model: '(unknown)', teams: false },
|
|
101
|
+
{ name: '101a', model: 'Yealink T54W', teams: false, suffix: 'a', kind: '' },
|
|
102
|
+
{ name: '101b', model: 'Yealink T31P', teams: false, suffix: 'b', kind: '' },
|
|
103
|
+
{ name: '101c', model: '(unknown)', teams: false, suffix: 'c', kind: '' },
|
|
104
104
|
]),
|
|
105
105
|
'[list] devices lists every device with its name, model and teams flag, in order',
|
|
106
106
|
);
|
|
@@ -108,7 +108,7 @@ ok(sysDev.devices.total === 0, 'a system user device is not counted');
|
|
|
108
108
|
ok(e103.teams === true, '[list] a device whose aor local part is <ext>t marks the extension Teams-connected');
|
|
109
109
|
ok(e103.deviceCount === 0 && e103.deviceModels.length === 0, '[list] and that connector is not counted as a device');
|
|
110
110
|
ok(
|
|
111
|
-
JSON.stringify(e103.devices) === JSON.stringify([{ name: '103t', model: '', teams: true }]),
|
|
111
|
+
JSON.stringify(e103.devices) === JSON.stringify([{ name: '103t', model: '', teams: true, suffix: 't', kind: 'Teams' }]),
|
|
112
112
|
'[list] devices includes the Teams connector with an empty model, even though deviceCount excludes it',
|
|
113
113
|
);
|
|
114
114
|
ok(d.extensions.find((x) => x.ext === '104')!.devices.length === 0, '[list] no devices means an empty devices list, not a throw');
|
|
@@ -386,5 +386,279 @@ ok(sysDev.devices.total === 0, 'a system user device is not counted');
|
|
|
386
386
|
);
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
+
// ── the device-suffix legend ────────────────────────────────────────────────────────────────────────
|
|
390
|
+
// A device's SUFFIX is what its name carries after the extension number, and the legend says what that
|
|
391
|
+
// suffix IS. The default is the three NetSapiens ships; a supplied legend replaces it wholesale, which
|
|
392
|
+
// is how a deployment without TeamMate turns Teams detection off and how one with its own app names it.
|
|
393
|
+
{
|
|
394
|
+
const sufSnap = (devices: Rec[]): Snapshot => ({
|
|
395
|
+
meta: { domain: 'suffix.example' },
|
|
396
|
+
users: [{ user: '1001', 'user-scope': 'Basic User', 'service-code': '' }],
|
|
397
|
+
devicesByUser: { '1001': devices },
|
|
398
|
+
} as Snapshot);
|
|
399
|
+
const dev = (name: string, model = ''): Rec => ({ device: `sip:${name}@suffix.example`, 'device-models-model': model });
|
|
400
|
+
const one = (snapshot: Snapshot, opts?: Parameters<typeof listDomainInventory>[1]) =>
|
|
401
|
+
listDomainInventory(snapshot, opts).extensions[0]!;
|
|
402
|
+
|
|
403
|
+
// The default legend, one device at a time.
|
|
404
|
+
{
|
|
405
|
+
const x = one(sufSnap([dev('1001wp'), dev('1001m'), dev('1001t'), dev('1001b'), dev('1001', 'Yealink T54W')]));
|
|
406
|
+
const by = (name: string) => x.devices.find((d) => d.name === name)!;
|
|
407
|
+
ok(by('1001wp').suffix === 'wp' && by('1001wp').kind === 'SNAPmobile Web', '[suffix] wp is SNAPmobile Web');
|
|
408
|
+
ok(by('1001m').suffix === 'm' && by('1001m').kind === 'SNAPmobile', '[suffix] m is SNAPmobile');
|
|
409
|
+
ok(by('1001t').suffix === 't' && by('1001t').kind === 'Teams', '[suffix] t is Teams');
|
|
410
|
+
ok(by('1001t').teams === true, '[suffix] and the t entry carries teams: true');
|
|
411
|
+
ok(by('1001b').suffix === 'b' && by('1001b').kind === '', '[suffix] a suffix the legend does not carry has no kind, rather than a guessed one');
|
|
412
|
+
ok(by('1001b').teams === false, '[suffix] and it is a handset');
|
|
413
|
+
ok(by('1001').suffix === '' && by('1001').kind === '', '[suffix] a bare extension name has no suffix and no kind');
|
|
414
|
+
ok(x.teams === true, '[suffix] the extension is Teams-connected');
|
|
415
|
+
ok(x.deviceCount === 4 && x.devices.length === 5, '[suffix] the connector is in the display list and out of the count');
|
|
416
|
+
ok(x.anyDevice === true, '[suffix] anyDevice is unchanged');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// A legend WITHOUT `t` — a deployment with no TeamMate. Teams detection is off entirely, so the
|
|
420
|
+
// device that used to be a connector is a handset like any other and is counted as one.
|
|
421
|
+
{
|
|
422
|
+
const x = one(sufSnap([dev('1001t', 'Yealink T54W')]), { deviceSuffixes: { wp: { label: 'SNAPmobile Web' } } });
|
|
423
|
+
ok(x.teams === false, '[suffix] a legend without t means no Teams connectors at all');
|
|
424
|
+
ok(x.devices[0]!.teams === false && x.devices[0]!.kind === '', '[suffix] that device is a plain handset with no kind');
|
|
425
|
+
ok(x.deviceCount === 1 && x.deviceModels[0] === 'Yealink T54W', '[suffix] and it is counted, model and all');
|
|
426
|
+
ok(x.devices[0]!.model === 'Yealink T54W', '[suffix] a handset keeps its model — the blank is only for a connector');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// A supplied legend REPLACES the default rather than merging into it.
|
|
430
|
+
{
|
|
431
|
+
const x = one(sufSnap([dev('1001r'), dev('1001wp')]), { deviceSuffixes: { r: { label: 'Acme App' } } });
|
|
432
|
+
ok(x.devices.find((d) => d.name === '1001r')!.kind === 'Acme App', '[suffix] a supplied suffix is labelled from the supplied legend');
|
|
433
|
+
ok(x.devices.find((d) => d.name === '1001wp')!.kind === '', '[suffix] and wp is unknown again, because the supplied legend replaced the default wholesale');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Case-insensitive on both sides of the comparison.
|
|
437
|
+
{
|
|
438
|
+
const x = one(sufSnap([dev('1001WP'), dev('1001T')]));
|
|
439
|
+
ok(x.devices[0]!.suffix === 'wp' && x.devices[0]!.kind === 'SNAPmobile Web', '[suffix] an upper-case device suffix matches the legend');
|
|
440
|
+
ok(x.devices[1]!.teams === true, '[suffix] including the Teams test');
|
|
441
|
+
const y = one(sufSnap([dev('1001r')]), { deviceSuffixes: { R: { label: 'Acme App' } } });
|
|
442
|
+
ok(y.devices[0]!.kind === 'Acme App', '[suffix] and an upper-case LEGEND key matches a lower-case device suffix');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// A name that does not start with the extension has no suffix — `sales1` is a differently-named
|
|
446
|
+
// device, not a device of kind `sales1`.
|
|
447
|
+
{
|
|
448
|
+
const x = one(sufSnap([dev('sales1')]));
|
|
449
|
+
ok(x.devices[0]!.suffix === '' && x.devices[0]!.kind === '', '[suffix] a name that does not start with the extension has no suffix');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// A user with NO extension number. The suffix is the whole of the Teams test now, and a suffix needs
|
|
453
|
+
// an extension to come after — so a device NAMED a bare `t` is a handset, counted as one.
|
|
454
|
+
{
|
|
455
|
+
const x = listDomainInventory({
|
|
456
|
+
meta: { domain: 'suffix.example' },
|
|
457
|
+
users: [{ user: '', 'user-scope': 'Basic User', 'service-code': '' }],
|
|
458
|
+
devicesByUser: { '': [dev('t', 'Yealink T54W')] },
|
|
459
|
+
} as Snapshot).extensions[0]!;
|
|
460
|
+
ok(x.devices[0]!.suffix === '' && x.devices[0]!.kind === '', '[suffix] a device named a bare t on a blank extension has no suffix');
|
|
461
|
+
ok(x.teams === false && x.devices[0]!.teams === false, '[suffix] so it is not a Teams connector');
|
|
462
|
+
ok(x.deviceCount === 1 && x.deviceModels[0] === 'Yealink T54W', '[suffix] and it is counted as the handset it reads as');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// The legend is keyed by a device-name suffix off a snapshot, so an inherited Object name is a
|
|
466
|
+
// reachable key. On a plain object `legend['constructor']` answers a function rather than undefined.
|
|
467
|
+
for (const evil of ['constructor', 'toString', 'hasOwnProperty']) {
|
|
468
|
+
const x = one(sufSnap([dev(`1001${evil}`)]));
|
|
469
|
+
ok(x.devices[0]!.kind === '' && x.devices[0]!.teams === false,
|
|
470
|
+
`[suffix] a suffix named ${evil} reads as unknown rather than picking up an inherited value`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
ok(DEFAULT_DEVICE_SUFFIXES.t!.teams === true && DEFAULT_DEVICE_SUFFIXES.wp!.teams === undefined,
|
|
474
|
+
'[suffix] the exported default marks only t as the Teams connector');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ── E911: endpoints bill, addresses locate, legacy numbers are derived ──────────────────────────────
|
|
478
|
+
{
|
|
479
|
+
// One domain default address, one endpoint bound to it by name, and a second endpoint that nothing
|
|
480
|
+
// defaults to. `emergency-address-id` on an ENDPOINT is the callback number; on an ADDRESS it is the
|
|
481
|
+
// `a-…` id — the same field name meaning two things is the trap this fixture pins down.
|
|
482
|
+
const e911: Snapshot = {
|
|
483
|
+
meta: { domain: 'acme.example' },
|
|
484
|
+
users: [
|
|
485
|
+
{ user: '100', 'service-code': '', site: 'North', 'emergency-address-id': 'a-1', 'caller-id-number-emergency': '3175550100' },
|
|
486
|
+
// Eleven digits where the endpoint says ten — the same endpoint, and the point of `emergencyDigits`.
|
|
487
|
+
{ user: '101', 'service-code': '', site: 'North', 'emergency-address-id': 'a-2', 'caller-id-number-emergency': '13175550101' },
|
|
488
|
+
// Both fields blank: inherits the domain default address AND its endpoint. Not legacy.
|
|
489
|
+
{ user: '102', 'service-code': '', site: 'South' },
|
|
490
|
+
// The wildcard is "not set", not a value.
|
|
491
|
+
{ user: '103', 'service-code': '', site: 'South', 'caller-id-number-emergency': '[*]' },
|
|
492
|
+
{ user: '700', 'service-code': 'system-aa', 'caller-id-number-emergency': '3175559999' },
|
|
493
|
+
],
|
|
494
|
+
addresses: [
|
|
495
|
+
{ 'emergency-address-id': 'a-1', 'address-name': 'HQ', 'address-line-1': '1 Main St', 'address-city': 'Springfield', domain_default: true },
|
|
496
|
+
{ 'emergency-address-id': 'a-2', 'address-name': 'Annex', 'address-line-1': '2 Side St', 'address-city': 'Springfield' },
|
|
497
|
+
],
|
|
498
|
+
addressEndpoints: [
|
|
499
|
+
{ 'emergency-address-id': '3175550100', 'address-name': 'HQ', 'caller-name': 'Acme HQ', 'address-line-1': '1 Main St', 'address-city': 'Springfield', 'count-users-configured': 3, sub_count_total: 9 },
|
|
500
|
+
{ 'emergency-address-id': '3175550101', 'address-name': 'Annex', 'caller-name': '', 'address-line-1': '', 'address-city': '' },
|
|
501
|
+
],
|
|
502
|
+
} as Snapshot;
|
|
503
|
+
|
|
504
|
+
const inv = countDomainInventory(e911);
|
|
505
|
+
const d = listDomainInventory(e911);
|
|
506
|
+
ok(inv.e911Endpoints === 2, '[e911] two provisioned endpoints');
|
|
507
|
+
ok(inv.e911Addresses === 2, '[e911] and the two addresses are still counted, as information');
|
|
508
|
+
ok(inv.e911Legacy === 0, '[e911] a domain on the endpoint model has no legacy numbers');
|
|
509
|
+
|
|
510
|
+
const hq = d.e911Endpoints[0]!;
|
|
511
|
+
ok(hq.key === 'e911:3175550100', '[e911] the key is the callback, taken from the endpoint’s emergency-address-id');
|
|
512
|
+
ok(hq.callback === '3175550100' && hq.callerName === 'Acme HQ', '[e911] the callback and the caller name come off the record');
|
|
513
|
+
ok(hq.billingAddress === '1 Main St, Springfield', '[e911] the billing address is one line, no wider than the address list');
|
|
514
|
+
ok(hq.users === 3, '[e911] users is count-users-configured, not sub_count_total');
|
|
515
|
+
ok(itemLabel(hq) === '3175550100 — Acme HQ, 1 Main St, Springfield', '[e911] the label names the number, then who and where');
|
|
516
|
+
ok(itemLabel(d.e911Endpoints[1]!) === '3175550101', '[e911] and an endpoint with neither is just its number — no dangling dash');
|
|
517
|
+
{
|
|
518
|
+
// NO CALLBACK. The label is what `applyAssignment` writes into acceptance history, so a leading
|
|
519
|
+
// dash is bad and an empty string is worse — it records a decision about a thing it cannot name.
|
|
520
|
+
const [named, blank] = listDomainInventory({ ...e911, addressEndpoints: [
|
|
521
|
+
{ 'caller-name': 'Acme Dock', 'address-line-1': '9 Dock Rd', 'address-city': 'Springfield' },
|
|
522
|
+
{ 'address-name': 'Nothing' },
|
|
523
|
+
] } as Snapshot).e911Endpoints;
|
|
524
|
+
ok(itemLabel(named!) === 'Acme Dock, 9 Dock Rd, Springfield', '[e911] an endpoint with no callback leads with what it does have, never a dash');
|
|
525
|
+
ok(itemLabel(blank!) === `(endpoint ${blank!.key.slice('e911:'.length)})`,
|
|
526
|
+
'[e911] and one with nothing at all is named by its derived key - never the empty string');
|
|
527
|
+
ok(itemLabel(blank!) !== '', '[e911] which is the fact that matters: an acceptance row can always name its item');
|
|
528
|
+
}
|
|
529
|
+
ok(itemsFor(d, 'e911Endpoints')!.length === 2, '[e911] itemsFor answers the endpoints path');
|
|
530
|
+
ok(itemsFor(d, 'e911Addresses')!.length === 2, '[e911] and the addresses path still answers separately');
|
|
531
|
+
|
|
532
|
+
const em = resolveEmergency(e911);
|
|
533
|
+
ok(em.defaultAddressId === 'a-1', '[e911] the domain default is the address flagged domain_default');
|
|
534
|
+
ok(em.defaultCallback === '3175550100', '[e911] whose callback is joined through the endpoint naming the same address');
|
|
535
|
+
ok(em.addressIdFor(e911.users![2]!) === 'a-1', '[e911] a user with a blank address id inherits the domain default');
|
|
536
|
+
ok(em.addressIdFor(e911.users![1]!) === 'a-2', '[e911] and one that sets its own keeps it');
|
|
537
|
+
ok(em.callbackFor(e911.users![2]!) === '3175550100', '[e911] a user with a blank caller ID inherits the default address’s callback');
|
|
538
|
+
ok(em.callbackFor(e911.users![1]!) === '3175550101', '[e911] an 11-digit caller ID resolves to the 10-digit endpoint');
|
|
539
|
+
ok(em.setCallbackFor(e911.users![3]!) === '', '[e911] the [*] wildcard is not set');
|
|
540
|
+
ok(em.callbackFor(e911.users![3]!) === '3175550100', '[e911] so that user inherits the default too');
|
|
541
|
+
|
|
542
|
+
// An endpoint whose record names no callback still has to be a distinguishable row.
|
|
543
|
+
const blank = listDomainInventory({ ...e911, addressEndpoints: [{ 'address-name': 'Dock', 'caller-name': 'Acme Dock' }] } as Snapshot).e911Endpoints[0]!;
|
|
544
|
+
ok(blank.key.startsWith('e911:~'), '[e911] an endpoint with no callback falls back to a derived key');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
{
|
|
548
|
+
// A LEGACY domain: no endpoints at all, every user with a blank emergency-address-id and one of two
|
|
549
|
+
// numbers set by hand. Measured shape — a 100-user domain with exactly two such numbers.
|
|
550
|
+
const legacy: Snapshot = {
|
|
551
|
+
meta: { domain: 'demo.12345.service' },
|
|
552
|
+
users: [
|
|
553
|
+
{ user: '100', 'service-code': '', site: 'North', 'caller-id-number-emergency': '3175550200' },
|
|
554
|
+
{ user: '101', 'service-code': '', site: 'North', 'caller-id-number-emergency': '13175550200' },
|
|
555
|
+
{ user: '102', 'service-code': '', site: 'South', 'caller-id-number-emergency': '3175550201' },
|
|
556
|
+
// Nothing set anywhere and no domain default to inherit: not legacy, and not anything else.
|
|
557
|
+
{ user: '103', 'service-code': '', site: 'South' },
|
|
558
|
+
// On its device rather than on the user.
|
|
559
|
+
{ user: '104', 'service-code': '', site: 'South' },
|
|
560
|
+
// A system user is not a seat and does not make a number legacy.
|
|
561
|
+
{ user: '700', 'service-code': 'system-queue', 'caller-id-number-emergency': '3175550299' },
|
|
562
|
+
],
|
|
563
|
+
devicesByUser: { '104': [{ device: 'sip:104@demo.12345.service', 'caller-id-number-emergency': '3175550201' }] },
|
|
564
|
+
// READ, and there are none — which is what a legacy domain looks like. An ABSENT list means the
|
|
565
|
+
// fetch never asked, and then no legacy count is derivable at all; that pair is tested below.
|
|
566
|
+
addressEndpoints: [],
|
|
567
|
+
} as Snapshot;
|
|
568
|
+
|
|
569
|
+
const inv = countDomainInventory(legacy);
|
|
570
|
+
const d = listDomainInventory(legacy);
|
|
571
|
+
ok(inv.e911Legacy === 2, '[legacy] two DISTINCT numbers across five seats, matching the carrier’s two lines');
|
|
572
|
+
ok(inv.e911Endpoints === 0 && inv.e911Addresses === 0, '[legacy] and no endpoints or addresses to count');
|
|
573
|
+
ok(d.e911Legacy.map((x) => x.key).join() === 'e911legacy:3175550200,e911legacy:3175550201', '[legacy] keyed by the digits');
|
|
574
|
+
ok(d.e911Legacy[0]!.users === 2, '[legacy] the 10- and 11-digit spellings are one number');
|
|
575
|
+
ok(d.e911Legacy[1]!.users === 2, '[legacy] and a device’s number counts when the user sets none');
|
|
576
|
+
ok(itemLabel(d.e911Legacy[0]!) === '3175550200 — legacy E911 (2 users)', '[legacy] the label says why a bare number is on an E911 row');
|
|
577
|
+
ok(itemLabel({ key: 'e911legacy:3175550299', number: '3175550299', users: 1 }) === '3175550299 — legacy E911 (1 user)',
|
|
578
|
+
'[legacy] and it agrees with itself on one — a label that reads "(1 users)" reads as a rendering fault');
|
|
579
|
+
ok(itemsFor(d, 'e911Legacy')!.length === 2, '[legacy] itemsFor answers the legacy path');
|
|
580
|
+
|
|
581
|
+
// HALF-MIGRATED: one of the two numbers is now a provisioned endpoint. It must be counted once, as an
|
|
582
|
+
// endpoint, or the domain pays for the same place twice.
|
|
583
|
+
const half = { ...legacy, addressEndpoints: [{ 'emergency-address-id': '3175550200', 'address-name': 'HQ', 'caller-name': 'Demo HQ' }] } as Snapshot;
|
|
584
|
+
const hi = countDomainInventory(half);
|
|
585
|
+
ok(hi.e911Endpoints === 1 && hi.e911Legacy === 1, '[legacy] a number that became an endpoint leaves the legacy count');
|
|
586
|
+
ok(listDomainInventory(half).e911Legacy[0]!.number === '3175550201', '[legacy] and the one still on the old model stays');
|
|
587
|
+
|
|
588
|
+
// A user with BOTH fields blank on a domain that HAS a default is on the new model, not the old one.
|
|
589
|
+
const withDefault = {
|
|
590
|
+
...legacy,
|
|
591
|
+
users: [{ user: '110', 'service-code': '', site: 'North' }],
|
|
592
|
+
devicesByUser: {},
|
|
593
|
+
addresses: [{ 'emergency-address-id': 'a-9', 'address-name': 'HQ', domain_default: true }],
|
|
594
|
+
addressEndpoints: [{ 'emergency-address-id': '3175550300', 'address-name': 'HQ', 'caller-name': 'Demo HQ' }],
|
|
595
|
+
} as Snapshot;
|
|
596
|
+
ok(countDomainInventory(withDefault).e911Legacy === 0, '[legacy] both fields blank is the domain default, not a legacy number');
|
|
597
|
+
const em = resolveEmergency(withDefault);
|
|
598
|
+
ok(legacyEmergencyNumber(withDefault.users![0]!, em) === '', '[legacy] and the predicate says so directly');
|
|
599
|
+
|
|
600
|
+
// A user who SETS an emergency address is on the new model whatever their caller ID says — even when
|
|
601
|
+
// that number matches no endpoint this snapshot can see. Without the address-id clause of
|
|
602
|
+
// `legacyEmergencyNumber` this reads as a legacy line, and the account is billed for one.
|
|
603
|
+
const addressed = {
|
|
604
|
+
...legacy,
|
|
605
|
+
users: [{ user: '120', 'service-code': '', site: 'North', 'emergency-address-id': 'a-9', 'caller-id-number-emergency': '3175550777' }],
|
|
606
|
+
devicesByUser: {},
|
|
607
|
+
addresses: [{ 'emergency-address-id': 'a-9', 'address-name': 'HQ', domain_default: true }],
|
|
608
|
+
addressEndpoints: [{ 'emergency-address-id': '3175550300', 'address-name': 'HQ', 'caller-name': 'Demo HQ' }],
|
|
609
|
+
} as Snapshot;
|
|
610
|
+
ok(countDomainInventory(addressed).e911Legacy === 0,
|
|
611
|
+
'[legacy] a user with a SET emergency address is never legacy, whatever its caller ID matches');
|
|
612
|
+
ok(legacyEmergencyNumber(addressed.users![0]!, resolveEmergency(addressed)) === '',
|
|
613
|
+
'[legacy] and the predicate refuses it on the address id alone, not on the number');
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
{
|
|
617
|
+
// ── the USER's own caller ID wins over its devices' ────────────────────────────────────────────
|
|
618
|
+
// The device is consulted only where the user sets nothing. Reversing the two would attribute the
|
|
619
|
+
// seat to whichever handset happened to be first in the record, which is not what the portal does.
|
|
620
|
+
const both = {
|
|
621
|
+
meta: { domain: 'acme.example' },
|
|
622
|
+
users: [{ user: '100', 'service-code': '', 'caller-id-number-emergency': '3175550100' }],
|
|
623
|
+
devicesByUser: { '100': [{ device: 'sip:100@acme.example', 'caller-id-number-emergency': '3175550101' }] },
|
|
624
|
+
addressEndpoints: [],
|
|
625
|
+
} as Snapshot;
|
|
626
|
+
ok(resolveEmergency(both).setCallbackFor(both.users![0]!) === '3175550100',
|
|
627
|
+
'[e911] a user carrying its own caller ID wins over a device carrying a different one');
|
|
628
|
+
const deviceOnly = { ...both, users: [{ user: '100', 'service-code': '' }] } as Snapshot;
|
|
629
|
+
ok(resolveEmergency(deviceOnly).setCallbackFor(deviceOnly.users![0]!) === '3175550101',
|
|
630
|
+
'[e911] and the device is read only where the user sets nothing');
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
{
|
|
634
|
+
// ── the ENDPOINTS list was never READ ──────────────────────────────────────────────────────────
|
|
635
|
+
// `undefined` (the fetch never asked) and `[]` (asked, and the domain has none) are different facts,
|
|
636
|
+
// and only the second one can support a legacy count: without the endpoint list there is nothing to
|
|
637
|
+
// exclude against, so every emergency caller ID on a fully-migrated domain would read as a legacy
|
|
638
|
+
// line. An under-count is safe here; a confident over-count on a billing page is not.
|
|
639
|
+
const users = [
|
|
640
|
+
{ user: '100', 'service-code': '', site: 'North', 'emergency-address-id': '', 'caller-id-number-emergency': '3175550100' },
|
|
641
|
+
{ user: '101', 'service-code': '', site: 'North', 'emergency-address-id': '', 'caller-id-number-emergency': '3175550101' },
|
|
642
|
+
];
|
|
643
|
+
const never = { meta: { domain: 'acme.example' }, users } as Snapshot;
|
|
644
|
+
const read = { ...never, addressEndpoints: [] } as Snapshot;
|
|
645
|
+
const provisioned = { ...never, addressEndpoints: [
|
|
646
|
+
{ 'emergency-address-id': '3175550100', 'address-name': 'HQ', 'caller-name': 'Acme HQ' },
|
|
647
|
+
{ 'emergency-address-id': '3175550101', 'address-name': 'Annex', 'caller-name': 'Acme Annex' },
|
|
648
|
+
] } as Snapshot;
|
|
649
|
+
ok(countDomainInventory(never).e911Legacy === 0 && listDomainInventory(never).e911Legacy.length === 0,
|
|
650
|
+
'[legacy] no endpoint read at all answers 0 legacy numbers rather than inventing two');
|
|
651
|
+
ok(countDomainInventory(read).e911Legacy === 2,
|
|
652
|
+
'[legacy] while an endpoint list that was read and is EMPTY is a real legacy domain');
|
|
653
|
+
ok(countDomainInventory(provisioned).e911Legacy === 0 && countDomainInventory(provisioned).e911Endpoints === 2,
|
|
654
|
+
'[legacy] and the same two users on the endpoint model are two endpoints and no legacy numbers');
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
ok(emergencyDigits('+1 (317) 555-0100') === '3175550100' && emergencyDigits('13175550100') === '3175550100',
|
|
658
|
+
'[e911] a caller ID normalises to ten digits however it is punctuated');
|
|
659
|
+
ok(emergencyDigits('[*]') === '' && emergencyDigits('') === '' && emergencyDigits(undefined) === '',
|
|
660
|
+
'[e911] and the three ways of saying "not set" all answer empty');
|
|
661
|
+
|
|
662
|
+
|
|
389
663
|
console.log(`\n${pass} passed, ${fail} failed`);
|
|
390
664
|
if (fail) process.exit(1);
|