openvidu-meet/meet-ce/backend/tests/helpers/assertion-helpers.ts
CSantosM accb35c7e1 Adds recording encoding options to room config and start recording
Adds configuration options for recording encoding, including presets and advanced settings, allowing users to customize video and audio quality.

This enhancement introduces new schemas for recording encoding presets and advanced options, enabling users to select from predefined encoding profiles or fine-tune specific video and audio parameters.

A conversion helper is implemented to translate between the internal encoding configurations and the format required by the LiveKit SDK.

backend: Adds recording encoding configuration options

Allows users to specify custom audio and video encoding settings for recordings, overriding room defaults.

This enhancement provides greater flexibility in controlling recording quality and file size. It introduces new schema definitions for encoding options and validates these configurations through Zod schemas.

Enforces complete video/audio encoding options

Requires both video and audio configurations with all their properties
when using advanced encoding options for recordings. This change ensures
complete encoding setups and prevents potential recording failures due to
missing encoding parameters. It also corrects a typo of keyframeInterval.

Add video depth option to recording encoding settings
2026-02-02 17:00:01 +01:00

733 lines
24 KiB
TypeScript

import { expect } from '@jest/globals';
import {
MeetingEndAction,
MeetRecordingAccess,
MeetRecordingEncodingOptions,
MeetRecordingEncodingPreset,
MeetRecordingInfo,
MeetRecordingLayout,
MeetRecordingStatus,
MeetRoom,
MeetRoomAutoDeletionPolicy,
MeetRoomConfig,
MeetRoomDeletionPolicyWithMeeting,
MeetRoomDeletionPolicyWithRecordings,
MeetRoomMemberPermissions,
MeetRoomMemberRole,
MeetRoomStatus
} from '@openvidu-meet/typings';
import { Response } from 'supertest';
import { container } from '../../src/config/dependency-injector.config';
import { INTERNAL_CONFIG } from '../../src/config/internal-config';
import { TokenService } from '../../src/services/token.service';
export const DEFAULT_RECORDING_ENCODING_PRESET = MeetRecordingEncodingPreset.H264_720P_30;
export const DEFAULT_RECORDING_LAYOUT = MeetRecordingLayout.GRID;
export const expectErrorResponse = (
response: Response,
status = 422,
error = 'Unprocessable Entity',
message = 'Invalid request',
details?: Array<{ field?: string; message: string }>
) => {
expect(response.status).toBe(status);
expect(response.body).toMatchObject({
...(error ? { error } : {}),
...(message ? { message } : {})
});
if (details === undefined) {
expect(response.body.details).toBeUndefined();
return;
}
expect(Array.isArray(response.body.details)).toBe(true);
expect(response.body.details).toEqual(
expect.arrayContaining(
details.map((d) => {
const matcher: any = { message: expect.stringContaining(d.message) };
if (d.field !== undefined) {
matcher.field = d.field;
}
return expect.objectContaining(matcher);
})
)
);
};
export const expectValidationError = (response: Response, field: string, message: string) => {
expectErrorResponse(response, 422, 'Unprocessable Entity', 'Invalid request', [{ field, message }]);
};
/**
* Asserts that a rooms response matches the expected values for testing purposes.
* Validates the room array length and pagination properties.
*
* @param body - The API response body to validate
* @param expectedRoomLength - The expected number of rooms in the response
* @param expectedMaxItems - The expected maximum number of items in pagination
* @param expectedTruncated - The expected value for pagination.isTruncated flag
* @param expectedNextPageToken - The expected presence of pagination.nextPageToken
* (if true, expects nextPageToken to be defined;
* if false, expects nextPageToken to be undefined)
*/
export const expectSuccessRoomsResponse = (
response: Response,
expectedRoomLength: number,
expectedMaxItems: number,
expectedTruncated: boolean,
expectedNextPageToken: boolean
) => {
const { body } = response;
expect(response.status).toBe(200);
expect(body).toBeDefined();
expect(body.rooms).toBeDefined();
expect(Array.isArray(body.rooms)).toBe(true);
expect(body.rooms.length).toBe(expectedRoomLength);
expect(body.pagination).toBeDefined();
expect(body.pagination.isTruncated).toBe(expectedTruncated);
expectedNextPageToken
? expect(body.pagination.nextPageToken).toBeDefined()
: expect(body.pagination.nextPageToken).toBeUndefined();
expect(body.pagination.maxItems).toBe(expectedMaxItems);
};
export const expectSuccessRoomResponse = (
response: Response,
roomName: string,
roomIdPrefix?: string,
autoDeletionDate?: number,
config?: MeetRoomConfig
) => {
expect(response.status).toBe(200);
expectValidRoom(response.body, roomName, roomIdPrefix, config, autoDeletionDate);
};
export const expectSuccessRoomConfigResponse = (response: Response, config: MeetRoomConfig) => {
expect(response.status).toBe(200);
expect(response.body).toBeDefined();
expect(response.body).toEqual(config);
};
export const expectValidRoom = (
room: MeetRoom,
name: string,
roomIdPrefix?: string,
config?: MeetRoomConfig,
autoDeletionDate?: number,
autoDeletionPolicy?: MeetRoomAutoDeletionPolicy,
status?: MeetRoomStatus,
meetingEndAction?: MeetingEndAction
) => {
expect(room).toBeDefined();
expect(room.roomId).toBeDefined();
expect(room.roomName).toBeDefined();
expect(room.roomName).toBe(name);
expect(room.roomId).not.toBe('');
if (roomIdPrefix) {
expect(room.roomId.startsWith(roomIdPrefix)).toBe(true);
}
expect(room.creationDate).toBeDefined();
if (autoDeletionDate !== undefined) {
expect(room.autoDeletionDate).toBeDefined();
expect(room.autoDeletionDate).toBe(autoDeletionDate);
} else {
expect(room.autoDeletionDate).toBeUndefined();
}
if (autoDeletionPolicy !== undefined) {
expect(room.autoDeletionPolicy).toBeDefined();
expect(room.autoDeletionPolicy).toEqual(autoDeletionPolicy);
} else {
expect(room.autoDeletionPolicy).toEqual({
withMeeting: MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS,
withRecordings: MeetRoomDeletionPolicyWithRecordings.CLOSE
});
}
expect(room.config).toBeDefined();
if (config !== undefined) {
// Use toMatchObject to allow encoding defaults to be added without breaking tests
expect(room.config).toMatchObject(config as any);
} else {
expect(room.config).toEqual({
recording: {
enabled: true,
layout: DEFAULT_RECORDING_LAYOUT,
encoding: DEFAULT_RECORDING_ENCODING_PRESET,
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
},
chat: { enabled: true },
virtualBackground: { enabled: true },
e2ee: { enabled: false },
captions: { enabled: true }
});
}
expect(room.moderatorUrl).toBeDefined();
expect(room.speakerUrl).toBeDefined();
expect(room.moderatorUrl).toContain(room.roomId);
expect(room.speakerUrl).toContain(room.roomId);
expect(room.status).toBeDefined();
expect(room.status).toEqual(status || MeetRoomStatus.OPEN);
expect(room.meetingEndAction).toBeDefined();
expect(room.meetingEndAction).toEqual(meetingEndAction || MeetingEndAction.NONE);
};
export const expectValidRecording = (
recording: MeetRecordingInfo,
recordingId: string,
roomId: string,
roomName: string,
status: MeetRecordingStatus
) => {
expect(recording).toBeDefined();
expect(recording.recordingId).toBeDefined();
expect(recording.roomId).toBeDefined();
expect(recording.roomName).toBeDefined();
expect(recording.recordingId).toBe(recordingId);
expect(recording.roomId).toBe(roomId);
expect(recording.roomName).toBe(roomName);
expect(recording.startDate).toBeDefined();
expect(recording.status).toBeDefined();
expect(recording.status).toBe(status);
expect(recording.filename).toBeDefined();
expect(recording.details).toBeDefined();
expect(recording.layout).toBeDefined();
// Validate layout is a valid value
if (recording.layout !== undefined) {
expect(Object.values(MeetRecordingLayout)).toContain(recording.layout);
}
// Validate encoding is present and has a valid value
expect(recording.encoding).toBeDefined();
if (recording.encoding !== undefined) {
if (typeof recording.encoding === 'string') {
// Encoding preset: should match the default H264_720P_30
expect(recording.encoding).toBe('H264_720P_30');
} else {
// Advanced encoding options: should have valid codec values
expect(typeof recording.encoding).toBe('object');
const encodingObj = recording.encoding as MeetRecordingEncodingOptions;
if (encodingObj.video?.codec) {
expect(['H264_BASELINE', 'H264_MAIN', 'H264_HIGH', 'VP8']).toContain(encodingObj.video.codec);
}
if (encodingObj.audio?.codec) {
expect(['OPUS', 'AAC']).toContain(encodingObj.audio.codec);
}
}
}
};
export const expectValidRoomWithFields = (room: MeetRoom, fields: string[] = []) => {
expect(room).toBeDefined();
expectObjectFields(room, fields);
};
export const expectValidRecordingWithFields = (rec: MeetRecordingInfo, fields: string[] = []) => {
expect(rec).toBeDefined();
expectObjectFields(rec, fields);
};
const expectObjectFields = (obj: unknown, present: string[] = [], absent: string[] = []) => {
present.forEach((key) => {
expect(obj).toHaveProperty(key);
expect((obj as any)[key]).not.toBeUndefined();
});
absent.forEach((key) => {
// Si la propiedad existe, debe ser undefined
expect(Object.prototype.hasOwnProperty.call(obj, key) ? (obj as any)[key] : undefined).toBeUndefined();
});
};
// Validate recording location header in the response
export const expectValidRecordingLocationHeader = (response: Response) => {
const locationHeader = response.headers.location;
expect(locationHeader).toBeDefined();
const locationHeaderUrl = new URL(locationHeader);
expect(locationHeaderUrl.pathname).toBe(
`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${response.body.recordingId}`
);
};
/**
* Validates a successful recording media response, supporting edge cases and range requests.
*
* @param response - The HTTP response object to validate
* @param range - Optional range header that was sent in the request
* @param fullSize - Optional total file size for range validation
* @param options - Optional configuration to handle edge cases:
* - allowSizeDifference: Allows a difference between content-length and actual body size (default: false)
* - ignoreRangeFormat: Ignores exact range format checking (useful for adjusted ranges) (default: false)
* - expectedStatus: Override the expected status code (default: auto-determined based on range)
*/
export const expectSuccessRecordingMediaResponse = (
response: Response,
range?: string,
fullSize?: number,
options?: {
allowSizeDifference?: boolean;
ignoreRangeFormat?: boolean;
expectedStatus?: number;
}
) => {
// Default options
const opts = {
allowSizeDifference: false,
ignoreRangeFormat: false,
...options
};
// Determine expected status
const expectedStatus = opts.expectedStatus ?? (range ? 206 : 200);
// Basic validations for any successful response
expect(response.status).toBe(expectedStatus);
expect(response.headers['content-type']).toBe('video/mp4');
expect(response.headers['accept-ranges']).toBe('bytes');
expect(response.headers['content-length']).toBeDefined();
expect(parseInt(response.headers['content-length'])).toBeGreaterThan(0);
expect(response.headers['cache-control']).toBeDefined();
// Verify response is binary data with some size
expect(response.body).toBeInstanceOf(Buffer);
expect(response.body.length).toBeGreaterThan(0);
// Handle range responses (206 Partial Content)
if (range && expectedStatus === 206) {
// Verify the content-range header
expect(response.headers['content-range']).toBeDefined();
// If ignoreRangeFormat is true, only check the format of the content-range header
if (opts.ignoreRangeFormat) {
expect(response.headers['content-range']).toMatch(/^bytes \d+-\d+\/\d+$/);
if (fullSize) {
// Verify the total size in content-range header
const totalSizeMatch = response.headers['content-range'].match(/\/(\d+)$/);
if (totalSizeMatch) {
expect(parseInt(totalSizeMatch[1])).toBe(fullSize);
}
}
} else {
// Extract the requested range from the request header
const rangeMatch = range.match(/^bytes=(\d+)-(\d*)$/);
if (!rangeMatch) {
throw new Error(`Invalid range format: ${range}`);
}
const requestedStart = parseInt(rangeMatch[1]);
const requestedEnd = rangeMatch[2] ? parseInt(rangeMatch[2]) : fullSize ? fullSize - 1 : undefined;
expect(requestedStart).not.toBeNaN();
// Verify the range in the response
const contentRangeMatch = response.headers['content-range'].match(/^bytes (\d+)-(\d+)\/(\d+)$/);
if (!contentRangeMatch) {
throw new Error(`Invalid content-range format: ${response.headers['content-range']}`);
}
const actualStart = parseInt(contentRangeMatch[1]);
const actualEnd = parseInt(contentRangeMatch[2]);
const actualTotal = parseInt(contentRangeMatch[3]);
// Verify the start matches
expect(actualStart).toBe(requestedStart);
// If full size is provided, verify the total is correct
if (fullSize) {
expect(actualTotal).toBe(fullSize);
// The end may be adjusted if it exceeds the total size
if (requestedEnd !== undefined && requestedEnd >= fullSize) {
expect(actualEnd).toBe(fullSize - 1);
} else if (requestedEnd !== undefined) {
expect(actualEnd).toBe(requestedEnd);
}
}
}
// Verify that Content-Length is consistent
const declaredLength = parseInt(response.headers['content-length']);
expect(declaredLength).toBeGreaterThan(0);
// If size differences are not allowed, body length must match exactly
if (!opts.allowSizeDifference) {
expect(response.body.length).toBe(declaredLength);
} else {
// Allow some difference but ensure it's within a reasonable tolerance
const bodyLength = response.body.length;
const diff = Math.abs(bodyLength - declaredLength);
const tolerance = Math.max(declaredLength * 0.05, 10); // 5% or at least 10 bytes
expect(diff).toBeLessThanOrEqual(tolerance);
}
} else if (expectedStatus === 200) {
// For full content responses
const declaredLength = parseInt(response.headers['content-length']);
if (!opts.allowSizeDifference) {
expect(response.body.length).toBe(declaredLength);
}
// If full size is provided, content-length must match
if (fullSize !== undefined) {
expect(declaredLength).toBe(fullSize);
}
}
};
export const expectValidStartRecordingResponse = (
response: Response,
roomId: string,
roomName: string,
expectedLayout?: MeetRecordingLayout,
expectedEncoding?: MeetRecordingEncodingPreset | MeetRecordingEncodingOptions
) => {
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('recordingId');
expectValidRecordingLocationHeader(response);
const recordingId = response.body.recordingId;
expect(recordingId).toBeDefined();
expect(recordingId).toContain(roomId);
expect(response.body).toHaveProperty('roomId', roomId);
expect(response.body).toHaveProperty('roomName', roomName);
expect(response.body).toHaveProperty('startDate');
expect(response.body).toHaveProperty('status', 'active');
expect(response.body).toHaveProperty('filename');
expect(response.body).toHaveProperty('layout');
expect(response.body).not.toHaveProperty('duration');
expect(response.body).not.toHaveProperty('endDate');
expect(response.body).not.toHaveProperty('size');
expect(response.body.layout).toBeDefined();
expect(response.body.encoding).toBeDefined();
// Validate expected layout if provided
if (expectedLayout) {
expect(response.body.layout).toEqual(expectedLayout);
} else {
// Default layout
expect(response.body.layout).toEqual(DEFAULT_RECORDING_LAYOUT);
}
if (expectedEncoding !== undefined) {
if (typeof expectedEncoding === 'string') {
// Encoding preset
expect(response.body.encoding).toEqual(expectedEncoding);
} else {
// Advanced encoding options
expect(response.body.encoding).toMatchObject(expectedEncoding as any);
}
} else {
// Default encoding preset
expect(response.body.encoding).toEqual(DEFAULT_RECORDING_ENCODING_PRESET);
}
};
export const expectValidStopRecordingResponse = (
response: Response,
recordingId: string,
roomId: string,
roomName: string,
expectedLayout?: MeetRecordingLayout,
expectedEncoding?: MeetRecordingEncodingPreset | MeetRecordingEncodingOptions
) => {
expect(response.status).toBe(202);
expect(response.body).toBeDefined();
expectValidRecordingLocationHeader(response);
expect(response.body).toHaveProperty('recordingId', recordingId);
expect([MeetRecordingStatus.COMPLETE, MeetRecordingStatus.ENDING]).toContain(response.body.status);
expect(response.body).toHaveProperty('roomId', roomId);
expect(response.body).toHaveProperty('roomName', roomName);
expect(response.body).toHaveProperty('filename');
expect(response.body).toHaveProperty('startDate');
expect(response.body).toHaveProperty('duration', expect.any(Number));
expect(response.body).toHaveProperty('layout');
expect(response.body).toHaveProperty('encoding');
// Validate layout is a valid value
if (expectedLayout) {
expect(response.body.layout).toEqual(expectedLayout);
} else {
// Default layout
expect(response.body.layout).toEqual(DEFAULT_RECORDING_LAYOUT);
}
// Validate encoding property
if (expectedEncoding) {
expect(response.body.encoding).toEqual(expectedEncoding);
} else {
// Default encoding preset
expect(response.body.encoding).toEqual(DEFAULT_RECORDING_ENCODING_PRESET);
}
};
export const expectValidGetRecordingResponse = (
response: Response,
expectedConfig: {
recordingId: string;
roomId: string;
roomName: string;
recordingStatus?: MeetRecordingStatus;
recordingDuration?: number;
recordingLayout?: MeetRecordingLayout;
recordingEncoding?: MeetRecordingEncodingPreset | MeetRecordingEncodingOptions;
}
) => {
expect(response.status).toBe(200);
expect(response.body).toBeDefined();
const body = response.body;
const { recordingId, roomId, roomName, recordingStatus, recordingDuration, recordingLayout, recordingEncoding } =
expectedConfig;
expect(body).toMatchObject({ recordingId, roomId, roomName });
// Validate layout property
expect(body).toHaveProperty('layout');
expect(body.layout).toBeDefined();
if (recordingLayout !== undefined) {
expect(body.layout).toBe(recordingLayout);
} else {
// Default layout
expect(body.layout).toBe(DEFAULT_RECORDING_LAYOUT);
}
// Validate encoding property
expect(body).toHaveProperty('encoding');
expect(body.encoding).toBeDefined();
// Validate encoding property is present and coherent
if (recordingEncoding !== undefined) {
if (typeof recordingEncoding === 'string') {
expect(body.layout).toBe(recordingLayout);
} else {
expect(body.encoding).toMatchObject(recordingEncoding as any);
}
} else {
// Default encoding preset
expect(body.encoding).toBe(DEFAULT_RECORDING_ENCODING_PRESET);
}
expect(body.status).toBeDefined();
if (recordingStatus !== undefined) {
expect(body.status).toBe(recordingStatus);
}
const isRecFinished =
recordingStatus &&
(recordingStatus === MeetRecordingStatus.COMPLETE ||
recordingStatus === MeetRecordingStatus.ABORTED ||
recordingStatus === MeetRecordingStatus.FAILED ||
recordingStatus === MeetRecordingStatus.LIMIT_REACHED);
expect(body).toEqual(
expect.objectContaining({
recordingId: expect.stringMatching(new RegExp(`^${recordingId}$`)),
roomId: expect.stringMatching(new RegExp(`^${roomId}$`)),
roomName: expect.stringMatching(new RegExp(`^${roomName}$`)),
...(isRecFinished ? { status: expect.any(String) } : {}),
...(isRecFinished ? { duration: expect.any(Number) } : {}),
...(isRecFinished ? { startDate: expect.any(Number) } : {}),
...(isRecFinished ? { endDate: expect.any(Number) } : {}),
...(isRecFinished ? { size: expect.any(Number) } : {}),
filename: expect.any(String),
...(isRecFinished ? { details: expect.any(String) } : {})
})
);
if (isRecFinished) {
expect(body.endDate).toBeGreaterThanOrEqual(body.startDate);
expect(body.duration).toBeGreaterThanOrEqual(0);
}
if (isRecFinished && recordingDuration) {
expect(body.duration).toBeLessThanOrEqual(recordingDuration);
const computedSec = (body.endDate - body.startDate) / 1000;
const diffSec = Math.abs(recordingDuration - computedSec);
// Estimate 5 seconds of tolerace because of time to start/stop recording
expect(diffSec).toBeLessThanOrEqual(5);
}
};
export const expectSuccessListRecordingResponse = (
response: Response,
recordingLength: number,
isTruncated: boolean,
nextPageToken: boolean,
maxItems = 10
) => {
expect(response.status).toBe(200);
expect(response.body).toBeDefined();
expect(response.body.recordings).toBeDefined();
expect(Array.isArray(response.body.recordings)).toBe(true);
expect(response.body.recordings.length).toBe(recordingLength);
expect(response.body.pagination).toBeDefined();
expect(response.body.pagination.isTruncated).toBe(isTruncated);
if (nextPageToken) {
expect(response.body.pagination.nextPageToken).toBeDefined();
} else {
expect(response.body.pagination.nextPageToken).toBeUndefined();
}
expect(response.body.pagination.maxItems).toBeDefined();
expect(response.body.pagination.maxItems).toBeGreaterThan(0);
expect(response.body.pagination.maxItems).toBeLessThanOrEqual(100);
expect(response.body.pagination.maxItems).toBe(maxItems);
};
export const expectValidGetRecordingUrlResponse = (response: Response, recordingId: string) => {
expect(response.status).toBe(200);
const recordingUrl = response.body.url;
expect(recordingUrl).toBeDefined();
const parsedUrl = new URL(recordingUrl);
expect(parsedUrl.pathname).toBe(`/recording/${recordingId}`);
expect(parsedUrl.searchParams.get('secret')).toBeDefined();
};
export const expectValidRoomMemberRolesAndPermissionsResponse = (response: Response, roomId: string) => {
expect(response.status).toBe(200);
expect(response.body).toEqual(
expect.arrayContaining([
{
role: MeetRoomMemberRole.MODERATOR,
permissions: getPermissions(roomId, MeetRoomMemberRole.MODERATOR, true, true)
},
{
role: MeetRoomMemberRole.SPEAKER,
permissions: getPermissions(roomId, MeetRoomMemberRole.SPEAKER, true, false)
}
])
);
};
export const expectValidRoomMemberRoleAndPermissionsResponse = (
response: Response,
roomId: string,
role: MeetRoomMemberRole
) => {
expect(response.status).toBe(200);
expect(response.body).toEqual({
role: role,
permissions: getPermissions(roomId, role, true, role === MeetRoomMemberRole.MODERATOR)
});
};
export const getPermissions = (
roomId: string,
role: MeetRoomMemberRole,
canRetrieveRecordings: boolean,
canDeleteRecordings: boolean,
addJoinPermission = true
): MeetRoomMemberPermissions => {
switch (role) {
case MeetRoomMemberRole.MODERATOR:
return {
livekit: {
roomJoin: addJoinPermission,
room: roomId,
canPublish: true,
canSubscribe: true,
canPublishData: true,
canUpdateOwnMetadata: true
},
meet: {
canRecord: true,
canRetrieveRecordings,
canDeleteRecordings,
canChat: true,
canChangeVirtualBackground: true
}
};
case MeetRoomMemberRole.SPEAKER:
return {
livekit: {
roomJoin: addJoinPermission,
room: roomId,
canPublish: true,
canSubscribe: true,
canPublishData: true,
canUpdateOwnMetadata: true
},
meet: {
canRecord: false,
canRetrieveRecordings,
canDeleteRecordings,
canChat: true,
canChangeVirtualBackground: true
}
};
}
};
export const expectValidRoomMemberTokenResponse = (
response: Response,
roomId: string,
role: MeetRoomMemberRole,
addJoinPermission = false,
participantName?: string,
participantIdentityPrefix?: string,
canRetrieveRecordings?: boolean,
canDeleteRecordings?: boolean
) => {
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('token');
const token = response.body.token;
const decodedToken = decodeJWTToken(token);
canRetrieveRecordings = canRetrieveRecordings ?? true;
canDeleteRecordings = canDeleteRecordings ?? role === MeetRoomMemberRole.MODERATOR;
const permissions = getPermissions(roomId, role, canRetrieveRecordings, canDeleteRecordings, addJoinPermission);
if (addJoinPermission) {
expect(participantName).toBeDefined();
expect(decodedToken).toHaveProperty('name', participantName);
expect(decodedToken).toHaveProperty('sub');
if (participantIdentityPrefix) {
expect(decodedToken.sub?.startsWith(participantIdentityPrefix)).toBe(true);
}
} else {
expect(decodedToken).not.toHaveProperty('name');
expect(decodedToken).not.toHaveProperty('sub');
}
expect(decodedToken).toHaveProperty('video', permissions.livekit);
expect(decodedToken).toHaveProperty('metadata');
const metadata = JSON.parse(decodedToken.metadata || '{}');
expect(metadata).toHaveProperty('livekitUrl');
expect(metadata).toHaveProperty('role', role);
expect(metadata).toHaveProperty('permissions', permissions.meet);
};
const decodeJWTToken = (token: string) => {
const tokenService = container.get(TokenService);
return tokenService.getClaimsIgnoringExpiration(token);
};