Files
selfphoneng/apps/api/test/ringgroups.test.ts
T
iamdoubz d746772b97 destinations: full picker everywhere, numbers in summaries, Ring everyone
The DestinationPicker now offers every type that exists (user, voicemail,
ring group, digital receptionist, office hours, announcement, outside
number, hang up) on every page that routes calls; queues and
conferences join in M8. API summaries show the number of dialable
targets ("Ring group Sales (500)", 05 §3). Wizard step 8 gains "Ring
everyone": it creates or refreshes a ring group "Everyone" of all
extensions (unanswered → the first user's voicemail); an IVR isn't
created there because it needs a recorded greeting. m3-t5 now routes
the main number through "Ring everyone" and still passes.

Task: M6-T6
2026-10-18 11:04:42 -05:00

125 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: AGPL-3.0-or-later
// M6-T2: /api/ring-groups (03 §6.7, 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/ring-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 [n, name] of [
['101', 'Dan Doe'],
['102', 'Sue Smith'],
] as const)
ext[n] = (await req('POST', '/api/extensions', { number: n, name })).json().id;
});
afterAll(() => t?.close());
it('creates a group with ordered members; the dialplan rings them', async () => {
const r = await req('POST', '/api/ring-groups', {
number: '500',
name: 'Sales',
memberIds: [ext['102'], ext['101']],
noAnswerDestination: { type: 'voicemail', id: ext['101'] },
});
expect(r.statusCode, r.body).toBe(201);
groupId = r.json().id;
expect(r.json()).toMatchObject({
strategy: 'ringall',
ringSeconds: 25,
memberIds: [ext['102'], ext['101']],
noAnswerSummary: 'Voicemail of Dan Doe (101)',
});
const conf = await dialplan();
expect(conf).toContain(`exten => 500,1,Goto(sp-rg-${groupId},s,1)`);
expect(conf).toContain('Dial(Local/102@sp-rg-member&Local/101@sp-rg-member,25,t)');
});
it('checks the number, members and destination', async () => {
const bad = (body: object) =>
req('POST', '/api/ring-groups', {
number: '501',
name: 'X',
noAnswerDestination: { type: 'hangup' },
...body,
});
expect((await bad({ number: '101' })).statusCode).toBe(400); // outside 500–549
expect((await bad({ number: '500' })).statusCode).toBe(409);
expect((await bad({ memberIds: [ext['101'], ext['101']] })).statusCode).toBe(400);
expect((await bad({ memberIds: ['00000000-0000-4000-8000-000000000999'] })).statusCode).toBe(
400,
);
expect(
(
await bad({
noAnswerDestination: { type: 'ivr', id: '00000000-0000-4000-8000-000000000999' },
})
).statusCode,
).toBe(400);
expect(
(
await req('PATCH', `/api/ring-groups/${groupId}`, {
noAnswerDestination: { type: 'ring_group', id: groupId },
})
).statusCode,
).toBe(400);
});
it('hunt: one member at a time; reorder via PATCH', async () => {
const r = await req('PATCH', `/api/ring-groups/${groupId}`, {
strategy: 'hunt',
ringSeconds: 15,
memberIds: [ext['101'], ext['102']],
});
expect(r.statusCode, r.body).toBe(200);
const conf = await dialplan();
expect(conf.indexOf('Dial(Local/101@sp-rg-member,15,t)')).toBeLessThan(
conf.indexOf('Dial(Local/102@sp-rg-member,15,t)'),
);
});
it('a phone number can point at it; delete is refused while it does', async () => {
const trunkId = (
await req('POST', '/api/trunks', {
name: 'voip.ms',
serverHost: '127.0.0.1',
username: '123456_pbx',
password: 'pw',
})
).json().id;
const did = await req('POST', '/api/dids', {
number: '3125551234',
trunkId,
destination: { type: 'ring_group', id: groupId },
});
expect(did.json().destinationSummary).toBe('Ring group Sales (500)');
expect(await dialplan()).toContain(`Goto(sp-rg-${groupId},s,1)`);
const busy = await req('DELETE', `/api/ring-groups/${groupId}`);
expect(busy.statusCode).toBe(409);
await req('PATCH', `/api/dids/${did.json().id}`, { destination: { type: 'hangup' } });
expect((await req('DELETE', `/api/ring-groups/${groupId}`)).statusCode).toBe(200);
expect(await dialplan()).not.toContain(`sp-rg-${groupId}`);
});
});