openvidu/openvidu-dashboard/dashboard-ce/backend/test/unit/egress.test.ts

130 lines
4.6 KiB
TypeScript

import type * as z from 'zod';
import { describe, expect, it } from 'vitest';
import { type EgressDocument } from '@openvidu-dashboard/typings';
import { outputsSummary, toEgressListItem } from '../../src/egress/egress-mappers.js';
import { egressesListPlan } from '../../src/egress/egress-query.js';
import { egressListItem } from '../../src/egress/egress-responses.js';
import { egressesListQuery } from '../../src/egress/egress-schemas.js';
import { loadFixture } from '../support/fixtures.js';
const NOW = new Date('2026-10-13T12:00:00Z');
const list = egressesListQuery(() => NOW);
const egresses = loadFixture<EgressDocument>('egresses');
function fixture(id: string): EgressDocument {
const doc = egresses.find((candidate) => candidate._id === id);
if (doc === undefined) {
throw new Error(`the fixtures need ${id}`);
}
return structuredClone(doc);
}
function firstIssue(schema: z.ZodType, value: unknown) {
const result = schema.safeParse(value);
return result.success
? undefined
: { path: result.error.issues[0]?.path, message: result.error.issues[0]?.message };
}
/** Every credential path of the request oneof, in the raw protocol shapes a leaking writer would store. */
describe('egress mappers', () => {
it('lists the public fields with an outputs summary and an error flag', () => {
const failed = toEgressListItem(fixture('EG_trackF4iled9'));
const active = toEgressListItem(fixture('EG_streamHls7Wz'));
expect(failed).toMatchObject({
status: 'failed',
hasError: true,
errorCode: 403,
outputs: { kinds: ['file'], count: 1 }
});
expect(failed).not.toHaveProperty('error');
expect(active).toMatchObject({
status: 'active',
hasError: false,
outputs: { kinds: ['stream', 'segments'], count: 2 }
});
expect(active).not.toHaveProperty('endedAt');
expect(active).not.toHaveProperty('closedBy');
expect(outputsSummary([{ kind: 'file' }, { kind: 'stream' }, { kind: 'file' }])).toEqual({
kinds: ['file', 'stream'],
count: 3
});
for (const doc of egresses) {
const item = toEgressListItem(doc);
expect(egressListItem.strict().parse(item)).toEqual(item);
}
});
});
describe('egress schemas and list plan', () => {
it('rejects the invalid parameters on their own path', () => {
expect(firstIssue(list, { sort: 'durationMs' })?.path).toEqual(['sort']);
expect(firstIssue(list, { status: 'running' })?.path).toEqual(['status']);
expect(firstIssue(list, { type: 'audio' })?.path).toEqual(['type']);
expect(firstIssue(list, { room: 'demo-room' })?.path).toEqual(['room']);
expect(firstIssue(list, { q: 'd' })).toEqual({ path: ['q'], message: 'must have at least two characters' });
});
it('picks the index of every filter', () => {
const plan = (query: Record<string, string>) => egressesListPlan(list.parse(query));
expect(plan({})).toEqual({ kind: 'page', filter: {}, hint: 'startedAt_id' });
expect(plan({ type: 'web' })).toEqual({
kind: 'page',
filter: { type: { $in: ['web'] } },
hint: 'startedAt_id'
});
expect(plan({ status: 'starting,active,ending' })).toEqual({
kind: 'page',
filter: { status: { $in: ['starting', 'active', 'ending'] } },
hint: 'status_startedAt'
});
expect(plan({ room: 'RM_abc', status: 'failed', type: 'track' })).toEqual({
kind: 'page',
filter: { roomSid: 'RM_abc', status: { $in: ['failed'] }, type: { $in: ['track'] } },
hint: 'roomSid_startedAt'
});
expect(plan({ q: 'EG_ab12cd', status: 'active', room: 'RM_x' })).toEqual({
kind: 'id',
filter: { _id: 'EG_ab12cd' }
});
expect(plan({ q: 'Demo', status: 'failed' })).toEqual({
kind: 'page',
filter: { roomNameLower: { $regex: '^demo' }, status: { $in: ['failed'] } },
hint: 'roomNameLower_startedAt'
});
expect(plan({ q: 'demo', room: 'RM_abc' })).toEqual({
kind: 'page',
filter: { roomSid: 'RM_abc', roomNameLower: { $regex: '^demo' } },
hint: 'roomSid_startedAt'
});
});
it('ranks by end time and duration over the start range, egresses in flight at asOf', () => {
const ranked = egressesListPlan(list.parse({ sort: 'duration', status: 'complete' }));
const asOf = new Date('2026-10-13T12:00:00.000Z');
expect(ranked).toMatchObject({
kind: 'ranked',
filter: { status: { $in: ['complete'] } },
hint: 'status_startedAt'
});
expect(ranked.kind === 'ranked' ? ranked.rank(asOf) : undefined).toEqual({
$ifNull: ['$durationMs', { $subtract: [asOf, '$startedAt'] }]
});
const ended = egressesListPlan(list.parse({ sort: 'endedAt' }));
expect(ended.kind === 'ranked' ? ended.rank(asOf) : undefined).toEqual({ $ifNull: ['$endedAt', asOf] });
expect(() => list.parse({ sort: 'durationMs' })).toThrow();
});
});