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.
This commit is contained in:
parent
ecef2844a0
commit
d4a87f8a45
@ -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
|
||||
@ -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
|
||||
@ -5,11 +5,20 @@
|
||||
description: >
|
||||
Creates a new OpenVidu Meet room.
|
||||
The room will be available for participants to join using the generated URLs.
|
||||
<br/>
|
||||
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:
|
||||
|
||||
@ -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}'`);
|
||||
|
||||
@ -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<string, unknown>;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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<MeetRoom>} A promise that resolves to the created OpenVidu room.
|
||||
*
|
||||
* @throws {Error} If the room creation fails.
|
||||
*
|
||||
*/
|
||||
async createMeetRoom(roomOptions: MeetRoomOptions): Promise<MeetRoom> {
|
||||
async createMeetRoom(roomOptions: MeetRoomOptions, responseOpts?: GetMeetRoomOptions): Promise<MeetRoom> {
|
||||
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<MeetRoom> {
|
||||
async getMeetRoom(roomId: string, opts?: GetMeetRoomOptions): Promise<MeetRoom> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
// 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
|
||||
|
||||
@ -387,7 +387,32 @@ export const deleteAllUsers = async () => {
|
||||
|
||||
// ROOM HELPERS
|
||||
|
||||
export const createRoom = async (options: MeetRoomOptions = {}, accessToken?: string): Promise<MeetRoom> => {
|
||||
/**
|
||||
* 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<MeetRoom> => {
|
||||
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;
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user