diff --git a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts b/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts index 0c7fbb2..b48caf8 100644 --- a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts +++ b/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts @@ -74,6 +74,8 @@ export abstract class PrismaRepository implements IRepository { } } + // TODO : using any is not good, but needed for nested entities + // TODO : Refactor for good clean architecture ? async create(entity: Partial | any, include?: any): Promise { try { const res = await this._prisma[this._model].create({ @@ -101,7 +103,6 @@ export abstract class PrismaRepository implements IRepository { where: { uuid }, data: entity, }); - return updatedEntity; } catch (e) { if (e instanceof PrismaClientKnownRequestError) { @@ -147,6 +148,27 @@ export abstract class PrismaRepository implements IRepository { const entity = await this._prisma[this._model].delete({ where: { uuid }, }); + + return entity; + } catch (e) { + if (e instanceof PrismaClientKnownRequestError) { + throw new DatabaseException( + PrismaClientKnownRequestError.name, + e.code, + e.message, + ); + } else { + throw new DatabaseException(); + } + } + } + + async deleteMany(where: any): Promise { + try { + const entity = await this._prisma[this._model].deleteMany({ + where: where, + }); + return entity; } catch (e) { if (e instanceof PrismaClientKnownRequestError) { diff --git a/src/modules/database/src/interfaces/repository.interface.ts b/src/modules/database/src/interfaces/repository.interface.ts index 9228712..8912545 100644 --- a/src/modules/database/src/interfaces/repository.interface.ts +++ b/src/modules/database/src/interfaces/repository.interface.ts @@ -13,4 +13,5 @@ export interface IRepository { update(uuid: string, entity: Partial, include?: any): Promise; updateWhere(where: any, entity: Partial | any, include?: any): Promise; delete(uuid: string): Promise; + deleteMany(where: any): Promise; } diff --git a/src/modules/database/tests/unit/prisma-repository.spec.ts b/src/modules/database/tests/unit/prisma-repository.spec.ts index 064ba6e..3595eeb 100644 --- a/src/modules/database/tests/unit/prisma-repository.spec.ts +++ b/src/modules/database/tests/unit/prisma-repository.spec.ts @@ -3,6 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PrismaService } from '../../src/adapters/secondaries/prisma-service'; import { PrismaRepository } from '../../src/adapters/secondaries/prisma-repository.abstract'; import { DatabaseException } from '../../src/exceptions/database.exception'; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; class FakeEntity { uuid?: string; @@ -57,7 +58,20 @@ const mockPrismaService = { return Promise.resolve([fakeEntities, fakeEntities.length]); }), fake: { - create: jest.fn().mockResolvedValue(fakeEntityCreated), + create: jest + .fn() + .mockResolvedValueOnce(fakeEntityCreated) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new Error('an unknown error'); + }), findMany: jest.fn().mockImplementation((params?: any) => { if (params?.where?.limit == 1) { @@ -77,46 +91,123 @@ const mockPrismaService = { ); } - if (!entity) { + if (!entity && params?.where?.uuid == 'unknown') { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + } else if (!entity) { throw new Error('no entity'); } return entity; }), - findFirst: jest.fn().mockImplementation((params?: any) => { - if (params?.where?.name) { - return Promise.resolve( - fakeEntities.find((entity) => entity.name === params?.where?.name), - ); - } - }), - - update: jest.fn().mockImplementation((params: any) => { - const entity = fakeEntities.find( - (entity) => entity.uuid === params.where.uuid, - ); - Object.entries(params.data).map(([key, value]) => { - entity[key] = value; - }); - - return Promise.resolve(entity); - }), - - delete: jest.fn().mockImplementation((params: any) => { - let found = false; - - fakeEntities.forEach((entity, index) => { - if (entity.uuid === params?.where?.uuid) { - found = true; - fakeEntities.splice(index, 1); + findFirst: jest + .fn() + .mockImplementationOnce((params?: any) => { + if (params?.where?.name) { + return Promise.resolve( + fakeEntities.find((entity) => entity.name === params?.where?.name), + ); } - }); + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new Error('an unknown error'); + }), - if (!found) { - throw new Error(); - } - }), + update: jest + .fn() + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + .mockImplementationOnce((params: any) => { + const entity = fakeEntities.find( + (entity) => entity.name === params.where.name, + ); + Object.entries(params.data).map(([key, value]) => { + entity[key] = value; + }); + + return Promise.resolve(entity); + }) + .mockImplementation((params: any) => { + const entity = fakeEntities.find( + (entity) => entity.uuid === params.where.uuid, + ); + Object.entries(params.data).map(([key, value]) => { + entity[key] = value; + }); + + return Promise.resolve(entity); + }), + + delete: jest + .fn() + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + .mockImplementation((params: any) => { + let found = false; + + fakeEntities.forEach((entity, index) => { + if (entity.uuid === params?.where?.uuid) { + found = true; + fakeEntities.splice(index, 1); + } + }); + + if (!found) { + throw new Error(); + } + }), + + deleteMany: jest + .fn() + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((params?: any) => { + throw new PrismaClientKnownRequestError('unknown request', { + code: 'code', + clientVersion: 'version', + }); + }) + .mockImplementation((params: any) => { + let found = false; + + fakeEntities.forEach((entity, index) => { + if (entity.uuid === params?.where?.uuid) { + found = true; + fakeEntities.splice(index, 1); + } + }); + + if (!found) { + throw new Error(); + } + }), }, }; @@ -180,14 +271,32 @@ describe('PrismaRepository', () => { expect(newEntity).toBe(fakeEntityCreated); expect(prisma.fake.create).toHaveBeenCalledTimes(1); }); + + it('should throw a DatabaseException for client error', async () => { + await expect( + fakeRepository.create(fakeEntityToCreate), + ).rejects.toBeInstanceOf(DatabaseException); + }); + + it('should throw a DatabaseException if uuid is not found', async () => { + await expect( + fakeRepository.create(fakeEntityToCreate), + ).rejects.toBeInstanceOf(DatabaseException); + }); }); - describe('findOne', () => { + describe('findOneByUuid', () => { it('should find an entity by uuid', async () => { const entity = await fakeRepository.findOneByUuid(fakeEntities[0].uuid); expect(entity).toBe(fakeEntities[0]); }); + it('should throw a DatabaseException for client error', async () => { + await expect( + fakeRepository.findOneByUuid('unknown'), + ).rejects.toBeInstanceOf(DatabaseException); + }); + it('should throw a DatabaseException if uuid is not found', async () => { await expect( fakeRepository.findOneByUuid('wrong-uuid'), @@ -195,8 +304,55 @@ describe('PrismaRepository', () => { }); }); + describe('findOne', () => { + it('should find one entity', async () => { + const entity = await fakeRepository.findOne({ + name: fakeEntities[0].name, + }); + + expect(entity.name).toBe(fakeEntities[0].name); + }); + + it('should throw a DatabaseException for client error', async () => { + await expect( + fakeRepository.findOne({ + name: fakeEntities[0].name, + }), + ).rejects.toBeInstanceOf(DatabaseException); + }); + + it('should throw a DatabaseException for unknown error', async () => { + await expect( + fakeRepository.findOne({ + name: fakeEntities[0].name, + }), + ).rejects.toBeInstanceOf(DatabaseException); + }); + }); + describe('update', () => { - it('should update an entity', async () => { + it('should throw a DatabaseException for client error', async () => { + await expect( + fakeRepository.update('fake-uuid', { name: 'error' }), + ).rejects.toBeInstanceOf(DatabaseException); + await expect( + fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }), + ).rejects.toBeInstanceOf(DatabaseException); + }); + + it('should update an entity with name', async () => { + const newName = 'new-random-name'; + + await fakeRepository.updateWhere( + { name: fakeEntities[0].name }, + { + name: newName, + }, + ); + expect(fakeEntities[0].name).toBe(newName); + }); + + it('should update an entity with uuid', async () => { const newName = 'random-name'; await fakeRepository.update(fakeEntities[0].uuid, { @@ -209,10 +365,19 @@ describe('PrismaRepository', () => { await expect( fakeRepository.update('fake-uuid', { name: 'error' }), ).rejects.toBeInstanceOf(DatabaseException); + await expect( + fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }), + ).rejects.toBeInstanceOf(DatabaseException); }); }); describe('delete', () => { + it('should throw a DatabaseException for client error', async () => { + await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf( + DatabaseException, + ); + }); + it('should delete an entity', async () => { const savedUuid = fakeEntities[0].uuid; @@ -232,13 +397,29 @@ describe('PrismaRepository', () => { }); }); - describe('findOne', () => { - it('should find one entity', async () => { - const entity = await fakeRepository.findOne({ - name: fakeEntities[0].name, - }); + describe('deleteMany', () => { + it('should throw a DatabaseException for client error', async () => { + await expect( + fakeRepository.deleteMany({ uuid: 'fake-uuid' }), + ).rejects.toBeInstanceOf(DatabaseException); + }); - expect(entity.name).toBe(fakeEntities[0].name); + it('should delete entities based on their uuid', async () => { + const savedUuid = fakeEntities[0].uuid; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const res = await fakeRepository.deleteMany({ uuid: savedUuid }); + + const deletedEntity = fakeEntities.find( + (entity) => entity.uuid === savedUuid, + ); + expect(deletedEntity).toBeUndefined(); + }); + + it("should throw an exception if an entity doesn't exist", async () => { + await expect( + fakeRepository.deleteMany({ uuid: 'fake-uuid' }), + ).rejects.toBeInstanceOf(DatabaseException); }); }); }); diff --git a/src/modules/users/adapters/primaries/users.controller.ts b/src/modules/users/adapters/primaries/users.controller.ts index 8b9f8c0..82b18a3 100644 --- a/src/modules/users/adapters/primaries/users.controller.ts +++ b/src/modules/users/adapters/primaries/users.controller.ts @@ -16,7 +16,7 @@ import { FindAllUsersQuery } from '../../queries/find-all-users.query'; import { FindUserByUuidQuery } from '../../queries/find-user-by-uuid.query'; import { UserPresenter } from './user.presenter'; import { ICollection } from '../../../database/src/interfaces/collection.interface'; -import { RpcValidationPipe } from '../../../../utils/rpc.validation-pipe'; +import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe'; @UsePipes( new RpcValidationPipe({ diff --git a/src/modules/users/tests/unit/logging.messager.usecase.spec.ts b/src/modules/users/tests/unit/logging.messager.usecase.spec.ts new file mode 100644 index 0000000..85082c8 --- /dev/null +++ b/src/modules/users/tests/unit/logging.messager.usecase.spec.ts @@ -0,0 +1,36 @@ +import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LoggingMessager } from '../../adapters/secondaries/logging.messager'; + +const mockAmqpConnection = { + publish: jest.fn().mockImplementation(), +}; + +describe('LoggingMessager', () => { + let loggingMessager: LoggingMessager; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + LoggingMessager, + { + provide: AmqpConnection, + useValue: mockAmqpConnection, + }, + ], + }).compile(); + + loggingMessager = module.get(LoggingMessager); + }); + + it('should be defined', () => { + expect(LoggingMessager).toBeDefined(); + }); + + it('should publish a message', async () => { + jest.spyOn(mockAmqpConnection, 'publish'); + await loggingMessager.publish('user.create.info', 'my-test'); + expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/modules/users/tests/unit/user.messager.usecase.spec.ts b/src/modules/users/tests/unit/user.messager.usecase.spec.ts new file mode 100644 index 0000000..0a0f11c --- /dev/null +++ b/src/modules/users/tests/unit/user.messager.usecase.spec.ts @@ -0,0 +1,36 @@ +import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; +import { Test, TestingModule } from '@nestjs/testing'; +import { UserMessager } from '../../adapters/secondaries/user.messager'; + +const mockAmqpConnection = { + publish: jest.fn().mockImplementation(), +}; + +describe('UserMessager', () => { + let userMessager: UserMessager; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + UserMessager, + { + provide: AmqpConnection, + useValue: mockAmqpConnection, + }, + ], + }).compile(); + + userMessager = module.get(UserMessager); + }); + + it('should be defined', () => { + expect(userMessager).toBeDefined(); + }); + + it('should publish a message', async () => { + jest.spyOn(mockAmqpConnection, 'publish'); + await userMessager.publish('user.create.info', 'my-test'); + expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/utils/rpc.validation-pipe.ts b/src/utils/pipes/rpc.validation-pipe.ts similarity index 100% rename from src/utils/rpc.validation-pipe.ts rename to src/utils/pipes/rpc.validation-pipe.ts diff --git a/src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts b/src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts new file mode 100644 index 0000000..2ab4551 --- /dev/null +++ b/src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts @@ -0,0 +1,20 @@ +import { ArgumentMetadata } from '@nestjs/common'; +import { UpdateUserRequest } from '../../../modules/users/domain/dtos/update-user.request'; +import { RpcValidationPipe } from '../../pipes/rpc.validation-pipe'; + +describe('RpcValidationPipe', () => { + it('should not validate request', async () => { + const target: RpcValidationPipe = new RpcValidationPipe({ + whitelist: true, + forbidUnknownValues: false, + }); + const metadata: ArgumentMetadata = { + type: 'body', + metatype: UpdateUserRequest, + data: '', + }; + await target.transform({}, metadata).catch((err) => { + expect(err.message).toEqual('Rpc Exception'); + }); + }); +});