diff --git a/src/modules/user/core/application/commands/delete-user/delete-user.command.ts b/src/modules/user/core/application/commands/delete-user/delete-user.command.ts new file mode 100644 index 0000000..2f2f037 --- /dev/null +++ b/src/modules/user/core/application/commands/delete-user/delete-user.command.ts @@ -0,0 +1,7 @@ +import { Command, CommandProps } from '@mobicoop/ddd-library'; + +export class DeleteUserCommand extends Command { + constructor(props: CommandProps) { + super(props); + } +} diff --git a/src/modules/user/core/application/commands/delete-user/delete-user.service.ts b/src/modules/user/core/application/commands/delete-user/delete-user.service.ts new file mode 100644 index 0000000..294b37c --- /dev/null +++ b/src/modules/user/core/application/commands/delete-user/delete-user.service.ts @@ -0,0 +1,21 @@ +import { Inject } from '@nestjs/common'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { DeleteUserCommand } from './delete-user.command'; +import { USER_REPOSITORY } from '@modules/user/user.di-tokens'; +import { UserRepositoryPort } from '../../ports/user.repository.port'; +import { UserEntity } from '@modules/user/core/domain/user.entity'; + +@CommandHandler(DeleteUserCommand) +export class DeleteUserService implements ICommandHandler { + constructor( + @Inject(USER_REPOSITORY) + private readonly userRepository: UserRepositoryPort, + ) {} + + async execute(command: DeleteUserCommand): Promise { + const user: UserEntity = await this.userRepository.findOneById(command.id); + user.delete(); + const isDeleted: boolean = await this.userRepository.delete(user); + return isDeleted; + } +} diff --git a/src/modules/user/core/application/event-handlers/publish-message-when-user-is-created.domain-event-handler.ts b/src/modules/user/core/application/event-handlers/publish-message-when-user-is-created.domain-event-handler.ts new file mode 100644 index 0000000..3fcab00 --- /dev/null +++ b/src/modules/user/core/application/event-handlers/publish-message-when-user-is-created.domain-event-handler.ts @@ -0,0 +1,18 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { MessagePublisherPort } from '@mobicoop/ddd-library'; +import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens'; +import { UserCreatedDomainEvent } from '../../domain/events/user-created.domain-events'; + +@Injectable() +export class PublishMessageWhenUserIsCreatedDomainEventHandler { + constructor( + @Inject(USER_MESSAGE_PUBLISHER) + private readonly messagePublisher: MessagePublisherPort, + ) {} + + @OnEvent(UserCreatedDomainEvent.name, { async: true, promisify: true }) + async handle(event: UserCreatedDomainEvent): Promise { + this.messagePublisher.publish('user.created', JSON.stringify(event)); + } +} diff --git a/src/modules/user/core/application/event-handlers/publish-message-when-user-is-deleted.domain-event-handler.ts b/src/modules/user/core/application/event-handlers/publish-message-when-user-is-deleted.domain-event-handler.ts new file mode 100644 index 0000000..d3c2893 --- /dev/null +++ b/src/modules/user/core/application/event-handlers/publish-message-when-user-is-deleted.domain-event-handler.ts @@ -0,0 +1,18 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { MessagePublisherPort } from '@mobicoop/ddd-library'; +import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens'; +import { UserDeletedDomainEvent } from '../../domain/events/user-deleted.domain-event'; + +@Injectable() +export class PublishMessageWhenUserIsDeletedDomainEventHandler { + constructor( + @Inject(USER_MESSAGE_PUBLISHER) + private readonly messagePublisher: MessagePublisherPort, + ) {} + + @OnEvent(UserDeletedDomainEvent.name, { async: true, promisify: true }) + async handle(event: UserDeletedDomainEvent): Promise { + this.messagePublisher.publish('user.deleted', JSON.stringify(event)); + } +} diff --git a/src/modules/user/core/application/event-handlers/publish-message-when-user-is-updated.domain-event-handler.ts b/src/modules/user/core/application/event-handlers/publish-message-when-user-is-updated.domain-event-handler.ts new file mode 100644 index 0000000..90b51b1 --- /dev/null +++ b/src/modules/user/core/application/event-handlers/publish-message-when-user-is-updated.domain-event-handler.ts @@ -0,0 +1,18 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { MessagePublisherPort } from '@mobicoop/ddd-library'; +import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens'; +import { UserUpdatedDomainEvent } from '../../domain/events/user-updated.domain-events'; + +@Injectable() +export class PublishMessageWhenUserIsUpdatedDomainEventHandler { + constructor( + @Inject(USER_MESSAGE_PUBLISHER) + private readonly messagePublisher: MessagePublisherPort, + ) {} + + @OnEvent(UserUpdatedDomainEvent.name, { async: true, promisify: true }) + async handle(event: UserUpdatedDomainEvent): Promise { + this.messagePublisher.publish('user.updated', JSON.stringify(event)); + } +} diff --git a/src/modules/user/core/domain/events/user-deleted.domain-event.ts b/src/modules/user/core/domain/events/user-deleted.domain-event.ts new file mode 100644 index 0000000..2f28885 --- /dev/null +++ b/src/modules/user/core/domain/events/user-deleted.domain-event.ts @@ -0,0 +1,7 @@ +import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library'; + +export class UserDeletedDomainEvent extends DomainEvent { + constructor(props: DomainEventProps) { + super(props); + } +} diff --git a/src/modules/user/core/domain/user.entity.ts b/src/modules/user/core/domain/user.entity.ts index 0bb810f..7c6448a 100644 --- a/src/modules/user/core/domain/user.entity.ts +++ b/src/modules/user/core/domain/user.entity.ts @@ -3,6 +3,7 @@ import { v4 } from 'uuid'; import { CreateUserProps, UpdateUserProps, UserProps } from './user.types'; import { UserCreatedDomainEvent } from './events/user-created.domain-events'; import { UserUpdatedDomainEvent } from './events/user-updated.domain-events'; +import { UserDeletedDomainEvent } from './events/user-deleted.domain-event'; export class UserEntity extends AggregateRoot { protected readonly _id: AggregateID; @@ -40,6 +41,14 @@ export class UserEntity extends AggregateRoot { ); } + delete(): void { + this.addEvent( + new UserDeletedDomainEvent({ + aggregateId: this.id, + }), + ); + } + validate(): void { // entity business rules validation to protect it's invariant before saving entity to a database } diff --git a/src/modules/user/interface/grpc-controllers/delete-user.grpc.controller.ts b/src/modules/user/interface/grpc-controllers/delete-user.grpc.controller.ts new file mode 100644 index 0000000..9e1cd5c --- /dev/null +++ b/src/modules/user/interface/grpc-controllers/delete-user.grpc.controller.ts @@ -0,0 +1,44 @@ +import { + DatabaseErrorException, + NotFoundException, + RpcExceptionCode, + RpcValidationPipe, +} from '@mobicoop/ddd-library'; +import { Controller, UsePipes } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; +import { GrpcMethod, RpcException } from '@nestjs/microservices'; +import { DeleteUserRequestDto } from './dtos/delete-user.request.dto'; +import { DeleteUserCommand } from '@modules/user/core/application/commands/delete-user/delete-user.command'; + +@UsePipes( + new RpcValidationPipe({ + whitelist: true, + forbidUnknownValues: false, + }), +) +@Controller() +export class DeleteUserGrpcController { + constructor(private readonly commandBus: CommandBus) {} + + @GrpcMethod('UserService', 'Delete') + async delete(data: DeleteUserRequestDto): Promise { + try { + await this.commandBus.execute(new DeleteUserCommand(data)); + } catch (error: any) { + if (error instanceof NotFoundException) + throw new RpcException({ + code: RpcExceptionCode.NOT_FOUND, + message: error.message, + }); + if (error instanceof DatabaseErrorException) + throw new RpcException({ + code: RpcExceptionCode.INTERNAL, + message: error.message, + }); + throw new RpcException({ + code: RpcExceptionCode.UNKNOWN, + message: error.message, + }); + } + } +} diff --git a/src/modules/user/interface/grpc-controllers/dtos/delete-user.request.dto.ts b/src/modules/user/interface/grpc-controllers/dtos/delete-user.request.dto.ts new file mode 100644 index 0000000..bf364f3 --- /dev/null +++ b/src/modules/user/interface/grpc-controllers/dtos/delete-user.request.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DeleteUserRequestDto { + @IsString() + @IsNotEmpty() + id: string; +} diff --git a/src/modules/user/tests/unit/core/delete-user.service.spec.ts b/src/modules/user/tests/unit/core/delete-user.service.spec.ts new file mode 100644 index 0000000..fb7fa7e --- /dev/null +++ b/src/modules/user/tests/unit/core/delete-user.service.spec.ts @@ -0,0 +1,50 @@ +import { DeleteUserCommand } from '@modules/user/core/application/commands/delete-user/delete-user.command'; +import { DeleteUserService } from '@modules/user/core/application/commands/delete-user/delete-user.service'; +import { DeleteUserRequestDto } from '@modules/user/interface/grpc-controllers/dtos/delete-user.request.dto'; +import { USER_REPOSITORY } from '@modules/user/user.di-tokens'; +import { Test, TestingModule } from '@nestjs/testing'; + +const deleteUserRequest: DeleteUserRequestDto = { + id: '165192d4-398a-4469-a16b-98c02cc6f531', +}; + +const mockUserEntity = { + delete: jest.fn(), +}; + +const mockUserRepository = { + findOneById: jest.fn().mockImplementation(() => mockUserEntity), + delete: jest.fn().mockImplementationOnce(() => true), +}; + +describe('Delete User Service', () => { + let deleteUserService: DeleteUserService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: USER_REPOSITORY, + useValue: mockUserRepository, + }, + DeleteUserService, + ], + }).compile(); + + deleteUserService = module.get(DeleteUserService); + }); + + it('should be defined', () => { + expect(deleteUserService).toBeDefined(); + }); + + describe('execution', () => { + const deleteUserCommand = new DeleteUserCommand(deleteUserRequest); + it('should delete a user', async () => { + const result: boolean = await deleteUserService.execute( + deleteUserCommand, + ); + expect(result).toBeTruthy(); + }); + }); +}); diff --git a/src/modules/user/tests/unit/core/publish-message-when-user-is-created.domain-event-handler.spec.ts b/src/modules/user/tests/unit/core/publish-message-when-user-is-created.domain-event-handler.spec.ts new file mode 100644 index 0000000..0b03b0f --- /dev/null +++ b/src/modules/user/tests/unit/core/publish-message-when-user-is-created.domain-event-handler.spec.ts @@ -0,0 +1,54 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PublishMessageWhenUserIsCreatedDomainEventHandler } from '@modules/user/core/application/event-handlers/publish-message-when-user-is-created.domain-event-handler'; +import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens'; +import { UserCreatedDomainEvent } from '@modules/user/core/domain/events/user-created.domain-events'; + +const mockMessagePublisher = { + publish: jest.fn().mockImplementation(), +}; + +describe('Publish message when user is created domain event handler', () => { + let publishMessageWhenUserIsCreatedDomainEventHandler: PublishMessageWhenUserIsCreatedDomainEventHandler; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: USER_MESSAGE_PUBLISHER, + useValue: mockMessagePublisher, + }, + PublishMessageWhenUserIsCreatedDomainEventHandler, + ], + }).compile(); + + publishMessageWhenUserIsCreatedDomainEventHandler = + module.get( + PublishMessageWhenUserIsCreatedDomainEventHandler, + ); + }); + + it('should publish a message', () => { + jest.spyOn(mockMessagePublisher, 'publish'); + const userCreatedDomainEvent: UserCreatedDomainEvent = { + id: 'some-domain-event-id', + aggregateId: 'some-aggregate-id', + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@email.com', + phone: '+33611223344', + metadata: { + timestamp: new Date('2023-06-28T05:00:00Z').getTime(), + correlationId: 'some-correlation-id', + }, + }; + publishMessageWhenUserIsCreatedDomainEventHandler.handle( + userCreatedDomainEvent, + ); + expect(publishMessageWhenUserIsCreatedDomainEventHandler).toBeDefined(); + expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1); + expect(mockMessagePublisher.publish).toHaveBeenCalledWith( + 'user.created', + '{"id":"some-domain-event-id","aggregateId":"some-aggregate-id","firstName":"John","lastName":"Doe","email":"john.doe@email.com","phone":"+33611223344","metadata":{"timestamp":1687928400000,"correlationId":"some-correlation-id"}}', + ); + }); +}); diff --git a/src/modules/user/tests/unit/core/publish-message-when-user-is-deleted.domain-event-handler.spec.ts b/src/modules/user/tests/unit/core/publish-message-when-user-is-deleted.domain-event-handler.spec.ts new file mode 100644 index 0000000..c4add5d --- /dev/null +++ b/src/modules/user/tests/unit/core/publish-message-when-user-is-deleted.domain-event-handler.spec.ts @@ -0,0 +1,54 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens'; +import { UserCreatedDomainEvent } from '@modules/user/core/domain/events/user-created.domain-events'; +import { PublishMessageWhenUserIsDeletedDomainEventHandler } from '@modules/user/core/application/event-handlers/publish-message-when-user-is-deleted.domain-event-handler'; + +const mockMessagePublisher = { + publish: jest.fn().mockImplementation(), +}; + +describe('Publish message when user is deleted domain event handler', () => { + let publishMessageWhenUserIsDeletedDomainEventHandler: PublishMessageWhenUserIsDeletedDomainEventHandler; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: USER_MESSAGE_PUBLISHER, + useValue: mockMessagePublisher, + }, + PublishMessageWhenUserIsDeletedDomainEventHandler, + ], + }).compile(); + + publishMessageWhenUserIsDeletedDomainEventHandler = + module.get( + PublishMessageWhenUserIsDeletedDomainEventHandler, + ); + }); + + it('should publish a message', () => { + jest.spyOn(mockMessagePublisher, 'publish'); + const userDeletedDomainEvent: UserCreatedDomainEvent = { + id: 'some-domain-event-id', + aggregateId: 'some-aggregate-id', + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@email.com', + phone: '+33611223344', + metadata: { + timestamp: new Date('2023-06-28T05:00:00Z').getTime(), + correlationId: 'some-correlation-id', + }, + }; + publishMessageWhenUserIsDeletedDomainEventHandler.handle( + userDeletedDomainEvent, + ); + expect(publishMessageWhenUserIsDeletedDomainEventHandler).toBeDefined(); + expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1); + expect(mockMessagePublisher.publish).toHaveBeenCalledWith( + 'user.deleted', + '{"id":"some-domain-event-id","aggregateId":"some-aggregate-id","firstName":"John","lastName":"Doe","email":"john.doe@email.com","phone":"+33611223344","metadata":{"timestamp":1687928400000,"correlationId":"some-correlation-id"}}', + ); + }); +}); diff --git a/src/modules/user/tests/unit/core/publish-message-when-user-is-updated.domain-event-handler.spec.ts b/src/modules/user/tests/unit/core/publish-message-when-user-is-updated.domain-event-handler.spec.ts new file mode 100644 index 0000000..af36963 --- /dev/null +++ b/src/modules/user/tests/unit/core/publish-message-when-user-is-updated.domain-event-handler.spec.ts @@ -0,0 +1,54 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { USER_MESSAGE_PUBLISHER } from '@modules/user/user.di-tokens'; +import { PublishMessageWhenUserIsUpdatedDomainEventHandler } from '@modules/user/core/application/event-handlers/publish-message-when-user-is-updated.domain-event-handler'; +import { UserUpdatedDomainEvent } from '@modules/user/core/domain/events/user-updated.domain-events'; + +const mockMessagePublisher = { + publish: jest.fn().mockImplementation(), +}; + +describe('Publish message when user is updated domain event handler', () => { + let publishMessageWhenUserIsUpdatedDomainEventHandler: PublishMessageWhenUserIsUpdatedDomainEventHandler; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: USER_MESSAGE_PUBLISHER, + useValue: mockMessagePublisher, + }, + PublishMessageWhenUserIsUpdatedDomainEventHandler, + ], + }).compile(); + + publishMessageWhenUserIsUpdatedDomainEventHandler = + module.get( + PublishMessageWhenUserIsUpdatedDomainEventHandler, + ); + }); + + it('should publish a message', () => { + jest.spyOn(mockMessagePublisher, 'publish'); + const userUpdatedDomainEvent: UserUpdatedDomainEvent = { + id: 'some-domain-event-id', + aggregateId: 'some-aggregate-id', + firstName: 'Jane', + lastName: 'Doe', + email: 'jane.doe@email.com', + phone: '+33611223344', + metadata: { + timestamp: new Date('2023-06-28T05:00:00Z').getTime(), + correlationId: 'some-correlation-id', + }, + }; + publishMessageWhenUserIsUpdatedDomainEventHandler.handle( + userUpdatedDomainEvent, + ); + expect(publishMessageWhenUserIsUpdatedDomainEventHandler).toBeDefined(); + expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1); + expect(mockMessagePublisher.publish).toHaveBeenCalledWith( + 'user.updated', + '{"id":"some-domain-event-id","aggregateId":"some-aggregate-id","firstName":"Jane","lastName":"Doe","email":"jane.doe@email.com","phone":"+33611223344","metadata":{"timestamp":1687928400000,"correlationId":"some-correlation-id"}}', + ); + }); +}); diff --git a/src/modules/user/tests/unit/core/user.entity.spec.ts b/src/modules/user/tests/unit/core/user.entity.spec.ts index 32aa437..6a3d471 100644 --- a/src/modules/user/tests/unit/core/user.entity.spec.ts +++ b/src/modules/user/tests/unit/core/user.entity.spec.ts @@ -1,5 +1,11 @@ +import { UserCreatedDomainEvent } from '@modules/user/core/domain/events/user-created.domain-events'; +import { UserDeletedDomainEvent } from '@modules/user/core/domain/events/user-deleted.domain-event'; +import { UserUpdatedDomainEvent } from '@modules/user/core/domain/events/user-updated.domain-events'; import { UserEntity } from '@modules/user/core/domain/user.entity'; -import { CreateUserProps } from '@modules/user/core/domain/user.types'; +import { + CreateUserProps, + UpdateUserProps, +} from '@modules/user/core/domain/user.types'; const createUserProps: CreateUserProps = { firstName: 'John', @@ -8,10 +14,41 @@ const createUserProps: CreateUserProps = { phone: '+33611223344', }; +const updateUserProps: UpdateUserProps = { + firstName: 'Jane', + lastName: 'Dane', + email: 'jane.dane@email.com', +}; + describe('User entity create', () => { it('should create a new user entity', async () => { const userEntity: UserEntity = UserEntity.create(createUserProps); expect(userEntity.id.length).toBe(36); expect(userEntity.getProps().email).toBe('john.doe@email.com'); + expect(userEntity.domainEvents.length).toBe(1); + expect(userEntity.domainEvents[0]).toBeInstanceOf(UserCreatedDomainEvent); + }); +}); + +describe('User entity update', () => { + it('should update a user entity', async () => { + const userEntity: UserEntity = UserEntity.create(createUserProps); + userEntity.update(updateUserProps); + expect(userEntity.getProps().firstName).toBe('Jane'); + expect(userEntity.getProps().lastName).toBe('Dane'); + expect(userEntity.getProps().email).toBe('jane.dane@email.com'); + // 2 events because UserEntity.create sends a UserCreatedDomainEvent + expect(userEntity.domainEvents.length).toBe(2); + expect(userEntity.domainEvents[1]).toBeInstanceOf(UserUpdatedDomainEvent); + }); +}); + +describe('User entity delete', () => { + it('should delete a user entity', async () => { + const userEntity: UserEntity = UserEntity.create(createUserProps); + userEntity.delete(); + // 2 events because UserEntity.create sends a UserCreatedDomainEvent + expect(userEntity.domainEvents.length).toBe(2); + expect(userEntity.domainEvents[1]).toBeInstanceOf(UserDeletedDomainEvent); }); }); diff --git a/src/modules/user/tests/unit/interface/delete-user.grpc.controller.spec.ts b/src/modules/user/tests/unit/interface/delete-user.grpc.controller.spec.ts new file mode 100644 index 0000000..ebdff5c --- /dev/null +++ b/src/modules/user/tests/unit/interface/delete-user.grpc.controller.spec.ts @@ -0,0 +1,99 @@ +import { + DatabaseErrorException, + NotFoundException, +} from '@mobicoop/ddd-library'; +import { RpcExceptionCode } from '@mobicoop/ddd-library'; +import { DeleteUserGrpcController } from '@modules/user/interface/grpc-controllers/delete-user.grpc.controller'; +import { DeleteUserRequestDto } from '@modules/user/interface/grpc-controllers/dtos/delete-user.request.dto'; +import { CommandBus } from '@nestjs/cqrs'; +import { RpcException } from '@nestjs/microservices'; +import { Test, TestingModule } from '@nestjs/testing'; + +const deleteUserRequest: DeleteUserRequestDto = { + id: '78153e03-4861-4f58-a705-88526efee53b', +}; + +const mockCommandBus = { + execute: jest + .fn() + .mockImplementationOnce(() => ({})) + .mockImplementationOnce(() => { + throw new NotFoundException(); + }) + .mockImplementationOnce(() => { + throw new DatabaseErrorException(); + }) + .mockImplementationOnce(() => { + throw new Error(); + }), +}; + +describe('Delete User Grpc Controller', () => { + let deleteUserGrpcController: DeleteUserGrpcController; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: CommandBus, + useValue: mockCommandBus, + }, + DeleteUserGrpcController, + ], + }).compile(); + + deleteUserGrpcController = module.get( + DeleteUserGrpcController, + ); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(deleteUserGrpcController).toBeDefined(); + }); + + it('should delete a user', async () => { + jest.spyOn(mockCommandBus, 'execute'); + await deleteUserGrpcController.delete(deleteUserRequest); + expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); + }); + + it('should throw a dedicated RpcException if user does not exist', async () => { + jest.spyOn(mockCommandBus, 'execute'); + expect.assertions(3); + try { + await deleteUserGrpcController.delete(deleteUserRequest); + } catch (e: any) { + expect(e).toBeInstanceOf(RpcException); + expect(e.error.code).toBe(RpcExceptionCode.NOT_FOUND); + } + expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); + }); + + it('should throw a dedicated RpcException if a database error occurs', async () => { + jest.spyOn(mockCommandBus, 'execute'); + expect.assertions(3); + try { + await deleteUserGrpcController.delete(deleteUserRequest); + } catch (e: any) { + expect(e).toBeInstanceOf(RpcException); + expect(e.error.code).toBe(RpcExceptionCode.INTERNAL); + } + expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); + }); + + it('should throw a generic RpcException', async () => { + jest.spyOn(mockCommandBus, 'execute'); + expect.assertions(3); + try { + await deleteUserGrpcController.delete(deleteUserRequest); + } catch (e: any) { + expect(e).toBeInstanceOf(RpcException); + expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN); + } + expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); + }); +});