import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; import { Request } from 'express'; import http from 'http'; import { container } from '../../../src/config/dependency-injector.config.js'; import { MeetStorageService } from '../../../src/services/index.js'; import { MeetRecordingInfo, MeetRecordingStatus } from '../../../src/typings/ce/recording.model.js'; import { MeetWebhookEvent, MeetWebhookEventType } from '../../../src/typings/ce/webhook.model.js'; import { deleteAllRecordings, deleteAllRooms, deleteRoom, disconnectFakeParticipants, endMeeting, sleep, startTestServer, updateWebbhookPreferences } from '../../helpers/request-helpers.js'; import { setupSingleRoom, setupSingleRoomWithRecording, startWebhookServer, stopWebhookServer } from '../../helpers/test-scenarios.js'; describe('Webhook Integration Tests', () => { let receivedWebhooks: { headers: http.IncomingHttpHeaders; body: MeetWebhookEvent }[] = []; let storageService: MeetStorageService; beforeAll(async () => { startTestServer(); storageService = container.get(MeetStorageService); // 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 preferences await updateWebbhookPreferences({ enabled: true, url: `http://localhost:5080/webhook` }); }); afterAll(async () => { await stopWebhookServer(); await storageService['initializeGlobalPreferences'](); await disconnectFakeParticipants(); await Promise.all([deleteAllRooms(), deleteAllRecordings()]); }); it('should not send webhooks when disabled', async () => { await updateWebbhookPreferences({ enabled: false }); await setupSingleRoom(true); // Wait for the room to be created await sleep('3s'); 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; // Wait for the room to be created await sleep('1s'); // 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); expect(meetingStartedWebhook?.headers['x-signature']).toBeDefined(); expect(meetingStartedWebhook?.headers['x-timestamp']).toBeDefined(); }); it('should send meeting_ended webhook when meeting is closed', async () => { const context = await setupSingleRoom(true); const roomData = context.room; const moderatorCookie = context.moderatorCookie; // Close the room await endMeeting(roomData.roomId, moderatorCookie); // Wait for the room to be closed await sleep('1s'); // 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); expect(meetingEndedWebhook?.headers['x-signature']).toBeDefined(); expect(meetingEndedWebhook?.headers['x-timestamp']).toBeDefined(); }); 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: 'force' }); // 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); expect(meetingEndedWebhook?.headers['x-signature']).toBeDefined(); expect(meetingEndedWebhook?.headers['x-timestamp']).toBeDefined(); }); 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; 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); expect(recordingStartedWebhook?.headers['x-signature']).toBeDefined(); expect(recordingStartedWebhook?.headers['x-timestamp']).toBeDefined(); expect(recordingStartedWebhook?.body.event).toBe(MeetWebhookEventType.RECORDING_STARTED); expect(data.status).toBe(MeetRecordingStatus.STARTING); // 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); expect(recordingUpdatedWebhook?.headers['x-signature']).toBeDefined(); expect(recordingUpdatedWebhook?.headers['x-timestamp']).toBeDefined(); expect(recordingUpdatedWebhook?.body.event).toBe(MeetWebhookEventType.RECORDING_UPDATED); expect(data.status).toBe(MeetRecordingStatus.ACTIVE); // 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); expect(recordingEndedWebhook?.headers['x-signature']).toBeDefined(); expect(recordingEndedWebhook?.headers['x-timestamp']).toBeDefined(); expect(recordingEndedWebhook?.body.event).toBe(MeetWebhookEventType.RECORDING_ENDED); expect(data.status).not.toBe(MeetRecordingStatus.ENDING); expect(data.status).toBe(MeetRecordingStatus.COMPLETE); }); });