diff --git a/src/app.module.ts b/src/app.module.ts index 7daeff6..b589dcc 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,8 +5,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import configuration from './config/configuration'; import { SequelizeModule, SequelizeModuleOptions } from '@nestjs/sequelize'; import { format } from 'sql-formatter'; -import { UserModule } from './modules/user/user.module'; -import { WebhookModule } from './modules/webhook/webhook.module'; +import { SharedModule } from './shared/shared.module'; import { LoggerModule } from './common/logger/logger.module'; const defaultDbConfig = ( @@ -77,8 +76,7 @@ const defaultDbConfig = ( }), }), CallModule, - UserModule, - WebhookModule, + SharedModule, ], controllers: [], }) diff --git a/src/externals/http/http.module.ts b/src/externals/http/http.module.ts index f44dccf..b25a479 100644 --- a/src/externals/http/http.module.ts +++ b/src/externals/http/http.module.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ import { Module } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; import { HttpsAgent } from 'agentkeepalive'; diff --git a/src/externals/payments.service.ts b/src/externals/payments.service.ts index 8bb46c9..080c341 100644 --- a/src/externals/payments.service.ts +++ b/src/externals/payments.service.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { Inject, Injectable } from '@nestjs/common'; diff --git a/src/modules/call/call.controller.spec.ts b/src/modules/call/call.controller.spec.ts index 647e625..d00966f 100644 --- a/src/modules/call/call.controller.spec.ts +++ b/src/modules/call/call.controller.spec.ts @@ -6,8 +6,8 @@ import { NotFoundException, } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { UsersInRoomDto } from '../room/dto/users-in-room.dto'; -import { RoomUserUseCase } from '../room/room-user.usecase'; +import { UsersInRoomDto } from './dto/users-in-room.dto'; +import { RoomService } from './services/room.service'; import { CallController } from './call.controller'; import { CallUseCase } from './call.usecase'; import { JoinCallDto, JoinCallResponseDto } from './dto/join-call.dto'; @@ -17,7 +17,7 @@ import { createMockUserToken, mockUserPayload } from './fixtures'; describe('Testing Call Endpoints', () => { let callController: CallController; let callUseCase: DeepMocked; - let roomUserUseCase: DeepMocked; + let roomService: DeepMocked; const mockRoomId = 'test-room-id'; const mockJoinCallDto: JoinCallDto = { @@ -51,21 +51,13 @@ describe('Testing Call Endpoints', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [CallController], - providers: [ - { - provide: CallUseCase, - useValue: createMock(), - }, - { - provide: RoomUserUseCase, - useValue: createMock(), - }, - ], - }).compile(); + }) + .useMocker(createMock) + .compile(); callController = module.get(CallController); callUseCase = module.get>(CallUseCase); - roomUserUseCase = module.get>(RoomUserUseCase); + roomService = module.get>(RoomService); }); describe('Creating a call', () => { @@ -88,15 +80,10 @@ describe('Testing Call Endpoints', () => { appId: 'jitsi-app-id', }; - callUseCase.validateUserHasNoActiveRoom.mockResolvedValueOnce(undefined); callUseCase.createCallAndRoom.mockResolvedValueOnce(mockResponse); const result = await callController.createCall(mockUserToken.payload); - expect(callUseCase.validateUserHasNoActiveRoom).toHaveBeenCalledWith( - mockUserToken.payload.uuid, - mockUserToken.payload.email, - ); expect(callUseCase.createCallAndRoom).toHaveBeenCalledWith( mockUserToken.payload, ); @@ -106,7 +93,7 @@ describe('Testing Call Endpoints', () => { it('When the room already exists, then an error indicating so is thrown', async () => { const mockUserToken = createMockUserToken(); - callUseCase.validateUserHasNoActiveRoom.mockRejectedValueOnce( + callUseCase.createCallAndRoom.mockRejectedValueOnce( new ConflictException('User already has an active room as host'), ); @@ -118,7 +105,7 @@ describe('Testing Call Endpoints', () => { it('When an unexpected error occurs, then an error indicating so is thrown', async () => { const mockUserToken = createMockUserToken(); - callUseCase.validateUserHasNoActiveRoom.mockRejectedValueOnce( + callUseCase.createCallAndRoom.mockRejectedValueOnce( new Error('Unexpected error'), ); @@ -198,7 +185,11 @@ describe('Testing Call Endpoints', () => { ); await expect( - callController.joinCall(mockRoomId, mockUserToken.payload), + callController.joinCall( + mockRoomId, + mockUserToken.payload, + mockJoinCallDto, + ), ).rejects.toThrow(NotFoundException); }); @@ -210,7 +201,11 @@ describe('Testing Call Endpoints', () => { ); await expect( - callController.joinCall(mockRoomId, mockUserToken.payload), + callController.joinCall( + mockRoomId, + mockUserToken.payload, + mockJoinCallDto, + ), ).rejects.toThrow(ConflictException); }); @@ -222,7 +217,11 @@ describe('Testing Call Endpoints', () => { ); await expect( - callController.joinCall(mockRoomId, mockUserToken.payload), + callController.joinCall( + mockRoomId, + mockUserToken.payload, + mockJoinCallDto, + ), ).rejects.toThrow(BadRequestException); }); @@ -232,7 +231,11 @@ describe('Testing Call Endpoints', () => { callUseCase.joinCall.mockRejectedValueOnce(new Error('Unexpected error')); await expect( - callController.joinCall(mockRoomId, mockUserToken.payload), + callController.joinCall( + mockRoomId, + mockUserToken.payload, + mockJoinCallDto, + ), ).rejects.toThrow(Error); }); @@ -265,51 +268,51 @@ describe('Testing Call Endpoints', () => { describe('Getting users in a call', () => { it('should get users in a call for authenticated user', async () => { - roomUserUseCase.getUsersInRoom.mockResolvedValue(mockUsersInRoom); + roomService.getUsersInRoom.mockResolvedValue(mockUsersInRoom); const result = await callController.getUsersInCall(mockRoomId); expect(result).toEqual(mockUsersInRoom); - expect(roomUserUseCase.getUsersInRoom).toHaveBeenCalledWith(mockRoomId); + expect(roomService.getUsersInRoom).toHaveBeenCalledWith(mockRoomId); }); it('should get users in a call for anonymous user', async () => { - roomUserUseCase.getUsersInRoom.mockResolvedValue(mockUsersInRoom); + roomService.getUsersInRoom.mockResolvedValue(mockUsersInRoom); const result = await callController.getUsersInCall(mockRoomId); expect(result).toEqual(mockUsersInRoom); - expect(roomUserUseCase.getUsersInRoom).toHaveBeenCalledWith(mockRoomId); + expect(roomService.getUsersInRoom).toHaveBeenCalledWith(mockRoomId); }); it('When the room is not found, it should propagate NotFoundException', async () => { - roomUserUseCase.getUsersInRoom.mockRejectedValueOnce( + roomService.getUsersInRoom.mockRejectedValue( new NotFoundException('Specified room not found'), ); await expect(callController.getUsersInCall(mockRoomId)).rejects.toThrow( NotFoundException, ); - expect(roomUserUseCase.getUsersInRoom).toHaveBeenCalledWith(mockRoomId); + expect(roomService.getUsersInRoom).toHaveBeenCalledWith(mockRoomId); }); it('When an unexpected error occurs, it should propagate the error', async () => { - roomUserUseCase.getUsersInRoom.mockRejectedValueOnce( + roomService.getUsersInRoom.mockRejectedValue( new Error('Unexpected error'), ); await expect(callController.getUsersInCall(mockRoomId)).rejects.toThrow( Error, ); - expect(roomUserUseCase.getUsersInRoom).toHaveBeenCalledWith(mockRoomId); + expect(roomService.getUsersInRoom).toHaveBeenCalledWith(mockRoomId); }); it('When no users are in the room, it should return an empty array', async () => { - roomUserUseCase.getUsersInRoom.mockResolvedValueOnce([]); + roomService.getUsersInRoom.mockResolvedValue([]); const result = await callController.getUsersInCall(mockRoomId); - expect(roomUserUseCase.getUsersInRoom).toHaveBeenCalledWith(mockRoomId); + expect(roomService.getUsersInRoom).toHaveBeenCalledWith(mockRoomId); expect(result).toEqual([]); expect(result.length).toBe(0); }); @@ -336,7 +339,7 @@ describe('Testing Call Endpoints', () => { const leaveCallDto = new LeaveCallDto(); leaveCallDto.userId = anonymousUserId; - callUseCase.leaveCall.mockResolvedValueOnce(); + callUseCase.leaveCall.mockResolvedValue(); await callController.leaveCall(mockRoomId, null, leaveCallDto); @@ -350,7 +353,7 @@ describe('Testing Call Endpoints', () => { const leaveCallDto = new LeaveCallDto(); leaveCallDto.userId = 'anonymous-user-id'; - callUseCase.leaveCall.mockResolvedValueOnce(); + callUseCase.leaveCall.mockResolvedValue(); const userToken = createMockUserToken(); await callController.leaveCall( @@ -367,7 +370,7 @@ describe('Testing Call Endpoints', () => { it('should pass undefined when neither authenticated user nor DTO with userId are provided', async () => { const emptyDto = new LeaveCallDto(); - callUseCase.leaveCall.mockResolvedValueOnce(); + callUseCase.leaveCall.mockResolvedValue(); await callController.leaveCall(mockRoomId, null, emptyDto); @@ -377,7 +380,7 @@ describe('Testing Call Endpoints', () => { it('should propagate NotFoundException when room is not found', async () => { const userToken = createMockUserToken(); - callUseCase.leaveCall.mockRejectedValueOnce( + callUseCase.leaveCall.mockRejectedValue( new NotFoundException('Specified room not found'), ); @@ -387,7 +390,7 @@ describe('Testing Call Endpoints', () => { }); it('should propagate BadRequestException when no userId is provided', async () => { - callUseCase.leaveCall.mockRejectedValueOnce( + callUseCase.leaveCall.mockRejectedValue( new BadRequestException('User ID is required'), ); diff --git a/src/modules/call/call.controller.ts b/src/modules/call/call.controller.ts index 8903f17..932e18d 100644 --- a/src/modules/call/call.controller.ts +++ b/src/modules/call/call.controller.ts @@ -1,10 +1,10 @@ import { BadRequestException, Body, - ConflictException, Controller, Get, HttpCode, + HttpException, InternalServerErrorException, Logger, Param, @@ -27,8 +27,8 @@ import { JwtAuthGuard } from '../auth/auth.guard'; import { OptionalAuth } from '../auth/decorators/optional-auth.decorator'; import { User } from '../auth/decorators/user.decorator'; import { UserTokenData } from '../auth/dto/user.dto'; -import { UsersInRoomDto } from '../room/dto/users-in-room.dto'; -import { RoomUserUseCase } from '../room/room-user.usecase'; +import { UsersInRoomDto } from './dto/users-in-room.dto'; +import { RoomService } from './services/room.service'; import { CallUseCase } from './call.usecase'; import { CreateCallResponseDto } from './dto/create-call.dto'; import { JoinCallDto, JoinCallResponseDto } from './dto/join-call.dto'; @@ -41,7 +41,7 @@ export class CallController { constructor( private readonly callUseCase: CallUseCase, - private readonly roomUserUseCase: RoomUserUseCase, + private readonly roomService: RoomService, ) {} @Post('/') @@ -70,32 +70,26 @@ export class CallController { } try { - await this.callUseCase.validateUserHasNoActiveRoom(uuid, email); const call = await this.callUseCase.createCallAndRoom(user); return call; } catch (error) { const err = error as Error; this.logger.error( - `Failed to create call: ${err.message}`, { userId: uuid, email: email, - error: err.name, + err, }, - err.stack, + 'Failed to create a call and room', ); - if ( - error instanceof BadRequestException || - error instanceof ConflictException || - error instanceof InternalServerErrorException - ) { + if (error instanceof HttpException) { throw error; } throw new InternalServerErrorException( 'An unexpected error occurred while creating the call', - { cause: err.stack ?? err.message }, + { cause: err.message }, ); } } @@ -151,7 +145,7 @@ export class CallController { @ApiNotFoundResponse({ description: 'Call/Room not found' }) @ApiInternalServerErrorResponse({ description: 'Internal server error' }) getUsersInCall(@Param('id') roomId: string): Promise { - return this.roomUserUseCase.getUsersInRoom(roomId); + return this.roomService.getUsersInRoom(roomId); } @Post('/:id/users/leave') diff --git a/src/modules/call/call.module.ts b/src/modules/call/call.module.ts index 274a94a..dbb1ef2 100644 --- a/src/modules/call/call.module.ts +++ b/src/modules/call/call.module.ts @@ -1,33 +1,41 @@ import { Module } from '@nestjs/common'; -import { CallService } from './call.service'; +import { CallService } from './services/call.service'; import { CallController } from './call.controller'; import { PaymentService } from '../../externals/payments.service'; import { HttpClientModule } from '../../externals/http/http.module'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { AuthModule } from '../auth/auth.module'; -import { RoomUseCase } from '../room/room.usecase'; -import { SequelizeRoomRepository } from '../room/room.repository'; +import { RoomService } from './services/room.service'; +import { SequelizeRoomRepository } from './infrastructure/room.repository'; import { SequelizeModule } from '@nestjs/sequelize'; -import { RoomModel } from '../room/models/room.model'; -import { RoomModule } from '../room/room.module'; +import { RoomModel } from './models/room.model'; +import { RoomUserModel } from './models/room-user.model'; +import { SequelizeRoomUserRepository } from './infrastructure/room-user.repository'; import { CallUseCase } from './call.usecase'; +import { AvatarService } from '../../externals/avatar/avatar.service'; +import { SharedModule } from '../../shared/shared.module'; +import { JitsiWebhookService } from './webhooks/jitsi/jitsi-webhook.service'; +import { JitsiWebhookController } from './webhooks/jitsi/jitsi-webhook.controller'; @Module({ - controllers: [CallController], + controllers: [CallController, JitsiWebhookController], providers: [ CallService, PaymentService, ConfigService, - RoomUseCase, + RoomService, SequelizeRoomRepository, + SequelizeRoomUserRepository, CallUseCase, + AvatarService, + JitsiWebhookService, ], imports: [ HttpClientModule, ConfigModule, AuthModule, - RoomModule, - SequelizeModule.forFeature([RoomModel]), + SharedModule, + SequelizeModule.forFeature([RoomModel, RoomUserModel]), ], }) export class CallModule {} diff --git a/src/modules/call/call.usecase.spec.ts b/src/modules/call/call.usecase.spec.ts index a57bb67..10ee906 100644 --- a/src/modules/call/call.usecase.spec.ts +++ b/src/modules/call/call.usecase.spec.ts @@ -3,17 +3,16 @@ import { BadRequestException, ConflictException, ForbiddenException, - InternalServerErrorException, NotFoundException, } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { RoomUser } from '../room/room-user.domain'; -import { RoomUserUseCase } from '../room/room-user.usecase'; -import { Room } from '../room/room.domain'; -import { RoomUseCase } from '../room/room.usecase'; -import { CallService } from './call.service'; +import { RoomUser } from './domain/room-user.domain'; +import { Room } from './domain/room.domain'; +import { RoomService } from './services/room.service'; +import { CallService } from './services/call.service'; import { CallUseCase } from './call.usecase'; -import { mockCallResponse, mockRoomData, mockUserPayload } from './fixtures'; +import { mockRoomData, mockUserPayload } from './fixtures'; +import { v4 } from 'uuid'; jest.mock('uuid', () => ({ v4: jest.fn(() => 'generated-uuid'), @@ -22,40 +21,23 @@ jest.mock('uuid', () => ({ describe('CallUseCase', () => { let callUseCase: CallUseCase; let callService: DeepMocked; - let roomUseCase: DeepMocked; - let roomUserUseCase: DeepMocked; + let roomService: DeepMocked; beforeEach(async () => { - callService = createMock(); - roomUseCase = createMock(); - roomUserUseCase = createMock(); - const module: TestingModule = await Test.createTestingModule({ - providers: [ - CallUseCase, - { - provide: CallService, - useValue: callService, - }, - { - provide: RoomUseCase, - useValue: roomUseCase, - }, - { - provide: RoomUserUseCase, - useValue: roomUserUseCase, - }, - ], - }).compile(); + providers: [CallUseCase], + }) + .useMocker(createMock) + .compile(); callUseCase = module.get(CallUseCase); + callService = module.get>(CallService); + roomService = module.get>(RoomService); }); describe('validateUserHasNoActiveRoom', () => { it('when user has no active room, then it should not throw', async () => { - const getOpenRoomByHostIdSpy = jest - .spyOn(roomUseCase, 'getOpenRoomByHostId') - .mockResolvedValueOnce(null); + roomService.getOpenRoomByHostId.mockResolvedValueOnce(null); await expect( callUseCase.validateUserHasNoActiveRoom( @@ -64,150 +46,49 @@ describe('CallUseCase', () => { ), ).resolves.not.toThrow(); - expect(getOpenRoomByHostIdSpy).toHaveBeenCalledWith(mockUserPayload.uuid); - }); - - it('when user already has an active room, then it should throw ConflictException', async () => { - const getOpenRoomByHostIdSpy = jest - .spyOn(roomUseCase, 'getOpenRoomByHostId') - .mockResolvedValueOnce(createMock(mockRoomData)); - - await expect( - callUseCase.validateUserHasNoActiveRoom( - mockUserPayload.uuid, - mockUserPayload.email, - ), - ).rejects.toThrow(ConflictException); - - expect(getOpenRoomByHostIdSpy).toHaveBeenCalledWith(mockUserPayload.uuid); - }); - - it('when an unexpected error occurs, then should throw InternalServerErrorException', async () => { - const getOpenRoomByHostIdSpy = jest - .spyOn(roomUseCase, 'getOpenRoomByHostId') - .mockRejectedValueOnce(new Error('Database error')); - - await expect( - callUseCase.validateUserHasNoActiveRoom( - mockUserPayload.uuid, - mockUserPayload.email, - ), - ).rejects.toThrow(InternalServerErrorException); - - expect(getOpenRoomByHostIdSpy).toHaveBeenCalledWith(mockUserPayload.uuid); - }); - }); - - describe('createCallAndRoom', () => { - it('when creating call and room, then should create a call token and room successfully', async () => { - const createCallTokenSpy = jest - .spyOn(callService, 'createCallToken') - .mockResolvedValueOnce(mockCallResponse); - const createRoomForCallSpy = jest - .spyOn(callUseCase, 'createRoomForCall') - .mockResolvedValueOnce(); - - const result = await callUseCase.createCallAndRoom(mockUserPayload); - - expect(createCallTokenSpy).toHaveBeenCalledWith(mockUserPayload); - expect(createRoomForCallSpy).toHaveBeenCalledWith( - mockCallResponse, + expect(roomService.getOpenRoomByHostId).toHaveBeenCalledWith( mockUserPayload.uuid, - mockUserPayload.email, ); - expect(result).toEqual(mockCallResponse); }); - it('when call service throws error, then should propagate errors', async () => { - const error = new Error('Failed to create call'); - const createCallTokenSpy = jest - .spyOn(callService, 'createCallToken') - .mockRejectedValueOnce(error); - - await expect( - callUseCase.createCallAndRoom(mockUserPayload), - ).rejects.toThrow(error); - - expect(createCallTokenSpy).toHaveBeenCalledWith(mockUserPayload); - }); - }); - - describe('createRoomForCall', () => { - it('when creating room for call, then should create room successfully', async () => { - const createRoomSpy = jest - .spyOn(roomUseCase, 'createRoom') - .mockResolvedValueOnce(createMock(mockRoomData)); - - await callUseCase.createRoomForCall( - mockCallResponse, - mockUserPayload.uuid, - mockUserPayload.email, + it('when user already has an active room, then it should throw', async () => { + roomService.getOpenRoomByHostId.mockResolvedValueOnce( + createMock(mockRoomData), ); - expect(createRoomSpy).toHaveBeenCalledWith({ - id: mockCallResponse.room, - hostId: mockUserPayload.uuid, - maxUsersAllowed: mockCallResponse.paxPerCall, - }); - }); - - it('when room creation fails, then should throw ConflictException', async () => { - const createRoomSpy = jest - .spyOn(roomUseCase, 'createRoom') - .mockRejectedValueOnce(new Error('Room already exists')); - await expect( - callUseCase.createRoomForCall( - mockCallResponse, + callUseCase.validateUserHasNoActiveRoom( mockUserPayload.uuid, mockUserPayload.email, ), ).rejects.toThrow(ConflictException); - expect(createRoomSpy).toHaveBeenCalledWith({ - id: mockCallResponse.room, - hostId: mockUserPayload.uuid, - maxUsersAllowed: mockCallResponse.paxPerCall, - }); + expect(roomService.getOpenRoomByHostId).toHaveBeenCalledWith( + mockUserPayload.uuid, + ); }); }); - describe('handleError', () => { - const context = { - uuid: mockUserPayload.uuid, - email: mockUserPayload.email, + describe('createCallAndRoom', () => { + const mockCallToken = { + token: 'call-token', + room: v4(), + paxPerCall: 10, + appId: 'jitsi-app-id', }; - it('when handling BadRequestException, then should not throw', () => { - const error = new BadRequestException('Bad request'); - - expect(() => { - callUseCase.handleError(error, context); - }).not.toThrow(); - }); - - it('when handling ConflictException, then should not throw', () => { - const error = new ConflictException('Conflict'); - - expect(() => { - callUseCase.handleError(error, context); - }).not.toThrow(); - }); - - it('when handling InternalServerErrorException, then should not throw', () => { - const error = new InternalServerErrorException('Internal error'); - - expect(() => { - callUseCase.handleError(error, context); - }).not.toThrow(); - }); + it('when creating call and room and user does not have an active room, then should create a call token and room successfully', async () => { + callService.createCallToken.mockResolvedValueOnce(mockCallToken); + roomService.createRoom.mockResolvedValueOnce(undefined); + jest + .spyOn(callUseCase, 'validateUserHasNoActiveRoom') + .mockResolvedValueOnce(null); - it('when handling unknown errors, then should throw', () => { - const error = new Error('Unknown error'); + const result = await callUseCase.createCallAndRoom(mockUserPayload); - expect(() => { - callUseCase.handleError(error, context); - }).toThrow(InternalServerErrorException); + expect(callService.createCallToken).toHaveBeenCalledWith(mockUserPayload); + expect(roomService.createRoom).toHaveBeenCalledWith(expect.any(Room)); + expect(result).toEqual(mockCallToken); }); }); @@ -248,26 +129,21 @@ describe('CallUseCase', () => { lastName: userLastName, }; - const getRoomByRoomIdSpy = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(roomMock); - const addUserToRoomSpy = jest - .spyOn(roomUserUseCase, 'addUserToRoom') - .mockResolvedValueOnce(roomUserMock); - const createCallTokenForParticipantSpy = jest - .spyOn(callService, 'createCallTokenForParticipant') - .mockReturnValueOnce(callToken); + roomService.getRoomByRoomId.mockResolvedValueOnce(roomMock); + roomService.addUserToRoom.mockResolvedValueOnce(roomUserMock); + callService.createCallTokenForParticipant.mockReturnValueOnce(callToken); const result = await callUseCase.joinCall(roomId, userData); - expect(getRoomByRoomIdSpy).toHaveBeenCalledWith(roomId); - expect(addUserToRoomSpy).toHaveBeenCalledWith(roomId, { + expect(roomService.getRoomByRoomId).toHaveBeenCalledWith(roomId); + expect(roomService.addUserToRoom).toHaveBeenCalledWith(roomId, { userId, name: userName, lastName: userLastName, anonymous: false, + email: undefined, }); - expect(createCallTokenForParticipantSpy).toHaveBeenCalledWith( + expect(callService.createCallTokenForParticipant).toHaveBeenCalledWith( userId, roomId, false, @@ -294,27 +170,21 @@ describe('CallUseCase', () => { name: userName, }; - const getRoomByRoomIdSpy = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(roomMock); - const addUserToRoomSpy = jest - .spyOn(roomUserUseCase, 'addUserToRoom') - .mockResolvedValueOnce(anonymousUserMock); - const createCallTokenForParticipantSpy = jest - .spyOn(callService, 'createCallTokenForParticipant') - .mockReturnValueOnce(callToken); + roomService.getRoomByRoomId.mockResolvedValueOnce(roomMock); + roomService.addUserToRoom.mockResolvedValueOnce(anonymousUserMock); + callService.createCallTokenForParticipant.mockReturnValueOnce(callToken); const result = await callUseCase.joinCall(roomId, userData); - expect(getRoomByRoomIdSpy).toHaveBeenCalledWith(roomId); - expect(addUserToRoomSpy).toHaveBeenCalledWith( + expect(roomService.getRoomByRoomId).toHaveBeenCalledWith(roomId); + expect(roomService.addUserToRoom).toHaveBeenCalledWith( roomId, expect.objectContaining({ name: userName, anonymous: true, }), ); - expect(createCallTokenForParticipantSpy).toHaveBeenCalledWith( + expect(callService.createCallTokenForParticipant).toHaveBeenCalledWith( anonymousUserMock.userId, roomId, true, @@ -337,30 +207,27 @@ describe('CallUseCase', () => { it('when joining call as host, then should join successfully and open the room', async () => { const userData = { userId: roomMock.hostId }; - roomMock.isClosed = true; - jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(roomMock); - const openRoomSpy = jest - .spyOn(roomUseCase, 'openRoom') - .mockResolvedValueOnce(); + const closedRoomMock = { ...roomMock, isClosed: true }; + + roomService.getRoomByRoomId.mockResolvedValueOnce(closedRoomMock); + roomService.addUserToRoom.mockResolvedValueOnce(roomUserMock); + callService.createCallTokenForParticipant.mockReturnValueOnce(callToken); + roomService.openRoom.mockResolvedValueOnce(); await callUseCase.joinCall(roomId, userData); - expect(openRoomSpy).toHaveBeenCalledWith(roomId); + expect(roomService.openRoom).toHaveBeenCalledWith(roomId); }); it('when room does not exist, then should throw NotFoundException', async () => { const userData = { userId }; - const getRoomByRoomIdSpy = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(null); + roomService.getRoomByRoomId.mockResolvedValueOnce(null); await expect(callUseCase.joinCall(roomId, userData)).rejects.toThrow( NotFoundException, ); - expect(getRoomByRoomIdSpy).toHaveBeenCalledWith(roomId); + expect(roomService.getRoomByRoomId).toHaveBeenCalledWith(roomId); }); it('when non-owner tries to join closed room, then should throw', async () => { @@ -371,35 +238,29 @@ describe('CallUseCase', () => { hostId: 'different-host-id', }; - const getRoomByRoomIdSpy = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(closedRoomMock); + roomService.getRoomByRoomId.mockResolvedValueOnce(closedRoomMock); await expect(callUseCase.joinCall(roomId, userData)).rejects.toThrow( ForbiddenException, ); - expect(getRoomByRoomIdSpy).toHaveBeenCalledWith(roomId); + expect(roomService.getRoomByRoomId).toHaveBeenCalledWith(roomId); }); - it('when roomUserUseCase throws BadRequestException, then should propagate error', async () => { + it('when roomService throws BadRequestException, then should propagate error', async () => { const userData = { userId }; const error = new BadRequestException('Invalid user data'); const openRoomMock = { ...roomMock, isClosed: false }; - const getRoomByRoomIdSpy = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(openRoomMock); - const addUserToRoomSpy = jest - .spyOn(roomUserUseCase, 'addUserToRoom') - .mockRejectedValueOnce(error); + roomService.getRoomByRoomId.mockResolvedValueOnce(openRoomMock); + roomService.addUserToRoom.mockRejectedValueOnce(error); await expect(callUseCase.joinCall(roomId, userData)).rejects.toThrow( BadRequestException, ); - expect(getRoomByRoomIdSpy).toHaveBeenCalledWith(roomId); - expect(addUserToRoomSpy).toHaveBeenCalledWith( + expect(roomService.getRoomByRoomId).toHaveBeenCalledWith(roomId); + expect(roomService.addUserToRoom).toHaveBeenCalledWith( roomId, expect.objectContaining({ userId, @@ -408,27 +269,27 @@ describe('CallUseCase', () => { ); }); - it('when roomUserUseCase throws ConflictException, then should propagate error', async () => { + it('when roomService throws ConflictException, then should propagate error', async () => { const userData = { userId }; const error = new ConflictException('User already in room'); const openRoomMock = { ...roomMock, isClosed: false }; - roomUseCase.getRoomByRoomId.mockResolvedValueOnce(openRoomMock); - roomUserUseCase.addUserToRoom.mockRejectedValueOnce(error); + roomService.getRoomByRoomId.mockResolvedValueOnce(openRoomMock); + roomService.addUserToRoom.mockRejectedValueOnce(error); await expect(callUseCase.joinCall(roomId, userData)).rejects.toThrow( ConflictException, ); }); - it('When there is an unknown error, then throw an internal server error', async () => { + it('When there is an unknown error, then propagate the error', async () => { const userData = { userId }; const error = new Error('Unknown error'); - roomUseCase.getRoomByRoomId.mockRejectedValueOnce(error); + roomService.getRoomByRoomId.mockRejectedValueOnce(error); await expect(callUseCase.joinCall(roomId, userData)).rejects.toThrow( - InternalServerErrorException, + Error, ); }); }); @@ -447,7 +308,7 @@ describe('CallUseCase', () => { ...mockRoomData, isClosed: false, }); - roomUseCase.getRoomByRoomId.mockResolvedValueOnce(openRoomMock); + roomService.getRoomByRoomId.mockResolvedValueOnce(openRoomMock); const registeredRoomUser = new RoomUser({ id: 1, @@ -458,7 +319,7 @@ describe('CallUseCase', () => { anonymous: false, }); - roomUserUseCase.addUserToRoom.mockResolvedValueOnce(registeredRoomUser); + roomService.addUserToRoom.mockResolvedValueOnce(registeredRoomUser); const createCallTokenForParticipantSpy = jest .spyOn(callService, 'createCallTokenForParticipant') .mockReturnValueOnce(callToken); @@ -497,7 +358,7 @@ describe('CallUseCase', () => { ...mockRoomData, isClosed: false, }); - roomUseCase.getRoomByRoomId.mockResolvedValueOnce(openRoomMock); + roomService.getRoomByRoomId.mockResolvedValueOnce(openRoomMock); const anonymousRoomUser = new RoomUser({ id: 1, @@ -507,19 +368,15 @@ describe('CallUseCase', () => { anonymous: true, }); - const addUserToRoomSpy = jest - .spyOn(roomUserUseCase, 'addUserToRoom') - .mockResolvedValueOnce(anonymousRoomUser); - jest - .spyOn(callService, 'createCallTokenForParticipant') - .mockReturnValueOnce(callToken); + roomService.addUserToRoom.mockResolvedValueOnce(anonymousRoomUser); + callService.createCallTokenForParticipant.mockReturnValueOnce(callToken); await callUseCase.joinCall(roomId, { name, anonymous: true, }); - expect(addUserToRoomSpy).toHaveBeenCalledWith( + expect(roomService.addUserToRoom).toHaveBeenCalledWith( roomId, expect.objectContaining({ name, @@ -540,7 +397,7 @@ describe('CallUseCase', () => { ...mockRoomData, isClosed: false, }); - roomUseCase.getRoomByRoomId.mockResolvedValueOnce(openRoomMock); + roomService.getRoomByRoomId.mockResolvedValueOnce(openRoomMock); const userWithoutId = new RoomUser({ id: 1, @@ -550,18 +407,14 @@ describe('CallUseCase', () => { anonymous: true, }); - const addUserToRoomSpy = jest - .spyOn(roomUserUseCase, 'addUserToRoom') - .mockResolvedValueOnce(userWithoutId); - jest - .spyOn(callService, 'createCallTokenForParticipant') - .mockReturnValueOnce(callToken); + roomService.addUserToRoom.mockResolvedValueOnce(userWithoutId); + callService.createCallTokenForParticipant.mockReturnValueOnce(callToken); await callUseCase.joinCall(roomId, { name, }); - expect(addUserToRoomSpy).toHaveBeenCalledWith( + expect(roomService.addUserToRoom).toHaveBeenCalledWith( roomId, expect.objectContaining({ name, @@ -580,135 +433,140 @@ describe('CallUseCase', () => { beforeEach(() => { roomMock = createMock({ ...mockRoomData, id: roomId, hostId }); - roomUseCase.getRoomByRoomId.mockResolvedValue(roomMock); - roomUserUseCase.removeUserFromRoom.mockResolvedValue(); - roomUseCase.closeRoom.mockResolvedValue(); - roomUseCase.removeRoom.mockResolvedValue(); + roomService.getRoomByRoomId.mockResolvedValue(roomMock); + roomService.removeUserFromRoom.mockResolvedValue(); + roomService.closeRoom.mockResolvedValue(); + roomService.removeRoom.mockResolvedValue(); }); - it('when userId is not provided, then should throw', async () => { - await expect(callUseCase.leaveCall(roomId, undefined)).rejects.toThrow( - BadRequestException, - ); + it('when a valid userId is provided, then should handle leave call normally', async () => { + roomService.removeUserFromRoom.mockResolvedValueOnce(undefined); + roomService.countUsersInRoom.mockResolvedValueOnce(0); - expect(roomUseCase.getRoomByRoomId).not.toHaveBeenCalled(); - expect(roomUserUseCase.removeUserFromRoom).not.toHaveBeenCalled(); + await callUseCase.leaveCall(roomId, participantId); + + expect(roomService.getRoomByRoomId).toHaveBeenCalledWith(roomId); + expect(roomService.removeUserFromRoom).toHaveBeenCalledWith( + participantId, + roomMock, + ); + expect(roomService.removeRoom).toHaveBeenCalledWith(roomId); }); it('when room does not exist, then should throw', async () => { - roomUseCase.getRoomByRoomId.mockResolvedValueOnce(null); + roomService.getRoomByRoomId.mockResolvedValueOnce(null); await expect( callUseCase.leaveCall(roomId, participantId), ).rejects.toThrow(NotFoundException); - expect(roomUseCase.getRoomByRoomId).toHaveBeenCalledWith(roomId); - expect(roomUserUseCase.removeUserFromRoom).not.toHaveBeenCalled(); - expect(roomUseCase.closeRoom).not.toHaveBeenCalled(); - expect(roomUseCase.removeRoom).not.toHaveBeenCalled(); + expect(roomService.getRoomByRoomId).toHaveBeenCalledWith(roomId); + expect(roomService.removeUserFromRoom).not.toHaveBeenCalled(); + expect(roomService.closeRoom).not.toHaveBeenCalled(); + expect(roomService.removeRoom).not.toHaveBeenCalled(); }); it('when host leaves a non-empty room, then should remove user and close room', async () => { - roomUserUseCase.countUsersInRoom.mockResolvedValueOnce(1); + roomService.countUsersInRoom.mockResolvedValueOnce(1); await callUseCase.leaveCall(roomId, hostId); - expect(roomUseCase.getRoomByRoomId).toHaveBeenCalledWith(roomId); - expect(roomUserUseCase.removeUserFromRoom).toHaveBeenCalledWith( + expect(roomService.getRoomByRoomId).toHaveBeenCalledWith(roomId); + expect(roomService.removeUserFromRoom).toHaveBeenCalledWith( hostId, roomMock, ); - expect(roomUserUseCase.countUsersInRoom).toHaveBeenCalledWith(roomId); - expect(roomUseCase.closeRoom).toHaveBeenCalledWith(roomId); - expect(roomUseCase.removeRoom).not.toHaveBeenCalled(); + expect(roomService.countUsersInRoom).toHaveBeenCalledWith(roomId); + expect(roomService.closeRoom).toHaveBeenCalledWith(roomId); + expect(roomService.removeRoom).not.toHaveBeenCalled(); }); it('when the last user (host) leaves, then should remove user and delete room', async () => { - roomUserUseCase.countUsersInRoom.mockResolvedValueOnce(0); + roomService.countUsersInRoom.mockResolvedValueOnce(0); await callUseCase.leaveCall(roomId, hostId); - expect(roomUseCase.getRoomByRoomId).toHaveBeenCalledWith(roomId); - expect(roomUserUseCase.removeUserFromRoom).toHaveBeenCalledWith( + expect(roomService.getRoomByRoomId).toHaveBeenCalledWith(roomId); + expect(roomService.removeUserFromRoom).toHaveBeenCalledWith( hostId, roomMock, ); - expect(roomUserUseCase.countUsersInRoom).toHaveBeenCalledWith(roomId); - expect(roomUseCase.removeRoom).toHaveBeenCalledWith(roomId); - expect(roomUseCase.closeRoom).not.toHaveBeenCalled(); + expect(roomService.countUsersInRoom).toHaveBeenCalledWith(roomId); + expect(roomService.removeRoom).toHaveBeenCalledWith(roomId); + expect(roomService.closeRoom).not.toHaveBeenCalled(); }); it('when the last user (participant) leaves, then should remove user and delete room', async () => { - roomUserUseCase.countUsersInRoom.mockResolvedValueOnce(0); + roomService.countUsersInRoom.mockResolvedValueOnce(0); await callUseCase.leaveCall(roomId, participantId); - expect(roomUseCase.getRoomByRoomId).toHaveBeenCalledWith(roomId); - expect(roomUserUseCase.removeUserFromRoom).toHaveBeenCalledWith( + expect(roomService.getRoomByRoomId).toHaveBeenCalledWith(roomId); + expect(roomService.removeUserFromRoom).toHaveBeenCalledWith( participantId, roomMock, ); - expect(roomUserUseCase.countUsersInRoom).toHaveBeenCalledWith(roomId); - expect(roomUseCase.removeRoom).toHaveBeenCalledWith(roomId); - expect(roomUseCase.closeRoom).not.toHaveBeenCalled(); + expect(roomService.countUsersInRoom).toHaveBeenCalledWith(roomId); + expect(roomService.removeRoom).toHaveBeenCalledWith(roomId); + expect(roomService.closeRoom).not.toHaveBeenCalled(); }); it('when a participant leaves a non-empty room, then should remove user but not close or delete room', async () => { - roomUserUseCase.countUsersInRoom.mockResolvedValueOnce(2); + roomService.countUsersInRoom.mockResolvedValueOnce(2); await callUseCase.leaveCall(roomId, participantId); - expect(roomUseCase.getRoomByRoomId).toHaveBeenCalledWith(roomId); - expect(roomUserUseCase.removeUserFromRoom).toHaveBeenCalledWith( + expect(roomService.getRoomByRoomId).toHaveBeenCalledWith(roomId); + expect(roomService.removeUserFromRoom).toHaveBeenCalledWith( participantId, roomMock, ); - expect(roomUserUseCase.countUsersInRoom).toHaveBeenCalledWith(roomId); - expect(roomUseCase.closeRoom).not.toHaveBeenCalled(); - expect(roomUseCase.removeRoom).not.toHaveBeenCalled(); + expect(roomService.countUsersInRoom).toHaveBeenCalledWith(roomId); + expect(roomService.closeRoom).not.toHaveBeenCalled(); + expect(roomService.removeRoom).not.toHaveBeenCalled(); }); it('when host leaves call, then should leave successfully and close the room', async () => { - roomUserUseCase.countUsersInRoom.mockResolvedValueOnce(1); + roomService.countUsersInRoom.mockResolvedValueOnce(1); roomMock.hostId = hostId; await callUseCase.leaveCall(roomId, hostId); - expect(roomUseCase.getRoomByRoomId).toHaveBeenCalledWith(roomId); - expect(roomUserUseCase.removeUserFromRoom).toHaveBeenCalledWith( + expect(roomService.getRoomByRoomId).toHaveBeenCalledWith(roomId); + expect(roomService.removeUserFromRoom).toHaveBeenCalledWith( hostId, roomMock, ); - expect(roomUserUseCase.countUsersInRoom).toHaveBeenCalledWith(roomId); - expect(roomUseCase.closeRoom).toHaveBeenCalledWith(roomId); - expect(roomUseCase.removeRoom).not.toHaveBeenCalled(); + expect(roomService.countUsersInRoom).toHaveBeenCalledWith(roomId); + expect(roomService.closeRoom).toHaveBeenCalledWith(roomId); + expect(roomService.removeRoom).not.toHaveBeenCalled(); }); it('when anonymous user leaves call, then should leave successfully', async () => { - roomUserUseCase.countUsersInRoom.mockResolvedValueOnce(1); + roomService.countUsersInRoom.mockResolvedValueOnce(1); await callUseCase.leaveCall(roomId, anonymousUserId); - expect(roomUseCase.getRoomByRoomId).toHaveBeenCalledWith(roomId); - expect(roomUserUseCase.removeUserFromRoom).toHaveBeenCalledWith( + expect(roomService.getRoomByRoomId).toHaveBeenCalledWith(roomId); + expect(roomService.removeUserFromRoom).toHaveBeenCalledWith( anonymousUserId, roomMock, ); - expect(roomUserUseCase.countUsersInRoom).toHaveBeenCalledWith(roomId); + expect(roomService.countUsersInRoom).toHaveBeenCalledWith(roomId); }); - it('when error occurs during leave call operation, then should handle errors', async () => { + it('when error occurs during leave call operation, then should propagate error', async () => { const error = new Error('Database error'); - roomUseCase.getRoomByRoomId.mockRejectedValueOnce(error); + roomService.getRoomByRoomId.mockRejectedValueOnce(error); await expect( callUseCase.leaveCall(roomId, participantId), - ).rejects.toThrow(InternalServerErrorException); + ).rejects.toThrow(error); }); - it('when roomUserUseCase throws, then should propagate error', async () => { + it('when roomService throws, then should propagate error', async () => { const error = new BadRequestException('Invalid user'); - roomUseCase.getRoomByRoomId.mockResolvedValueOnce(roomMock); - roomUserUseCase.removeUserFromRoom.mockRejectedValueOnce(error); + roomService.getRoomByRoomId.mockResolvedValueOnce(roomMock); + roomService.removeUserFromRoom.mockRejectedValueOnce(error); await expect( callUseCase.leaveCall(roomId, participantId), diff --git a/src/modules/call/call.usecase.ts b/src/modules/call/call.usecase.ts index be266f2..11ad6be 100644 --- a/src/modules/call/call.usecase.ts +++ b/src/modules/call/call.usecase.ts @@ -1,19 +1,16 @@ import { - BadRequestException, ConflictException, ForbiddenException, Injectable, - InternalServerErrorException, Logger, NotFoundException, } from '@nestjs/common'; import { v4 as uuidv4 } from 'uuid'; import { UserTokenData } from '../auth/dto/user.dto'; -import { RoomUserUseCase } from '../room/room-user.usecase'; -import { Room } from '../room/room.domain'; -import { RoomUseCase } from '../room/room.usecase'; -import { User } from '../user/user.domain'; -import { CallService } from './call.service'; +import { RoomService } from './services/room.service'; +import { Room } from './domain/room.domain'; +import { User } from '../../shared/user/user.domain'; +import { CallService } from './services/call.service'; import { CreateCallResponseDto } from './dto/create-call.dto'; import { JoinCallResponseDto } from './dto/join-call.dto'; @@ -23,111 +20,39 @@ export class CallUseCase { constructor( private readonly callService: CallService, - private readonly roomUseCase: RoomUseCase, - private readonly roomUserUseCase: RoomUserUseCase, + private readonly roomService: RoomService, ) {} - async validateUserHasNoActiveRoom( - uuid: string, - email: string, - ): Promise { - try { - const existingRoom = await this.roomUseCase.getOpenRoomByHostId(uuid); - if (existingRoom) { - this.logger.warn( - `User ${email} already has an active room as host: ${existingRoom.id}`, - ); - throw new ConflictException('User already has an active room as host'); - } - } catch (error) { - if (error instanceof ConflictException) { - throw error; - } - const err = error as Error; - this.logger.error( - `Failed to validate user room: ${err.message}`, - { userId: uuid, email }, - err.stack, - ); - throw new InternalServerErrorException( - 'Failed to validate user room status', - { cause: err }, - ); - } - } - async createCallAndRoom( user: User | UserTokenData['payload'], ): Promise { - try { - const call = await this.callService.createCallToken(user); - await this.createRoomForCall(call, user.uuid, user.email); - this.logger.log(`Successfully created call for user: ${user.email}`); - return call; - } catch (error) { - const err = error as Error; - this.logger.error( - `Failed to create call and room: ${err.message}`, - { userId: user.uuid, email: user.email }, - err.stack, - ); - throw err; - } + await this.validateUserHasNoActiveRoom(user.uuid, user.email); + + const call = await this.callService.createCallToken(user); + + const newRoom = new Room({ + id: call.room, + hostId: user.uuid, + maxUsersAllowed: call.paxPerCall, + }); + + await this.roomService.createRoom(newRoom); + + return call; } - async createRoomForCall( - call: CreateCallResponseDto, + async validateUserHasNoActiveRoom( uuid: string, email: string, ): Promise { - try { - await this.roomUseCase.createRoom( - new Room({ - id: call.room, - hostId: uuid, - maxUsersAllowed: call.paxPerCall, - }), - ); - } catch (error) { - const err = error as Error; - this.logger.error( - `Failed to create room: ${err.message}`, - { roomId: call.room, userId: uuid, email }, - err.stack, - ); - throw new ConflictException( - 'Failed to create room. Room might already exist.', - ); - } - } + const activeRoom = await this.roomService.getOpenRoomByHostId(uuid); - public handleError( - error: unknown, - context: { uuid: string; email: string }, - ): void { - const err = error as Error; - this.logger.error( - `Failed to create call: ${err.message}`, - { - userId: context.uuid, - email: context.email, - error: err.name, - }, - err.stack, - ); - - if ( - error instanceof BadRequestException || - error instanceof ConflictException || - error instanceof InternalServerErrorException - ) { - return; + if (activeRoom) { + this.logger.warn( + `User ${email} already has an active room as host: ${activeRoom.id}`, + ); + throw new ConflictException('User already has an active room as host'); } - - throw new InternalServerErrorException( - 'An unexpected error occurred while creating the call', - { cause: err }, - ); } async joinCall( @@ -140,70 +65,42 @@ export class CallUseCase { email?: string; }, ): Promise { - try { - const room = await this.roomUseCase.getRoomByRoomId(roomId); - if (!room) { - throw new NotFoundException(`Specified room not found`); - } - - const processedUserData = this.processUserData(userData); - const isOwner = processedUserData.userId === room.hostId; - - if (!isOwner && room.isClosed) { - throw new ForbiddenException('Room is closed'); - } - - const roomUser = await this.roomUserUseCase.addUserToRoom( - roomId, - processedUserData, - ); + const room = await this.roomService.getRoomByRoomId(roomId); + if (!room) { + throw new NotFoundException(`Specified room not found`); + } - // Generate token for the user - const tokenData = this.callService.createCallTokenForParticipant( - roomUser.userId, - roomId, - !!roomUser.anonymous, - isOwner, - processedUserData, - ); + const processedUserData = this.processUserData(userData); + const isOwner = processedUserData.userId === room.hostId; - if (processedUserData.userId === room.hostId && room.isClosed) { - await this.roomUseCase.openRoom(roomId); - } + if (!isOwner && room.isClosed) { + throw new ForbiddenException('Room is closed'); + } - return { - token: tokenData.token, - room: roomId, - userId: roomUser.userId, - appId: tokenData.appId, - }; - } catch (error) { - if ( - error instanceof BadRequestException || - error instanceof ConflictException || - error instanceof NotFoundException || - error instanceof InternalServerErrorException || - error instanceof ForbiddenException - ) { - throw error; - } + const roomUser = await this.roomService.addUserToRoom( + roomId, + processedUserData, + ); - const err = error as Error; - this.logger.error( - `Failed to join call: ${err.message}`, - { - roomId, - userData, - error: err.name, - }, - err.stack, - ); + // Generate token for the user + const tokenData = this.callService.createCallTokenForParticipant( + roomUser.userId, + roomId, + !!roomUser.anonymous, + isOwner, + processedUserData, + ); - throw new InternalServerErrorException( - 'An unexpected error occurred while joining the call', - { cause: err }, - ); + if (processedUserData.userId === room.hostId && room.isClosed) { + await this.roomService.openRoom(roomId); } + + return { + token: tokenData.token, + room: roomId, + userId: roomUser.userId, + appId: tokenData.appId, + }; } private processUserData(userData: { @@ -240,51 +137,22 @@ export class CallUseCase { } async leaveCall(roomId: string, userId: string): Promise { - if (!userId) { - throw new BadRequestException('User ID is required'); - } - - try { - const room = await this.roomUseCase.getRoomByRoomId(roomId); - if (!room) { - throw new NotFoundException(`Specified room not found`); - } - - const isHostLeaving = room.hostId === userId; + const room = await this.roomService.getRoomByRoomId(roomId); - await this.roomUserUseCase.removeUserFromRoom(userId, room); + if (!room) { + throw new NotFoundException(`Specified room not found`); + } - const remainingUsers = - await this.roomUserUseCase.countUsersInRoom(roomId); + const isHostLeaving = room.hostId === userId; - if (remainingUsers === 0) { - await this.roomUseCase.removeRoom(roomId); - } else if (isHostLeaving) { - await this.roomUseCase.closeRoom(roomId); - } - } catch (error) { - if ( - error instanceof BadRequestException || - error instanceof NotFoundException - ) { - throw error; - } + await this.roomService.removeUserFromRoom(userId, room); - const err = error as Error; - this.logger.error( - `Failed to leave call: ${err.message}`, - { - roomId, - userId, - error: err.name, - }, - err.stack, - ); + const remainingUsers = await this.roomService.countUsersInRoom(roomId); - throw new InternalServerErrorException( - 'An unexpected error occurred while leaving the call', - { cause: err }, - ); + if (remainingUsers === 0) { + await this.roomService.removeRoom(roomId); + } else if (isHostLeaving) { + await this.roomService.closeRoom(roomId); } } } diff --git a/src/modules/room/room-user.domain.ts b/src/modules/call/domain/room-user.domain.ts similarity index 100% rename from src/modules/room/room-user.domain.ts rename to src/modules/call/domain/room-user.domain.ts diff --git a/src/modules/room/room.domain.ts b/src/modules/call/domain/room.domain.ts similarity index 100% rename from src/modules/room/room.domain.ts rename to src/modules/call/domain/room.domain.ts diff --git a/src/modules/room/dto/users-in-room.dto.ts b/src/modules/call/dto/users-in-room.dto.ts similarity index 100% rename from src/modules/room/dto/users-in-room.dto.ts rename to src/modules/call/dto/users-in-room.dto.ts diff --git a/src/modules/call/fixtures.ts b/src/modules/call/fixtures.ts index 60b1ada..a0bf81e 100644 --- a/src/modules/call/fixtures.ts +++ b/src/modules/call/fixtures.ts @@ -1,6 +1,6 @@ import { Chance } from 'chance'; import { UserTokenData } from '../auth/dto/user.dto'; -import { Room } from '../room/room.domain'; +import { Room } from './domain/room.domain'; import { CreateCallResponseDto } from './dto/create-call.dto'; const randomDataGenerator = new Chance(); diff --git a/src/modules/room/room-user.repository.ts b/src/modules/call/infrastructure/room-user.repository.ts similarity index 88% rename from src/modules/room/room-user.repository.ts rename to src/modules/call/infrastructure/room-user.repository.ts index 5d4d37c..ef278fc 100644 --- a/src/modules/room/room-user.repository.ts +++ b/src/modules/call/infrastructure/room-user.repository.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/sequelize'; -import { OmitCreateProperties } from 'src/types/OmitCreateProperties'; -import { RoomUser, RoomUserAttributes } from './room-user.domain'; -import { RoomUserModel } from './models/room-user.model'; +import { OmitCreateProperties } from '../../../shared/types/OmitCreateProperties'; +import { RoomUser, RoomUserAttributes } from '../domain/room-user.domain'; +import { RoomUserModel } from '../models/room-user.model'; @Injectable() export class SequelizeRoomUserRepository { diff --git a/src/modules/room/room.repository.ts b/src/modules/call/infrastructure/room.repository.ts similarity index 85% rename from src/modules/room/room.repository.ts rename to src/modules/call/infrastructure/room.repository.ts index 201b291..cfc9988 100644 --- a/src/modules/room/room.repository.ts +++ b/src/modules/call/infrastructure/room.repository.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/sequelize'; -import { OmitCreateProperties } from 'src/types/OmitCreateProperties'; -import { Room, RoomAttributes } from './room.domain'; -import { RoomModel } from './models/room.model'; +import { OmitCreateProperties } from '../../../shared/types/OmitCreateProperties'; +import { Room, RoomAttributes } from '../domain/room.domain'; +import { RoomModel } from '../models/room.model'; @Injectable() export class SequelizeRoomRepository { diff --git a/src/modules/room/models/room-user.model.ts b/src/modules/call/models/room-user.model.ts similarity index 100% rename from src/modules/room/models/room-user.model.ts rename to src/modules/call/models/room-user.model.ts diff --git a/src/modules/room/models/room.model.ts b/src/modules/call/models/room.model.ts similarity index 100% rename from src/modules/room/models/room.model.ts rename to src/modules/call/models/room.model.ts diff --git a/src/modules/room/room-user.repository.spec.ts b/src/modules/call/room-user.repository.spec.ts similarity index 98% rename from src/modules/room/room-user.repository.spec.ts rename to src/modules/call/room-user.repository.spec.ts index de502fe..be4dbfa 100644 --- a/src/modules/room/room-user.repository.spec.ts +++ b/src/modules/call/room-user.repository.spec.ts @@ -1,9 +1,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { SequelizeRoomUserRepository } from './room-user.repository'; +import { SequelizeRoomUserRepository } from './infrastructure/room-user.repository'; import { RoomUserModel } from './models/room-user.model'; import { getModelToken } from '@nestjs/sequelize'; -import { RoomUser } from './room-user.domain'; +import { RoomUser } from './domain/room-user.domain'; describe('SequelizeRoomUserRepository', () => { let repository: SequelizeRoomUserRepository; diff --git a/src/modules/room/room.repository.spec.ts b/src/modules/call/room.repository.spec.ts similarity index 98% rename from src/modules/room/room.repository.spec.ts rename to src/modules/call/room.repository.spec.ts index 29717a4..e7d0766 100644 --- a/src/modules/room/room.repository.spec.ts +++ b/src/modules/call/room.repository.spec.ts @@ -1,6 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { SequelizeRoomRepository } from './room.repository'; +import { SequelizeRoomRepository } from './infrastructure/room.repository'; import { RoomModel } from './models/room.model'; import { getModelToken } from '@nestjs/sequelize'; import { mockRoomData } from '../call/fixtures'; diff --git a/src/modules/call/call.service.spec.ts b/src/modules/call/services/call.service.spec.ts similarity index 68% rename from src/modules/call/call.service.spec.ts rename to src/modules/call/services/call.service.spec.ts index 31d135d..4b593d9 100644 --- a/src/modules/call/call.service.spec.ts +++ b/src/modules/call/services/call.service.spec.ts @@ -1,56 +1,54 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { createMock } from '@golevelup/ts-jest'; import { UnauthorizedException } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { Test } from '@nestjs/testing'; +import { Test, TestingModule } from '@nestjs/testing'; import * as jwt from 'jsonwebtoken'; import * as uuid from 'uuid'; -import configuration from '../../config/configuration'; -import { PaymentService, Tier } from '../../externals/payments.service'; +import { PaymentService, Tier } from '../../../externals/payments.service'; import { CallService } from './call.service'; -import { mockUserPayload } from './fixtures'; +import { mockUserPayload } from '../fixtures'; jest.mock('uuid'); jest.mock('jsonwebtoken'); +jest.mock('../../../lib/jitsi', () => ({ + getJitsiJWTSecret: jest.fn(() => 'mock-secret'), + getJitsiJWTHeader: jest.fn(() => ({ alg: 'RS256', typ: 'JWT' })), + getJitsiJWTPayload: jest.fn(() => ({ iss: 'jitsi', aud: 'jitsi' })), +})); + +jest.mock('../../../config/configuration', () => { + return jest.fn(() => ({ + jitsi: { + appId: 'jitsi-app-id', + }, + })); +}); describe('Call service', () => { let callService: CallService; - let paymentService: DeepMocked; + let paymentService: PaymentService; + let moduleRef: TestingModule; beforeEach(async () => { - paymentService = createMock(); - - const module = await Test.createTestingModule({ - imports: [ - ConfigModule.forRoot({ - envFilePath: [`.env.${process.env.NODE_ENV}`], - load: [configuration], - isGlobal: true, - }), - ], - providers: [ - { - provide: PaymentService, - useValue: paymentService, - }, - CallService, - ], - }).compile(); - - callService = module.get(CallService); + moduleRef = await Test.createTestingModule({ + providers: [CallService], + }) + .useMocker(createMock) + .compile(); + + callService = moduleRef.get(CallService); + paymentService = moduleRef.get(PaymentService); }); it('When the user has meet enabled, then a call token should be created', async () => { const userPayload = mockUserPayload; - const getUserTierSpy = jest - .spyOn(paymentService, 'getUserTier') - .mockResolvedValue({ - featuresPerService: { - meet: { - enabled: true, - paxPerCall: 10, - }, + jest.spyOn(paymentService, 'getUserTier').mockResolvedValue({ + featuresPerService: { + meet: { + enabled: true, + paxPerCall: 10, }, - } as Tier); + }, + } as Tier); (uuid.v4 as jest.Mock).mockReturnValue('test-room-id'); (jwt.sign as jest.Mock).mockReturnValue('test-jitsi-token'); @@ -64,27 +62,25 @@ describe('Call service', () => { paxPerCall: 10, }); - expect(getUserTierSpy).toHaveBeenCalledWith(userPayload.uuid); + expect(paymentService.getUserTier).toHaveBeenCalledWith(userPayload.uuid); }); it('When the user does not have meet enabled, then an error indicating so is thrown', async () => { const userPayload = mockUserPayload; - const getUserTierSpy = jest - .spyOn(paymentService, 'getUserTier') - .mockResolvedValue({ - featuresPerService: { - meet: { - enabled: false, - paxPerCall: 0, - }, + jest.spyOn(paymentService, 'getUserTier').mockResolvedValue({ + featuresPerService: { + meet: { + enabled: false, + paxPerCall: 0, }, - } as Tier); + }, + } as Tier); await expect(callService.createCallToken(userPayload)).rejects.toThrow( UnauthorizedException, ); - expect(getUserTierSpy).toHaveBeenCalledWith(userPayload.uuid); + expect(paymentService.getUserTier).toHaveBeenCalledWith(userPayload.uuid); }); describe('createCallTokenForParticipant', () => { diff --git a/src/modules/call/call.service.ts b/src/modules/call/services/call.service.ts similarity index 85% rename from src/modules/call/call.service.ts rename to src/modules/call/services/call.service.ts index bd8bfa0..1f87700 100644 --- a/src/modules/call/call.service.ts +++ b/src/modules/call/services/call.service.ts @@ -6,16 +6,16 @@ import { } from '@nestjs/common'; import jwt, { JwtHeader } from 'jsonwebtoken'; import { v4 } from 'uuid'; -import configuration from '../../config/configuration'; -import { PaymentService, Tier } from '../../externals/payments.service'; +import configuration from '../../../config/configuration'; +import { PaymentService, Tier } from '../../../externals/payments.service'; import { getJitsiJWTHeader, getJitsiJWTPayload, getJitsiJWTSecret, -} from '../../lib/jitsi'; -import { UserTokenData } from '../auth/dto/user.dto'; -import { UserDataForToken } from '../user/user.attributes'; -import { User } from '../user/user.domain'; +} from '../../../lib/jitsi'; +import { UserTokenData } from '../../auth/dto/user.dto'; +import { UserDataForToken } from '../../../shared/user/user.attributes'; +import { User } from '../../../shared/user/user.domain'; export function SignWithRS256AndHeader( payload: object, @@ -74,20 +74,20 @@ export class CallService { 'User does not have permission to create a call', ); - const newRoom = v4(); + const newRoomId = v4(); const token = generateJitsiJWT( { id: user.uuid, email: user.email, name: `${user.name} ${user.lastname}`, }, - newRoom, + newRoomId, true, ); return { token, - room: newRoom, + room: newRoomId, paxPerCall: meetFeatures.paxPerCall, appId: configuration().jitsi.appId, }; diff --git a/src/modules/call/services/room.service.spec.ts b/src/modules/call/services/room.service.spec.ts new file mode 100644 index 0000000..ddc5f2d --- /dev/null +++ b/src/modules/call/services/room.service.spec.ts @@ -0,0 +1,180 @@ +import { createMock } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { RoomService } from './room.service'; +import { SequelizeRoomRepository } from '../infrastructure/room.repository'; +import { Room } from '../domain/room.domain'; +import { mockRoomData } from '../fixtures'; + +describe('Room Service', () => { + let roomService: RoomService; + let roomRepository: SequelizeRoomRepository; + let moduleRef: TestingModule; + + beforeEach(async () => { + moduleRef = await Test.createTestingModule({ + providers: [RoomService], + }) + .useMocker(createMock) + .compile(); + + roomService = moduleRef.get(RoomService); + roomRepository = moduleRef.get( + SequelizeRoomRepository, + ); + }); + + describe('Creating a room', () => { + it('should create a room successfully', async () => { + const mockRoom = createMock(mockRoomData); + jest.spyOn(roomRepository, 'create').mockResolvedValueOnce(mockRoom); + + const result = await roomService.createRoom(mockRoom); + + expect(roomRepository.create).toHaveBeenCalledWith(mockRoom); + expect(result).toEqual(mockRoom); + }); + }); + + describe('getRoomByRoomId', () => { + it('should return room when found', async () => { + const mockRoom = createMock(mockRoomData); + jest.spyOn(roomRepository, 'findById').mockResolvedValueOnce(mockRoom); + + const result = await roomService.getRoomByRoomId(mockRoomData.id); + + expect(roomRepository.findById).toHaveBeenCalledWith(mockRoomData.id); + expect(result).toEqual(mockRoom); + }); + + it('should return null when room not found', async () => { + jest.spyOn(roomRepository, 'findById').mockResolvedValueOnce(null); + + const result = await roomService.getRoomByRoomId(mockRoomData.id); + + expect(roomRepository.findById).toHaveBeenCalledWith(mockRoomData.id); + expect(result).toBeNull(); + }); + }); + + describe('getRoomByHostId', () => { + it('should return room when found', async () => { + const mockRoom = createMock(mockRoomData); + jest + .spyOn(roomRepository, 'findByHostId') + .mockResolvedValueOnce(mockRoom); + + const result = await roomService.getRoomByHostId(mockRoomData.hostId); + + expect(roomRepository.findByHostId).toHaveBeenCalledWith( + mockRoomData.hostId, + ); + expect(result).toEqual(mockRoom); + }); + + it('should return null when room not found', async () => { + jest.spyOn(roomRepository, 'findByHostId').mockResolvedValueOnce(null); + + const result = await roomService.getRoomByHostId(mockRoomData.hostId); + + expect(roomRepository.findByHostId).toHaveBeenCalledWith( + mockRoomData.hostId, + ); + expect(result).toBeNull(); + }); + }); + + describe('updateRoom', () => { + const mockUpdateData = { + maxUsersAllowed: 10, + }; + + it('should update room successfully', async () => { + const mockUpdatedRoom = createMock({ + ...mockRoomData, + maxUsersAllowed: 10, + }); + + jest.spyOn(roomRepository, 'update').mockResolvedValueOnce(); + jest + .spyOn(roomRepository, 'findById') + .mockResolvedValueOnce(mockUpdatedRoom); + + const result = await roomService.updateRoom( + mockRoomData.id, + mockUpdateData, + ); + + expect(roomRepository.update).toHaveBeenCalledWith( + mockRoomData.id, + mockUpdateData, + ); + expect(roomRepository.findById).toHaveBeenCalledWith(mockRoomData.id); + expect(result).toEqual(mockUpdatedRoom); + }); + }); + + describe('removeRoom', () => { + it('should remove room successfully', async () => { + jest.spyOn(roomRepository, 'delete').mockResolvedValueOnce(); + + await roomService.removeRoom(mockRoomData.id); + + expect(roomRepository.delete).toHaveBeenCalledWith(mockRoomData.id); + }); + }); + + describe('closeRoom', () => { + it('should set isClosed to true', async () => { + jest.spyOn(roomRepository, 'update').mockResolvedValueOnce(); + const roomId = mockRoomData.id; + await roomService.closeRoom(roomId); + expect(roomRepository.update).toHaveBeenCalledWith(roomId, { + isClosed: true, + }); + }); + }); + + describe('openRoom', () => { + it('should set isClosed to false', async () => { + jest.spyOn(roomRepository, 'update').mockResolvedValueOnce(); + const roomId = mockRoomData.id; + await roomService.openRoom(roomId); + expect(roomRepository.update).toHaveBeenCalledWith(roomId, { + isClosed: false, + }); + }); + }); + + describe('getOpenRoomByHostId', () => { + it('should return room when found', async () => { + const mockRoom = createMock(mockRoomData); + jest + .spyOn(roomRepository, 'findByHostId') + .mockResolvedValueOnce(mockRoom); + + const result = await roomService.getOpenRoomByHostId(mockRoomData.hostId); + + expect(roomRepository.findByHostId).toHaveBeenCalledWith( + mockRoomData.hostId, + { + isClosed: false, + }, + ); + expect(result).toEqual(mockRoom); + }); + + it('should return null when room not found', async () => { + jest.spyOn(roomRepository, 'findByHostId').mockResolvedValueOnce(null); + + const result = await roomService.getOpenRoomByHostId(mockRoomData.hostId); + + expect(roomRepository.findByHostId).toHaveBeenCalledWith( + mockRoomData.hostId, + { + isClosed: false, + }, + ); + expect(result).toBeNull(); + }); + }); +}); diff --git a/src/modules/room/room-user.usecase.ts b/src/modules/call/services/room.service.ts similarity index 64% rename from src/modules/room/room-user.usecase.ts rename to src/modules/call/services/room.service.ts index b61b17b..12c7662 100644 --- a/src/modules/room/room-user.usecase.ts +++ b/src/modules/call/services/room.service.ts @@ -4,25 +4,60 @@ import { ConflictException, BadRequestException, } from '@nestjs/common'; -import { SequelizeRoomUserRepository } from './room-user.repository'; -import { RoomUser } from './room-user.domain'; +import { SequelizeRoomRepository } from '../infrastructure/room.repository'; +import { SequelizeRoomUserRepository } from '../infrastructure/room-user.repository'; +import { Room, RoomAttributes } from '../domain/room.domain'; +import { RoomUser } from '../domain/room-user.domain'; import { v4 as uuidv4 } from 'uuid'; -import { RoomUseCase } from './room.usecase'; -import { UsersInRoomDto } from './dto/users-in-room.dto'; -import { UserRepository } from '../user/user.repository'; -import { AvatarService } from '../../externals/avatar/avatar.service'; -import { User } from '../user/user.domain'; -import { Room } from './room.domain'; +import { UsersInRoomDto } from '../dto/users-in-room.dto'; +import { UserRepository } from '../../../shared/user/user.repository'; +import { AvatarService } from '../../../externals/avatar/avatar.service'; +import { User } from '../../../shared/user/user.domain'; @Injectable() -export class RoomUserUseCase { +export class RoomService { constructor( + private readonly roomRepository: SequelizeRoomRepository, private readonly roomUserRepository: SequelizeRoomUserRepository, - private readonly roomUseCase: RoomUseCase, private readonly userRepository: UserRepository, private readonly avatarService: AvatarService, ) {} + async createRoom(data: Room) { + return this.roomRepository.create(data); + } + + async getRoomByRoomId(id: RoomAttributes['id']) { + return this.roomRepository.findById(id); + } + + async getRoomByHostId(hostId: string) { + return await this.roomRepository.findByHostId(hostId); + } + + async getOpenRoomByHostId(hostId: string) { + return await this.roomRepository.findByHostId(hostId, { + isClosed: false, + }); + } + + async updateRoom(id: RoomAttributes['id'], data: Partial) { + await this.roomRepository.update(id, data); + return this.getRoomByRoomId(id); + } + + async removeRoom(id: string) { + return this.roomRepository.delete(id); + } + + async closeRoom(id: string) { + return this.roomRepository.update(id, { isClosed: true }); + } + + async openRoom(id: string) { + return this.roomRepository.update(id, { isClosed: false }); + } + async addUserToRoom( roomId: string, userData: { @@ -32,7 +67,7 @@ export class RoomUserUseCase { anonymous?: boolean; }, ): Promise { - const room = await this.roomUseCase.getRoomByRoomId(roomId); + const room = await this.getRoomByRoomId(roomId); if (!room) { throw new NotFoundException(`Specified room not found`); } @@ -83,8 +118,21 @@ export class RoomUserUseCase { })); } + async countUsersInRoom(roomId: string): Promise { + const room = await this.getRoomByRoomId(roomId); + if (!room) { + throw new NotFoundException(`Specified room not found`); + } + + return this.roomUserRepository.countByRoomId(roomId); + } + + async removeUserFromRoom(userId: string, room: Room): Promise { + await this.roomUserRepository.deleteByUserIdAndRoomId(userId, room.id); + } + private async getRoomOrThrow(roomId: string) { - const room = await this.roomUseCase.getRoomByRoomId(roomId); + const room = await this.getRoomByRoomId(roomId); if (!room) { throw new NotFoundException(`Specified room not found`); } @@ -111,17 +159,4 @@ export class RoomUserUseCase { return userAvatars; } - - async countUsersInRoom(roomId: string): Promise { - const room = await this.roomUseCase.getRoomByRoomId(roomId); - if (!room) { - throw new NotFoundException(`Specified room not found`); - } - - return this.roomUserRepository.countByRoomId(roomId); - } - - async removeUserFromRoom(userId: string, room: Room): Promise { - await this.roomUserRepository.deleteByUserIdAndRoomId(userId, room.id); - } } diff --git a/src/modules/webhook/jitsi/interfaces/JitsiGenericWebHookPayload.ts b/src/modules/call/webhooks/jitsi/interfaces/JitsiGenericWebHookPayload.ts similarity index 100% rename from src/modules/webhook/jitsi/interfaces/JitsiGenericWebHookPayload.ts rename to src/modules/call/webhooks/jitsi/interfaces/JitsiGenericWebHookPayload.ts diff --git a/src/modules/webhook/jitsi/interfaces/JitsiParticipantLeftData.ts b/src/modules/call/webhooks/jitsi/interfaces/JitsiParticipantLeftData.ts similarity index 100% rename from src/modules/webhook/jitsi/interfaces/JitsiParticipantLeftData.ts rename to src/modules/call/webhooks/jitsi/interfaces/JitsiParticipantLeftData.ts diff --git a/src/modules/webhook/jitsi/interfaces/request.interface.ts b/src/modules/call/webhooks/jitsi/interfaces/request.interface.ts similarity index 100% rename from src/modules/webhook/jitsi/interfaces/request.interface.ts rename to src/modules/call/webhooks/jitsi/interfaces/request.interface.ts diff --git a/src/modules/webhook/jitsi/jitsi-webhook.controller.spec.ts b/src/modules/call/webhooks/jitsi/jitsi-webhook.controller.spec.ts similarity index 100% rename from src/modules/webhook/jitsi/jitsi-webhook.controller.spec.ts rename to src/modules/call/webhooks/jitsi/jitsi-webhook.controller.spec.ts diff --git a/src/modules/webhook/jitsi/jitsi-webhook.controller.ts b/src/modules/call/webhooks/jitsi/jitsi-webhook.controller.ts similarity index 100% rename from src/modules/webhook/jitsi/jitsi-webhook.controller.ts rename to src/modules/call/webhooks/jitsi/jitsi-webhook.controller.ts diff --git a/src/modules/webhook/jitsi/jitsi-webhook.service.spec.ts b/src/modules/call/webhooks/jitsi/jitsi-webhook.service.spec.ts similarity index 74% rename from src/modules/webhook/jitsi/jitsi-webhook.service.spec.ts rename to src/modules/call/webhooks/jitsi/jitsi-webhook.service.spec.ts index 1a6853c..d540f63 100644 --- a/src/modules/webhook/jitsi/jitsi-webhook.service.spec.ts +++ b/src/modules/call/webhooks/jitsi/jitsi-webhook.service.spec.ts @@ -3,9 +3,8 @@ import { Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import * as crypto from 'crypto'; -import { RoomUserUseCase } from '../../room/room-user.usecase'; -import { Room } from '../../room/room.domain'; -import { RoomUseCase } from '../../room/room.usecase'; +import { Room } from '../../domain/room.domain'; +import { RoomService } from '../../services/room.service'; import { JitsiGenericWebHookEvent, JitsiWebhookPayload, @@ -29,8 +28,7 @@ jest.mock('crypto', () => { describe('JitsiWebhookService', () => { let service: JitsiWebhookService; - let roomUseCase: DeepMocked; - let roomUserUseCase: DeepMocked; + let roomService: DeepMocked; let configService: DeepMocked; const minimalRoom = new Room({ @@ -40,29 +38,22 @@ describe('JitsiWebhookService', () => { }); beforeEach(async () => { - roomUseCase = createMock(); - roomUserUseCase = createMock(); - configService = createMock(); - const module: TestingModule = await Test.createTestingModule({ - providers: [ - JitsiWebhookService, - { - provide: ConfigService, - useValue: configService, - }, - { - provide: RoomUseCase, - useValue: roomUseCase, - }, - { - provide: RoomUserUseCase, - useValue: roomUserUseCase, - }, - ], - }).compile(); + providers: [JitsiWebhookService], + }) + .useMocker(createMock) + .compile(); service = module.get(JitsiWebhookService); + roomService = module.get>(RoomService); + configService = module.get>(ConfigService); + + // Default config mock setup + configService.get.mockImplementation((key, defaultValue) => { + if (key === 'jitsiWebhook.events.participantLeft') return true; + if (key === 'jitsiWebhook.secret') return undefined; + return defaultValue; + }); // Mock logger jest.spyOn(Logger.prototype, 'log').mockImplementation(() => undefined); @@ -85,7 +76,8 @@ describe('JitsiWebhookService', () => { return undefined; }); - roomUseCase.getRoomByRoomId.mockResolvedValueOnce(minimalRoom); + roomService.getRoomByRoomId.mockResolvedValue(minimalRoom); + roomService.removeUserFromRoom.mockResolvedValue(undefined); const mockEvent: JitsiParticipantLeftWebHookPayload = { idempotencyKey: 'test-key', @@ -105,13 +97,9 @@ describe('JitsiWebhookService', () => { }, }; - const removeUserFromRoomSpy = jest - .spyOn(roomUserUseCase, 'removeUserFromRoom') - .mockResolvedValueOnce(undefined); - await service.handleParticipantLeft(mockEvent); - expect(removeUserFromRoomSpy).toHaveBeenCalledWith( + expect(roomService.removeUserFromRoom).toHaveBeenCalledWith( 'test-participant-id', minimalRoom, ); @@ -130,7 +118,9 @@ describe('JitsiWebhookService', () => { isClosed: false, }); - roomUseCase.getRoomByRoomId.mockResolvedValueOnce(ownerRoom); + roomService.getRoomByRoomId.mockResolvedValue(ownerRoom); + roomService.closeRoom.mockResolvedValue(undefined); + roomService.removeUserFromRoom.mockResolvedValue(undefined); const mockEvent: JitsiParticipantLeftWebHookPayload = { idempotencyKey: 'test-key', @@ -150,19 +140,11 @@ describe('JitsiWebhookService', () => { }, }; - const closeRoomSpy = jest - .spyOn(roomUseCase, 'closeRoom') - .mockResolvedValueOnce(undefined); - - const removeUserFromRoomSpy = jest - .spyOn(roomUserUseCase, 'removeUserFromRoom') - .mockResolvedValueOnce(undefined); - await service.handleParticipantLeft(mockEvent); - expect(closeRoomSpy).toHaveBeenCalledWith('test-room-id'); + expect(roomService.closeRoom).toHaveBeenCalledWith('test-room-id'); - expect(removeUserFromRoomSpy).toHaveBeenCalledWith( + expect(roomService.removeUserFromRoom).toHaveBeenCalledWith( 'test-participant-id', ownerRoom, ); @@ -181,7 +163,9 @@ describe('JitsiWebhookService', () => { isClosed: false, }); - roomUseCase.getRoomByRoomId.mockResolvedValueOnce(ownerRoom); + roomService.getRoomByRoomId.mockResolvedValue(ownerRoom); + roomService.closeRoom.mockResolvedValue(undefined); + roomService.removeUserFromRoom.mockResolvedValue(undefined); const mockEvent: JitsiParticipantLeftWebHookPayload = { idempotencyKey: 'test-key', @@ -201,19 +185,11 @@ describe('JitsiWebhookService', () => { }, }; - const closeRoomSpy = jest - .spyOn(roomUseCase, 'closeRoom') - .mockResolvedValueOnce(undefined); - - const removeUserFromRoomSpy = jest - .spyOn(roomUserUseCase, 'removeUserFromRoom') - .mockResolvedValueOnce(undefined); - await service.handleParticipantLeft(mockEvent); - expect(closeRoomSpy).not.toHaveBeenCalled(); + expect(roomService.closeRoom).not.toHaveBeenCalled(); - expect(removeUserFromRoomSpy).toHaveBeenCalledWith( + expect(roomService.removeUserFromRoom).toHaveBeenCalledWith( 'test-participant-id', ownerRoom, ); @@ -225,6 +201,9 @@ describe('JitsiWebhookService', () => { return undefined; }); + // Create a new service instance with the updated config mock + const testService = new JitsiWebhookService(configService, roomService); + const mockEvent: JitsiParticipantLeftWebHookPayload = { idempotencyKey: 'test-key', customerId: 'customer-id', @@ -243,19 +222,9 @@ describe('JitsiWebhookService', () => { }, }; - service = new JitsiWebhookService( - configService, - roomUseCase, - roomUserUseCase, - ); - - const removeUserFromRoomSpy = jest - .spyOn(roomUserUseCase, 'removeUserFromRoom') - .mockResolvedValueOnce(undefined); - - await service.handleParticipantLeft(mockEvent); + await testService.handleParticipantLeft(mockEvent); - expect(removeUserFromRoomSpy).not.toHaveBeenCalled(); + expect(roomService.removeUserFromRoom).not.toHaveBeenCalled(); }); it('should handle missing room ID in FQN', async () => { @@ -282,13 +251,9 @@ describe('JitsiWebhookService', () => { }, }; - const removeUserFromRoomSpy = jest - .spyOn(roomUserUseCase, 'removeUserFromRoom') - .mockResolvedValueOnce(undefined); - await service.handleParticipantLeft(mockEvent); - expect(removeUserFromRoomSpy).not.toHaveBeenCalled(); + expect(roomService.removeUserFromRoom).not.toHaveBeenCalled(); }); it('should handle missing participant ID', async () => { @@ -315,13 +280,9 @@ describe('JitsiWebhookService', () => { }, }; - const removeUserFromRoomSpy = jest - .spyOn(roomUserUseCase, 'removeUserFromRoom') - .mockResolvedValueOnce(undefined); - await service.handleParticipantLeft(mockEvent); - expect(removeUserFromRoomSpy).not.toHaveBeenCalled(); + expect(roomService.removeUserFromRoom).not.toHaveBeenCalled(); }); it('should handle errors thrown during processing', async () => { @@ -330,7 +291,7 @@ describe('JitsiWebhookService', () => { return undefined; }); - roomUseCase.getRoomByRoomId.mockResolvedValueOnce(minimalRoom); + roomService.getRoomByRoomId.mockResolvedValueOnce(minimalRoom); const mockEvent: JitsiParticipantLeftWebHookPayload = { idempotencyKey: 'test-key', @@ -351,9 +312,8 @@ describe('JitsiWebhookService', () => { }; const error = new Error('Failed to process'); - jest - .spyOn(roomUserUseCase, 'removeUserFromRoom') - .mockRejectedValueOnce(error); + roomService.getRoomByRoomId.mockResolvedValue(minimalRoom); + roomService.removeUserFromRoom.mockRejectedValue(error); await expect(service.handleParticipantLeft(mockEvent)).rejects.toThrow( error, @@ -372,15 +332,14 @@ describe('JitsiWebhookService', () => { return defaultValue; }); - service = new JitsiWebhookService( - configService, - roomUseCase, - roomUserUseCase, - ); + // Create a new service instance with the updated config mock + const testService = new JitsiWebhookService(configService, roomService); const headers = { 'content-type': 'application/json' }; - expect(service.validateWebhookRequest(headers, mockPayload)).toBe(true); + expect(testService.validateWebhookRequest(headers, mockPayload)).toBe( + true, + ); }); it('should fail validation if signature is missing', () => { @@ -389,16 +348,13 @@ describe('JitsiWebhookService', () => { return defaultValue; }); - service = new JitsiWebhookService( - configService, - roomUseCase, - roomUserUseCase, - ); - + const testService = new JitsiWebhookService(configService, roomService); const headers = { 'content-type': 'application/json' }; const rawBody = JSON.stringify({ test: 'data' }); - expect(service.validateWebhookRequest(headers, mockPayload)).toBe(false); + expect(testService.validateWebhookRequest(headers, mockPayload)).toBe( + false, + ); }); it('should fail validation if raw body is missing', () => { @@ -407,18 +363,15 @@ describe('JitsiWebhookService', () => { return defaultValue; }); - service = new JitsiWebhookService( - configService, - roomUseCase, - roomUserUseCase, - ); - + const testService = new JitsiWebhookService(configService, roomService); const headers = { 'content-type': 'application/json', 'x-jaas-signature': 'signature', }; - expect(service.validateWebhookRequest(headers, mockPayload)).toBe(false); + expect(testService.validateWebhookRequest(headers, mockPayload)).toBe( + false, + ); }); it('should validate correctly with valid signature', () => { @@ -428,12 +381,7 @@ describe('JitsiWebhookService', () => { return defaultValue; }); - service = new JitsiWebhookService( - configService, - roomUseCase, - roomUserUseCase, - ); - + const testService = new JitsiWebhookService(configService, roomService); const signature = 't=1757430085,v1=LnyXpAysJpOLDj6kZ43+QrzcqpXcPW/do7LlSCfhVVs='; @@ -444,7 +392,9 @@ describe('JitsiWebhookService', () => { (crypto.timingSafeEqual as jest.Mock).mockReturnValue(true); - expect(service.validateWebhookRequest(headers, mockPayload)).toBe(true); + expect(testService.validateWebhookRequest(headers, mockPayload)).toBe( + true, + ); }); it('should fail validation with invalid signature', () => { @@ -454,12 +404,7 @@ describe('JitsiWebhookService', () => { return defaultValue; }); - service = new JitsiWebhookService( - configService, - roomUseCase, - roomUserUseCase, - ); - + const testService = new JitsiWebhookService(configService, roomService); const headers = { 'content-type': 'application/json', 'x-jaas-signature': 'invalid-signature', @@ -467,7 +412,9 @@ describe('JitsiWebhookService', () => { (crypto.timingSafeEqual as jest.Mock).mockReturnValue(false); - expect(service.validateWebhookRequest(headers, mockPayload)).toBe(false); + expect(testService.validateWebhookRequest(headers, mockPayload)).toBe( + false, + ); }); }); }); diff --git a/src/modules/webhook/jitsi/jitsi-webhook.service.ts b/src/modules/call/webhooks/jitsi/jitsi-webhook.service.ts similarity index 91% rename from src/modules/webhook/jitsi/jitsi-webhook.service.ts rename to src/modules/call/webhooks/jitsi/jitsi-webhook.service.ts index 663e1c5..8e2e5cf 100644 --- a/src/modules/webhook/jitsi/jitsi-webhook.service.ts +++ b/src/modules/call/webhooks/jitsi/jitsi-webhook.service.ts @@ -1,8 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as crypto from 'crypto'; -import { RoomUserUseCase } from '../../room/room-user.usecase'; -import { RoomUseCase } from '../../room/room.usecase'; +import { RoomService } from '../../services/room.service'; import { JitsiWebhookPayload } from './interfaces/JitsiGenericWebHookPayload'; import { JitsiParticipantLeftWebHookPayload } from './interfaces/JitsiParticipantLeftData'; @Injectable() @@ -13,8 +12,7 @@ export class JitsiWebhookService { constructor( private readonly configService: ConfigService, - private readonly roomUseCase: RoomUseCase, - private readonly roomUserUseCase: RoomUserUseCase, + private readonly roomService: RoomService, ) { this.webhookSecret = this.configService.get('jitsiWebhook.secret'); this.participantLeftEnabled = this.configService.get( @@ -57,7 +55,7 @@ export class JitsiWebhookService { return; } - const room = await this.roomUseCase.getRoomByRoomId(roomId); + const room = await this.roomService.getRoomByRoomId(roomId); if (!room) { this.logger.warn(`Room with ID ${roomId} not found`); @@ -65,9 +63,9 @@ export class JitsiWebhookService { } const isOwner = participantId === room.hostId; - if (isOwner) await this.roomUseCase.closeRoom(roomId); + if (isOwner) await this.roomService.closeRoom(roomId); - await this.roomUserUseCase.removeUserFromRoom(participantId, room); + await this.roomService.removeUserFromRoom(participantId, room); this.logger.log( `Successfully processed PARTICIPANT_LEFT event for participant ${participantId} in room ${roomId}`, diff --git a/src/modules/room/room-user.usecase.spec.ts b/src/modules/room/room-user.usecase.spec.ts deleted file mode 100644 index e1aa3da..0000000 --- a/src/modules/room/room-user.usecase.spec.ts +++ /dev/null @@ -1,485 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { RoomUserUseCase } from './room-user.usecase'; -import { SequelizeRoomUserRepository } from './room-user.repository'; -import { RoomUseCase } from './room.usecase'; -import { RoomUser } from './room-user.domain'; -import { Room } from './room.domain'; -import { - BadRequestException, - ConflictException, - NotFoundException, -} from '@nestjs/common'; -import { v4 as uuidv4 } from 'uuid'; -import { UserRepository } from '../user/user.repository'; -import { createMockUser } from '../user/fixtures'; -import { AvatarService } from '../../externals/avatar/avatar.service'; - -jest.mock('uuid'); - -describe('RoomUserUseCase', () => { - let roomUserUseCase: RoomUserUseCase; - let roomUserRepository: DeepMocked; - let roomUseCase: DeepMocked; - let userRepository: DeepMocked; - let avatarService: DeepMocked; - - const mockRoomData = { - id: 'test-room-id', - hostId: 'test-host-id', - maxUsersAllowed: 5, - }; - - const mockRoomUserData = { - id: 1, - roomId: 'test-room-id', - userId: 'test-user-id', - name: 'Test User', - lastName: 'Smith', - anonymous: false, - }; - - beforeEach(async () => { - const roomUserRepositoryMock = createMock(); - const roomUseCaseMock = createMock(); - const userRepositoryMock = createMock(); - const avatarServiceMock = createMock(); - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - RoomUserUseCase, - { - provide: SequelizeRoomUserRepository, - useValue: roomUserRepositoryMock, - }, - { - provide: RoomUseCase, - useValue: roomUseCaseMock, - }, - { - provide: UserRepository, - useValue: userRepositoryMock, - }, - { - provide: AvatarService, - useValue: avatarServiceMock, - }, - ], - }).compile(); - - roomUserUseCase = module.get(RoomUserUseCase); - roomUserRepository = module.get>( - SequelizeRoomUserRepository, - ); - roomUseCase = module.get>(RoomUseCase); - userRepository = module.get>(UserRepository); - avatarService = module.get>(AvatarService); - - (uuidv4 as jest.Mock).mockReturnValue('generated-uuid'); - }); - - describe('Add User To Room', () => { - it('When the room does not exist, then a NotFoundException is thrown', async () => { - const getRoomByRoomIdMock = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(null); - - await expect( - roomUserUseCase.addUserToRoom('nonexistent-room', { - userId: 'test-user-id', - }), - ).rejects.toThrow(NotFoundException); - - expect(getRoomByRoomIdMock).toHaveBeenCalledWith('nonexistent-room'); - }); - - it('When the room is full, then a BadRequestException is thrown', async () => { - const getRoomByRoomIdMock = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(createMock(mockRoomData)); - const countByRoomIdMock = jest - .spyOn(roomUserRepository, 'countByRoomId') - .mockResolvedValueOnce(5); // Room is full (max 5) - - await expect( - roomUserUseCase.addUserToRoom('test-room-id', { - userId: 'test-user-id', - }), - ).rejects.toThrow(BadRequestException); - - expect(getRoomByRoomIdMock).toHaveBeenCalledWith('test-room-id'); - expect(countByRoomIdMock).toHaveBeenCalledWith('test-room-id'); - }); - - it('When no userId is provided or user is anonymous, then a UUID is generated', async () => { - const getRoomByRoomIdMock = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(new Room(mockRoomData)); - const countByRoomIdMock = jest - .spyOn(roomUserRepository, 'countByRoomId') - .mockResolvedValueOnce(2); // Room has space - jest - .spyOn(roomUserRepository, 'findByUserIdAndRoomId') - .mockResolvedValueOnce(null); - const createMock = jest - .spyOn(roomUserRepository, 'create') - .mockResolvedValueOnce( - new RoomUser({ - ...mockRoomUserData, - userId: 'generated-uuid', - anonymous: true, - }), - ); - - const result = await roomUserUseCase.addUserToRoom('test-room-id', { - anonymous: true, - }); - - expect(uuidv4).toHaveBeenCalled(); - expect(createMock).toHaveBeenCalledWith( - expect.objectContaining({ - roomId: 'test-room-id', - userId: 'generated-uuid', - anonymous: true, - }), - ); - expect(result.userId).toBe('generated-uuid'); - expect(result.anonymous).toBe(true); - }); - - it('When the user is already in the room, then a ConflictException is thrown', async () => { - const getRoomByRoomIdMock = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(createMock(mockRoomData)); - const countByRoomIdMock = jest - .spyOn(roomUserRepository, 'countByRoomId') - .mockResolvedValueOnce(2); // Room has space - const findByUserIdAndRoomIdMock = jest - .spyOn(roomUserRepository, 'findByUserIdAndRoomId') - .mockResolvedValueOnce( - new RoomUser({ - ...mockRoomUserData, - userId: 'test-user-id', - }), - ); - - await expect( - roomUserUseCase.addUserToRoom('test-room-id', { - userId: 'test-user-id', - }), - ).rejects.toThrow(ConflictException); - - expect(findByUserIdAndRoomIdMock).toHaveBeenCalledWith( - 'test-user-id', - 'test-room-id', - ); - }); - - it('When all conditions are met, then the user is added to the room successfully', async () => { - const getRoomByRoomIdMock = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(new Room(mockRoomData)); - const countByRoomIdMock = jest - .spyOn(roomUserRepository, 'countByRoomId') - .mockResolvedValueOnce(2); // Room has space - const findByUserIdAndRoomIdMock = jest - .spyOn(roomUserRepository, 'findByUserIdAndRoomId') - .mockResolvedValueOnce(null); - const createMock = jest - .spyOn(roomUserRepository, 'create') - .mockResolvedValueOnce( - new RoomUser({ - ...mockRoomUserData, - userId: 'test-user-id', - }), - ); - - const result = await roomUserUseCase.addUserToRoom('test-room-id', { - userId: 'test-user-id', - name: 'Test User', - lastName: 'Smith', - }); - - expect(createMock).toHaveBeenCalledWith( - expect.objectContaining({ - roomId: 'test-room-id', - userId: 'test-user-id', - name: 'Test User', - lastName: 'Smith', - anonymous: false, - }), - ); - expect(result).toEqual(expect.objectContaining(mockRoomUserData)); - }); - }); - - describe('Get Users In Room', () => { - it('When the room does not exist, then a NotFoundException is thrown', async () => { - const getRoomByRoomIdMock = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(null); - - await expect( - roomUserUseCase.getUsersInRoom('nonexistent-room'), - ).rejects.toThrow(NotFoundException); - - expect(getRoomByRoomIdMock).toHaveBeenCalledWith('nonexistent-room'); - }); - - it('When the room exists, then all users in the room are returned with their avatars', async () => { - // Setup test data - const mockRoom = new Room(mockRoomData); - const mockRoomUsers = [ - new RoomUser(mockRoomUserData), - new RoomUser({ - ...mockRoomUserData, - id: 2, - userId: 'user-2', - name: 'User 2', - lastName: 'Last 2', - }), - ]; - const mockUsers = [ - createMockUser({ - uuid: 'test-user-id', - avatar: 'avatar-path-1', - }), - createMockUser({ - uuid: 'user-2', - avatar: 'avatar-path-2', - }), - ]; - const mockAvatarUrls = { - 'test-user-id': 'https://example.com/avatar1.jpg', - 'user-2': 'https://example.com/avatar2.jpg', - }; - - const getRoomByRoomIdMock = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(mockRoom); - const findAllByRoomIdMock = jest - .spyOn(roomUserRepository, 'findAllByRoomId') - .mockResolvedValueOnce(mockRoomUsers); - const findManyByUuidMock = jest - .spyOn(userRepository, 'findManyByUuid') - .mockResolvedValueOnce(mockUsers); - - const getDownloadUrlMock = jest - .spyOn(avatarService, 'getDownloadUrl') - .mockImplementation((path) => { - if (path === 'avatar-path-1') - return Promise.resolve(mockAvatarUrls['test-user-id']); - if (path === 'avatar-path-2') - return Promise.resolve(mockAvatarUrls['user-2']); - return Promise.resolve(null); - }); - - // Execute the method - const result = await roomUserUseCase.getUsersInRoom('test-room-id'); - - // Verify the results - expect(getRoomByRoomIdMock).toHaveBeenCalledWith('test-room-id'); - expect(findAllByRoomIdMock).toHaveBeenCalledWith('test-room-id'); - expect(findManyByUuidMock).toHaveBeenCalledWith( - expect.arrayContaining(['test-user-id', 'user-2']), - ); - expect(getDownloadUrlMock).toHaveBeenCalledTimes(2); - - // Check the structure and content of the result - expect(result).toHaveLength(2); - expect(result[0]).toEqual({ - id: 'test-user-id', - name: 'Test User', - lastName: 'Smith', - anonymous: false, - avatar: 'https://example.com/avatar1.jpg', - }); - expect(result[1]).toEqual({ - id: 'user-2', - name: 'User 2', - lastName: 'Last 2', - anonymous: false, - avatar: 'https://example.com/avatar2.jpg', - }); - }); - - it('When users have no avatars, the avatar field should be null', async () => { - const mockRoom = new Room(mockRoomData); - const mockRoomUsers = [new RoomUser(mockRoomUserData)]; - const mockUsers = [ - createMockUser({ - uuid: 'test-user-id', - avatar: null, - }), - ]; - - const getRoomByRoomIdMock = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(mockRoom); - const findAllByRoomIdMock = jest - .spyOn(roomUserRepository, 'findAllByRoomId') - .mockResolvedValueOnce(mockRoomUsers); - const findManyByUuidMock = jest - .spyOn(userRepository, 'findManyByUuid') - .mockResolvedValueOnce(mockUsers); - - const result = await roomUserUseCase.getUsersInRoom('test-room-id'); - - const getDownloadUrlMock = jest.spyOn(avatarService, 'getDownloadUrl'); - - // Verify the results - expect(getRoomByRoomIdMock).toHaveBeenCalledWith('test-room-id'); - expect(findAllByRoomIdMock).toHaveBeenCalledWith('test-room-id'); - expect(findManyByUuidMock).toHaveBeenCalledWith(['test-user-id']); - expect(getDownloadUrlMock).not.toHaveBeenCalled(); - - expect(result).toHaveLength(1); - expect(result[0]).toEqual({ - id: 'test-user-id', - name: 'Test User', - lastName: 'Smith', - anonymous: false, - avatar: null, - }); - }); - - it('When the room has no users, it should return an empty array', async () => { - const mockRoom = new Room(mockRoomData); - - const getRoomByRoomIdMock = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(mockRoom); - const findAllByRoomIdMock = jest - .spyOn(roomUserRepository, 'findAllByRoomId') - .mockResolvedValueOnce([]); - const findManyByUuidMock = jest - .spyOn(userRepository, 'findManyByUuid') - .mockResolvedValueOnce([]); - - const result = await roomUserUseCase.getUsersInRoom('test-room-id'); - - const getDownloadUrlMock = jest.spyOn(avatarService, 'getDownloadUrl'); - - expect(getRoomByRoomIdMock).toHaveBeenCalledWith('test-room-id'); - expect(findAllByRoomIdMock).toHaveBeenCalledWith('test-room-id'); - expect(findManyByUuidMock).toHaveBeenCalledWith([]); - expect(getDownloadUrlMock).not.toHaveBeenCalled(); - - expect(result).toEqual([]); - expect(result).toHaveLength(0); - }); - - it('When user record is not found for a room user, avatars should still be handled correctly', async () => { - const mockRoom = new Room(mockRoomData); - const mockRoomUsers = [ - new RoomUser(mockRoomUserData), - new RoomUser({ - ...mockRoomUserData, - id: 2, - userId: 'user-2', - name: 'User 2', - lastName: 'Last 2', - }), - ]; - const mockUsers = [ - createMockUser({ - uuid: 'test-user-id', - avatar: 'avatar-path-1', - }), - ]; - const mockAvatarUrls = { - 'test-user-id': 'https://example.com/avatar1.jpg', - }; - - const getRoomByRoomIdMock = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(mockRoom); - const findAllByRoomIdMock = jest - .spyOn(roomUserRepository, 'findAllByRoomId') - .mockResolvedValueOnce(mockRoomUsers); - const findManyByUuidMock = jest - .spyOn(userRepository, 'findManyByUuid') - .mockResolvedValueOnce(mockUsers); - - const getDownloadUrlMock = jest - .spyOn(avatarService, 'getDownloadUrl') - .mockImplementation((path) => { - if (path === 'avatar-path-1') - return Promise.resolve(mockAvatarUrls['test-user-id']); - return Promise.resolve(null); - }); - - const result = await roomUserUseCase.getUsersInRoom('test-room-id'); - - expect(getRoomByRoomIdMock).toHaveBeenCalledWith('test-room-id'); - expect(findAllByRoomIdMock).toHaveBeenCalledWith('test-room-id'); - expect(findManyByUuidMock).toHaveBeenCalledWith( - expect.arrayContaining(['test-user-id', 'user-2']), - ); - expect(getDownloadUrlMock).toHaveBeenCalledTimes(1); - - expect(result).toHaveLength(2); - expect(result[0]).toEqual({ - id: 'test-user-id', - name: 'Test User', - lastName: 'Smith', - anonymous: false, - avatar: 'https://example.com/avatar1.jpg', - }); - expect(result[1]).toEqual({ - id: 'user-2', - name: 'User 2', - lastName: 'Last 2', - anonymous: false, - avatar: null, - }); - }); - }); - - describe('Count Users In Room', () => { - it('When the room does not exist, then a NotFoundException is thrown', async () => { - const getRoomByRoomIdMock = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(null); - - await expect( - roomUserUseCase.countUsersInRoom('nonexistent-room'), - ).rejects.toThrow(NotFoundException); - - expect(getRoomByRoomIdMock).toHaveBeenCalledWith('nonexistent-room'); - }); - - it('When the room exists, then the count of users in the room is returned', async () => { - const getRoomByRoomIdMock = jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(createMock(mockRoomData)); - const countByRoomIdMock = jest - .spyOn(roomUserRepository, 'countByRoomId') - .mockResolvedValueOnce(3); - - const result = await roomUserUseCase.countUsersInRoom('test-room-id'); - - expect(countByRoomIdMock).toHaveBeenCalledWith('test-room-id'); - expect(result).toBe(3); - }); - }); - - describe('Remove User From Room', () => { - const mockRoom = new Room(mockRoomData); - it('When the room exists, then the user is removed from the room', async () => { - jest - .spyOn(roomUseCase, 'getRoomByRoomId') - .mockResolvedValueOnce(createMock(mockRoomData)); - const deleteByUserIdAndRoomIdMock = jest - .spyOn(roomUserRepository, 'deleteByUserIdAndRoomId') - .mockResolvedValueOnce(undefined); - - await roomUserUseCase.removeUserFromRoom('test-user-id', mockRoom); - - expect(deleteByUserIdAndRoomIdMock).toHaveBeenCalledWith( - 'test-user-id', - 'test-room-id', - ); - }); - }); -}); diff --git a/src/modules/room/room.dto.ts b/src/modules/room/room.dto.ts deleted file mode 100644 index a28e292..0000000 --- a/src/modules/room/room.dto.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface RoomDto { - id: string; - host_id: string; - max_users_allowed: number; -} diff --git a/src/modules/room/room.module.ts b/src/modules/room/room.module.ts deleted file mode 100644 index 21ffc8a..0000000 --- a/src/modules/room/room.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Module } from '@nestjs/common'; -import { SequelizeModule } from '@nestjs/sequelize'; -import { RoomModel } from './models/room.model'; -import { RoomUseCase } from './room.usecase'; -import { SequelizeRoomRepository } from './room.repository'; -import { RoomUserModel } from './models/room-user.model'; -import { SequelizeRoomUserRepository } from './room-user.repository'; -import { RoomUserUseCase } from './room-user.usecase'; -import { AvatarService } from 'src/externals/avatar/avatar.service'; -import { UserModule } from '../user/user.module'; - -@Module({ - imports: [SequelizeModule.forFeature([RoomModel, RoomUserModel]), UserModule], - providers: [ - RoomUseCase, - SequelizeRoomRepository, - RoomUserUseCase, - SequelizeRoomUserRepository, - AvatarService, - ], - exports: [RoomUseCase, RoomUserUseCase], -}) -export class RoomModule {} diff --git a/src/modules/room/room.usecase.spec.ts b/src/modules/room/room.usecase.spec.ts deleted file mode 100644 index 1359aa5..0000000 --- a/src/modules/room/room.usecase.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { RoomUseCase } from './room.usecase'; -import { SequelizeRoomRepository } from './room.repository'; -import { Room } from './room.domain'; -import { mockRoomData } from '../call/fixtures'; - -describe('Room Use Cases', () => { - let roomUseCase: RoomUseCase; - let roomRepository: DeepMocked; - - beforeEach(async () => { - roomRepository = createMock(); - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - RoomUseCase, - { - provide: SequelizeRoomRepository, - useValue: roomRepository, - }, - ], - }).compile(); - - roomUseCase = module.get(RoomUseCase); - }); - - describe('Creating a room', () => { - it('should create a room successfully', async () => { - const mockRoom = createMock(mockRoomData); - const createRoomSpy = jest - .spyOn(roomRepository, 'create') - .mockResolvedValueOnce(mockRoom); - - const result = await roomUseCase.createRoom(mockRoom); - - expect(createRoomSpy).toHaveBeenCalledWith(mockRoom); - expect(result).toEqual(mockRoom); - }); - }); - - describe('getRoomByRoomId', () => { - it('should return room when found', async () => { - const mockRoom = createMock(mockRoomData); - const findRoomByIdSpy = jest - .spyOn(roomRepository, 'findById') - .mockResolvedValueOnce(mockRoom); - - const result = await roomUseCase.getRoomByRoomId(mockRoomData.id); - - expect(findRoomByIdSpy).toHaveBeenCalledWith(mockRoomData.id); - expect(result).toEqual(mockRoom); - }); - - it('should return null when room not found', async () => { - const findRoomByIdSpy = jest - .spyOn(roomRepository, 'findById') - .mockResolvedValueOnce(null); - - const result = await roomUseCase.getRoomByRoomId(mockRoomData.id); - - expect(findRoomByIdSpy).toHaveBeenCalledWith(mockRoomData.id); - expect(result).toBeNull(); - }); - }); - - describe('getRoomByHostId', () => { - it('should return room when found', async () => { - const mockRoom = createMock(mockRoomData); - const findRoomByHostIdSpy = jest - .spyOn(roomRepository, 'findByHostId') - .mockResolvedValueOnce(mockRoom); - - const result = await roomUseCase.getRoomByHostId(mockRoomData.hostId); - - expect(findRoomByHostIdSpy).toHaveBeenCalledWith(mockRoomData.hostId); - expect(result).toEqual(mockRoom); - }); - - it('should return null when room not found', async () => { - const findRoomByHostIdSpy = jest - .spyOn(roomRepository, 'findByHostId') - .mockResolvedValueOnce(null); - - const result = await roomUseCase.getRoomByHostId(mockRoomData.hostId); - - expect(findRoomByHostIdSpy).toHaveBeenCalledWith(mockRoomData.hostId); - expect(result).toBeNull(); - }); - }); - - describe('updateRoom', () => { - const mockUpdateData = { - maxUsersAllowed: 10, - }; - - it('should update room successfully', async () => { - const mockUpdatedRoom = createMock({ - ...mockRoomData, - maxUsersAllowed: 10, - }); - - const updateRoomSpy = jest - .spyOn(roomRepository, 'update') - .mockResolvedValueOnce(); - const findRoomByIdSpy = jest - .spyOn(roomRepository, 'findById') - .mockResolvedValueOnce(mockUpdatedRoom); - - const result = await roomUseCase.updateRoom( - mockRoomData.id, - mockUpdateData, - ); - - expect(updateRoomSpy).toHaveBeenCalledWith( - mockRoomData.id, - mockUpdateData, - ); - expect(findRoomByIdSpy).toHaveBeenCalledWith(mockRoomData.id); - expect(result).toEqual(mockUpdatedRoom); - }); - }); - - describe('removeRoom', () => { - it('should remove room successfully', async () => { - const deleteRoomSpy = jest - .spyOn(roomRepository, 'delete') - .mockResolvedValueOnce(); - - await roomUseCase.removeRoom(mockRoomData.id); - - expect(deleteRoomSpy).toHaveBeenCalledWith(mockRoomData.id); - }); - }); - - describe('closeRoom', () => { - it('should set isClosed to true', async () => { - const updateSpy = jest - .spyOn(roomRepository, 'update') - .mockResolvedValueOnce(); - const roomId = mockRoomData.id; - await roomUseCase.closeRoom(roomId); - expect(updateSpy).toHaveBeenCalledWith(roomId, { isClosed: true }); - }); - }); - - describe('openRoom', () => { - it('should set isClosed to false', async () => { - const updateSpy = jest - .spyOn(roomRepository, 'update') - .mockResolvedValueOnce(); - const roomId = mockRoomData.id; - await roomUseCase.openRoom(roomId); - expect(updateSpy).toHaveBeenCalledWith(roomId, { isClosed: false }); - }); - }); - - describe('getOpenRoomByHostId', () => { - it('should return room when found', async () => { - const mockRoom = createMock(mockRoomData); - const findRoomByHostIdSpy = jest - .spyOn(roomRepository, 'findByHostId') - .mockResolvedValueOnce(mockRoom); - - const result = await roomUseCase.getOpenRoomByHostId(mockRoomData.hostId); - - expect(findRoomByHostIdSpy).toHaveBeenCalledWith(mockRoomData.hostId, { - isClosed: false, - }); - expect(result).toEqual(mockRoom); - }); - - it('should return null when room not found', async () => { - const findRoomByHostIdSpy = jest - .spyOn(roomRepository, 'findByHostId') - .mockResolvedValueOnce(null); - - const result = await roomUseCase.getOpenRoomByHostId(mockRoomData.hostId); - - expect(findRoomByHostIdSpy).toHaveBeenCalledWith(mockRoomData.hostId, { - isClosed: false, - }); - expect(result).toBeNull(); - }); - }); -}); diff --git a/src/modules/room/room.usecase.ts b/src/modules/room/room.usecase.ts deleted file mode 100644 index f44f029..0000000 --- a/src/modules/room/room.usecase.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { SequelizeRoomRepository } from './room.repository'; -import { Room, RoomAttributes } from './room.domain'; - -@Injectable() -export class RoomUseCase { - constructor(private readonly roomRepository: SequelizeRoomRepository) {} - - async createRoom(data: Room) { - return this.roomRepository.create(data); - } - - async getRoomByRoomId(id: RoomAttributes['id']) { - return this.roomRepository.findById(id); - } - - async getRoomByHostId(hostId: string) { - return await this.roomRepository.findByHostId(hostId); - } - - async getOpenRoomByHostId(hostId: string) { - return await this.roomRepository.findByHostId(hostId, { - isClosed: false, - }); - } - - async updateRoom(id: RoomAttributes['id'], data: Partial) { - await this.roomRepository.update(id, data); - return this.getRoomByRoomId(id); - } - - async removeRoom(id: string) { - return this.roomRepository.delete(id); - } - - async closeRoom(id: string) { - return this.roomRepository.update(id, { isClosed: true }); - } - - async openRoom(id: string) { - return this.roomRepository.update(id, { isClosed: false }); - } -} diff --git a/src/modules/webhook/jitsi/jitsi-webhook.module.ts b/src/modules/webhook/jitsi/jitsi-webhook.module.ts deleted file mode 100644 index d2d479a..0000000 --- a/src/modules/webhook/jitsi/jitsi-webhook.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from '@nestjs/common'; -import { JitsiWebhookController } from './jitsi-webhook.controller'; -import { JitsiWebhookService } from './jitsi-webhook.service'; -import { ConfigModule } from '@nestjs/config'; -import { HttpClientModule } from '../../../externals/http/http.module'; -import { RoomModule } from '../../room/room.module'; - -@Module({ - imports: [ConfigModule, HttpClientModule, RoomModule], - controllers: [JitsiWebhookController], - providers: [JitsiWebhookService], - exports: [JitsiWebhookService], -}) -export class JitsiWebhookModule {} diff --git a/src/modules/webhook/webhook.module.ts b/src/modules/webhook/webhook.module.ts deleted file mode 100644 index 7a0bb18..0000000 --- a/src/modules/webhook/webhook.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { JitsiWebhookModule } from './jitsi/jitsi-webhook.module'; - -@Module({ - imports: [JitsiWebhookModule], - exports: [JitsiWebhookModule], -}) -export class WebhookModule {} diff --git a/src/shared/shared.module.ts b/src/shared/shared.module.ts new file mode 100644 index 0000000..9acb2ee --- /dev/null +++ b/src/shared/shared.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { UserModule } from './user/user.module'; + +@Module({ + imports: [UserModule], + exports: [UserModule], +}) +export class SharedModule {} diff --git a/src/types/OmitCreateProperties.ts b/src/shared/types/OmitCreateProperties.ts similarity index 100% rename from src/types/OmitCreateProperties.ts rename to src/shared/types/OmitCreateProperties.ts diff --git a/src/modules/user/fixtures.ts b/src/shared/user/fixtures.ts similarity index 100% rename from src/modules/user/fixtures.ts rename to src/shared/user/fixtures.ts diff --git a/src/modules/user/models/user.model.ts b/src/shared/user/models/user.model.ts similarity index 100% rename from src/modules/user/models/user.model.ts rename to src/shared/user/models/user.model.ts diff --git a/src/modules/user/user.attributes.ts b/src/shared/user/user.attributes.ts similarity index 100% rename from src/modules/user/user.attributes.ts rename to src/shared/user/user.attributes.ts diff --git a/src/modules/user/user.domain.ts b/src/shared/user/user.domain.ts similarity index 100% rename from src/modules/user/user.domain.ts rename to src/shared/user/user.domain.ts diff --git a/src/modules/user/user.module.ts b/src/shared/user/user.module.ts similarity index 100% rename from src/modules/user/user.module.ts rename to src/shared/user/user.module.ts diff --git a/src/modules/user/user.repository.spec.ts b/src/shared/user/user.repository.spec.ts similarity index 100% rename from src/modules/user/user.repository.spec.ts rename to src/shared/user/user.repository.spec.ts diff --git a/src/modules/user/user.repository.ts b/src/shared/user/user.repository.ts similarity index 100% rename from src/modules/user/user.repository.ts rename to src/shared/user/user.repository.ts