paging: paging groups, *80 intercom, auto-answer headers
/api/paging-groups CRUD (members as memberIds). The generator renders
sp-page-{id} per 03 §6.11 (desk phones only, auto-answer headers via
sp-sub-autoanswer) plus the number in sp-local-numbers. Added the `s`
option so phones already on a call aren't paged, which also keeps a
pager who is a member from being paged themselves. A group with no
members just hangs up.
*80<ext> intercom checks the target first: digits only and an endpoint
in sp-from-ext, else ss-noservice (the spec's one-liner passed
${EXTEN:3} straight into Page()). Both written back into 03.
Web: Paging page (list, drawer form with member checkboxes). Golden
case paging. e2e m8-t4 with baresip phones that honor the headers
(sip_autoanswer): an ordinary call rings, a page is answered by itself
and muted, the busy pager is skipped, *80 is two-way, *80999 refused.
The check on a real Yealink/Grandstream stays open (09).
Task: M8-T4
This commit is contained in:
1 parent
a6c423de97
commit
e95271e694
47 files changed
+1466
-22
No files matched your search
@@ -67,6 +67,7 @@ function snapshot(): Snapshot {
|
||||
ringGroups: [],
|
||||
queues: [],
|
||||
conferences: [],
|
||||
pagingGroups: [],
|
||||
ivrs: [],
|
||||
timeConditions: [],
|
||||
holidays: [],
|
||||
|
||||
@@ -156,6 +156,7 @@ function snapshot(): Snapshot {
|
||||
: ({ type: 'voicemail', id: CDR_IDS.ext('102') } as const),
|
||||
})),
|
||||
conferences: [],
|
||||
pagingGroups: [],
|
||||
ivrs: [
|
||||
{
|
||||
id: CDR_IDS.ivr,
|
||||
|
||||
@@ -106,6 +106,7 @@ function snapshot(): Snapshot {
|
||||
ringGroups: [],
|
||||
queues: [],
|
||||
conferences: [],
|
||||
pagingGroups: [],
|
||||
ivrs: [],
|
||||
timeConditions: [],
|
||||
holidays: [],
|
||||
|
||||
@@ -62,6 +62,7 @@ function snapshot(): Snapshot {
|
||||
ringGroups: [],
|
||||
queues: [],
|
||||
conferences: [],
|
||||
pagingGroups: [],
|
||||
ivrs: [],
|
||||
timeConditions: [],
|
||||
holidays: [],
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// M8-T4: /api/paging-groups (03 §6.11, 04 §7.3).
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import type { LightMyRequestResponse } from 'fastify';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { hasTestDb } from './db.ts';
|
||||
import { ADMIN, H, createTestApp } from './app.ts';
|
||||
|
||||
describe.skipIf(!hasTestDb)('/api/paging-groups', () => {
|
||||
let t: Awaited<ReturnType<typeof createTestApp>>;
|
||||
let req: (method: string, url: string, payload?: unknown) => Promise<LightMyRequestResponse>;
|
||||
const ext: Record<string, string> = {};
|
||||
let groupId: string;
|
||||
const dialplan = async () => {
|
||||
await t.app.generator.apply({ trigger: 'test' });
|
||||
return readFile(join(t.genDir, 'extensions_selfphoneng.conf'), 'utf8');
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
t = await createTestApp();
|
||||
await t.clearMustChange(ADMIN.email);
|
||||
const cookie = await t.login(ADMIN.email, ADMIN.password);
|
||||
req = (method, url, payload) =>
|
||||
t.app.inject({
|
||||
method: method as 'GET',
|
||||
url,
|
||||
payload: payload as object,
|
||||
headers: { ...H, cookie },
|
||||
});
|
||||
for (const [number, name] of [
|
||||
['102', 'Ann Lee'],
|
||||
['101', 'Dan Doe'],
|
||||
] as const)
|
||||
ext[number] = (await req('POST', '/api/extensions', { number, name })).json().id;
|
||||
});
|
||||
afterAll(() => t?.close());
|
||||
|
||||
it('creates a group: desk phones only, auto-answer, busy phones skipped', async () => {
|
||||
const r = await req('POST', '/api/paging-groups', {
|
||||
number: '850',
|
||||
name: 'Warehouse',
|
||||
memberIds: [ext['102'], ext['101']],
|
||||
});
|
||||
expect(r.statusCode, r.body).toBe(201);
|
||||
groupId = r.json().id;
|
||||
expect(r.json()).toMatchObject({ duplex: false, memberIds: [ext['101'], ext['102']] });
|
||||
const d = await dialplan();
|
||||
expect(d).toContain(`exten => 850,1,Goto(sp-page-${groupId},s,1)`);
|
||||
expect(d).toContain('exten => s,1,Page(PJSIP/101&PJSIP/102,iqsb(sp-sub-autoanswer^s^1),30)');
|
||||
});
|
||||
|
||||
it('checks the number and the members', async () => {
|
||||
const bad = (body: object) =>
|
||||
req('POST', '/api/paging-groups', { number: '851', name: 'X', ...body });
|
||||
expect((await bad({ number: '800' })).statusCode).toBe(400); // conference range
|
||||
expect((await bad({ number: '850' })).statusCode).toBe(409);
|
||||
expect((await bad({ memberIds: [ext['101'], ext['101']] })).statusCode).toBe(400);
|
||||
expect((await bad({ memberIds: ['00000000-0000-4000-8000-000000000000'] })).statusCode).toBe(
|
||||
400,
|
||||
);
|
||||
});
|
||||
|
||||
it('two-way and fewer members; delete removes it from the dialplan', async () => {
|
||||
const r = await req('PATCH', `/api/paging-groups/${groupId}`, {
|
||||
duplex: true,
|
||||
memberIds: [ext['102']],
|
||||
});
|
||||
expect(r.statusCode, r.body).toBe(200);
|
||||
expect(await dialplan()).toContain(
|
||||
'exten => s,1,Page(PJSIP/102,diqsb(sp-sub-autoanswer^s^1),30)',
|
||||
);
|
||||
expect((await req('DELETE', `/api/paging-groups/${groupId}`)).statusCode).toBe(200);
|
||||
expect(await dialplan()).not.toContain('sp-page-');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user