@dszp/netsapiens-lib 0.6.0 → 0.8.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 +69 -11
- 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 +102 -12
- package/dist/inventory.d.ts.map +1 -1
- package/dist/inventory.js +135 -23
- package/dist/inventory.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/index.ts +1 -1
- package/src/inventory.selftest.ts +226 -5
- package/src/inventory.ts +212 -35
- 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, listDomainInventory, itemsFor, itemLabel, destinationOf, 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;
|
|
@@ -60,6 +60,7 @@ ok(inv.transcriptionEnabled === 2, 'yes and a provider name both count; no and a
|
|
|
60
60
|
ok(inv.dids.total === 4, 'four phone numbers');
|
|
61
61
|
ok(inv.dids.tollFree === 2, '800 and 833 are toll-free');
|
|
62
62
|
ok(inv.dids.local === 2, 'and the rest are local');
|
|
63
|
+
ok(inv.dids.fax === 0 && inv.dids.all === 4, 'no fax-server hosts were supplied, so nothing is a fax line and all equals total');
|
|
63
64
|
ok(inv.e911Addresses === 2, 'two address records');
|
|
64
65
|
ok(inv.smsNumbers === 1, 'one SMS number');
|
|
65
66
|
ok(inv.devices.total === 5, 'five devices across all extensions');
|
|
@@ -71,6 +72,7 @@ ok(inv.devices.byModel['(unknown)'] === 1, 'a device with no model is counted un
|
|
|
71
72
|
const empty = countDomainInventory({ meta: { domain: 'empty.example' } } as Snapshot);
|
|
72
73
|
ok(empty.extensions.total === 0, 'empty snapshot: no extensions');
|
|
73
74
|
ok(empty.dids.total === 0 && empty.dids.tollFree === 0 && empty.dids.local === 0, 'empty snapshot: no numbers');
|
|
75
|
+
ok(empty.dids.fax === 0 && empty.dids.all === 0, 'empty snapshot: no fax lines either');
|
|
74
76
|
ok(empty.devices.total === 0, 'empty snapshot: no devices');
|
|
75
77
|
ok(empty.smsNumbers === 0 && empty.e911Addresses === 0, 'empty snapshot: no addresses and no SMS numbers');
|
|
76
78
|
|
|
@@ -96,9 +98,9 @@ ok(sysDev.devices.total === 0, 'a system user device is not counted');
|
|
|
96
98
|
const e101 = d.extensions.find((x) => x.ext === '101')!;
|
|
97
99
|
ok(
|
|
98
100
|
JSON.stringify(e101.devices) === JSON.stringify([
|
|
99
|
-
{ name: '101a', model: 'Yealink T54W', teams: false },
|
|
100
|
-
{ name: '101b', model: 'Yealink T31P', teams: false },
|
|
101
|
-
{ 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: '' },
|
|
102
104
|
]),
|
|
103
105
|
'[list] devices lists every device with its name, model and teams flag, in order',
|
|
104
106
|
);
|
|
@@ -106,7 +108,7 @@ ok(sysDev.devices.total === 0, 'a system user device is not counted');
|
|
|
106
108
|
ok(e103.teams === true, '[list] a device whose aor local part is <ext>t marks the extension Teams-connected');
|
|
107
109
|
ok(e103.deviceCount === 0 && e103.deviceModels.length === 0, '[list] and that connector is not counted as a device');
|
|
108
110
|
ok(
|
|
109
|
-
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' }]),
|
|
110
112
|
'[list] devices includes the Teams connector with an empty model, even though deviceCount excludes it',
|
|
111
113
|
);
|
|
112
114
|
ok(d.extensions.find((x) => x.ext === '104')!.devices.length === 0, '[list] no devices means an empty devices list, not a throw');
|
|
@@ -141,6 +143,7 @@ ok(sysDev.devices.total === 0, 'a system user device is not counted');
|
|
|
141
143
|
ok(c.extensions.withAnyDevice + c.extensions.withNoDevice === c.extensions.total, '[fold] the two presence leaves partition the total');
|
|
142
144
|
ok(c.dids.total === d.dids.length, '[fold] dids.total equals the number list length');
|
|
143
145
|
ok(c.dids.tollFree === d.dids.filter((n) => n.kind === 'tollFree').length, '[fold] dids.tollFree equals the toll-free items');
|
|
146
|
+
ok(c.dids.all === d.dids.length && c.dids.fax === d.dids.filter((n) => n.fax).length, '[fold] dids.all is the whole list and dids.fax the fax lines in it');
|
|
144
147
|
ok(c.e911Addresses === d.e911Addresses.length && c.smsNumbers === d.smsNumbers.length, '[fold] address and SMS counts equal the lists');
|
|
145
148
|
}
|
|
146
149
|
|
|
@@ -173,6 +176,129 @@ ok(sysDev.devices.total === 0, 'a system user device is not counted');
|
|
|
173
176
|
ok(itemLabel(d.smsNumbers[0]!) === '13175550100', '[label] SMS is its number');
|
|
174
177
|
}
|
|
175
178
|
|
|
179
|
+
// ── a device is named by `device`, and only falls back to `aor` ──────────────────────────────────────
|
|
180
|
+
// A LIVE /users/<ext>/devices record names the device in `device` and frequently carries no `aor` at all.
|
|
181
|
+
// Reading `aor` alone blanked every device name and — worse — broke the `<ext>t` Teams test, so a Teams
|
|
182
|
+
// connector read as a handset and inflated deviceCount, devices.total and byDeviceCount while
|
|
183
|
+
// teamsConnected read zero. That is the shape this block pins.
|
|
184
|
+
{
|
|
185
|
+
const live = {
|
|
186
|
+
meta: { domain: 'live.example' },
|
|
187
|
+
users: [
|
|
188
|
+
{ user: '1001', 'user-scope': 'Basic User', 'service-code': '' },
|
|
189
|
+
{ user: '1002', 'user-scope': 'Basic User', 'service-code': '' },
|
|
190
|
+
{ user: '1003', 'user-scope': 'Basic User', 'service-code': '' },
|
|
191
|
+
],
|
|
192
|
+
devicesByUser: {
|
|
193
|
+
// `device` only — the live shape.
|
|
194
|
+
'1001': [
|
|
195
|
+
{ device: 'sip:1001a@live.example', 'device-models-model': 'Yealink T54W' },
|
|
196
|
+
{ device: 'sip:1001t@live.example', 'device-models-model': 'Teams' },
|
|
197
|
+
],
|
|
198
|
+
// BOTH, disagreeing: `device` wins, being the field the system names the device by.
|
|
199
|
+
'1002': [{ device: 'sip:1002a@live.example', aor: 'sip:wrong@live.example', 'device-models-model': 'Yealink T31P' }],
|
|
200
|
+
// NEITHER: the name is empty rather than guessed, and an empty name is not a `t`.
|
|
201
|
+
'1003': [{ 'device-models-model': 'Yealink T31P' }],
|
|
202
|
+
},
|
|
203
|
+
} as Snapshot;
|
|
204
|
+
|
|
205
|
+
const d = listDomainInventory(live);
|
|
206
|
+
const e1 = d.extensions.find((x) => x.ext === '1001')!;
|
|
207
|
+
ok(e1.devices[0]!.name === '1001a', '[device] a record with `device` and no `aor` is still named');
|
|
208
|
+
ok(e1.teams === true, '[device] and its <ext>t connector is detected — reading `aor` alone made this false on every live domain');
|
|
209
|
+
ok(e1.devices[1]!.teams === true && e1.devices[1]!.model === '', '[device] the connector row is marked and prints no model');
|
|
210
|
+
ok(e1.deviceCount === 1 && JSON.stringify(e1.deviceModels) === JSON.stringify(['Yealink T54W']),
|
|
211
|
+
'[device] so the connector is excluded from the handset count rather than inflating it');
|
|
212
|
+
|
|
213
|
+
const e2 = d.extensions.find((x) => x.ext === '1002')!;
|
|
214
|
+
ok(e2.devices[0]!.name === '1002a', '[device] where a record carries both, `device` wins');
|
|
215
|
+
|
|
216
|
+
const e3 = d.extensions.find((x) => x.ext === '1003')!;
|
|
217
|
+
ok(e3.devices[0]!.name === '', '[device] a record with neither field has an empty name, not "undefined"');
|
|
218
|
+
ok(e3.teams === false && e3.deviceCount === 1, '[device] and an unnamed device is a handset, never a connector');
|
|
219
|
+
|
|
220
|
+
const c = countDomainInventory(live);
|
|
221
|
+
ok(c.teamsConnected === 1, '[device] teamsConnected counts the one connector');
|
|
222
|
+
ok(c.devices.total === 3, '[device] and devices.total is the three handsets, the connector excluded');
|
|
223
|
+
|
|
224
|
+
// The `aor`-only fixture at the top of this file still behaves exactly as it did — the fallback is a
|
|
225
|
+
// fallback, not a replacement.
|
|
226
|
+
ok(listDomainInventory(snap).extensions.find((x) => x.ext === '103')!.teams === true,
|
|
227
|
+
'[device] a record carrying only `aor` is unchanged');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── fax lines ────────────────────────────────────────────────────────────────────────────────────────
|
|
231
|
+
// The portal's "Fax Server" treatment is an ordinary phone number whose dial rule hands it to a host:
|
|
232
|
+
// to-connection, plus dial-rule-translation-destination-host. There is no fax endpoint in the API and
|
|
233
|
+
// the ATA is not a device on any user, so the host is the only thing that says "fax line" — and the host
|
|
234
|
+
// belongs to the deployment, not to this library, which is why it arrives as an option.
|
|
235
|
+
{
|
|
236
|
+
const faxSnap = {
|
|
237
|
+
meta: { domain: 'fax.example' },
|
|
238
|
+
users: [{ user: '100', 'user-scope': 'Basic User', 'service-code': '', 'name-first-name': 'Ann', 'name-last-name': 'Lee' }],
|
|
239
|
+
phonenumbers: [
|
|
240
|
+
{ phonenumber: '13175550100', 'dial-rule-translation-destination-user': '100' },
|
|
241
|
+
{
|
|
242
|
+
phonenumber: '13175550199',
|
|
243
|
+
'dial-rule-application': 'to-connection',
|
|
244
|
+
'dial-rule-translation-destination-host': '203.0.113.7',
|
|
245
|
+
'dial-rule-description': 'Portal Created: Phonenumber -> FaxServer',
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
} as Snapshot;
|
|
249
|
+
const HOSTS = { faxServerHosts: ['203.0.113.7'] };
|
|
250
|
+
|
|
251
|
+
const d = listDomainInventory(faxSnap, HOSTS);
|
|
252
|
+
const fx = d.dids[1]!;
|
|
253
|
+
ok(fx.fax === true, '[fax] a number whose destination host is the fax server is a fax line');
|
|
254
|
+
ok(d.dids[0]!.fax === false, '[fax] and the one routed to a user is not');
|
|
255
|
+
ok(fx.kind === 'local', '[fax] a fax line still has a kind — it is a local or toll-free number like any other');
|
|
256
|
+
ok(fx.destination === 'to fax server', '[fax] its destination reads "to fax server" — never the bare host');
|
|
257
|
+
ok(!fx.destination.includes('203.0.113.7'), '[fax] the fax server address does not reach a reader');
|
|
258
|
+
ok(fx.description === 'Portal Created: Phonenumber -> FaxServer', '[fax] the portal note is carried as written; it is not what the test reads');
|
|
259
|
+
|
|
260
|
+
const c = countDomainInventory(faxSnap, HOSTS);
|
|
261
|
+
ok(c.dids.fax === 1, '[fax] dids.fax counts it');
|
|
262
|
+
ok(c.dids.total === 1 && c.dids.local === 1 && c.dids.tollFree === 0, '[fax] and dids.total/local/tollFree leave it out — it is billed as a fax line, not as a DID');
|
|
263
|
+
ok(c.dids.all === 2, '[fax] dids.all is every number, fax lines included');
|
|
264
|
+
ok(c.dids.total + c.dids.fax === c.dids.all, '[fax] total + fax partitions all');
|
|
265
|
+
|
|
266
|
+
const keys = (p: string) => (itemsFor(d, p) ?? []).map((x) => x.key).join(',');
|
|
267
|
+
ok(keys('dids.fax') === 'did:13175550199', '[fax] itemsFor dids.fax is the fax lines');
|
|
268
|
+
ok(keys('dids.total') === 'did:13175550100', '[fax] itemsFor dids.total excludes them, exactly as the count does');
|
|
269
|
+
ok(keys('dids.local') === 'did:13175550100', '[fax] and so does dids.local');
|
|
270
|
+
ok(keys('dids.all') === 'did:13175550100,did:13175550199', '[fax] dids.all is everything');
|
|
271
|
+
ok(itemLabel(fx) === '13175550199', '[fax] the label is unchanged — a fax line is named by its number like any other');
|
|
272
|
+
|
|
273
|
+
// The SAME snapshot with no hosts supplied. A library that guessed a fax server would be wrong on
|
|
274
|
+
// every deployment but the one it was written against, so it guesses nothing.
|
|
275
|
+
const bare = listDomainInventory(faxSnap);
|
|
276
|
+
ok(bare.dids[1]!.fax === false, '[fax] no hosts supplied, nothing is a fax line');
|
|
277
|
+
ok(bare.dids[1]!.destination === 'to connection', '[fax] and the destination falls back to the application');
|
|
278
|
+
const bareCounts = countDomainInventory(faxSnap);
|
|
279
|
+
ok(bareCounts.dids.fax === 0 && bareCounts.dids.total === 2 && bareCounts.dids.local === 2,
|
|
280
|
+
'[fax] the fax line is counted as the local DID it otherwise looks like');
|
|
281
|
+
ok(bareCounts.dids.all === bareCounts.dids.total, '[fax] all equals total when nothing is a fax line');
|
|
282
|
+
ok(countDomainInventory(faxSnap, { faxServerHosts: [] }).dids.fax === 0, '[fax] an empty host list is the same as none');
|
|
283
|
+
ok(countDomainInventory(faxSnap, { faxServerHosts: ['', ' '] }).dids.fax === 0, '[fax] and so is a list of blanks — a blank host must not match a number with no host');
|
|
284
|
+
|
|
285
|
+
// Host matching is trimmed and case-insensitive on BOTH sides: a hostname is not case-sensitive, and
|
|
286
|
+
// a comma-separated setting arrives with spaces around its entries.
|
|
287
|
+
const named = {
|
|
288
|
+
meta: { domain: 'fax2.example' },
|
|
289
|
+
phonenumbers: [{ phonenumber: '13175550198', 'dial-rule-application': 'to-connection', 'dial-rule-translation-destination-host': ' Fax.Example.COM ' }],
|
|
290
|
+
} as Snapshot;
|
|
291
|
+
ok(countDomainInventory(named, { faxServerHosts: [' fax.example.com '] }).dids.fax === 1, '[fax] host matching trims and lower-cases both sides');
|
|
292
|
+
ok(countDomainInventory(named, { faxServerHosts: ['other.example.com'] }).dids.fax === 0, '[fax] a host that is not on the list is not a fax line');
|
|
293
|
+
|
|
294
|
+
// The description is a note the portal writes and an operator can edit, so it is never the test.
|
|
295
|
+
const noteOnly = {
|
|
296
|
+
meta: { domain: 'fax3.example' },
|
|
297
|
+
phonenumbers: [{ phonenumber: '13175550197', 'dial-rule-application': 'to-connection', 'dial-rule-description': 'Portal Created: Phonenumber -> FaxServer' }],
|
|
298
|
+
} as Snapshot;
|
|
299
|
+
ok(countDomainInventory(noteOnly, { faxServerHosts: ['203.0.113.7'] }).dids.fax === 0, '[fax] the "-> FaxServer" description alone does not make a fax line');
|
|
300
|
+
}
|
|
301
|
+
|
|
176
302
|
// ── blank identity fields never collide onto one key ─────────────────────────────────────────────────
|
|
177
303
|
{
|
|
178
304
|
const blank = {
|
|
@@ -251,6 +377,101 @@ ok(sysDev.devices.total === 0, 'a system user device is not counted');
|
|
|
251
377
|
);
|
|
252
378
|
ok(destinationOf({ 'dial-rule-application': 'to-connection' }, byExt) === 'to connection', '[destinationOf] no destination, application only — to- stripped so it does not read "to to-connection"');
|
|
253
379
|
ok(destinationOf({}, byExt) === '', '[destinationOf] neither field set is empty, not "to undefined"');
|
|
380
|
+
const faxRule: Rec = { 'dial-rule-application': 'to-connection', 'dial-rule-translation-destination-host': '203.0.113.7' };
|
|
381
|
+
ok(destinationOf(faxRule, byExt) === 'to connection', '[destinationOf] with no host list a fax rule is just a connection');
|
|
382
|
+
ok(destinationOf(faxRule, byExt, ['203.0.113.7']) === 'to fax server', '[destinationOf] with the host on the list it reads "to fax server"');
|
|
383
|
+
ok(
|
|
384
|
+
destinationOf({ ...faxRule, 'dial-rule-translation-destination-user': '100' }, byExt, ['203.0.113.7']) === 'to fax server',
|
|
385
|
+
'[destinationOf] the fax test wins over a destination user, so no reader is shown a bare IP',
|
|
386
|
+
);
|
|
387
|
+
}
|
|
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');
|
|
254
475
|
}
|
|
255
476
|
|
|
256
477
|
console.log(`\n${pass} passed, ${fail} failed`);
|