/api/sounds (admin) takes the raw file, converts it with ffmpeg in one pass to <id>.wav (8 kHz) and <id>.g722 (HD for G.722 calls) in SOUNDS_DIR, measures it with ffprobe, and stores it as custom/<id>. Delete answers 409 with what still plays it (IVR greeting/invalid sound, announcements, the recording notice). Sounds & music page: drop or pick a file (name suggested from the file name), play, rename, delete. MOH classes stay in M8. The web client and greetings share one raw-upload helper; the api test audio helpers moved to test/audio.ts. e2e m6-t1: drag-and-drop upload in the browser, Asterisk plays custom/<id>, rename, delete refused while in use, then deleted. Task: M6-T1
93 lines
3.6 KiB
TypeScript
93 lines
3.6 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
// M6-T1: /api/sounds (04 §6): upload → wav + g722, play, rename, delete with references.
|
|
import { existsSync, readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import type { LightMyRequestResponse } from 'fastify';
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
import { setSetting } from '../src/services/settings.ts';
|
|
import { hasFfmpeg, silenceWav } from './audio.ts';
|
|
import { hasTestDb } from './db.ts';
|
|
import { ADMIN, H, createTestApp } from './app.ts';
|
|
|
|
describe.skipIf(!hasTestDb || !hasFfmpeg)('/api/sounds', () => {
|
|
let t: Awaited<ReturnType<typeof createTestApp>>;
|
|
let req: (
|
|
method: string,
|
|
url: string,
|
|
payload?: unknown,
|
|
headers?: Record<string, string>,
|
|
) => Promise<LightMyRequestResponse>;
|
|
let id: string;
|
|
const dir = () => t.app.config.SOUNDS_DIR;
|
|
|
|
beforeAll(async () => {
|
|
t = await createTestApp();
|
|
await t.clearMustChange(ADMIN.email);
|
|
const cookie = await t.login(ADMIN.email, ADMIN.password);
|
|
req = (method, url, payload, headers) =>
|
|
t.app.inject({
|
|
method: method as 'GET',
|
|
url,
|
|
payload: payload as object,
|
|
headers: { ...H, cookie, ...headers },
|
|
});
|
|
});
|
|
afterAll(() => t?.close());
|
|
|
|
it('upload converts to an 8 kHz wav plus a G.722 copy and measures it', async () => {
|
|
const r = await req(
|
|
'POST',
|
|
'/api/sounds?name=Main%20greeting&filename=greeting.wav',
|
|
silenceWav(3),
|
|
{ 'content-type': 'audio/wav' },
|
|
);
|
|
expect(r.statusCode, r.body).toBe(201);
|
|
const dto = r.json();
|
|
id = dto.id;
|
|
expect(dto).toMatchObject({
|
|
name: 'Main greeting',
|
|
asteriskName: `custom/${id}`,
|
|
originalFilename: 'greeting.wav',
|
|
});
|
|
expect(dto.durationMs).toBeGreaterThan(2900);
|
|
expect(dto.durationMs).toBeLessThan(3100);
|
|
expect(readFileSync(join(dir(), `${id}.wav`)).readUInt32LE(24)).toBe(8000);
|
|
expect(existsSync(join(dir(), `${id}.g722`))).toBe(true);
|
|
});
|
|
|
|
it('rejects non-audio and a missing name', async () => {
|
|
const junk = await req('POST', '/api/sounds?name=x', Buffer.from('nope'), {
|
|
'content-type': 'audio/mpeg',
|
|
});
|
|
expect(junk.statusCode).toBe(400);
|
|
expect(junk.json().error.message).toMatch(/isn't audio/);
|
|
const noName = await req('POST', '/api/sounds', silenceWav(), { 'content-type': 'audio/wav' });
|
|
expect(noName.statusCode).toBe(400);
|
|
});
|
|
|
|
it('lists, plays and renames', async () => {
|
|
expect((await req('GET', '/api/sounds')).json().items.map((s: { id: string }) => s.id)).toEqual(
|
|
[id],
|
|
);
|
|
const audio = await req('GET', `/api/sounds/${id}/audio`);
|
|
expect(audio.statusCode).toBe(200);
|
|
expect(audio.rawPayload.subarray(0, 4).toString()).toBe('RIFF');
|
|
const r = await req('PATCH', `/api/sounds/${id}`, { name: 'Welcome' });
|
|
expect(r.json().name).toBe('Welcome');
|
|
});
|
|
|
|
it('delete is refused while something plays it, then removes the files', async () => {
|
|
await setSetting(t.db, 'recordings.announcementSoundId', id);
|
|
const busy = await req('DELETE', `/api/sounds/${id}`);
|
|
expect(busy.statusCode).toBe(409);
|
|
expect(busy.json().error.details.usedBy).toEqual([
|
|
'the call recording notice (Settings → Recording)',
|
|
]);
|
|
await setSetting(t.db, 'recordings.announcementSoundId', null);
|
|
expect((await req('DELETE', `/api/sounds/${id}`)).statusCode).toBe(200);
|
|
expect(existsSync(join(dir(), `${id}.wav`))).toBe(false);
|
|
expect(existsSync(join(dir(), `${id}.g722`))).toBe(false);
|
|
expect((await req('GET', `/api/sounds/${id}`)).statusCode).toBe(404);
|
|
});
|
|
});
|