test: enhance type safety in request helpers

This commit is contained in:
juancarmore 2025-11-07 09:59:55 +01:00
parent d52524241f
commit 1d55311757
10 changed files with 86 additions and 74 deletions

View File

@ -1,4 +1,5 @@
import {
MeetAppearanceConfig,
MeetChatConfig,
MeetE2EEConfig,
MeetRecordingAccess,
@ -10,6 +11,7 @@ import {
MeetRoomFilters,
MeetRoomOptions,
MeetRoomStatus,
MeetRoomTheme,
MeetRoomThemeMode,
MeetVirtualBackgroundConfig,
ParticipantRole,
@ -100,7 +102,7 @@ const hexColorSchema = z
'Must be a valid hex color code (with or without alpha)'
);
const RoomThemeSchema = z.object({
const RoomThemeSchema: z.ZodType<MeetRoomTheme> = z.object({
name: z
.string()
.min(1, 'Theme name cannot be empty')
@ -115,7 +117,7 @@ const RoomThemeSchema = z.object({
surfaceColor: hexColorSchema.optional()
});
export const AppearanceConfigSchema = z.object({
export const AppearanceConfigSchema: z.ZodType<MeetAppearanceConfig> = z.object({
themes: z.array(RoomThemeSchema).length(1, 'There must be exactly one theme defined')
});

View File

@ -55,11 +55,11 @@ export const errorLivekitNotAvailable = (): OpenViduMeetError => {
return new OpenViduMeetError('LiveKit Error', 'LiveKit is not available', 503);
};
export const errorS3NotAvailable = (error: any): OpenViduMeetError => {
export const errorS3NotAvailable = (error: unknown): OpenViduMeetError => {
return new OpenViduMeetError('S3 Error', `S3 is not available ${error}`, 503);
};
export const errorAzureNotAvailable = (error: any): OpenViduMeetError => {
export const errorAzureNotAvailable = (error: unknown): OpenViduMeetError => {
return new OpenViduMeetError('ABS Error', `Azure Blob Storage is not available ${error}`, 503);
};

View File

@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { expect } from '@jest/globals';
import {
AuthMode,
AuthTransportMode,
MeetAppearanceConfig,
MeetRecordingAccess,
MeetRecordingInfo,
MeetRecordingStatus,
@ -11,7 +11,10 @@ import {
MeetRoomDeletionPolicyWithMeeting,
MeetRoomDeletionPolicyWithRecordings,
MeetRoomOptions,
MeetTokenMetadata,
ParticipantOptions,
ParticipantRole,
SecurityConfig,
WebhookConfig
} from '@openvidu-meet/typings';
import { ChildProcess, spawn } from 'child_process';
@ -112,7 +115,7 @@ export const getRoomsAppearanceConfig = async () => {
return response;
};
export const updateRoomsAppearanceConfig = async (config: any) => {
export const updateRoomsAppearanceConfig = async (config: { appearance: MeetAppearanceConfig }) => {
checkAppIsRunning();
const accessToken = await loginUser();
@ -162,7 +165,7 @@ export const getSecurityConfig = async () => {
return response;
};
export const updateSecurityConfig = async (config: any) => {
export const updateSecurityConfig = async (config: SecurityConfig) => {
checkAppIsRunning();
const accessToken = await loginUser();
@ -282,7 +285,7 @@ export const createRoom = async (options: MeetRoomOptions = {}): Promise<MeetRoo
return response.body;
};
export const getRooms = async (query: Record<string, any> = {}) => {
export const getRooms = async (query: Record<string, unknown> = {}) => {
checkAppIsRunning();
return await request(app)
@ -359,7 +362,7 @@ export const updateRoomStatus = async (roomId: string, status: string) => {
.send({ status });
};
export const deleteRoom = async (roomId: string, query: Record<string, any> = {}) => {
export const deleteRoom = async (roomId: string, query: Record<string, unknown> = {}) => {
checkAppIsRunning();
const result = await request(app)
@ -370,7 +373,7 @@ export const deleteRoom = async (roomId: string, query: Record<string, any> = {}
return result;
};
export const bulkDeleteRooms = async (roomIds: any[], withMeeting?: string, withRecordings?: string) => {
export const bulkDeleteRooms = async (roomIds: string[], withMeeting?: string, withRecordings?: string) => {
checkAppIsRunning();
const result = await request(app)
@ -445,7 +448,10 @@ export const getRoomRoleBySecret = async (roomId: string, secret: string) => {
return response;
};
export const generateParticipantTokenRequest = async (participantOptions: any, previousToken?: string) => {
export const generateParticipantTokenRequest = async (
participantOptions: ParticipantOptions,
previousToken?: string
) => {
checkAppIsRunning();
// Disable authentication to generate the token
@ -489,7 +495,7 @@ export const generateParticipantToken = async (
return `Bearer ${response.body.token}`;
};
export const refreshParticipantToken = async (participantOptions: any, previousToken: string) => {
export const refreshParticipantToken = async (participantOptions: ParticipantOptions, previousToken: string) => {
checkAppIsRunning();
// Disable authentication to generate the token
@ -534,7 +540,11 @@ export const joinFakeParticipant = async (roomId: string, participantIdentity: s
* @param participantIdentity The identity of the participant
* @param metadata The metadata to update
*/
export const updateParticipantMetadata = async (roomId: string, participantIdentity: string, metadata: any) => {
export const updateParticipantMetadata = async (
roomId: string,
participantIdentity: string,
metadata: MeetTokenMetadata
) => {
await ensureLivekitCliInstalled();
spawn('lk', [
'room',
@ -748,7 +758,7 @@ export const deleteRecording = async (recordingId: string) => {
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY);
};
export const bulkDeleteRecordings = async (recordingIds: any[], recordingToken?: string): Promise<Response> => {
export const bulkDeleteRecordings = async (recordingIds: string[], recordingToken?: string): Promise<Response> => {
checkAppIsRunning();
const req = request(app)
@ -822,7 +832,7 @@ export const stopAllRecordings = async (moderatorToken: string) => {
await sleep('1s');
};
export const getAllRecordings = async (query: Record<string, any> = {}) => {
export const getAllRecordings = async (query: Record<string, unknown> = {}) => {
checkAppIsRunning();
return await request(app)
@ -845,7 +855,7 @@ export const deleteAllRecordings = async () => {
let nextPageToken: string | undefined;
do {
const response: any = await getAllRecordings({
const response = await getAllRecordings({
fields: 'recordingId',
maxItems: 100,
nextPageToken

View File

@ -218,7 +218,7 @@ describe('Rooms Appearance Config API Tests', () => {
}
]
}
});
} as unknown as { appearance: MeetAppearanceConfig });
expectValidationError(response, 'appearance.themes.0.enabled', 'Expected boolean, received string');
});
@ -233,7 +233,7 @@ describe('Rooms Appearance Config API Tests', () => {
}
]
}
});
} as unknown as { appearance: MeetAppearanceConfig });
expectValidationError(
response,
@ -253,7 +253,7 @@ describe('Rooms Appearance Config API Tests', () => {
}
]
}
});
} as unknown as { appearance: MeetAppearanceConfig });
expectValidationError(response, 'appearance.themes.0.backgroundColor', 'Must be a valid hex color code');
response = await updateRoomsAppearanceConfig({
@ -266,7 +266,7 @@ describe('Rooms Appearance Config API Tests', () => {
}
]
}
});
} as unknown as { appearance: MeetAppearanceConfig });
expectValidationError(response, 'appearance.themes.0.primaryColor', 'Must be a valid hex color code');
response = await updateRoomsAppearanceConfig({
@ -279,7 +279,7 @@ describe('Rooms Appearance Config API Tests', () => {
}
]
}
});
} as unknown as { appearance: MeetAppearanceConfig });
expectValidationError(response, 'appearance.themes.0.secondaryColor', 'Must be a valid hex color code');
response = await updateRoomsAppearanceConfig({
@ -292,7 +292,7 @@ describe('Rooms Appearance Config API Tests', () => {
}
]
}
});
} as unknown as { appearance: MeetAppearanceConfig });
expectValidationError(response, 'appearance.themes.0.surfaceColor', 'Must be a valid hex color code');
});
@ -326,7 +326,7 @@ describe('Rooms Appearance Config API Tests', () => {
}
]
}
});
} as unknown as { appearance: MeetAppearanceConfig });
expectValidationError(response, 'appearance.themes.0.name', 'Required');
response = await updateRoomsAppearanceConfig({
@ -338,7 +338,7 @@ describe('Rooms Appearance Config API Tests', () => {
}
]
}
});
} as unknown as { appearance: MeetAppearanceConfig });
expectValidationError(response, 'appearance.themes.0.enabled', 'Required');
response = await updateRoomsAppearanceConfig({
@ -350,14 +350,14 @@ describe('Rooms Appearance Config API Tests', () => {
}
]
}
});
} as unknown as { appearance: MeetAppearanceConfig });
expectValidationError(response, 'appearance.themes.0.baseTheme', 'Required');
});
it('should reject when appearance is not an object', async () => {
const response = await updateRoomsAppearanceConfig({
appearance: 'invalid'
});
} as unknown as { appearance: MeetAppearanceConfig });
expectValidationError(response, 'appearance', 'Expected object, received string');
});
@ -367,7 +367,7 @@ describe('Rooms Appearance Config API Tests', () => {
appearance: {
themes: 'invalid'
}
});
} as unknown as { appearance: MeetAppearanceConfig });
expectValidationError(response, 'appearance.themes', 'Expected array, received string');
});

View File

@ -1,5 +1,5 @@
import { afterEach, beforeAll, describe, expect, it } from '@jest/globals';
import { AuthMode, AuthTransportMode, AuthType } from '@openvidu-meet/typings';
import { AuthMode, AuthTransportMode, AuthType, SecurityConfig } from '@openvidu-meet/typings';
import { expectValidationError } from '../../../helpers/assertion-helpers.js';
import {
getSecurityConfig,
@ -48,7 +48,7 @@ describe('Security Config API Tests', () => {
},
authModeToAccessRoom: 'invalid'
}
});
} as unknown as SecurityConfig);
expectValidationError(
response,
@ -65,7 +65,7 @@ describe('Security Config API Tests', () => {
},
authModeToAccessRoom: AuthMode.ALL_USERS
}
});
} as unknown as SecurityConfig);
expectValidationError(
response,
@ -83,7 +83,7 @@ describe('Security Config API Tests', () => {
authModeToAccessRoom: AuthMode.ALL_USERS,
authTransportMode: 'invalid'
}
});
} as unknown as SecurityConfig);
expectValidationError(
response,
@ -98,7 +98,7 @@ describe('Security Config API Tests', () => {
authMode: AuthMode.NONE,
authTransportMode: AuthTransportMode.HEADER
}
});
} as unknown as SecurityConfig);
expectValidationError(response, 'authentication.authMethod', 'Required');
response = await updateSecurityConfig({
@ -108,7 +108,7 @@ describe('Security Config API Tests', () => {
},
authModeToAccessRoom: AuthMode.NONE
}
});
} as unknown as SecurityConfig);
expectValidationError(response, 'authentication.authTransportMode', 'Required');
response = await updateSecurityConfig({
@ -118,14 +118,14 @@ describe('Security Config API Tests', () => {
},
authTransportMode: AuthTransportMode.HEADER
}
});
} as unknown as SecurityConfig);
expectValidationError(response, 'authentication.authModeToAccessRoom', 'Required');
});
it('should reject when authentication is not an object', async () => {
const response = await updateSecurityConfig({
authentication: 'invalid'
});
} as unknown as SecurityConfig);
expectValidationError(response, 'authentication', 'Expected object, received string');
});

View File

@ -1,6 +1,6 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it } from '@jest/globals';
import { AuthTransportMode, ParticipantOptions, ParticipantRole } from '@openvidu-meet/typings';
import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js';
import { AuthTransportMode, ParticipantRole } from '@openvidu-meet/typings';
import { expectValidationError, expectValidParticipantTokenResponse } from '../../../helpers/assertion-helpers.js';
import {
changeAuthTransportMode,
@ -204,7 +204,7 @@ describe('Participant API Tests', () => {
const response = await generateParticipantTokenRequest({
secret: roomData.moderatorSecret,
participantName
});
} as unknown as ParticipantOptions);
expectValidationError(response, 'roomId', 'Required');
});
@ -212,7 +212,7 @@ describe('Participant API Tests', () => {
const response = await generateParticipantTokenRequest({
roomId: roomData.room.roomId,
participantName
});
} as unknown as ParticipantOptions);
expectValidationError(response, 'secret', 'Required');
});

View File

@ -1,6 +1,6 @@
import { afterAll, beforeAll, describe, expect, it } from '@jest/globals';
import { AuthTransportMode, ParticipantOptions, ParticipantRole } from '@openvidu-meet/typings';
import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js';
import { AuthTransportMode, ParticipantRole } from '@openvidu-meet/typings';
import { expectValidationError, expectValidParticipantTokenResponse } from '../../../helpers/assertion-helpers.js';
import {
changeAuthTransportMode,
@ -191,7 +191,7 @@ describe('Participant API Tests', () => {
secret: roomData.moderatorSecret,
participantName,
participantIdentity: participantName
},
} as unknown as ParticipantOptions,
roomData.moderatorToken
);
expectValidationError(response, 'roomId', 'Required');
@ -203,7 +203,7 @@ describe('Participant API Tests', () => {
roomId: roomData.room.roomId,
participantName,
participantIdentity: participantName
},
} as unknown as ParticipantOptions,
roomData.moderatorToken
);
expectValidationError(response, 'secret', 'Required');

View File

@ -29,7 +29,7 @@ describe('Recording API Tests', () => {
describe('Bulk Delete Recording Tests', () => {
it('should return 400 when mixed valid and non-existent IDs are provided', async () => {
const testContext = await setupMultiRecordingsTestContext(3, 3, 3);
const recordingIds = testContext.rooms.map((room) => room.recordingId);
const recordingIds = testContext.rooms.map((room) => room.recordingId!);
const nonExistentIds = ['nonExistent--EG_000--1234', 'nonExistent--EG_111--5678'];
const mixedIds = [...recordingIds, ...nonExistentIds];
@ -52,10 +52,10 @@ describe('Recording API Tests', () => {
const testContext = await setupMultiRecordingsTestContext(3, 3, 2);
const activeRecordingRoom = testContext.getLastRoom();
const recordingIds = testContext.rooms
.map((room) => room.recordingId)
.filter((id) => id !== activeRecordingRoom!.recordingId);
.map((room) => room.recordingId!)
.filter((id) => id !== activeRecordingRoom!.recordingId!);
const activeRecordingId = activeRecordingRoom?.recordingId;
const activeRecordingId = activeRecordingRoom!.recordingId!;
let deleteResponse = await bulkDeleteRecordings([...recordingIds, activeRecordingId]);
expect(deleteResponse.status).toBe(400);
@ -83,7 +83,7 @@ describe('Recording API Tests', () => {
it('should not delete any recordings and return 400', async () => {
const testContext = await setupMultiRecordingsTestContext(2, 2, 0);
const recordingIds = testContext.rooms.map((room) => room.recordingId);
const recordingIds = testContext.rooms.map((room) => room.recordingId!);
const deleteResponse = await bulkDeleteRecordings(recordingIds);
expect(deleteResponse.status).toBe(400);
expect(deleteResponse.body).toEqual({
@ -106,7 +106,7 @@ describe('Recording API Tests', () => {
it('should delete all recordings and return 200 when all operations succeed', async () => {
const response = await setupMultiRecordingsTestContext(5, 5, 5);
const recordingIds = response.rooms.map((room) => room.recordingId);
const recordingIds = response.rooms.map((room) => room.recordingId!);
const deleteResponse = await bulkDeleteRecordings(recordingIds);
expect(deleteResponse.status).toBe(200);
@ -116,14 +116,14 @@ describe('Recording API Tests', () => {
// Create a room and start a recording
const roomData = await setupSingleRoomWithRecording(true);
const roomId = roomData.room.roomId;
const recordingId = roomData.recordingId;
const recordingId = roomData.recordingId!;
// Generate a recording token for the room
const recordingToken = await generateRecordingToken(roomId, roomData.moderatorSecret);
// Create another room and start a recording
const otherRoomData = await setupSingleRoomWithRecording(true);
const otherRecordingId = otherRoomData.recordingId;
const otherRecordingId = otherRoomData.recordingId!;
// Intenta eliminar ambas grabaciones usando el token de la primera sala
const deleteResponse = await bulkDeleteRecordings([recordingId, otherRecordingId], recordingToken);
@ -145,7 +145,7 @@ describe('Recording API Tests', () => {
it('should handle single recording deletion correctly', async () => {
const testContext = await setupMultiRecordingsTestContext(1, 1, 1);
const recordingId = testContext.rooms[0].recordingId;
const recordingId = testContext.rooms[0].recordingId!;
const deleteResponse = await bulkDeleteRecordings([recordingId]);
expect(deleteResponse.status).toBe(200);
@ -153,7 +153,7 @@ describe('Recording API Tests', () => {
it('should handle duplicate recording IDs by treating them as a single delete', async () => {
const testContext = await setupMultiRecordingsTestContext(1, 1, 1);
const recordingId = testContext.getRoomByIndex(0)!.recordingId;
const recordingId = testContext.getRoomByIndex(0)!.recordingId!;
const deleteResponse = await bulkDeleteRecordings([recordingId, recordingId]);
expect(deleteResponse.status).toBe(200);

View File

@ -1,9 +1,9 @@
import { afterAll, beforeAll, describe, expect, it } from '@jest/globals';
import { MeetRecordingAccess, MeetRoom } from '@openvidu-meet/typings';
import { Express } from 'express';
import request from 'supertest';
import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js';
import { MEET_INITIAL_API_KEY } from '../../../../src/environment.js';
import { MeetRecordingAccess, MeetRoom } from '@openvidu-meet/typings';
import { expectValidRoom } from '../../../helpers/assertion-helpers.js';
import {
createRoom,
@ -22,7 +22,7 @@ describe('E2EE Room Configuration Tests', () => {
let app: Express;
beforeAll(async () => {
app = startTestServer();
app = await startTestServer();
});
afterAll(async () => {

View File

@ -1,7 +1,7 @@
import { afterEach, beforeAll, describe, expect, it, jest } from '@jest/globals';
import { MeetRecordingAccess, MeetRoomConfig, MeetSignalType } from '@openvidu-meet/typings';
import { container } from '../../../../src/config/index.js';
import { FrontendEventService } from '../../../../src/services/index.js';
import { MeetSignalType, MeetRecordingAccess } from '@openvidu-meet/typings';
import {
createRoom,
deleteAllRooms,
@ -114,6 +114,23 @@ describe('Room API Tests', () => {
expect(getResponse.status).toBe(200);
expect(getResponse.body.config).toEqual(partialConfig);
});
it('should return 404 when updating non-existent room', async () => {
const nonExistentRoomId = 'non-existent-room';
const config = {
recording: {
enabled: false,
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
},
chat: { enabled: false },
virtualBackground: { enabled: false }
};
const response = await updateRoomConfig(nonExistentRoomId, config);
expect(response.status).toBe(404);
expect(response.body.message).toContain(`'${nonExistentRoomId}' does not exist`);
});
});
describe('Update Room Config Validation failures', () => {
@ -130,7 +147,7 @@ describe('Room API Tests', () => {
// Missing chat config
virtualBackground: { enabled: false }
};
const response = await updateRoomConfig(roomId, invalidConfig);
const response = await updateRoomConfig(roomId, invalidConfig as unknown as MeetRoomConfig);
expect(response.status).toBe(422);
expect(response.body.error).toContain('Unprocessable Entity');
@ -151,7 +168,7 @@ describe('Room API Tests', () => {
chat: { enabled: false },
virtualBackground: { enabled: false }
};
const response = await updateRoomConfig(createdRoom.roomId, invalidConfig);
const response = await updateRoomConfig(createdRoom.roomId, invalidConfig as unknown as MeetRoomConfig);
expect(response.status).toBe(422);
expect(response.body.error).toContain('Unprocessable Entity');
@ -164,7 +181,7 @@ describe('Room API Tests', () => {
});
const emptyConfig = {};
const response = await updateRoomConfig(createdRoom.roomId, emptyConfig);
const response = await updateRoomConfig(createdRoom.roomId, emptyConfig as unknown as MeetRoomConfig);
expect(response.status).toBe(422);
expect(response.body.error).toContain('Unprocessable Entity');
@ -188,22 +205,5 @@ describe('Room API Tests', () => {
expect(response.body.error).toContain('Unprocessable Entity');
expect(JSON.stringify(response.body.details)).toContain('recording.allowAccessTo');
});
it('should return 404 when updating non-existent room', async () => {
const nonExistentRoomId = 'non-existent-room';
const config = {
recording: {
enabled: false,
allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER
},
chat: { enabled: false },
virtualBackground: { enabled: false }
};
const response = await updateRoomConfig(nonExistentRoomId, config);
expect(response.status).toBe(404);
expect(response.body.message).toContain(`'${nonExistentRoomId}' does not exist`);
});
});
});