=> {
const rooms: RoomData[] = [];
for (let i = 0; i < numRooms; i++) {
- const roomData = await setupSingleRoom(withParticipants, 'TEST_ROOM');
+ const roomData = await setupSingleRoom(withParticipants, 'TEST_ROOM', roomConfig);
rooms.push(roomData);
}
diff --git a/meet-ce/backend/tests/integration/api/rooms/create-room.test.ts b/meet-ce/backend/tests/integration/api/rooms/create-room.test.ts
index 71f43d58..ffad8f29 100644
--- a/meet-ce/backend/tests/integration/api/rooms/create-room.test.ts
+++ b/meet-ce/backend/tests/integration/api/rooms/create-room.test.ts
@@ -63,7 +63,8 @@ describe('Room API Tests', () => {
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
},
chat: { enabled: false },
- virtualBackground: { enabled: true }
+ virtualBackground: { enabled: true },
+ e2ee: { enabled: true }
}
};
diff --git a/meet-ce/backend/tests/integration/api/rooms/e2ee-room-config.test.ts b/meet-ce/backend/tests/integration/api/rooms/e2ee-room-config.test.ts
new file mode 100644
index 00000000..e5945fd2
--- /dev/null
+++ b/meet-ce/backend/tests/integration/api/rooms/e2ee-room-config.test.ts
@@ -0,0 +1,263 @@
+import { afterAll, beforeAll, describe, expect, it } from '@jest/globals';
+import { Express } from 'express';
+import request from 'supertest';
+import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js';
+import { MEET_INITIAL_API_KEY } from '../../../../src/environment.js';
+import { MeetRecordingAccess, MeetRoom } from '@openvidu-meet/typings';
+import { expectValidRoom } from '../../../helpers/assertion-helpers.js';
+import {
+ createRoom,
+ deleteAllRecordings,
+ deleteAllRooms,
+ getRoomConfig,
+ startRecording,
+ startTestServer,
+ updateRoomConfig
+} from '../../../helpers/request-helpers.js';
+import { setupMultiRoomTestContext } from '../../../helpers/test-scenarios.js';
+
+const ROOMS_PATH = `${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`;
+
+describe('E2EE Room Configuration Tests', () => {
+ let app: Express;
+
+ beforeAll(async () => {
+ app = startTestServer();
+ });
+
+ afterAll(async () => {
+ await deleteAllRecordings();
+ await deleteAllRooms();
+ });
+
+ describe('E2EE Default Configuration', () => {
+ it('Should create a room with E2EE disabled by default', async () => {
+ const room = await createRoom({
+ roomName: 'Test E2EE Default'
+ });
+
+ expectValidRoom(room, 'Test E2EE Default');
+ expect(room.config.e2ee).toBeDefined();
+ expect(room.config.e2ee?.enabled).toBe(false);
+ });
+ });
+
+ describe('E2EE Enabled Configuration', () => {
+ it('Should create a room with E2EE enabled and recording automatically disabled', async () => {
+ const payload = {
+ roomName: 'Test E2EE Enabled',
+ config: {
+ recording: {
+ enabled: true, // This should be automatically disabled
+ allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
+ },
+ chat: { enabled: true },
+ virtualBackground: { enabled: true },
+ e2ee: { enabled: true }
+ }
+ };
+
+ const room = await createRoom(payload);
+
+ expect(room.roomName).toBe('Test E2EE Enabled');
+ expect(room.config.e2ee?.enabled).toBe(true);
+ expect(room.config.recording.enabled).toBe(false); // Recording should be disabled
+ });
+ });
+
+ describe('E2EE and Recording Interaction', () => {
+ it('Should not allow starting recording in a room with E2EE enabled', async () => {
+ const context = await setupMultiRoomTestContext(1, true, {
+ recording: {
+ enabled: true,
+ allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
+ },
+ chat: { enabled: true },
+ virtualBackground: { enabled: true },
+ e2ee: { enabled: true }
+ });
+
+ const { room, moderatorToken } = context.getRoomByIndex(0)!;
+
+ // Try to start recording (should fail because recording is not enabled in room config)
+ const response = await startRecording(room.roomId, moderatorToken);
+
+ // The endpoint returns 404 when the recording endpoint doesn't exist for disabled recording rooms
+ expect(403).toBe(response.status);
+ expect(response.body.message).toBe(`Recording is disabled for room '${room.roomId}'`);
+ });
+
+ it('Should disable recording when updating room config to enable E2EE', async () => {
+ // Create room with recording enabled and E2EE disabled
+ const room = await createRoom({
+ roomName: 'Test E2EE Update',
+ config: {
+ recording: {
+ enabled: true,
+ allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
+ },
+ chat: { enabled: true },
+ virtualBackground: { enabled: true },
+ e2ee: { enabled: false }
+ }
+ });
+
+ expect(room.config.recording.enabled).toBe(true);
+ expect(room.config.e2ee?.enabled).toBe(false);
+
+ // Update room to enable E2EE (recording should be automatically disabled)
+ const updatedConfig = {
+ recording: {
+ enabled: true, // This should be automatically disabled
+ allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
+ },
+ chat: { enabled: true },
+ virtualBackground: { enabled: true },
+ e2ee: { enabled: true }
+ };
+
+ const response = await updateRoomConfig(room.roomId, updatedConfig);
+
+ expect(response.status).toBe(200);
+
+ // Fetch the updated room to verify changes
+ const { status, body: config } = await getRoomConfig(room.roomId);
+
+ expect(status).toBe(200);
+ expect(config.e2ee?.enabled).toBe(true);
+ expect(config.recording.enabled).toBe(false);
+ });
+
+ // TODO: Add test for enabling E2EE when there are active recordings in the room
+ });
+
+ describe('E2EE Validation Tests', () => {
+ it('Should fail when e2ee is not an object', async () => {
+ const payload = {
+ roomName: 'Test Invalid E2EE',
+ config: {
+ recording: {
+ enabled: true,
+ allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
+ },
+ chat: { enabled: true },
+ virtualBackground: { enabled: true },
+ e2ee: 'invalid-e2ee' // Should be an object
+ }
+ };
+
+ const response = await request(app)
+ .post(ROOMS_PATH)
+ .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY)
+ .send(payload)
+ .expect(422);
+
+ expect(JSON.stringify(response.body.details)).toContain('Expected object');
+ });
+
+ it('Should fail when e2ee.enabled is not a boolean', async () => {
+ const payload = {
+ roomName: 'Test Invalid E2EE Enabled',
+ config: {
+ recording: {
+ enabled: true,
+ allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
+ },
+ chat: { enabled: true },
+ virtualBackground: { enabled: true },
+ e2ee: { enabled: 'yes' } // Should be a boolean
+ }
+ };
+
+ const response = await request(app)
+ .post(ROOMS_PATH)
+ .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY)
+ .send(payload)
+ .expect(422);
+
+ expect(JSON.stringify(response.body.details)).toContain('Expected boolean');
+ });
+ });
+
+ describe('E2EE Update Configuration Tests', () => {
+ it('Should successfully update room config with E2EE disabled to enabled', async () => {
+ const room = await createRoom({
+ roomName: 'Test E2EE Update Enabled'
+ });
+
+ expect(room.config.e2ee?.enabled).toBe(false);
+
+ const { status, body } = await updateRoomConfig(room.roomId, {
+ recording: {
+ enabled: false,
+ allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
+ },
+ chat: { enabled: true },
+ virtualBackground: { enabled: true },
+ e2ee: { enabled: true }
+ });
+
+ expect(status).toBe(200);
+ expect(body.message).toBeDefined();
+
+ // Fetch the updated room to verify changes
+ const { body: config } = await getRoomConfig(room.roomId);
+
+ expect(config.e2ee?.enabled).toBe(true);
+ expect(config.recording.enabled).toBe(false);
+ });
+ });
+
+ describe('E2EE and Room Status Tests', () => {
+ it('Should return E2EE configuration when listing rooms', async () => {
+ await deleteAllRooms();
+
+ const room1 = await createRoom({
+ roomName: 'E2EE Enabled Room',
+ config: {
+ recording: {
+ enabled: false,
+ allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
+ },
+ chat: { enabled: true },
+ virtualBackground: { enabled: true },
+ e2ee: { enabled: true }
+ }
+ });
+
+ const room2 = await createRoom({
+ roomName: 'E2EE Disabled Room',
+ config: {
+ recording: {
+ enabled: true,
+ allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
+ },
+ chat: { enabled: true },
+ virtualBackground: { enabled: true },
+ e2ee: { enabled: false }
+ }
+ });
+
+ const response = await request(app)
+ .get(ROOMS_PATH)
+ .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY)
+ .expect(200);
+
+ // Filter out any rooms from other test suites
+ const testRooms = response.body.rooms.filter(
+ (r: MeetRoom) => r.roomId === room1.roomId || r.roomId === room2.roomId
+ );
+
+ expect(testRooms).toHaveLength(2);
+
+ const e2eeEnabledRoom = testRooms.find((r: MeetRoom) => r.roomId === room1.roomId);
+ const e2eeDisabledRoom = testRooms.find((r: MeetRoom) => r.roomId === room2.roomId);
+
+ expect(e2eeEnabledRoom.config.e2ee?.enabled).toBe(true);
+ expect(e2eeEnabledRoom.config.recording.enabled).toBe(false);
+
+ expect(e2eeDisabledRoom.config.e2ee?.enabled).toBe(false);
+ expect(e2eeDisabledRoom.config.recording.enabled).toBe(true);
+ });
+ });
+});
diff --git a/meet-ce/backend/tests/integration/api/rooms/get-room-preferences.test.ts b/meet-ce/backend/tests/integration/api/rooms/get-room-preferences.test.ts
index c6b492de..e420b409 100644
--- a/meet-ce/backend/tests/integration/api/rooms/get-room-preferences.test.ts
+++ b/meet-ce/backend/tests/integration/api/rooms/get-room-preferences.test.ts
@@ -3,6 +3,7 @@ import { MeetRecordingAccess } from '@openvidu-meet/typings';
import { expectSuccessRoomConfigResponse } from '../../../helpers/assertion-helpers.js';
import { deleteAllRooms, getRoomConfig, startTestServer } from '../../../helpers/request-helpers.js';
import { setupSingleRoom } from '../../../helpers/test-scenarios.js';
+import { Response } from 'supertest';
describe('Room API Tests', () => {
const DEFAULT_CONFIG = {
@@ -11,7 +12,8 @@ describe('Room API Tests', () => {
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
},
chat: { enabled: true },
- virtualBackground: { enabled: true }
+ virtualBackground: { enabled: true },
+ e2ee: { enabled: false }
};
beforeAll(() => {
@@ -28,7 +30,7 @@ describe('Room API Tests', () => {
const roomData = await setupSingleRoom();
const roomId = roomData.room.roomId;
- const response = await getRoomConfig(roomId);
+ const response: Response = await getRoomConfig(roomId);
expectSuccessRoomConfigResponse(response, DEFAULT_CONFIG);
});
@@ -41,7 +43,8 @@ describe('Room API Tests', () => {
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
},
chat: { enabled: true },
- virtualBackground: { enabled: false }
+ virtualBackground: { enabled: false },
+ e2ee: { enabled: false }
}
};
diff --git a/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts b/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts
index 22106a16..7f5f69b9 100644
--- a/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts
+++ b/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts
@@ -41,7 +41,8 @@ describe('Room API Tests', () => {
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
},
chat: { enabled: true },
- virtualBackground: { enabled: false }
+ virtualBackground: { enabled: false },
+ e2ee: { enabled: false }
}
};
// Create a room with custom config
diff --git a/meet-ce/backend/tests/integration/api/rooms/update-room-preferences.test.ts b/meet-ce/backend/tests/integration/api/rooms/update-room-preferences.test.ts
index 33c7d81e..aecaa809 100644
--- a/meet-ce/backend/tests/integration/api/rooms/update-room-preferences.test.ts
+++ b/meet-ce/backend/tests/integration/api/rooms/update-room-preferences.test.ts
@@ -38,7 +38,8 @@ describe('Room API Tests', () => {
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
},
chat: { enabled: true },
- virtualBackground: { enabled: true }
+ virtualBackground: { enabled: true },
+ e2ee: { enabled: false }
}
});
@@ -49,7 +50,8 @@ describe('Room API Tests', () => {
allowAccessTo: MeetRecordingAccess.ADMIN
},
chat: { enabled: false },
- virtualBackground: { enabled: false }
+ virtualBackground: { enabled: false },
+ e2ee: { enabled: true }
};
const updateResponse = await updateRoomConfig(createdRoom.roomId, updatedConfig);
@@ -86,7 +88,8 @@ describe('Room API Tests', () => {
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
},
chat: { enabled: true },
- virtualBackground: { enabled: true }
+ virtualBackground: { enabled: true },
+ e2ee: { enabled: false }
}
});
@@ -97,7 +100,8 @@ describe('Room API Tests', () => {
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
},
chat: { enabled: true },
- virtualBackground: { enabled: true }
+ virtualBackground: { enabled: true },
+ e2ee: { enabled: false }
};
const updateResponse = await updateRoomConfig(createdRoom.roomId, partialConfig);
diff --git a/meet-ce/frontend/angular.json b/meet-ce/frontend/angular.json
index 89cf8eda..1ab52503 100644
--- a/meet-ce/frontend/angular.json
+++ b/meet-ce/frontend/angular.json
@@ -24,7 +24,15 @@
"polyfills": ["zone.js"],
"tsConfig": "src/tsconfig.app.json",
"inlineStyleLanguage": "scss",
- "assets": ["src/favicon.ico", "src/assets"],
+ "assets": [
+ "src/favicon.ico",
+ "src/assets",
+ {
+ "glob": "livekit-client.e2ee.worker.mjs",
+ "input": "../../node_modules/livekit-client/dist/",
+ "output": "assets/livekit/"
+ }
+ ],
"styles": ["src/styles.scss"],
"scripts": [],
"preserveSymlinks": true
@@ -186,31 +194,31 @@
}
}
}
- },
- "schematics": {
- "@schematics/angular:component": {
- "type": "component"
- },
- "@schematics/angular:directive": {
- "type": "directive"
- },
- "@schematics/angular:service": {
- "type": "service"
- },
- "@schematics/angular:guard": {
- "typeSeparator": "."
- },
- "@schematics/angular:interceptor": {
- "typeSeparator": "."
- },
- "@schematics/angular:module": {
- "typeSeparator": "."
- },
- "@schematics/angular:pipe": {
- "typeSeparator": "."
- },
- "@schematics/angular:resolver": {
- "typeSeparator": "."
- }
- }
+ },
+ "schematics": {
+ "@schematics/angular:component": {
+ "type": "component"
+ },
+ "@schematics/angular:directive": {
+ "type": "directive"
+ },
+ "@schematics/angular:service": {
+ "type": "service"
+ },
+ "@schematics/angular:guard": {
+ "typeSeparator": "."
+ },
+ "@schematics/angular:interceptor": {
+ "typeSeparator": "."
+ },
+ "@schematics/angular:module": {
+ "typeSeparator": "."
+ },
+ "@schematics/angular:pipe": {
+ "typeSeparator": "."
+ },
+ "@schematics/angular:resolver": {
+ "typeSeparator": "."
+ }
+ }
}
diff --git a/meet-ce/frontend/package.json b/meet-ce/frontend/package.json
index 5556c840..29ce9f27 100644
--- a/meet-ce/frontend/package.json
+++ b/meet-ce/frontend/package.json
@@ -2,6 +2,7 @@
"name": "@openvidu-meet/frontend",
"version": "3.4.1",
"scripts": {
+ "clean": "rm -rf node_modules dist test-results",
"dev": "pnpm exec ng build --configuration development --watch",
"build": "func() { pnpm exec ng build --configuration production --base-href=\"${1:-/}\"; }; func",
"lib:serve": "ng build shared-meet-components --watch",
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-lobby/meeting-lobby.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-lobby/meeting-lobby.component.html
index 3531f4ef..2d5efd5e 100644
--- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-lobby/meeting-lobby.component.html
+++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-lobby/meeting-lobby.component.html
@@ -5,6 +5,12 @@
video_chat
{{ roomName }}
+ @if (isE2EEEnabled) {
+
+ lock
+ This meeting is end-to-end encrypted
+
+ }
@@ -27,6 +33,7 @@
@if (!roomClosed) {