From d5cf17e734caea0d0dd92c932bf7a0255de92b3a Mon Sep 17 00:00:00 2001 From: sbriat Date: Fri, 27 Jan 2023 16:34:06 +0100 Subject: [PATCH] improve tests --- .../primaries/authentication.controller.ts | 2 +- .../authentication.repository.spec.ts | 2 +- .../integration/username.repository.spec.ts | 2 +- .../authentication-messager.usecase.spec.ts | 41 +++ .../unit/logging-messager.usecase.spec.ts | 36 +++ .../validate-authentication.usecase.spec.ts | 2 +- .../secondaries/prisma-repository.abstract.ts | 2 +- ...baseException.ts => database.exception.ts} | 0 .../tests/unit/prisma-repository.spec.ts | 281 ++++++++++++++---- .../unit/rpc-validation-pipe.usecase.spec.ts | 22 ++ 10 files changed, 320 insertions(+), 70 deletions(-) create mode 100644 src/modules/authentication/tests/unit/authentication-messager.usecase.spec.ts create mode 100644 src/modules/authentication/tests/unit/logging-messager.usecase.spec.ts rename src/modules/database/src/exceptions/{DatabaseException.ts => database.exception.ts} (100%) create mode 100644 src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts diff --git a/src/modules/authentication/adapters/primaries/authentication.controller.ts b/src/modules/authentication/adapters/primaries/authentication.controller.ts index 5fc1901..3fb7032 100644 --- a/src/modules/authentication/adapters/primaries/authentication.controller.ts +++ b/src/modules/authentication/adapters/primaries/authentication.controller.ts @@ -3,7 +3,7 @@ import { InjectMapper } from '@automapper/nestjs'; import { Controller, UsePipes } from '@nestjs/common'; import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; -import { DatabaseException } from 'src/modules/database/src/exceptions/DatabaseException'; +import { DatabaseException } from 'src/modules/database/src/exceptions/database.exception'; import { AddUsernameCommand } from '../../commands/add-username.command'; import { CreateAuthenticationCommand } from '../../commands/create-authentication.command'; import { DeleteAuthenticationCommand } from '../../commands/delete-authentication.command'; diff --git a/src/modules/authentication/tests/integration/authentication.repository.spec.ts b/src/modules/authentication/tests/integration/authentication.repository.spec.ts index ffd78ce..f0443db 100644 --- a/src/modules/authentication/tests/integration/authentication.repository.spec.ts +++ b/src/modules/authentication/tests/integration/authentication.repository.spec.ts @@ -1,7 +1,7 @@ import { TestingModule, Test } from '@nestjs/testing'; import { DatabaseModule } from '../../../database/database.module'; import { PrismaService } from '../../../database/src/adapters/secondaries/prisma-service'; -import { DatabaseException } from '../../../database/src/exceptions/DatabaseException'; +import { DatabaseException } from '../../../database/src/exceptions/database.exception'; import { AuthenticationRepository } from '../../adapters/secondaries/authentication.repository'; import { v4 } from 'uuid'; import * as bcrypt from 'bcrypt'; diff --git a/src/modules/authentication/tests/integration/username.repository.spec.ts b/src/modules/authentication/tests/integration/username.repository.spec.ts index 20a2303..2d13008 100644 --- a/src/modules/authentication/tests/integration/username.repository.spec.ts +++ b/src/modules/authentication/tests/integration/username.repository.spec.ts @@ -1,7 +1,7 @@ import { TestingModule, Test } from '@nestjs/testing'; import { DatabaseModule } from '../../../database/database.module'; import { PrismaService } from '../../../database/src/adapters/secondaries/prisma-service'; -import { DatabaseException } from '../../../database/src/exceptions/DatabaseException'; +import { DatabaseException } from '../../../database/src/exceptions/database.exception'; import { v4 } from 'uuid'; import { Type } from '../../domain/dtos/type.enum'; import { UsernameRepository } from '../../adapters/secondaries/username.repository'; diff --git a/src/modules/authentication/tests/unit/authentication-messager.usecase.spec.ts b/src/modules/authentication/tests/unit/authentication-messager.usecase.spec.ts new file mode 100644 index 0000000..01f28a7 --- /dev/null +++ b/src/modules/authentication/tests/unit/authentication-messager.usecase.spec.ts @@ -0,0 +1,41 @@ +import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthenticationMessager } from '../../adapters/secondaries/authentication.messager'; + +const mockAmqpConnection = { + publish: jest.fn().mockImplementation(), +}; + +describe('AuthenticationMessager', () => { + let authenticationMessager: AuthenticationMessager; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + AuthenticationMessager, + { + provide: AmqpConnection, + useValue: mockAmqpConnection, + }, + ], + }).compile(); + + authenticationMessager = module.get( + AuthenticationMessager, + ); + }); + + it('should be defined', () => { + expect(authenticationMessager).toBeDefined(); + }); + + it('should publish a message', async () => { + jest.spyOn(mockAmqpConnection, 'publish'); + await authenticationMessager.publish( + 'authentication.create.info', + 'my-test', + ); + expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/modules/authentication/tests/unit/logging-messager.usecase.spec.ts b/src/modules/authentication/tests/unit/logging-messager.usecase.spec.ts new file mode 100644 index 0000000..8836fc7 --- /dev/null +++ b/src/modules/authentication/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('authentication.create.info', 'my-test'); + expect(mockAmqpConnection.publish).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/modules/authentication/tests/unit/validate-authentication.usecase.spec.ts b/src/modules/authentication/tests/unit/validate-authentication.usecase.spec.ts index ebe8f92..0c7e418 100644 --- a/src/modules/authentication/tests/unit/validate-authentication.usecase.spec.ts +++ b/src/modules/authentication/tests/unit/validate-authentication.usecase.spec.ts @@ -9,7 +9,7 @@ import { ValidateAuthenticationQuery } from '../../queries/validate-authenticati import { UsernameRepository } from '../../adapters/secondaries/username.repository'; import { Type } from '../../domain/dtos/type.enum'; import { NotFoundException, UnauthorizedException } from '@nestjs/common'; -import { DatabaseException } from '../../../database/src/exceptions/DatabaseException'; +import { DatabaseException } from '../../../database/src/exceptions/database.exception'; import { ValidateAuthenticationRequest } from '../../domain/dtos/validate-authentication.request'; const mockAuthenticationRepository = { 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 125d7a9..8d80045 100644 --- a/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts +++ b/src/modules/database/src/adapters/secondaries/prisma-repository.abstract.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; -import { DatabaseException } from '../../exceptions/DatabaseException'; +import { DatabaseException } from '../../exceptions/database.exception'; import { ICollection } from '../../interfaces/collection.interface'; import { IRepository } from '../../interfaces/repository.interface'; import { PrismaService } from './prisma-service'; diff --git a/src/modules/database/src/exceptions/DatabaseException.ts b/src/modules/database/src/exceptions/database.exception.ts similarity index 100% rename from src/modules/database/src/exceptions/DatabaseException.ts rename to src/modules/database/src/exceptions/database.exception.ts diff --git a/src/modules/database/tests/unit/prisma-repository.spec.ts b/src/modules/database/tests/unit/prisma-repository.spec.ts index 9374cf1..3595eeb 100644 --- a/src/modules/database/tests/unit/prisma-repository.spec.ts +++ b/src/modules/database/tests/unit/prisma-repository.spec.ts @@ -2,7 +2,8 @@ import { Injectable } from '@nestjs/common'; 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/DatabaseException'; +import { DatabaseException } from '../../src/exceptions/database.exception'; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime'; class FakeEntity { uuid?: string; @@ -33,7 +34,7 @@ const fakeEntityCreated: FakeEntity = { uuid: 'some-uuid', }; -let fakeEntities: FakeEntity[] = []; +const fakeEntities: FakeEntity[] = []; Array.from({ length: 10 }).forEach(() => { fakeEntities.push(createRandomEntity()); }); @@ -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,60 +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; + }); - deleteMany: jest.fn().mockImplementation((params: any) => { - const foundEntities = fakeEntities.filter((entity) => - entity.name.startsWith(params?.where?.name), - ); + 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; + }); - if (foundEntities.length == 0) { - throw new Error(); - } + return Promise.resolve(entity); + }), - fakeEntities = fakeEntities.filter( - (entity) => !entity.name.startsWith(params?.where?.name), - ); - }), + 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(); + } + }), }, }; @@ -194,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'), @@ -209,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, { @@ -223,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; @@ -246,28 +397,28 @@ 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); - }); - }); - describe('deleteMany', () => { - it('should delete many entities', async () => { - await fakeRepository.deleteMany({ name: 'name' }); - const nbEntities = fakeEntities.filter((entity) => - entity.name.startsWith('name'), - ).length; - expect(nbEntities).toBe(0); + it('should throw a DatabaseException for client error', async () => { + await expect( + fakeRepository.deleteMany({ uuid: 'fake-uuid' }), + ).rejects.toBeInstanceOf(DatabaseException); + }); + + 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({ name: 'fake-name' }), + fakeRepository.deleteMany({ uuid: 'fake-uuid' }), ).rejects.toBeInstanceOf(DatabaseException); }); }); 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..b02a9b7 --- /dev/null +++ b/src/utils/tests/unit/rpc-validation-pipe.usecase.spec.ts @@ -0,0 +1,22 @@ +import { ArgumentMetadata } from '@nestjs/common'; +import { ValidateAuthenticationRequest } from '../../../modules/authentication/domain/dtos/validate-authentication.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: ValidateAuthenticationRequest, + data: '', + }; + await target + .transform({}, metadata) + .catch((err) => { + expect(err.message).toEqual('Rpc Exception'); + }); + }); +});