Moves recording API to public endpoint
This commit refactors the recording API endpoints from the internal API to the public API. This change allows users to start and stop recordings using API keys, enabling more secure and flexible access control for recording functionality. It also centralizes recording-related logic in the public API, simplifying the codebase and improving maintainability.
This commit is contained in:
parent
55aab084b0
commit
215b11e93f
@ -2,7 +2,7 @@ description: Conflict — The recording cannot be started due to resource state
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../schemas/error.yaml'
|
||||
$ref: '../schemas/error.yaml'
|
||||
examples:
|
||||
already_recording:
|
||||
summary: Room is already being recorded
|
||||
@ -2,7 +2,7 @@ description: Conflict — The recording is starting or already stopped
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../schemas/error.yaml'
|
||||
$ref: '../schemas/error.yaml'
|
||||
examples:
|
||||
starting_recording:
|
||||
summary: Recording is starting
|
||||
@ -2,7 +2,7 @@ description: Service Unavailable — The recording service is unavailable
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../schemas/error.yaml'
|
||||
$ref: '../schemas/error.yaml'
|
||||
examples:
|
||||
starting_timeout:
|
||||
summary: Recording service timed out
|
||||
@ -2,7 +2,7 @@ description: Successfully created the OpenVidu Meet recording
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../schemas/meet-recording.yaml'
|
||||
$ref: '../schemas/meet-recording.yaml'
|
||||
example:
|
||||
recordingId: 'room-123--EG_XYZ--XX445'
|
||||
roomId: 'room-123'
|
||||
@ -8,7 +8,7 @@ headers:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../schemas/meet-recording.yaml'
|
||||
$ref: '../schemas/meet-recording.yaml'
|
||||
example:
|
||||
recordingId: 'room-123--EG_XYZ--XX445'
|
||||
roomId: 'room-123'
|
||||
@ -35,6 +35,8 @@ paths:
|
||||
$ref: './paths/recordings.yaml#/~1recordings~1{recordingId}~1media'
|
||||
/recordings/{recordingId}/url:
|
||||
$ref: './paths/recordings.yaml#/~1recordings~1{recordingId}~1url'
|
||||
/recordings/{recordingId}/stop:
|
||||
$ref: './paths/recordings.yaml#/~1recordings~1{recordingId}~1stop'
|
||||
components:
|
||||
securitySchemes:
|
||||
$ref: './components/security.yaml'
|
||||
|
||||
@ -34,10 +34,6 @@ paths:
|
||||
$ref: './paths/internal/meet-global-config.yaml#/~1config~1rooms~1appearance'
|
||||
/rooms/{roomId}/token:
|
||||
$ref: './paths/internal/rooms.yaml#/~1rooms~1{roomId}~1token'
|
||||
/recordings:
|
||||
$ref: './paths/internal/recordings.yaml#/~1recordings'
|
||||
/recordings/{recordingId}/stop:
|
||||
$ref: './paths/internal/recordings.yaml#/~1recordings~1{recordingId}~1stop'
|
||||
/meetings/{roomId}:
|
||||
$ref: './paths/internal/meetings.yaml#/~1meetings~1{roomId}'
|
||||
/meetings/{roomId}/participants/{participantIdentity}:
|
||||
@ -63,8 +59,6 @@ components:
|
||||
$ref: components/schemas/internal/rooms-appearance-config.yaml
|
||||
MeetRoom:
|
||||
$ref: components/schemas/meet-room.yaml
|
||||
MeetRecording:
|
||||
$ref: components/schemas/meet-recording.yaml
|
||||
MeetAnalytics:
|
||||
$ref: components/schemas/internal/meet-analytics.yaml
|
||||
Error:
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
/recordings:
|
||||
post:
|
||||
operationId: startRecording
|
||||
summary: Start a recording
|
||||
description: >
|
||||
Start a new recording for an OpenVidu Meet room with the specified room ID.
|
||||
tags:
|
||||
- Internal API - Recordings
|
||||
security:
|
||||
- roomMemberTokenHeader: []
|
||||
requestBody:
|
||||
$ref: '../../components/requestBodies/internal/start-recording-request.yaml'
|
||||
responses:
|
||||
'201':
|
||||
$ref: '../../components/responses/internal/success-start-recording.yaml'
|
||||
'401':
|
||||
$ref: '../../components/responses/unauthorized-error.yaml'
|
||||
'403':
|
||||
$ref: '../../components/responses/forbidden-not-allowed-error.yaml'
|
||||
'404':
|
||||
$ref: '../../components/responses/error-room-not-found.yaml'
|
||||
'409':
|
||||
$ref: '../../components/responses/internal/error-recording-conflict.yaml'
|
||||
'422':
|
||||
$ref: '../../components/responses/validation-error.yaml'
|
||||
'500':
|
||||
$ref: '../../components/responses/internal-server-error.yaml'
|
||||
'503':
|
||||
$ref: '../../components/responses/internal/error-service-unavailable.yaml'
|
||||
/recordings/{recordingId}/stop:
|
||||
post:
|
||||
operationId: stopRecording
|
||||
summary: Stop a recording
|
||||
description: |
|
||||
Stops a recording with the specified recording ID.
|
||||
|
||||
> **Note:** The recording must be in an `active` state; otherwise, a 409 error is returned.
|
||||
tags:
|
||||
- Internal API - Recordings
|
||||
security:
|
||||
- roomMemberTokenHeader: []
|
||||
parameters:
|
||||
- $ref: '../../components/parameters/recording-id.yaml'
|
||||
responses:
|
||||
'202':
|
||||
$ref: '../../components/responses/internal/success-stop-recording.yaml'
|
||||
'401':
|
||||
$ref: '../../components/responses/unauthorized-error.yaml'
|
||||
'403':
|
||||
$ref: '../../components/responses/forbidden-error.yaml'
|
||||
'404':
|
||||
$ref: '../../components/responses/error-recording-not-found.yaml'
|
||||
'409':
|
||||
$ref: '../../components/responses/internal/error-recording-not-active.yaml'
|
||||
'422':
|
||||
$ref: '../../components/responses/validation-error.yaml'
|
||||
'500':
|
||||
$ref: '../../components/responses/internal-server-error.yaml'
|
||||
@ -1,4 +1,34 @@
|
||||
/recordings:
|
||||
post:
|
||||
operationId: startRecording
|
||||
summary: Start a recording
|
||||
description: >
|
||||
Start a new recording for an OpenVidu Meet room with the specified room ID.
|
||||
tags:
|
||||
- OpenVidu Meet - Recordings
|
||||
security:
|
||||
- apiKeyHeader: []
|
||||
- roomMemberTokenHeader: []
|
||||
requestBody:
|
||||
$ref: '../components/requestBodies/start-recording-request.yaml'
|
||||
responses:
|
||||
'201':
|
||||
$ref: '../components/responses/success-start-recording.yaml'
|
||||
'401':
|
||||
$ref: '../components/responses/unauthorized-error.yaml'
|
||||
'403':
|
||||
$ref: '../components/responses/forbidden-not-allowed-error.yaml'
|
||||
'404':
|
||||
$ref: '../components/responses/error-room-not-found.yaml'
|
||||
'409':
|
||||
$ref: '../components/responses/error-recording-conflict.yaml'
|
||||
'422':
|
||||
$ref: '../components/responses/validation-error.yaml'
|
||||
'500':
|
||||
$ref: '../components/responses/internal-server-error.yaml'
|
||||
'503':
|
||||
$ref: '../components/responses/error-service-unavailable.yaml'
|
||||
|
||||
get:
|
||||
operationId: getRecordings
|
||||
summary: Get all recordings
|
||||
@ -249,6 +279,36 @@
|
||||
$ref: '../components/responses/validation-error.yaml'
|
||||
'500':
|
||||
$ref: '../components/responses/internal-server-error.yaml'
|
||||
/recordings/{recordingId}/stop:
|
||||
post:
|
||||
operationId: stopRecording
|
||||
summary: Stop a recording
|
||||
description: |
|
||||
Stops a recording with the specified recording ID.
|
||||
|
||||
> **Note:** The recording must be in an `active` state; otherwise, a 409 error is returned.
|
||||
tags:
|
||||
- OpenVidu Meet - Recordings
|
||||
security:
|
||||
- apiKeyHeader: []
|
||||
- roomMemberTokenHeader: []
|
||||
parameters:
|
||||
- $ref: '../components/parameters/recording-id.yaml'
|
||||
responses:
|
||||
'202':
|
||||
$ref: '../components/responses/success-stop-recording.yaml'
|
||||
'401':
|
||||
$ref: '../components/responses/unauthorized-error.yaml'
|
||||
'403':
|
||||
$ref: '../components/responses/forbidden-error.yaml'
|
||||
'404':
|
||||
$ref: '../components/responses/error-recording-not-found.yaml'
|
||||
'409':
|
||||
$ref: '../components/responses/error-recording-not-active.yaml'
|
||||
'422':
|
||||
$ref: '../components/responses/validation-error.yaml'
|
||||
'500':
|
||||
$ref: '../components/responses/internal-server-error.yaml'
|
||||
/recordings/{recordingId}/url:
|
||||
get:
|
||||
operationId: getRecordingUrl
|
||||
|
||||
@ -46,9 +46,20 @@ export const withCanRecordPermission = async (req: Request, res: Response, next:
|
||||
|
||||
const requestSessionService = container.get(RequestSessionService);
|
||||
const tokenRoomId = requestSessionService.getRoomIdFromToken();
|
||||
|
||||
/**
|
||||
* If there is no token, the user is allowed to access the resource because one of the following reasons:
|
||||
*
|
||||
* - The request is invoked using the API key.
|
||||
* - The user is admin.
|
||||
*/
|
||||
if (!tokenRoomId) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const permissions = requestSessionService.getRoomMemberMeetPermissions();
|
||||
|
||||
if (!tokenRoomId || !permissions) {
|
||||
if (!permissions) {
|
||||
const error = errorInsufficientPermissions();
|
||||
return rejectRequestFromMeetError(res, error);
|
||||
}
|
||||
|
||||
@ -79,25 +79,24 @@ recordingRouter.get(
|
||||
withCanRetrieveRecordingsPermission,
|
||||
recordingCtrl.getRecordingUrl
|
||||
);
|
||||
|
||||
// Internal Recording Routes
|
||||
export const internalRecordingRouter: Router = Router();
|
||||
internalRecordingRouter.use(bodyParser.urlencoded({ extended: true }));
|
||||
internalRecordingRouter.use(bodyParser.json());
|
||||
|
||||
internalRecordingRouter.post(
|
||||
recordingRouter.post(
|
||||
'/',
|
||||
withAuth(apiKeyValidator, roomMemberTokenValidator),
|
||||
validateStartRecordingReq,
|
||||
withRecordingEnabled,
|
||||
withAuth(roomMemberTokenValidator),
|
||||
withCanRecordPermission,
|
||||
recordingCtrl.startRecording
|
||||
);
|
||||
internalRecordingRouter.post(
|
||||
recordingRouter.post(
|
||||
'/:recordingId/stop',
|
||||
withAuth(apiKeyValidator, roomMemberTokenValidator),
|
||||
withValidRecordingId,
|
||||
withRecordingEnabled,
|
||||
withAuth(roomMemberTokenValidator),
|
||||
withCanRecordPermission,
|
||||
recordingCtrl.stopRecording
|
||||
);
|
||||
|
||||
// Internal Recording Routes
|
||||
// export const internalRecordingRouter: Router = Router();
|
||||
// internalRecordingRouter.use(bodyParser.urlencoded({ extended: true }));
|
||||
// internalRecordingRouter.use(bodyParser.json());
|
||||
|
||||
@ -14,7 +14,7 @@ import { authRouter } from './routes/auth.routes.js';
|
||||
import { configRouter } from './routes/global-config.routes.js';
|
||||
import { livekitWebhookRouter } from './routes/livekit.routes.js';
|
||||
import { internalMeetingRouter } from './routes/meeting.routes.js';
|
||||
import { internalRecordingRouter, recordingRouter } from './routes/recording.routes.js';
|
||||
import { recordingRouter } from './routes/recording.routes.js';
|
||||
import { internalRoomRouter, roomRouter } from './routes/room.routes.js';
|
||||
import { userRouter } from './routes/user.routes.js';
|
||||
import {
|
||||
@ -89,7 +89,7 @@ const createApp = () => {
|
||||
app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/users`, userRouter);
|
||||
app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/rooms`, internalRoomRouter);
|
||||
app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/meetings`, internalMeetingRouter);
|
||||
app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings`, internalRecordingRouter);
|
||||
// app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings`, internalRecordingRouter);
|
||||
app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/config`, configRouter);
|
||||
app.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/analytics`, analyticsRouter);
|
||||
|
||||
|
||||
@ -581,21 +581,21 @@ export const endMeeting = async (roomId: string, moderatorToken: string) => {
|
||||
return response;
|
||||
};
|
||||
|
||||
export const startRecording = async (roomId: string, moderatorToken: string) => {
|
||||
export const startRecording = async (roomId: string) => {
|
||||
checkAppIsRunning();
|
||||
|
||||
return await request(app)
|
||||
.post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings`)
|
||||
.set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, moderatorToken)
|
||||
.post(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings`)
|
||||
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
||||
.send({ roomId });
|
||||
};
|
||||
|
||||
export const stopRecording = async (recordingId: string, moderatorToken: string) => {
|
||||
export const stopRecording = async (recordingId: string) => {
|
||||
checkAppIsRunning();
|
||||
|
||||
const response = await request(app)
|
||||
.post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings/${recordingId}/stop`)
|
||||
.set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, moderatorToken)
|
||||
.post(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${recordingId}/stop`)
|
||||
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
||||
.send();
|
||||
await sleep('2.5s');
|
||||
|
||||
@ -685,7 +685,7 @@ export const downloadRecordings = async (
|
||||
return await req;
|
||||
};
|
||||
|
||||
export const stopAllRecordings = async (moderatorToken: string) => {
|
||||
export const stopAllRecordings = async () => {
|
||||
checkAppIsRunning();
|
||||
|
||||
const response = await getAllRecordings();
|
||||
@ -701,8 +701,8 @@ export const stopAllRecordings = async (moderatorToken: string) => {
|
||||
console.log(`Stopping ${recordingIds.length} recordings...`, recordingIds);
|
||||
const tasks = recordingIds.map((recordingId: string) =>
|
||||
request(app)
|
||||
.post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings/${recordingId}/stop`)
|
||||
.set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, moderatorToken)
|
||||
.post(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${recordingId}/stop`)
|
||||
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY)
|
||||
.send()
|
||||
);
|
||||
const results = await Promise.all(tasks);
|
||||
|
||||
@ -100,7 +100,7 @@ export const setupSingleRoomWithRecording = async (
|
||||
roomName = 'TEST_ROOM'
|
||||
): Promise<RoomData> => {
|
||||
const roomData = await setupSingleRoom(true, roomName);
|
||||
const response = await startRecording(roomData.room.roomId, roomData.moderatorToken);
|
||||
const response = await startRecording(roomData.room.roomId);
|
||||
expectValidStartRecordingResponse(response, roomData.room.roomId, roomData.room.roomName);
|
||||
roomData.recordingId = response.body.recordingId;
|
||||
|
||||
@ -110,7 +110,7 @@ export const setupSingleRoomWithRecording = async (
|
||||
}
|
||||
|
||||
if (stopRecordingCond) {
|
||||
await stopRecording(roomData.recordingId!, roomData.moderatorToken);
|
||||
await stopRecording(roomData.recordingId!);
|
||||
}
|
||||
|
||||
return roomData;
|
||||
@ -145,7 +145,7 @@ export const setupMultiRecordingsTestContext = async (
|
||||
}
|
||||
|
||||
// Send start recording request
|
||||
const response = await startRecording(roomData.room.roomId, roomData.moderatorToken);
|
||||
const response = await startRecording(roomData.room.roomId);
|
||||
expectValidStartRecordingResponse(response, roomData.room.roomId, roomData.room.roomName);
|
||||
|
||||
// Store the recordingId in context
|
||||
@ -162,7 +162,7 @@ export const setupMultiRecordingsTestContext = async (
|
||||
// Stop recordings for the first numStops rooms
|
||||
const stopPromises = startedRooms.slice(0, numStops).map(async (roomData) => {
|
||||
if (roomData.recordingId) {
|
||||
await stopRecording(roomData.recordingId, roomData.moderatorToken);
|
||||
await stopRecording(roomData.recordingId);
|
||||
console.log(`Recording stopped for room ${roomData.room.roomId}`);
|
||||
return roomData.recordingId;
|
||||
}
|
||||
|
||||
@ -48,11 +48,7 @@ describe('Recording API Tests', () => {
|
||||
|
||||
it('should get an ACTIVE recording status', async () => {
|
||||
const contextAux = await setupMultiRecordingsTestContext(1, 1, 0);
|
||||
const {
|
||||
room: roomAux,
|
||||
recordingId: recordingIdAux = '',
|
||||
moderatorToken: moderatorTokenAux
|
||||
} = contextAux.getRoomByIndex(0)!;
|
||||
const { room: roomAux, recordingId: recordingIdAux = '' } = contextAux.getRoomByIndex(0)!;
|
||||
const response = await getRecording(recordingIdAux);
|
||||
|
||||
expectValidGetRecordingResponse(
|
||||
@ -63,7 +59,7 @@ describe('Recording API Tests', () => {
|
||||
MeetRecordingStatus.ACTIVE
|
||||
);
|
||||
|
||||
await stopAllRecordings(moderatorTokenAux);
|
||||
await stopAllRecordings();
|
||||
});
|
||||
|
||||
it('should return 404 when recording does not exist', async () => {
|
||||
|
||||
@ -65,7 +65,7 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
|
||||
try {
|
||||
// Attempt to start recording
|
||||
const result = await startRecording(roomData.room.roomId, roomData.moderatorToken);
|
||||
const result = await startRecording(roomData.room.roomId);
|
||||
expect(eventServiceOffSpy).toHaveBeenCalledWith(
|
||||
DistributedEventType.RECORDING_ACTIVE,
|
||||
expect.any(Function)
|
||||
@ -120,7 +120,7 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
|
||||
try {
|
||||
// Start recording with a short timeout
|
||||
const result = await startRecording(roomData.room.roomId, roomData.moderatorToken);
|
||||
const result = await startRecording(roomData.room.roomId);
|
||||
|
||||
expect(eventServiceOffSpy).toHaveBeenCalledWith(
|
||||
DistributedEventType.RECORDING_ACTIVE,
|
||||
@ -180,7 +180,7 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
|
||||
try {
|
||||
// Start recording in room1 (should timeout)
|
||||
const rec1 = await startRecording(room1.room.roomId, room1.moderatorToken);
|
||||
const rec1 = await startRecording(room1.room.roomId);
|
||||
expect(rec1.status).toBe(503);
|
||||
|
||||
setInternalConfig({
|
||||
@ -188,18 +188,18 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
});
|
||||
// ✅ EXPECTED BEHAVIOR: System should remain stable
|
||||
// Recording in different room should work normally
|
||||
const rec2 = await startRecording(room2.room.roomId, room2.moderatorToken);
|
||||
const rec2 = await startRecording(room2.room.roomId);
|
||||
expect(rec2.status).toBe(201);
|
||||
expectValidStartRecordingResponse(rec2, room2.room.roomId, room2.room.roomName);
|
||||
|
||||
let response = await stopRecording(rec2.body.recordingId!, room2.moderatorToken);
|
||||
let response = await stopRecording(rec2.body.recordingId!);
|
||||
expectValidStopRecordingResponse(response, rec2.body.recordingId!, room2.room.roomId, room2.room.roomName);
|
||||
|
||||
// ✅ EXPECTED BEHAVIOR: After timeout cleanup, room1 should be available again
|
||||
const rec3 = await startRecording(room1.room.roomId, room1.moderatorToken);
|
||||
const rec3 = await startRecording(room1.room.roomId);
|
||||
expect(rec3.status).toBe(201);
|
||||
expectValidStartRecordingResponse(rec3, room1.room.roomId, room1.room.roomName);
|
||||
response = await stopRecording(rec3.body.recordingId!, room1.moderatorToken);
|
||||
response = await stopRecording(rec3.body.recordingId!);
|
||||
expectValidStopRecordingResponse(response, rec3.body.recordingId!, room1.room.roomId, room1.room.roomName);
|
||||
} finally {
|
||||
startRoomCompositeSpy.mockRestore();
|
||||
@ -224,7 +224,7 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
try {
|
||||
// Start recordings in all rooms simultaneously (all should timeout)
|
||||
const results = await Promise.all(
|
||||
rooms.map((room) => startRecording(room.room.roomId, room.moderatorToken))
|
||||
rooms.map((room) => startRecording(room.room.roomId))
|
||||
);
|
||||
|
||||
// All should timeout
|
||||
@ -239,14 +239,14 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
|
||||
// ✅ EXPECTED BEHAVIOR: After timeouts, all rooms should be available again
|
||||
const retryResults = await Promise.all(
|
||||
rooms.map((room) => startRecording(room.room.roomId, room.moderatorToken))
|
||||
rooms.map((room) => startRecording(room.room.roomId))
|
||||
);
|
||||
|
||||
for (const startResult of retryResults) {
|
||||
expect(startResult.status).toBe(201);
|
||||
const room = rooms.find((r) => r.room.roomId === startResult.body.roomId)!;
|
||||
expectValidStartRecordingResponse(startResult, room.room.roomId, room.room.roomName);
|
||||
const stopResult = await stopRecording(startResult.body.recordingId!, room.moderatorToken);
|
||||
const stopResult = await stopRecording(startResult.body.recordingId!);
|
||||
expectValidStopRecordingResponse(
|
||||
stopResult,
|
||||
startResult.body.recordingId!,
|
||||
@ -270,18 +270,18 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
eventController.initialize();
|
||||
eventController.pauseEventsForRoom(roomDataA!.room.roomId);
|
||||
|
||||
const recordingPromiseA = startRecording(roomDataA!.room.roomId, roomDataA!.moderatorToken);
|
||||
const recordingPromiseA = startRecording(roomDataA!.room.roomId);
|
||||
|
||||
// Brief delay to ensure both recordings start in the right order
|
||||
await sleep('1s');
|
||||
|
||||
// Step 2: Start recording in roomB (this will complete quickly)
|
||||
const recordingResponseB = await startRecording(roomDataB!.room.roomId, roomDataB!.moderatorToken);
|
||||
const recordingResponseB = await startRecording(roomDataB!.room.roomId);
|
||||
expectValidStartRecordingResponse(recordingResponseB, roomDataB!.room.roomId, roomDataB!.room.roomName);
|
||||
const recordingIdB = recordingResponseB.body.recordingId;
|
||||
|
||||
// Step 3: Stop recording in roomB while roomA is still waiting for its event
|
||||
const stopResponseB = await stopRecording(recordingIdB, roomDataB!.moderatorToken);
|
||||
const stopResponseB = await stopRecording(recordingIdB);
|
||||
expectValidStopRecordingResponse(stopResponseB, recordingIdB, roomDataB!.room.roomId, roomDataB!.room.roomName);
|
||||
|
||||
eventController.releaseEventsForRoom(roomDataA!.room.roomId);
|
||||
@ -301,7 +301,7 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
const roomDataList = Array.from({ length: 5 }, (_, index) => context!.getRoomByIndex(index)!);
|
||||
|
||||
const startResponses = await Promise.all(
|
||||
roomDataList.map((roomData) => startRecording(roomData.room.roomId, roomData.moderatorToken))
|
||||
roomDataList.map((roomData) => startRecording(roomData.room.roomId))
|
||||
);
|
||||
|
||||
startResponses.forEach((response, index) => {
|
||||
@ -315,7 +315,7 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
const recordingIds = startResponses.map((res) => res.body.recordingId);
|
||||
|
||||
const stopResponses = await Promise.all(
|
||||
recordingIds.map((recordingId, index) => stopRecording(recordingId, roomDataList[index].moderatorToken))
|
||||
recordingIds.map((recordingId) => stopRecording(recordingId))
|
||||
);
|
||||
|
||||
stopResponses.forEach((response, index) => {
|
||||
@ -332,14 +332,14 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
context = await setupMultiRoomTestContext(2, true);
|
||||
const roomDataA = context.getRoomByIndex(0);
|
||||
const roomDataB = context.getRoomByIndex(1);
|
||||
const responseA = await startRecording(roomDataA!.room.roomId, roomDataA!.moderatorToken);
|
||||
const responseB = await startRecording(roomDataB!.room.roomId, roomDataB!.moderatorToken);
|
||||
const responseA = await startRecording(roomDataA!.room.roomId);
|
||||
const responseB = await startRecording(roomDataB!.room.roomId);
|
||||
const recordingIdA = responseA.body.recordingId;
|
||||
const recordingIdB = responseB.body.recordingId;
|
||||
|
||||
const [stopResponseA, stopResponseB] = await Promise.all([
|
||||
stopRecording(recordingIdA, roomDataA!.moderatorToken),
|
||||
stopRecording(recordingIdB, roomDataB!.moderatorToken)
|
||||
stopRecording(recordingIdA),
|
||||
stopRecording(recordingIdB)
|
||||
]);
|
||||
expectValidStopRecordingResponse(stopResponseA, recordingIdA, roomDataA!.room.roomId, roomDataA!.room.roomName);
|
||||
expectValidStopRecordingResponse(stopResponseB, recordingIdB, roomDataB!.room.roomId, roomDataB!.room.roomName);
|
||||
@ -350,8 +350,8 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
const roomData = context.getRoomByIndex(0)!;
|
||||
|
||||
const [firstRecordingResponse, secondRecordingResponse] = await Promise.all([
|
||||
startRecording(roomData.room.roomId, roomData.moderatorToken),
|
||||
startRecording(roomData.room.roomId, roomData.moderatorToken)
|
||||
startRecording(roomData.room.roomId),
|
||||
startRecording(roomData.room.roomId)
|
||||
]);
|
||||
|
||||
console.log('First recording response:', firstRecordingResponse.body);
|
||||
@ -366,7 +366,7 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
if (firstRecordingResponse.status === 201) {
|
||||
expectValidStartRecordingResponse(firstRecordingResponse, roomData.room.roomId, roomData.room.roomName);
|
||||
// stop the first recording
|
||||
const stopResponse = await stopRecording(firstRecordingResponse.body.recordingId, roomData.moderatorToken);
|
||||
const stopResponse = await stopRecording(firstRecordingResponse.body.recordingId);
|
||||
expectValidStopRecordingResponse(
|
||||
stopResponse,
|
||||
firstRecordingResponse.body.recordingId,
|
||||
@ -376,7 +376,7 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
} else {
|
||||
expectValidStartRecordingResponse(secondRecordingResponse, roomData.room.roomId, roomData.room.roomName);
|
||||
// stop the second recording
|
||||
const stopResponse = await stopRecording(secondRecordingResponse.body.recordingId, roomData.moderatorToken);
|
||||
const stopResponse = await stopRecording(secondRecordingResponse.body.recordingId);
|
||||
expectValidStopRecordingResponse(
|
||||
stopResponse,
|
||||
secondRecordingResponse.body.recordingId,
|
||||
@ -393,12 +393,12 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
const recordingTaskScheduler = container.get(RecordingScheduledTasksService);
|
||||
const gcSpy = jest.spyOn(recordingTaskScheduler as any, 'performActiveRecordingLocksGC');
|
||||
|
||||
const startResponse = await startRecording(roomData.room.roomId, roomData.moderatorToken);
|
||||
const startResponse = await startRecording(roomData.room.roomId);
|
||||
expectValidStartRecordingResponse(startResponse, roomData.room.roomId, roomData.room.roomName);
|
||||
const recordingId = startResponse.body.recordingId;
|
||||
|
||||
// Execute garbage collection while stopping the recording
|
||||
const stopPromise = stopRecording(recordingId, roomData.moderatorToken);
|
||||
const stopPromise = stopRecording(recordingId);
|
||||
const gcPromise = recordingTaskScheduler['performActiveRecordingLocksGC']();
|
||||
|
||||
// Both operations should complete
|
||||
@ -465,18 +465,18 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
const room2 = context.getRoomByIndex(1)!;
|
||||
const room3 = context.getRoomByIndex(2)!;
|
||||
|
||||
const start1 = await startRecording(room1.room.roomId, room1.moderatorToken);
|
||||
const start2 = await startRecording(room2.room.roomId, room2.moderatorToken);
|
||||
const start1 = await startRecording(room1.room.roomId);
|
||||
const start2 = await startRecording(room2.room.roomId);
|
||||
|
||||
const recordingId1 = start1.body.recordingId;
|
||||
const recordingId2 = start2.body.recordingId;
|
||||
|
||||
await stopRecording(recordingId1, room1.moderatorToken);
|
||||
await stopRecording(recordingId2, room2.moderatorToken);
|
||||
await stopRecording(recordingId1);
|
||||
await stopRecording(recordingId2);
|
||||
|
||||
// Bulk delete the recordings while starting a new one
|
||||
const bulkDeletePromise = bulkDeleteRecordings([recordingId1, recordingId2]);
|
||||
const startNewRecordingPromise = startRecording(room3.room.roomId, room3.moderatorToken);
|
||||
const startNewRecordingPromise = startRecording(room3.room.roomId);
|
||||
|
||||
// Both operations should complete successfully
|
||||
const [bulkDeleteResult, newRecordingResult] = await Promise.all([bulkDeletePromise, startNewRecordingPromise]);
|
||||
@ -486,7 +486,7 @@ describe('Recording API Race Conditions Tests', () => {
|
||||
// Check that the new recording started successfully
|
||||
expectValidStartRecordingResponse(newRecordingResult, room3.room.roomId, room3.room.roomName);
|
||||
|
||||
const newStopResponse = await stopRecording(newRecordingResult.body.recordingId, room3.moderatorToken);
|
||||
const newStopResponse = await stopRecording(newRecordingResult.body.recordingId);
|
||||
expectValidStopRecordingResponse(
|
||||
newStopResponse,
|
||||
newRecordingResult.body.recordingId,
|
||||
|
||||
@ -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 { expectValidStopRecordingResponse } from '../../../helpers/assertion-helpers.js';
|
||||
import {
|
||||
deleteAllRecordings,
|
||||
deleteAllRooms,
|
||||
@ -15,14 +14,13 @@ import {
|
||||
loginUser,
|
||||
startTestServer,
|
||||
stopAllRecordings,
|
||||
stopRecording,
|
||||
updateRecordingAccessConfigInRoom
|
||||
} from '../../../helpers/request-helpers.js';
|
||||
import { setupSingleRoom, setupSingleRoomWithRecording } from '../../../helpers/test-scenarios.js';
|
||||
import { RoomData } from '../../../interfaces/scenarios.js';
|
||||
|
||||
const RECORDINGS_PATH = `${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings`;
|
||||
const INTERNAL_RECORDINGS_PATH = `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings`;
|
||||
// const INTERNAL_RECORDINGS_PATH = `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings`;
|
||||
|
||||
describe('Recording API Security Tests', () => {
|
||||
let app: Express;
|
||||
@ -45,17 +43,21 @@ describe('Recording API Security Tests', () => {
|
||||
roomData = await setupSingleRoom(true);
|
||||
});
|
||||
|
||||
it('should fail when request includes API key', async () => {
|
||||
afterEach(async () => {
|
||||
await stopAllRecordings();
|
||||
});
|
||||
|
||||
it('should success when request includes API key', async () => {
|
||||
const response = await request(app)
|
||||
.post(INTERNAL_RECORDINGS_PATH)
|
||||
.post(RECORDINGS_PATH)
|
||||
.send({ roomId: roomData.room.roomId })
|
||||
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY);
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.status).toBe(201);
|
||||
});
|
||||
|
||||
it('should fail when user is authenticated as admin', async () => {
|
||||
const response = await request(app)
|
||||
.post(INTERNAL_RECORDINGS_PATH)
|
||||
.post(RECORDINGS_PATH)
|
||||
.send({ roomId: roomData.room.roomId })
|
||||
.set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, adminAccessToken);
|
||||
expect(response.status).toBe(401);
|
||||
@ -63,22 +65,17 @@ describe('Recording API Security Tests', () => {
|
||||
|
||||
it('should succeed when participant is moderator', async () => {
|
||||
const response = await request(app)
|
||||
.post(INTERNAL_RECORDINGS_PATH)
|
||||
.post(RECORDINGS_PATH)
|
||||
.send({ roomId: roomData.room.roomId })
|
||||
.set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.moderatorToken);
|
||||
expect(response.status).toBe(201);
|
||||
|
||||
// Stop recording to clean up
|
||||
const recordingId = response.body.recordingId;
|
||||
const stopResponse = await stopRecording(recordingId, roomData.moderatorToken);
|
||||
expectValidStopRecordingResponse(stopResponse, recordingId, roomData.room.roomId, roomData.room.roomName);
|
||||
});
|
||||
|
||||
it('should fail when participant is moderator of a different room', async () => {
|
||||
const newRoomData = await setupSingleRoom();
|
||||
|
||||
const response = await request(app)
|
||||
.post(INTERNAL_RECORDINGS_PATH)
|
||||
.post(RECORDINGS_PATH)
|
||||
.send({ roomId: roomData.room.roomId })
|
||||
.set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, newRoomData.moderatorToken);
|
||||
expect(response.status).toBe(403);
|
||||
@ -86,7 +83,7 @@ describe('Recording API Security Tests', () => {
|
||||
|
||||
it('should fail when participant is speaker', async () => {
|
||||
const response = await request(app)
|
||||
.post(INTERNAL_RECORDINGS_PATH)
|
||||
.post(RECORDINGS_PATH)
|
||||
.send({ roomId: roomData.room.roomId })
|
||||
.set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.speakerToken);
|
||||
expect(response.status).toBe(403);
|
||||
@ -101,43 +98,36 @@ describe('Recording API Security Tests', () => {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await stopAllRecordings(roomData.moderatorToken);
|
||||
await stopAllRecordings();
|
||||
});
|
||||
|
||||
it('should fail when request includes API key', async () => {
|
||||
it('should success when request includes API key', async () => {
|
||||
const response = await request(app)
|
||||
.post(`${INTERNAL_RECORDINGS_PATH}/${roomData.recordingId}/stop`)
|
||||
.post(`${RECORDINGS_PATH}/${roomData.recordingId}/stop`)
|
||||
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.status).toBe(202);
|
||||
});
|
||||
|
||||
it('should fail when user is authenticated as admin', async () => {
|
||||
const response = await request(app)
|
||||
.post(`${INTERNAL_RECORDINGS_PATH}/${roomData.recordingId}/stop`)
|
||||
.post(`${RECORDINGS_PATH}/${roomData.recordingId}/stop`)
|
||||
.set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, adminAccessToken);
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
it('should succeed when participant is moderator', async () => {
|
||||
const response = await request(app)
|
||||
.post(`${INTERNAL_RECORDINGS_PATH}/${roomData.recordingId}/stop`)
|
||||
.set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.moderatorToken);
|
||||
expect(response.status).toBe(202);
|
||||
});
|
||||
|
||||
it('should fail when participant is moderator of a different room', async () => {
|
||||
const newRoomData = await setupSingleRoom();
|
||||
|
||||
const response = await request(app)
|
||||
.post(`${INTERNAL_RECORDINGS_PATH}/${roomData.recordingId}/stop`)
|
||||
.post(`${RECORDINGS_PATH}/${roomData.recordingId}/stop`)
|
||||
.set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, newRoomData.moderatorToken);
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
it('should fail when participant is speaker', async () => {
|
||||
const response = await request(app)
|
||||
.post(`${INTERNAL_RECORDINGS_PATH}/${roomData.recordingId}/stop`)
|
||||
.post(`${RECORDINGS_PATH}/${roomData.recordingId}/stop`)
|
||||
.set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.speakerToken);
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
@ -11,7 +11,7 @@ import { RecordingShareDialogComponent } from '../components/recording-share-dia
|
||||
})
|
||||
export class RecordingService {
|
||||
protected readonly RECORDINGS_API = `${HttpService.API_PATH_PREFIX}/recordings`;
|
||||
protected readonly INTERNAL_RECORDINGS_API = `${HttpService.INTERNAL_API_PATH_PREFIX}/recordings`;
|
||||
// protected readonly INTERNAL_RECORDINGS_API = `${HttpService.INTERNAL_API_PATH_PREFIX}/recordings`;
|
||||
|
||||
protected log;
|
||||
|
||||
@ -32,7 +32,7 @@ export class RecordingService {
|
||||
*/
|
||||
async startRecording(roomId: string): Promise<MeetRecordingInfo> {
|
||||
try {
|
||||
return this.httpService.postRequest(this.INTERNAL_RECORDINGS_API, { roomId });
|
||||
return this.httpService.postRequest(this.RECORDINGS_API, { roomId });
|
||||
} catch (error) {
|
||||
console.error('Error starting recording:', error);
|
||||
throw error;
|
||||
@ -51,7 +51,7 @@ export class RecordingService {
|
||||
}
|
||||
|
||||
try {
|
||||
const path = `${this.INTERNAL_RECORDINGS_API}/${recordingId}/stop`;
|
||||
const path = `${this.RECORDINGS_API}/${recordingId}/stop`;
|
||||
return this.httpService.postRequest(path, {});
|
||||
} catch (error) {
|
||||
console.error('Error stopping recording:', error);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user