/api/announcements CRUD (sound must be a prompt, destination must exist
and not be itself, delete refused while referenced); the generator
renders sp-ann-{id} (Answer, Playback, destApp) and destApp sends
announcement destinations there. The ivr golden case gains "press 3:
hours announcement, then back to the menu". Announcements are managed in
a section of Sounds & music (05 updated); the DestinationPicker gains
"Announcement".
Task: M6-T5
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
// M6-T5: /api/announcements (03 §6.11).
|
|
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 { soundFiles } from '../src/db/schema.ts';
|
|
import { hasTestDb } from './db.ts';
|
|
import { ADMIN, H, createTestApp } from './app.ts';
|
|
|
|
describe.skipIf(!hasTestDb)('/api/announcements', () => {
|
|
let t: Awaited<ReturnType<typeof createTestApp>>;
|
|
let req: (method: string, url: string, payload?: unknown) => Promise<LightMyRequestResponse>;
|
|
let dan: string;
|
|
const sound = '44444444-4444-4444-8444-444444444447';
|
|
let id: string;
|
|
|
|
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 },
|
|
});
|
|
dan = (await req('POST', '/api/extensions', { number: '101', name: 'Dan Doe' })).json().id;
|
|
await t.db.insert(soundFiles).values({
|
|
id: sound,
|
|
name: 'Our hours',
|
|
kind: 'prompt',
|
|
source: 'upload',
|
|
asteriskName: `custom/${sound}`,
|
|
durationMs: 4000,
|
|
});
|
|
});
|
|
afterAll(() => t?.close());
|
|
|
|
it('plays the sound, then continues; shows up in the dialplan', async () => {
|
|
const r = await req('POST', '/api/announcements', {
|
|
name: 'Hours',
|
|
soundId: sound,
|
|
destination: { type: 'voicemail', id: dan },
|
|
});
|
|
expect(r.statusCode, r.body).toBe(201);
|
|
id = r.json().id;
|
|
expect(r.json()).toMatchObject({
|
|
soundName: 'Our hours',
|
|
destinationSummary: 'Voicemail of Dan Doe (101)',
|
|
});
|
|
await t.app.generator.apply({ trigger: 'test' });
|
|
const conf = await readFile(join(t.genDir, 'extensions_selfphoneng.conf'), 'utf8');
|
|
expect(conf).toContain(
|
|
`[sp-ann-${id}]\nexten => s,1,Answer()\n same => n,Playback(custom/${sound})\n same => n,Goto(sp-vm-direct,101,1)`,
|
|
);
|
|
});
|
|
|
|
it('refuses a missing sound, itself as destination; sound and announcement are protected', async () => {
|
|
expect(
|
|
(
|
|
await req('POST', '/api/announcements', {
|
|
name: 'X',
|
|
soundId: '00000000-0000-4000-8000-000000000999',
|
|
destination: { type: 'hangup' },
|
|
})
|
|
).statusCode,
|
|
).toBe(400);
|
|
expect(
|
|
(
|
|
await req('PATCH', `/api/announcements/${id}`, {
|
|
destination: { type: 'announcement', id },
|
|
})
|
|
).statusCode,
|
|
).toBe(400);
|
|
const s = await req('DELETE', `/api/sounds/${sound}`);
|
|
expect(s.json().error.details.usedBy).toEqual(['announcement Hours']);
|
|
const ann2 = (
|
|
await req('POST', '/api/announcements', {
|
|
name: 'Before hours',
|
|
soundId: sound,
|
|
destination: { type: 'announcement', id },
|
|
})
|
|
).json();
|
|
expect(ann2.destinationSummary).toBe('Announcement Hours');
|
|
expect((await req('DELETE', `/api/announcements/${id}`)).statusCode).toBe(409);
|
|
expect((await req('DELETE', `/api/announcements/${ann2.id}`)).statusCode).toBe(200);
|
|
expect((await req('DELETE', `/api/announcements/${id}`)).statusCode).toBe(200);
|
|
});
|
|
});
|