From d4a87f8a458bcb819556e27d4cf5c0daf7ed5a54 Mon Sep 17 00:00:00 2001 From: CSantosM <4a.santos@gmail.com> Date: Fri, 6 Feb 2026 16:39:39 +0100 Subject: [PATCH] Enables response control via headers Adds functionality to control the room creation and retrieval responses using the `X-Fields` and `X-Expand` headers. - `X-Fields` allows clients to specify which fields to include in the response, optimizing bandwidth usage. - `X-Expand` allows clients to request the full data of expandable properties, such as `config`, avoiding subsequent GET requests. This change introduces new request validators, service methods, and helper functions to handle the header logic and process the room objects accordingly. --- .../parameters/x-expand-header.yaml | 19 +++ .../parameters/x-fields-header.yaml | 15 +++ meet-ce/backend/openapi/paths/rooms.yaml | 9 ++ .../src/controllers/room.controller.ts | 38 ++++-- meet-ce/backend/src/helpers/room.helper.ts | 35 +++++- .../room-validator.middleware.ts | 18 ++- .../src/models/zod-schemas/room.schema.ts | 5 + meet-ce/backend/src/services/room.service.ts | 49 ++++---- .../backend/tests/helpers/request-helpers.ts | 36 +++++- .../integration/api/rooms/create-room.test.ts | 74 +++++++++++- .../api/rooms/e2ee-room-config.test.ts | 114 +++++++++++------- .../recording-layout-room-config.test.ts | 6 +- .../api/rooms/update-room-config.test.ts | 56 +++++---- 13 files changed, 367 insertions(+), 107 deletions(-) create mode 100644 meet-ce/backend/openapi/components/parameters/x-expand-header.yaml create mode 100644 meet-ce/backend/openapi/components/parameters/x-fields-header.yaml diff --git a/meet-ce/backend/openapi/components/parameters/x-expand-header.yaml b/meet-ce/backend/openapi/components/parameters/x-expand-header.yaml new file mode 100644 index 00000000..b85ad096 --- /dev/null +++ b/meet-ce/backend/openapi/components/parameters/x-expand-header.yaml @@ -0,0 +1,19 @@ +name: X-Expand +in: header +description: > + Specifies which complex properties to include fully expanded in the response. + + By default, certain large or nested properties (like `config`) are replaced with expandable stubs + to optimize payload size and reduce network bandwidth. + + Use this header to include the full data of these properties in the creation response, + avoiding the need for a subsequent GET request. + + Provide a comma-separated list of property names to expand. +required: false +schema: + type: string +examples: + config: + value: 'config' + summary: Include full room configuration in response diff --git a/meet-ce/backend/openapi/components/parameters/x-fields-header.yaml b/meet-ce/backend/openapi/components/parameters/x-fields-header.yaml new file mode 100644 index 00000000..50123b8c --- /dev/null +++ b/meet-ce/backend/openapi/components/parameters/x-fields-header.yaml @@ -0,0 +1,15 @@ +name: X-Fields +in: header +description: > + Specifies which fields to include in the response for the room resource. + Provide a comma-separated list of field names to filter the response payload. + + This header allows you to optimize API responses by requesting only the data you need, + reducing bandwidth usage and improving performance. +required: false +schema: + type: string +examples: + basic: + value: 'roomId,roomName,accessUrl' + summary: Only return basic room information \ No newline at end of file diff --git a/meet-ce/backend/openapi/paths/rooms.yaml b/meet-ce/backend/openapi/paths/rooms.yaml index 70b63e78..77c67ffc 100644 --- a/meet-ce/backend/openapi/paths/rooms.yaml +++ b/meet-ce/backend/openapi/paths/rooms.yaml @@ -5,11 +5,20 @@ description: > Creates a new OpenVidu Meet room. The room will be available for participants to join using the generated URLs. +
+ You can control the response format using custom headers: + + - `X-Expand`: Include expanded properties (e.g., `config`) instead of stubs + + - `X-Fields`: Filter which fields to include in the response for efficiency tags: - OpenVidu Meet - Rooms security: - apiKeyHeader: [] - accessTokenHeader: [] + parameters: + - $ref: '../components/parameters/x-fields-header.yaml' + - $ref: '../components/parameters/x-expand-header.yaml' requestBody: $ref: '../components/requestBodies/create-room-request.yaml' responses: diff --git a/meet-ce/backend/src/controllers/room.controller.ts b/meet-ce/backend/src/controllers/room.controller.ts index 2d3df7e6..153d112b 100644 --- a/meet-ce/backend/src/controllers/room.controller.ts +++ b/meet-ce/backend/src/controllers/room.controller.ts @@ -2,26 +2,42 @@ import { MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings, MeetRoomDeletionSuccessCode, - MeetRoomFilters, MeetRoomOptions } from '@openvidu-meet/typings'; import { Request, Response } from 'express'; import { container } from '../config/dependency-injector.config.js'; import { INTERNAL_CONFIG } from '../config/internal-config.js'; +import { MeetRoomHelper } from '../helpers/room.helper.js'; import { handleError } from '../models/error.model.js'; +import { MeetRoomExpandableProperties, MeetRoomField, MeetRoomFilters } from '../models/room-request.js'; import { LoggerService } from '../services/logger.service.js'; import { RoomService } from '../services/room.service.js'; import { getBaseUrl } from '../utils/url.utils.js'; +interface RequestWithValidatedHeaders extends Request { + validatedHeaders?: { + 'x-fields'?: MeetRoomField[]; + 'x-expand'?: MeetRoomExpandableProperties[]; + }; +} + export const createRoom = async (req: Request, res: Response) => { const logger = container.get(LoggerService); const roomService = container.get(RoomService); const options: MeetRoomOptions = req.body; + const { validatedHeaders } = req as RequestWithValidatedHeaders; + const fields = validatedHeaders?.['x-fields']; + const expand = validatedHeaders?.['x-expand']; try { logger.verbose(`Creating room with options '${JSON.stringify(options)}'`); - const room = await roomService.createMeetRoom(options); + // Pass response options to service for consistent handling + const room = await roomService.createMeetRoom(options, { + fields, + collapse: MeetRoomHelper.toCollapseProperties(expand) + }); + res.set('Location', `${getBaseUrl()}${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${room.roomId}`); return res.status(201).json(room); } catch (error) { @@ -49,14 +65,22 @@ export const getRoom = async (req: Request, res: Response) => { const logger = container.get(LoggerService); const { roomId } = req.params; - const fields = req.query.fields as string | undefined; - const expand = req.query.expand as string | undefined; + // Zod already validated and transformed to typed arrays + const { fields, expand } = req.query as { + fields?: MeetRoomField[]; + expand?: MeetRoomExpandableProperties[]; + }; try { - logger.verbose(`Getting room '${roomId}' with expand: ${expand || 'none'}`); + logger.verbose(`Getting room '${roomId}' with expand: ${expand?.join(',') || 'none'}`); const roomService = container.get(RoomService); - const room = await roomService.getMeetRoom(roomId, fields, expand, true); + const collapse = MeetRoomHelper.toCollapseProperties(expand); + const room = await roomService.getMeetRoom(roomId, { + fields, + collapse, + checkPermissions: true + }); return res.status(200).json(room); } catch (error) { @@ -134,7 +158,7 @@ export const getRoomConfig = async (req: Request, res: Response) => { logger.verbose(`Getting room config for room '${roomId}'`); try { - const { config } = await roomService.getMeetRoom(roomId); + const { config } = await roomService.getMeetRoom(roomId, { fields: ['config'] }); return res.status(200).json(config); } catch (error) { handleError(res, error, `getting room config for room '${roomId}'`); diff --git a/meet-ce/backend/src/helpers/room.helper.ts b/meet-ce/backend/src/helpers/room.helper.ts index 093a3d8d..c83b2c32 100644 --- a/meet-ce/backend/src/helpers/room.helper.ts +++ b/meet-ce/backend/src/helpers/room.helper.ts @@ -4,7 +4,8 @@ import { MEET_ENV } from '../environment.js'; import { MEET_ROOM_EXPANDABLE_FIELDS, MeetRoomCollapsibleProperties, - MeetRoomExpandableProperties + MeetRoomExpandableProperties, + MeetRoomField } from '../models/room-request.js'; export class MeetRoomHelper { @@ -186,4 +187,36 @@ export class MeetRoomHelper { return collapsedRoom; } + + /** + * Filters a MeetRoom object to include only the specified fields. + * By default, returns the full room object. + * Only filters when fields are explicitly specified. + * + * @param room - The room object to filter + * @param fields - Optional list of fields to include (e.g., ['roomId', 'roomName']) + * @returns A MeetRoom object containing only the specified fields, or the full room if no fields are specified + * @example + * ``` + * // Filter to only roomId and roomName: + * const filtered = MeetRoomHelper.applyFieldsFilter(room, ['roomId', 'roomName']); + * // Result: { roomId: '123', roomName: 'My Room' } + * ``` + */ + static applyFieldsFilter(room: MeetRoom, fields?: MeetRoomField[]): MeetRoom { + // If no fields specified, return the full room + if (!room || !fields || fields.length === 0) { + return room; + } + + const filteredRoom = {} as Record; + + for (const key of Object.keys(room)) { + if (fields.includes(key as MeetRoomField)) { + filteredRoom[key] = room[key as keyof MeetRoom]; + } + } + + return filteredRoom as unknown as MeetRoom; + } } diff --git a/meet-ce/backend/src/middlewares/request-validators/room-validator.middleware.ts b/meet-ce/backend/src/middlewares/request-validators/room-validator.middleware.ts index d2a4be55..9583558b 100644 --- a/meet-ce/backend/src/middlewares/request-validators/room-validator.middleware.ts +++ b/meet-ce/backend/src/middlewares/request-validators/room-validator.middleware.ts @@ -2,6 +2,7 @@ import { NextFunction, Request, Response } from 'express'; import { rejectUnprocessableRequest } from '../../models/error.model.js'; import { BulkDeleteRoomsReqSchema, + CreateRoomHeadersSchema, DeleteRoomReqSchema, GetRoomQuerySchema, nonEmptySanitizedRoomId, @@ -14,13 +15,22 @@ import { } from '../../models/zod-schemas/room.schema.js'; export const validateCreateRoomReq = (req: Request, res: Response, next: NextFunction) => { - const { success, error, data } = RoomOptionsSchema.safeParse(req.body); + const bodyResult = RoomOptionsSchema.safeParse(req.body); - if (!success) { - return rejectUnprocessableRequest(res, error); + if (!bodyResult.success) { + return rejectUnprocessableRequest(res, bodyResult.error); } - req.body = data; + // Validate X-Fields and X-Expand headers + const headersResult = CreateRoomHeadersSchema.safeParse(req.headers); + + if (!headersResult.success) { + return rejectUnprocessableRequest(res, headersResult.error); + } + + req.body = bodyResult.data; + // Store validated headers in a custom property for controller access + (req as any).validatedHeaders = headersResult.data; next(); }; diff --git a/meet-ce/backend/src/models/zod-schemas/room.schema.ts b/meet-ce/backend/src/models/zod-schemas/room.schema.ts index 3e418bac..2c14e9d4 100644 --- a/meet-ce/backend/src/models/zod-schemas/room.schema.ts +++ b/meet-ce/backend/src/models/zod-schemas/room.schema.ts @@ -450,6 +450,11 @@ export const GetRoomQuerySchema = z.object({ expand: expandSchema }); +export const CreateRoomHeadersSchema = z.object({ + 'x-fields': fieldsSchema, + 'x-expand': expandSchema +}); + export const DeleteRoomReqSchema = z.object({ withMeeting: RoomDeletionPolicyWithMeetingSchema.optional().default(MeetRoomDeletionPolicyWithMeeting.FAIL), withRecordings: RoomDeletionPolicyWithRecordingsSchema.optional().default(MeetRoomDeletionPolicyWithRecordings.FAIL) diff --git a/meet-ce/backend/src/services/room.service.ts b/meet-ce/backend/src/services/room.service.ts index 701ac84c..d5be46d8 100644 --- a/meet-ce/backend/src/services/room.service.ts +++ b/meet-ce/backend/src/services/room.service.ts @@ -8,7 +8,6 @@ import { MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings, MeetRoomDeletionSuccessCode, - MeetRoomFilters, MeetRoomMemberPermissions, MeetRoomOptions, MeetRoomRoles, @@ -35,6 +34,7 @@ import { internalError, OpenViduMeetError } from '../models/error.model.js'; +import { GetMeetRoomOptions, MeetRoomFilters } from '../models/room-request.js'; import { RoomMemberRepository } from '../repositories/room-member.repository.js'; import { RoomRepository } from '../repositories/room.repository.js'; import { FrontendEventService } from './frontend-event.service.js'; @@ -71,13 +71,15 @@ export class RoomService { * Creates an OpenVidu Meet room with the specified options. * * @param {MeetRoomOptions} roomOptions - The options for creating the OpenVidu room. + * @param {GetMeetRoomOptions} responseOpts - Options for controlling the response format (fields, collapse) * @returns {Promise} A promise that resolves to the created OpenVidu room. * * @throws {Error} If the room creation fails. * */ - async createMeetRoom(roomOptions: MeetRoomOptions): Promise { + async createMeetRoom(roomOptions: MeetRoomOptions, responseOpts?: GetMeetRoomOptions): Promise { const { roomName, autoDeletionDate, autoDeletionPolicy, config, roles, anonymous } = roomOptions; + const { collapse, fields } = responseOpts || {}; // Generate a unique room ID based on the room name const roomIdPrefix = MeetRoomHelper.createRoomIdPrefixFromRoomName(roomName!) || 'room'; @@ -159,10 +161,12 @@ export class RoomService { rolesUpdatedAt: now, meetingEndAction: MeetingEndAction.NONE }; - const room = await this.roomRepository.create(meetRoom); + let room = await this.roomRepository.create(meetRoom); - // Avoid include full config in payload - return MeetRoomHelper.processRoomExpandProperties(room, ''); + room = MeetRoomHelper.applyCollapseProperties(room, collapse); + room = MeetRoomHelper.applyFieldsFilter(room, fields); + + return room; } /** @@ -222,7 +226,9 @@ export class RoomService { } await this.roomRepository.update(room); - // Send signal to frontend + // Send signal to frontend. + // Note: Rooms updates are not allowed during active meetings, so we don't need to send an immediate update signal to participants, + // as they will receive the updated config when they join the meeting or when the meeting is restarted. // await this.frontendEventService.sendRoomConfigUpdatedSignal(roomId, room); return room; } @@ -345,8 +351,8 @@ export class RoomService { const response = await this.roomRepository.find(queryOptions); - // Process rooms with expand logic - response.rooms = response.rooms.map((room) => MeetRoomHelper.processRoomExpandProperties(room, filters.expand)); + const collapse = MeetRoomHelper.toCollapseProperties(filters.expand); + response.rooms = response.rooms.map((room) => MeetRoomHelper.applyCollapseProperties(room, collapse)); return response; } @@ -405,15 +411,17 @@ export class RoomService { } /** - * Retrieves an OpenVidu room by its name. + * Retrieves a specific meeting room by its unique identifier. * * @param roomId - The name of the room to retrieve. - * @param fields - Optional fields to retrieve from the room. - * @param expand - Optional comma-separated list of properties to expand. - * @param checkPermissions - Whether to check permissions and remove sensitive properties. Defaults to false. - * @returns A promise that resolves to an {@link MeetRoom} object (with expandable properties as stubs when not expanded). + * @param opts - Optional parameters for retrieving the room: + * - fields: Array of fields to retrieve from the room + * - collapse: {@link MeetRoomCollapsible} list of properties to collapse into {@link ExpandableStub} + * - checkPermissions: Whether to check permissions and remove sensitive properties + * @returns A promise that resolves to an {@link MeetRoom} object */ - async getMeetRoom(roomId: string, fields?: string, expand?: string, checkPermissions = false): Promise { + async getMeetRoom(roomId: string, opts?: GetMeetRoomOptions): Promise { + const { collapse, checkPermissions, fields } = opts || {}; const room = await this.roomRepository.findByRoomId(roomId, fields); if (!room) { @@ -430,8 +438,7 @@ export class RoomService { } } - // Process expand - return MeetRoomHelper.processRoomExpandProperties(room, expand); + return MeetRoomHelper.applyCollapseProperties(room, collapse); } /** @@ -458,7 +465,7 @@ export class RoomService { ); // Check if there's an active meeting in the room and/or if it has recordings associated - const room = await this.getMeetRoom(roomId); + const room = await this.getMeetRoom(roomId, { fields: ['status'] }); const hasActiveMeeting = room.status === MeetRoomStatus.ACTIVE_MEETING; const hasRecordings = await this.recordingService.hasRoomRecordings(roomId); @@ -479,7 +486,7 @@ export class RoomService { hasRecordings, withMeeting, withRecordings, - updatedRoom + MeetRoomHelper.applyCollapseProperties(updatedRoom!, ['config']) ); } catch (error) { this.logger.error(`Error deleting room '${roomId}': ${error}`); @@ -851,7 +858,7 @@ export class RoomService { * @throws Error if room not found */ async isRoomOwner(roomId: string, userId: string): Promise { - const room = await this.getMeetRoom(roomId); + const room = await this.getMeetRoom(roomId, { fields: ['owner'] }); return room.owner === userId; } @@ -864,7 +871,7 @@ export class RoomService { * @throws Error if room not found */ async isValidRoomSecret(roomId: string, secret: string): Promise { - const room = await this.getMeetRoom(roomId); + const room = await this.getMeetRoom(roomId, { fields: ['anonymous'] }); const { moderatorSecret, speakerSecret } = MeetRoomHelper.extractSecretsFromRoom(room); return secret === moderatorSecret || secret === speakerSecret; } @@ -928,7 +935,7 @@ export class RoomService { */ async canUserAccessRoom(roomId: string, user: MeetUser): Promise { // Verify room exists first (throws 404 if not found) - const room = await this.getMeetRoom(roomId); + const room = await this.getMeetRoom(roomId, { fields: ['owner'] }); if (user.role === MeetUserRole.ADMIN) { // Admins can access all rooms diff --git a/meet-ce/backend/tests/helpers/request-helpers.ts b/meet-ce/backend/tests/helpers/request-helpers.ts index 5c2ec9cf..76900b3d 100644 --- a/meet-ce/backend/tests/helpers/request-helpers.ts +++ b/meet-ce/backend/tests/helpers/request-helpers.ts @@ -387,7 +387,32 @@ export const deleteAllUsers = async () => { // ROOM HELPERS -export const createRoom = async (options: MeetRoomOptions = {}, accessToken?: string): Promise => { +/** + * Creates a room with the specified options and optional headers for response customization. + * + * @param options - Room creation options (roomName, config, etc.) + * @param accessToken - Optional access token for authentication (uses API key if not provided) + * @param headers - Optional headers object supporting: + * - xFields: Comma-separated list of fields to include (e.g., 'roomId,roomName') + * - xExpand: Comma-separated list of properties to expand (e.g., 'config') + * @returns A Promise that resolves to the created MeetRoom + * @example + * ``` + * // Create room with default collapsed config + * const room = await createRoom({ roomName: 'Test' }); + * + * // Create room with specific fields only + * const room = await createRoom({ roomName: 'Test' }, undefined, { xFields: 'roomId,roomName' }); + * + * // Create room with expanded config + * const room = await createRoom({ roomName: 'Test' }, undefined, { xExpand: 'config' }); + * ``` + */ +export const createRoom = async ( + options: MeetRoomOptions = {}, + accessToken?: string, + headers?: { xFields?: string; xExpand?: string } +): Promise => { checkAppIsRunning(); const req = request(app) @@ -401,6 +426,15 @@ export const createRoom = async (options: MeetRoomOptions = {}, accessToken?: st req.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY); } + // Add optional headers for response customization + if (headers?.xFields) { + req.set('x-fields', headers.xFields); + } + + if (headers?.xExpand) { + req.set('x-expand', headers.xExpand); + } + const response = await req; return response.body; }; 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 abe90e45..04d983e5 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 @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; import { MeetRecordingAudioCodec, @@ -168,6 +169,56 @@ describe('Room API Tests', () => { validAutoDeletionDate ); }); + + it('should create a room with collapsed config by default', async () => { + const room = await createRoom({ + roomName: 'Collapsed Config Room' + }); + + expect(room.config).toBeDefined(); + expect((room.config as any)._expandable).toBe(true); + expect((room.config as any)._href).toBe( + `${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${room.roomId}?expand=config` + ); + }); + + it('should expand config when x-Expand header is provided', async () => { + const room = await createRoom( + { + roomName: 'Collapsed Config Room' + }, + undefined, + { xExpand: 'config' } + ); + + expect(room.config).toBeDefined(); + expect((room.config as any)._expandable).toBeUndefined(); + expect((room.config as any)._href).toBeUndefined(); + expect(room.config.recording.layout).toBe(DEFAULT_RECORDING_LAYOUT); + }); + + it('should filter fields when x-Field header is provided', async () => { + const room = await createRoom(undefined, undefined, { xFields: 'roomName' }); + + expect(room.roomName).toBeDefined(); + expect(room.roomId).toBeUndefined(); + expect(room.config).toBeUndefined(); + }); + + it('should filter fields and expand config when both xFields and xExpand are provided', async () => { + const room = await createRoom(undefined, undefined, { xFields: 'config', xExpand: 'config' }); + + expect(room.roomName).toBeUndefined(); + expect(room.config).toBeDefined(); + expect((room.config as any)._expandable).toBeUndefined(); + }); + + it('should not includes config if filter fields are provided without config', async () => { + const room = await createRoom(undefined, undefined, { xFields: 'roomName', xExpand: 'config' }); + + expect(room.roomName).toBeDefined(); + expect(room.config).toBeUndefined(); + }); }); describe('Room Name Sanitization Tests', () => { @@ -457,11 +508,32 @@ describe('Room API Tests', () => { }; expectValidRoom(room, 'Full Advanced Encoding Room', 'full_advanced_encoding_room', 'expandable'); const response = await getRoom(room.roomId, undefined, 'config'); - expectValidRoom(response.body, 'Full Advanced Encoding Room', 'full_advanced_encoding_room', expectedConfig); + expectValidRoom( + response.body, + 'Full Advanced Encoding Room', + 'full_advanced_encoding_room', + expectedConfig + ); }); }); describe('Room Creation Validation failures', () => { + it('should fail when x-Expand header has invalid value', async () => { + const payload = { + roomName: 'Test Room with Invalid Expand Header' + }; + + const response = await request(app) + .post(ROOMS_PATH) + .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY) + .set('x-Expand', 'invalidField') + .send(payload) + .expect(422); + + expect(response.body.error).toContain('Unprocessable Entity'); + expect(JSON.stringify(response.body.details)).toContain('Invalid expand properties.'); + }); + it('should fail when autoDeletionDate is negative', async () => { const payload = { autoDeletionDate: -5000, 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 index 844cbcfd..43f2678a 100644 --- 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 @@ -4,7 +4,6 @@ import { Express } from 'express'; import request from 'supertest'; import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js'; import { MEET_ENV } from '../../../../src/environment.js'; -import { expectValidRoom } from '../../../helpers/assertion-helpers.js'; import { createRoom, deleteAllRecordings, @@ -33,11 +32,19 @@ describe('E2EE Room Configuration Tests', () => { describe('E2EE Default Configuration', () => { it('Should create a room with E2EE disabled by default', async () => { - const room = await createRoom({ - roomName: 'Test E2EE Default' - }); + const room = await createRoom( + { + roomName: 'Test E2EE Default' + }, + undefined, + { xExpand: 'config' } + ); - expectValidRoom(room, 'Test E2EE Default'); + // Validate room structure (skip config validation in expectValidRoom since we're checking it below) + expect(room).toBeDefined(); + expect(room.roomName).toBe('Test E2EE Default'); + expect(room.roomId).toBeDefined(); + expect(room.config).toBeDefined(); expect(room.config.e2ee).toBeDefined(); expect(room.config.e2ee.enabled).toBe(false); }); @@ -58,7 +65,7 @@ describe('E2EE Room Configuration Tests', () => { } }; - const room = await createRoom(payload); + const room = await createRoom(payload, undefined, { xFields: 'roomName,config', xExpand: 'config' }); expect(room.roomName).toBe('Test E2EE Enabled'); expect(room.config.e2ee.enabled).toBe(true); @@ -84,18 +91,22 @@ describe('E2EE Room Configuration Tests', () => { 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 - }, - chat: { enabled: true }, - virtualBackground: { enabled: true }, - e2ee: { enabled: false }, - captions: { enabled: true } - } - }); + const room = await createRoom( + { + roomName: 'Test E2EE Update', + config: { + recording: { + enabled: true + }, + chat: { enabled: true }, + virtualBackground: { enabled: true }, + e2ee: { enabled: false }, + captions: { enabled: true } + } + }, + undefined, + { xExpand: 'config' } + ); expect(room.config.recording.enabled).toBe(true); expect(room.config.e2ee?.enabled).toBe(false); @@ -167,9 +178,13 @@ describe('E2EE Room Configuration Tests', () => { 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' - }); + const room = await createRoom( + { + roomName: 'Test E2EE Update Enabled' + }, + undefined, + { xExpand: 'config' } + ); expect(room.config.e2ee.enabled).toBe(false); @@ -198,35 +213,44 @@ describe('E2EE Room Configuration Tests', () => { it('Should return E2EE configuration when listing rooms', async () => { await deleteAllRooms(); - const room1 = await createRoom({ - roomName: 'E2EE Enabled Room', - config: { - recording: { - enabled: false - }, - chat: { enabled: true }, - virtualBackground: { enabled: true }, - e2ee: { enabled: true }, - captions: { enabled: true } - } - }); + const room1 = await createRoom( + { + roomName: 'E2EE Enabled Room', + config: { + recording: { + enabled: false + }, + chat: { enabled: true }, + virtualBackground: { enabled: true }, + e2ee: { enabled: true }, + captions: { enabled: true } + } + }, + undefined, + { xFields: 'roomId' } + ); - const room2 = await createRoom({ - roomName: 'E2EE Disabled Room', - config: { - recording: { - enabled: true - }, - chat: { enabled: true }, - virtualBackground: { enabled: true }, - e2ee: { enabled: false }, - captions: { enabled: true } - } - }); + const room2 = await createRoom( + { + roomName: 'E2EE Disabled Room', + config: { + recording: { + enabled: true + }, + chat: { enabled: true }, + virtualBackground: { enabled: true }, + e2ee: { enabled: false }, + captions: { enabled: true } + } + }, + undefined, + { xFields: 'roomId' } + ); const response = await request(app) .get(ROOMS_PATH) .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY) + .query({ expand: 'config' }) .expect(200); // Filter out any rooms from other test suites diff --git a/meet-ce/backend/tests/integration/api/rooms/recording-layout-room-config.test.ts b/meet-ce/backend/tests/integration/api/rooms/recording-layout-room-config.test.ts index a450c9a4..4721e93d 100644 --- a/meet-ce/backend/tests/integration/api/rooms/recording-layout-room-config.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/recording-layout-room-config.test.ts @@ -26,7 +26,7 @@ describe('Room API Tests', () => { } }; - const room = await createRoom(payload); + const room = await createRoom(payload, undefined, { xExpand: 'config' }); const expectedConfig = { recording: { @@ -53,7 +53,7 @@ describe('Room API Tests', () => { } }; - const room = await createRoom(payload); + const room = await createRoom(payload, undefined, { xExpand: 'config' }); const expectedConfig = { recording: { @@ -80,7 +80,7 @@ describe('Room API Tests', () => { } }; - const room = await createRoom(payload); + const room = await createRoom(payload, undefined, { xExpand: 'config' }); const expectedConfig = { recording: { diff --git a/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts b/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts index 99e9f808..a1e362bf 100644 --- a/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts @@ -60,7 +60,7 @@ describe('Room API Tests', () => { expect(updateResponse.body).toHaveProperty('message'); // Verify with a get request - const getResponse = await getRoom(createdRoom.roomId); + const getResponse = await getRoom(createdRoom.roomId, 'config', 'config'); expect(getResponse.status).toBe(200); expect(getResponse.body.config).toEqual({ ...updatedConfig, @@ -97,7 +97,7 @@ describe('Room API Tests', () => { expect(updateResponse.body).toHaveProperty('message'); // Verify with a get request - const getResponse = await getRoom(createdRoom.roomId); + const getResponse = await getRoom(createdRoom.roomId, undefined, 'config'); expect(getResponse.status).toBe(200); const expectedConfig: MeetRoomConfig = { @@ -179,7 +179,7 @@ describe('Room API Tests', () => { expect(updateResponse.status).toBe(200); // Verify with a get request - const getResponse = await getRoom(createdRoom.roomId); + const getResponse = await getRoom(createdRoom.roomId, 'config', 'config'); expect(getResponse.status).toBe(200); expect(getResponse.body.config.recording.encoding).toBe(MeetRecordingEncodingPreset.H264_1080P_60); }); @@ -221,12 +221,12 @@ describe('Room API Tests', () => { expect(updateResponse.status).toBe(200); // Verify with a get request - const getResponse = await getRoom(createdRoom.roomId); + const getResponse = await getRoom(createdRoom.roomId, undefined, 'config'); expect(getResponse.status).toBe(200); expect(getResponse.body.config.recording.encoding).toMatchObject(updatedConfig.recording.encoding); }); - it('should update room encoding from advanced options to preset', async () => { + it('should update room encoding config from advanced options to preset', async () => { const recordingEncoding = { video: { width: 1280, @@ -243,15 +243,19 @@ describe('Room API Tests', () => { frequency: 44100 } }; - const createdRoom = await createRoom({ - roomName: 'advanced-to-preset', - config: { - recording: { - enabled: true, - encoding: recordingEncoding + const createdRoom = await createRoom( + { + roomName: 'advanced-to-preset', + config: { + recording: { + enabled: true, + encoding: recordingEncoding + } } - } - }); + }, + undefined, + { xExpand: 'config' } + ); expect(createdRoom.config.recording.encoding).toMatchObject(recordingEncoding); // Update to preset encoding @@ -266,22 +270,26 @@ describe('Room API Tests', () => { expect(updateResponse.status).toBe(200); // Verify with a get request - const getResponse = await getRoom(createdRoom.roomId); + const getResponse = await getRoom(createdRoom.roomId, undefined, 'config'); expect(getResponse.status).toBe(200); expect(getResponse.body.config.recording.encoding).toBe(MeetRecordingEncodingPreset.PORTRAIT_H264_1080P_60); }); it('should update only encoding while keeping other recording config', async () => { - const createdRoom = await createRoom({ - roomName: 'partial-encoding-update', - config: { - recording: { - enabled: true, - layout: MeetRecordingLayout.SPEAKER, - encoding: MeetRecordingEncodingPreset.H264_720P_30 + const createdRoom = await createRoom( + { + roomName: 'partial-encoding-update', + config: { + recording: { + enabled: true, + layout: MeetRecordingLayout.SPEAKER, + encoding: MeetRecordingEncodingPreset.H264_720P_30 + } } - } - }); + }, + undefined, + { xExpand: 'config' } + ); expect(createdRoom.config.recording.layout).toBe(MeetRecordingLayout.SPEAKER); expect(createdRoom.config.recording.encoding).toBe(MeetRecordingEncodingPreset.H264_720P_30); @@ -298,7 +306,7 @@ describe('Room API Tests', () => { expect(updateResponse.status).toBe(200); // Verify with a get request - const getResponse = await getRoom(createdRoom.roomId); + const getResponse = await getRoom(createdRoom.roomId, 'config', 'config'); expect(getResponse.status).toBe(200); const expectedConfig = {