548 lines
16 KiB
TypeScript
548 lines
16 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from '@jest/globals';
|
|
import {
|
|
MeetRecordingAccess,
|
|
MeetRecordingLayout,
|
|
MeetRoomDeletionPolicyWithMeeting,
|
|
MeetRoomDeletionPolicyWithRecordings
|
|
} from '@openvidu-meet/typings';
|
|
import { Express } from 'express';
|
|
import ms from 'ms';
|
|
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, deleteAllRooms, startTestServer } from '../../../helpers/request-helpers.js';
|
|
|
|
const ROOMS_PATH = `${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`;
|
|
|
|
describe('Room API Tests', () => {
|
|
const validAutoDeletionDate = Date.now() + ms('2h');
|
|
|
|
let app: Express;
|
|
|
|
beforeAll(async () => {
|
|
app = await startTestServer();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await deleteAllRooms();
|
|
});
|
|
|
|
describe('Room Creation Tests', () => {
|
|
it('Should create a room with default name when roomName is omitted', async () => {
|
|
const room = await createRoom();
|
|
expectValidRoom(room, 'Room');
|
|
});
|
|
|
|
it('Should create a room without autoDeletionDate (default behavior)', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'Test Room'
|
|
});
|
|
expectValidRoom(room, 'Test Room');
|
|
});
|
|
|
|
it('Should create a room with a valid autoDeletionDate', async () => {
|
|
const room = await createRoom({
|
|
autoDeletionDate: validAutoDeletionDate,
|
|
roomName: 'Room'
|
|
});
|
|
|
|
expectValidRoom(room, 'Room', 'room', undefined, validAutoDeletionDate);
|
|
});
|
|
|
|
it('Should create a room when sending full valid payload', async () => {
|
|
const payload = {
|
|
roomName: 'Example Room',
|
|
autoDeletionDate: validAutoDeletionDate,
|
|
autoDeletionPolicy: {
|
|
withMeeting: MeetRoomDeletionPolicyWithMeeting.FORCE,
|
|
withRecordings: MeetRoomDeletionPolicyWithRecordings.FORCE
|
|
},
|
|
config: {
|
|
recording: {
|
|
enabled: false,
|
|
layout: MeetRecordingLayout.GRID,
|
|
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
|
},
|
|
chat: { enabled: false },
|
|
virtualBackground: { enabled: true },
|
|
e2ee: { enabled: true },
|
|
captions: { enabled: false }
|
|
}
|
|
};
|
|
|
|
const room = await createRoom(payload);
|
|
|
|
expectValidRoom(
|
|
room,
|
|
'Example Room',
|
|
'example_room',
|
|
payload.config,
|
|
validAutoDeletionDate,
|
|
payload.autoDeletionPolicy
|
|
);
|
|
});
|
|
|
|
it('Should create a room when sending partial config', async () => {
|
|
const payload = {
|
|
roomName: 'Partial Config Room',
|
|
autoDeletionDate: validAutoDeletionDate,
|
|
config: {
|
|
recording: {
|
|
enabled: false
|
|
},
|
|
captions: {
|
|
enabled: true
|
|
}
|
|
}
|
|
};
|
|
|
|
const room = await createRoom(payload);
|
|
|
|
const expectedConfig = {
|
|
recording: {
|
|
enabled: false,
|
|
layout: MeetRecordingLayout.GRID, // Default value
|
|
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER // Default value
|
|
},
|
|
chat: { enabled: true }, // Default value
|
|
virtualBackground: { enabled: true }, // Default value
|
|
e2ee: { enabled: false }, // Default value
|
|
captions: { enabled: true }
|
|
};
|
|
expectValidRoom(room, 'Partial Config Room', 'partial_config_room', expectedConfig, validAutoDeletionDate);
|
|
});
|
|
|
|
it('Should create a room when sending partial config with two fields', async () => {
|
|
const payload = {
|
|
roomName: 'Partial Config Room',
|
|
autoDeletionDate: validAutoDeletionDate,
|
|
config: {
|
|
chat: {
|
|
enabled: false
|
|
},
|
|
virtualBackground: {
|
|
enabled: false
|
|
}
|
|
}
|
|
};
|
|
|
|
const room = await createRoom(payload);
|
|
|
|
const expectedConfig = {
|
|
recording: {
|
|
enabled: true, // Default value
|
|
layout: MeetRecordingLayout.GRID, // Default value
|
|
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER // Default value
|
|
},
|
|
chat: { enabled: false },
|
|
virtualBackground: { enabled: false },
|
|
e2ee: { enabled: false }, // Default value
|
|
captions: { enabled: true } // Default value
|
|
};
|
|
expectValidRoom(room, 'Partial Config Room', 'partial_config_room', expectedConfig, validAutoDeletionDate);
|
|
});
|
|
});
|
|
|
|
describe('Room Name Sanitization Tests', () => {
|
|
it('should create room with Spanish characters and generate sanitized roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'Habitación José'
|
|
});
|
|
expectValidRoom(room, 'Habitación José', 'habitacion_jose');
|
|
});
|
|
|
|
it('should create room with German umlauts and generate sanitized roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'Café Müller'
|
|
});
|
|
expectValidRoom(room, 'Café Müller', 'cafe_muller');
|
|
});
|
|
|
|
it('should create room with French accents and generate sanitized roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'Réunion François'
|
|
});
|
|
expectValidRoom(room, 'Réunion François', 'reunion_francois');
|
|
});
|
|
|
|
it('should create room with uppercase letters and convert to lowercase in roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'MY ROOM'
|
|
});
|
|
expectValidRoom(room, 'MY ROOM', 'my_room');
|
|
});
|
|
|
|
it('should create room with mixed case and convert to lowercase in roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'MyRoom123'
|
|
});
|
|
expectValidRoom(room, 'MyRoom123', 'myroom123');
|
|
});
|
|
|
|
it('should create room with hyphens and convert to underscores in roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'my-test-room'
|
|
});
|
|
expectValidRoom(room, 'my-test-room', 'my_test_room');
|
|
});
|
|
|
|
it('should create room with spaces and convert to underscores in roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'My Test Room'
|
|
});
|
|
expectValidRoom(room, 'My Test Room', 'my_test_room');
|
|
});
|
|
|
|
it('should create room with multiple consecutive spaces and normalize in roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'My Test Room'
|
|
});
|
|
expectValidRoom(room, 'My Test Room', 'my_test_room');
|
|
});
|
|
|
|
it('should create room with special characters and remove them in roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'Room@#$%^&*()123'
|
|
});
|
|
expectValidRoom(room, 'Room@#$%^&*()123', 'room123');
|
|
});
|
|
|
|
it('should create room with emojis and remove them in roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'Meeting 🎉 Room'
|
|
});
|
|
expectValidRoom(room, 'Meeting 🎉 Room', 'meeting_room');
|
|
});
|
|
|
|
it('should create room with leading/trailing underscores and remove them in roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: '__test_room__'
|
|
});
|
|
expectValidRoom(room, '__test_room__', 'test_room');
|
|
});
|
|
|
|
it('should create room with leading/trailing hyphens and remove them in roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: '--test-room--'
|
|
});
|
|
expectValidRoom(room, '--test-room--', 'test_room');
|
|
});
|
|
|
|
it('should create room with multiple consecutive hyphens/underscores and normalize in roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'test___---room'
|
|
});
|
|
expectValidRoom(room, 'test___---room', 'test_room');
|
|
});
|
|
|
|
it('should create room with numbers and preserve them in roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'Room 123 456'
|
|
});
|
|
expectValidRoom(room, 'Room 123 456', 'room_123_456');
|
|
});
|
|
|
|
it('should create room with mix of all sanitization rules', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'SALA--Médica #2024 (Niños)'
|
|
});
|
|
expectValidRoom(room, 'SALA--Médica #2024 (Niños)', 'sala_medica_2024_ninos');
|
|
});
|
|
|
|
it('should create room with Portuguese characters and generate sanitized roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'Reunião São Paulo'
|
|
});
|
|
expectValidRoom(room, 'Reunião São Paulo', 'reuniao_sao_paulo');
|
|
});
|
|
|
|
it('should create room with Scandinavian characters and generate sanitized roomId', async () => {
|
|
const room = await createRoom({
|
|
roomName: 'Møde Åse'
|
|
});
|
|
expectValidRoom(room, 'Møde Åse', 'mde_ase');
|
|
});
|
|
|
|
it('should create room with Chinese characters and use default "room" prefix', async () => {
|
|
const room = await createRoom({
|
|
roomName: '会议室'
|
|
});
|
|
expectValidRoom(room, '会议室', 'room');
|
|
});
|
|
|
|
it('should create room with only special characters and use default "room" prefix', async () => {
|
|
const room = await createRoom({
|
|
roomName: '@#$%^&*()'
|
|
});
|
|
expectValidRoom(room, '@#$%^&*()', 'room');
|
|
});
|
|
|
|
it('should create room with emojis only and use default "room" prefix', async () => {
|
|
const room = await createRoom({
|
|
roomName: '🎉🎊🎈'
|
|
});
|
|
expectValidRoom(room, '🎉🎊🎈', 'room');
|
|
});
|
|
});
|
|
|
|
describe('Room Creation Validation failures', () => {
|
|
it('should fail when autoDeletionDate is negative', async () => {
|
|
const payload = {
|
|
autoDeletionDate: -5000,
|
|
roomName: 'TestRoom'
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post(ROOMS_PATH)
|
|
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
|
.send(payload)
|
|
.expect(422);
|
|
|
|
// Check that the error message contains the positive number validation
|
|
expect(response.body.error).toContain('Unprocessable Entity');
|
|
expect(JSON.stringify(response.body.details)).toContain('autoDeletionDate must be a positive integer');
|
|
});
|
|
|
|
it('should fail when autoDeletionDate is less than 1 hour in the future', async () => {
|
|
const payload = {
|
|
autoDeletionDate: Date.now() + ms('30m'),
|
|
roomName: 'TestRoom'
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post(ROOMS_PATH)
|
|
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
|
.send(payload)
|
|
.expect(422);
|
|
|
|
expect(response.body.error).toContain('Unprocessable Entity');
|
|
expect(JSON.stringify(response.body.details)).toContain(
|
|
`autoDeletionDate must be at least ${INTERNAL_CONFIG.MIN_ROOM_AUTO_DELETE_DURATION} in the future`
|
|
);
|
|
});
|
|
|
|
it('should fail when autoDeletionDate is not a number (string provided)', async () => {
|
|
const payload = {
|
|
autoDeletionDate: 'not-a-number',
|
|
roomName: 'TestRoom'
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post(ROOMS_PATH)
|
|
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
|
.send(payload)
|
|
.expect(422);
|
|
|
|
expect(JSON.stringify(response.body.details)).toContain('Expected number');
|
|
});
|
|
|
|
it('should fail when autoDeletionDate is a boolean', async () => {
|
|
const payload = {
|
|
autoDeletionDate: true,
|
|
roomName: 'TestRoom'
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post(ROOMS_PATH)
|
|
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
|
.send(payload)
|
|
.expect(422);
|
|
|
|
expect(JSON.stringify(response.body.details)).toContain('Expected number');
|
|
});
|
|
|
|
it('should fail when autoDeletionDate is omitted but provided as null', async () => {
|
|
const payload = {
|
|
autoDeletionDate: null,
|
|
roomName: 'TestRoom'
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post(ROOMS_PATH)
|
|
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
|
.send(payload)
|
|
.expect(422);
|
|
|
|
expect(JSON.stringify(response.body.details)).toContain('Expected number');
|
|
});
|
|
|
|
it('should fail when autoDeletionPolicy is not an object (string provided)', async () => {
|
|
const payload = {
|
|
roomName: 'TestRoom',
|
|
autoDeletionDate: validAutoDeletionDate,
|
|
autoDeletionPolicy: 'invalid-policy'
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post(ROOMS_PATH)
|
|
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
|
.send(payload)
|
|
.expect(422);
|
|
|
|
expect(JSON.stringify(response.body.details)).toContain('Expected object');
|
|
});
|
|
|
|
it('should fail when autoDeletionPolicy has invalid structure', async () => {
|
|
const payload = {
|
|
roomName: 'TestRoom',
|
|
autoDeletionDate: validAutoDeletionDate,
|
|
autoDeletionPolicy: {
|
|
withMeeting: 'invalid-value',
|
|
withRecordings: MeetRoomDeletionPolicyWithRecordings.CLOSE
|
|
}
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post(ROOMS_PATH)
|
|
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
|
.send(payload)
|
|
.expect(422);
|
|
|
|
expect(JSON.stringify(response.body.details)).toContain('Invalid enum value');
|
|
});
|
|
|
|
it('should fail when autoDeletionPolicy.withMeeting has FAIL policy', async () => {
|
|
const payload = {
|
|
roomName: 'TestRoom',
|
|
autoDeletionDate: validAutoDeletionDate,
|
|
autoDeletionPolicy: {
|
|
withMeeting: MeetRoomDeletionPolicyWithMeeting.FAIL,
|
|
withRecordings: MeetRoomDeletionPolicyWithRecordings.CLOSE
|
|
}
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post(ROOMS_PATH)
|
|
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
|
.send(payload)
|
|
.expect(422);
|
|
|
|
expect(JSON.stringify(response.body.details)).toContain(
|
|
'FAIL policy is not allowed for withMeeting auto-deletion policy'
|
|
);
|
|
});
|
|
|
|
it('should fail when autoDeletionPolicy.withRecordings has FAIL policy', async () => {
|
|
const payload = {
|
|
roomName: 'TestRoom',
|
|
autoDeletionDate: validAutoDeletionDate,
|
|
autoDeletionPolicy: {
|
|
withMeeting: MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS,
|
|
withRecordings: MeetRoomDeletionPolicyWithRecordings.FAIL
|
|
}
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post(ROOMS_PATH)
|
|
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
|
.send(payload)
|
|
.expect(422);
|
|
|
|
expect(JSON.stringify(response.body.details)).toContain(
|
|
'FAIL policy is not allowed for withRecordings auto-deletion policy'
|
|
);
|
|
});
|
|
|
|
it('should fail when roomName is not a string (number provided)', async () => {
|
|
const payload = {
|
|
roomName: 12345,
|
|
autoDeletionDate: validAutoDeletionDate
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post(ROOMS_PATH)
|
|
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
|
.send(payload)
|
|
.expect(422);
|
|
|
|
expect(JSON.stringify(response.body.details)).toContain('Expected string');
|
|
});
|
|
|
|
it('should fail when roomName is a boolean', async () => {
|
|
const payload = {
|
|
roomName: false,
|
|
autoDeletionDate: validAutoDeletionDate
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post(ROOMS_PATH)
|
|
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
|
.send(payload)
|
|
.expect(422);
|
|
|
|
expect(JSON.stringify(response.body.details)).toContain('Expected string');
|
|
});
|
|
|
|
it('should fail when config is not an object (string provided)', async () => {
|
|
const payload = {
|
|
roomName: 'TestRoom',
|
|
autoDeletionDate: validAutoDeletionDate,
|
|
config: 'invalid-config'
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post(ROOMS_PATH)
|
|
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
|
.send(payload)
|
|
.expect(422);
|
|
|
|
expect(JSON.stringify(response.body.details)).toContain('Expected object');
|
|
});
|
|
|
|
it('should fail when config has an invalid structure', async () => {
|
|
// Assuming config expects each sub-property to be an object with a boolean "enabled",
|
|
// here we deliberately use an invalid structure.
|
|
const payload = {
|
|
roomName: 'TestRoom',
|
|
autoDeletionDate: validAutoDeletionDate,
|
|
config: {
|
|
recording: {
|
|
enabled: 'yes', // invalid boolean
|
|
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
|
|
},
|
|
chat: { enabled: true },
|
|
virtualBackground: { enabled: true }
|
|
}
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post(ROOMS_PATH)
|
|
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
|
.send(payload)
|
|
.expect(422);
|
|
|
|
expect(JSON.stringify(response.body.details)).toContain('Expected boolean');
|
|
});
|
|
|
|
it('should fail with invalid JSON payload', async () => {
|
|
// In this case, instead of sending JSON object, send an invalid JSON string.
|
|
const response = await request(app)
|
|
.post(ROOMS_PATH)
|
|
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
|
.set('Content-Type', 'application/json')
|
|
.send('{"roomName": "TestRoom",') // invalid JSON syntax
|
|
.expect(400);
|
|
|
|
expect(response.body.error).toContain('Bad Request');
|
|
expect(response.body.message).toContain('Malformed body');
|
|
});
|
|
|
|
it('should fail when roomName is too long', async () => {
|
|
const longRoomId = 'a'.repeat(51);
|
|
const payload = {
|
|
roomName: longRoomId,
|
|
autoDeletionDate: validAutoDeletionDate
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post(ROOMS_PATH)
|
|
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
|
.send(payload)
|
|
.expect(422);
|
|
|
|
expect(JSON.stringify(response.body.details)).toContain('roomName cannot exceed 50 characters');
|
|
});
|
|
});
|
|
});
|