CSantosM c5bca6e133 backend: Enhances test reliability with active waiting
Replaces arbitrary `sleep()` calls in integration tests with explicit `wait-helpers`. These new helpers actively poll for specific conditions (e.g., room deletion, participant connection, recording status) directly from the database or LiveKit, rather than relying on fixed delays. This significantly reduces test flakiness and improves the accuracy of assertions.

Extracts LiveKit CLI interaction helpers (`joinFakeParticipant`, `disconnectFakeParticipants`, `updateParticipantMetadata`) into a dedicated `livekit-cli-helpers.ts` file for better organization and separation of concerns. Updates numerous integration tests to utilize the new waiting and LiveKit CLI helpers.
2026-03-06 16:38:12 +01:00

241 lines
8.8 KiB
TypeScript

import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals';
import {
MeetRecordingEncodingPreset,
MeetRecordingInfo,
MeetRecordingLayout,
MeetRecordingStatus,
MeetRoom,
MeetRoomConfig,
MeetRoomDeletionPolicyWithMeeting,
MeetWebhookEvent,
MeetWebhookEventType
} from '@openvidu-meet/typings';
import { Request } from 'express';
import http from 'http';
import {
deleteAllRecordings,
deleteAllRooms,
deleteRoom,
disconnectFakeParticipants,
endMeeting,
restoreDefaultGlobalConfig,
startTestServer,
updateWebhookConfig
} from '../../helpers/request-helpers.js';
import {
setupSingleRoom,
setupSingleRoomWithRecording,
startWebhookServer,
stopWebhookServer
} from '../../helpers/test-scenarios.js';
import { waitForRecordingToStop, waitForRoomToDelete } from '../../helpers/wait-helpers.js';
describe('Webhook Integration Tests', () => {
let receivedWebhooks: { headers: http.IncomingHttpHeaders; body: MeetWebhookEvent }[] = [];
const defaultRoomConfig: MeetRoomConfig = {
recording: {
enabled: true,
layout: MeetRecordingLayout.GRID,
encoding: MeetRecordingEncodingPreset.H264_720P_30
},
chat: { enabled: true },
virtualBackground: { enabled: true },
e2ee: { enabled: false },
captions: { enabled: true }
};
beforeAll(async () => {
await startTestServer();
// Start test server for webhooks
await startWebhookServer(5080, (req: Request) => {
receivedWebhooks.push({
headers: req.headers,
body: req.body
});
});
});
beforeEach(async () => {
receivedWebhooks = [];
// Enable webhooks in global config
await updateWebhookConfig({
enabled: true,
url: `http://localhost:5080/webhook`
});
});
afterAll(async () => {
await stopWebhookServer();
await restoreDefaultGlobalConfig();
await disconnectFakeParticipants();
await deleteAllRooms();
await deleteAllRecordings();
});
const expectValidSignature = (webhook: { headers: http.IncomingHttpHeaders; body: MeetWebhookEvent }) => {
expect(webhook.headers['x-signature']).toBeDefined();
expect(webhook.headers['x-timestamp']).toBeDefined();
};
it('should not send webhooks when disabled', async () => {
await updateWebhookConfig({
enabled: false
});
await setupSingleRoom(true);
expect(receivedWebhooks.length).toBe(0);
});
it('should send meeting_started webhook when room is created', async () => {
const context = await setupSingleRoom(true);
const roomData = context.room;
// Verify 'meetingStarted' webhook is sent
expect(receivedWebhooks.length).toBeGreaterThanOrEqual(1);
const meetingStartedWebhook = receivedWebhooks.find(
(w) => w.body.event === MeetWebhookEventType.MEETING_STARTED
);
expect(meetingStartedWebhook).toBeDefined();
expect(meetingStartedWebhook?.body.data.roomId).toBe(roomData.roomId);
expect(meetingStartedWebhook?.body.creationDate).toBeLessThanOrEqual(Date.now());
expect(meetingStartedWebhook?.body.creationDate).toBeGreaterThanOrEqual(Date.now() - 3000);
const room: MeetRoom = meetingStartedWebhook!.body.data as MeetRoom;
expect(room.roomId).toBe(roomData.roomId);
expect(room.config).toEqual(defaultRoomConfig);
expectValidSignature(meetingStartedWebhook!);
});
it('should send meeting_ended webhook when meeting is closed', async () => {
const context = await setupSingleRoom(true);
const roomData = context.room;
const moderatorToken = context.moderatorToken;
// Close the room
await endMeeting(roomData.roomId, moderatorToken);
// Verify 'meetingEnded' webhook is sent
expect(receivedWebhooks.length).toBeGreaterThanOrEqual(1);
const meetingEndedWebhook = receivedWebhooks.find((w) => w.body.event === MeetWebhookEventType.MEETING_ENDED);
expect(meetingEndedWebhook).toBeDefined();
expect(meetingEndedWebhook?.body.creationDate).toBeLessThanOrEqual(Date.now());
expect(meetingEndedWebhook?.body.creationDate).toBeGreaterThanOrEqual(Date.now() - 3000);
const room: MeetRoom = meetingEndedWebhook!.body.data as MeetRoom;
expect(room.roomId).toBe(roomData.roomId);
expect(room.config).toEqual(defaultRoomConfig);
expectValidSignature(meetingEndedWebhook!);
});
it('should send meeting_ended when room is forcefully deleted', async () => {
const context = await setupSingleRoom(true);
const roomData = context.room;
// Forcefully delete the room
await deleteRoom(roomData.roomId, { withMeeting: MeetRoomDeletionPolicyWithMeeting.FORCE });
await waitForRoomToDelete(roomData.roomId); // Wait for the webhook to process the deletion
// Verify 'meetingEnded' webhook is sent
expect(receivedWebhooks.length).toBeGreaterThanOrEqual(1);
const meetingEndedWebhook = receivedWebhooks.find((w) => w.body.event === MeetWebhookEventType.MEETING_ENDED);
expect(meetingEndedWebhook).toBeDefined();
expect(meetingEndedWebhook?.body.data.roomId).toBe(roomData.roomId);
expect(meetingEndedWebhook?.body.creationDate).toBeLessThanOrEqual(Date.now());
expect(meetingEndedWebhook?.body.creationDate).toBeGreaterThanOrEqual(Date.now() - 3000);
const room: MeetRoom = meetingEndedWebhook!.body.data as MeetRoom;
expect(room.roomId).toBe(roomData.roomId);
expect(room.config).toEqual(defaultRoomConfig);
expectValidSignature(meetingEndedWebhook!);
});
it('should send recordingStarted, recordingUpdated and recordingEnded webhooks when recording is started and stopped', async () => {
const startDate = Date.now();
const context = await setupSingleRoomWithRecording(true, '2s');
const roomData = context.room;
const recordingId = context.recordingId;
await waitForRecordingToStop(recordingId!); // Wait for the recording to stop and webhook to be processed
const recordingWebhooks = receivedWebhooks.filter((w) => w.body.event.startsWith('recording'));
// STARTED, ACTIVE, ENDING, COMPLETE
expect(recordingWebhooks.length).toBe(4);
// Check recording_started webhook
const recordingStartedWebhook = receivedWebhooks.find(
(w) => w.body.event === MeetWebhookEventType.RECORDING_STARTED
);
let data = recordingStartedWebhook?.body.data as MeetRecordingInfo;
expect(recordingStartedWebhook).toBeDefined();
expect(data.roomId).toBe(roomData.roomId);
expect(data.recordingId).toBe(recordingId);
expect(recordingStartedWebhook?.body.creationDate).toBeLessThan(Date.now());
expect(recordingStartedWebhook?.body.creationDate).toBeGreaterThan(startDate);
expectValidSignature(recordingStartedWebhook!);
expect(recordingStartedWebhook?.body.event).toBe(MeetWebhookEventType.RECORDING_STARTED);
expect(data.status).toBe(MeetRecordingStatus.STARTING);
expect(data.layout).toBeDefined();
if (data.layout) {
expect(Object.values(MeetRecordingLayout)).toContain(data.layout);
}
// Validate encoding is present and coherent with default value
expect(data.encoding).toBeDefined();
expect(data.encoding).toBe(MeetRecordingEncodingPreset.H264_720P_30);
// Check recording_updated webhook
const recordingUpdatedWebhook = receivedWebhooks.find(
(w) => w.body.event === MeetWebhookEventType.RECORDING_UPDATED
);
data = recordingUpdatedWebhook?.body.data as MeetRecordingInfo;
expect(recordingUpdatedWebhook).toBeDefined();
expect(data.roomId).toBe(roomData.roomId);
expect(data.recordingId).toBe(recordingId);
expect(recordingUpdatedWebhook?.body.creationDate).toBeLessThan(Date.now());
expect(recordingUpdatedWebhook?.body.creationDate).toBeGreaterThan(startDate);
expectValidSignature(recordingUpdatedWebhook!);
expect(recordingUpdatedWebhook?.body.event).toBe(MeetWebhookEventType.RECORDING_UPDATED);
expect(data.status).toBe(MeetRecordingStatus.ACTIVE);
expect(data.layout).toBeDefined();
if (data.layout) {
expect(Object.values(MeetRecordingLayout)).toContain(data.layout);
}
// Validate encoding is present and coherent with default value
expect(data.encoding).toBeDefined();
expect(data.encoding).toBe('H264_720P_30');
// Check recording_ended webhook
const recordingEndedWebhook = receivedWebhooks.find(
(w) => w.body.event === MeetWebhookEventType.RECORDING_ENDED
);
data = recordingEndedWebhook?.body.data as MeetRecordingInfo;
expect(recordingEndedWebhook).toBeDefined();
expect(data.roomId).toBe(roomData.roomId);
expect(data.recordingId).toBe(recordingId);
expect(recordingEndedWebhook?.body.creationDate).toBeLessThan(Date.now());
expect(recordingEndedWebhook?.body.creationDate).toBeGreaterThan(startDate);
expectValidSignature(recordingEndedWebhook!);
expect(recordingEndedWebhook?.body.event).toBe(MeetWebhookEventType.RECORDING_ENDED);
expect(data.status).not.toBe(MeetRecordingStatus.ENDING);
expect(data.status).toBe(MeetRecordingStatus.COMPLETE);
expect(data.layout).toBeDefined();
if (data.layout) {
expect(Object.values(MeetRecordingLayout)).toContain(data.layout);
}
// Validate encoding is present and coherent with default value
expect(data.encoding).toBeDefined();
expect(data.encoding).toBe('H264_720P_30');
});
});