Merge branch 'improveTests' into 'main'

improve tests

See merge request v3/services/user!13
This commit is contained in:
Sylvain Briat 2023-01-27 15:54:29 +00:00
commit c11a316057
8 changed files with 339 additions and 43 deletions

View File

@ -74,6 +74,8 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
} }
} }
// TODO : using any is not good, but needed for nested entities
// TODO : Refactor for good clean architecture ?
async create(entity: Partial<T> | any, include?: any): Promise<T> { async create(entity: Partial<T> | any, include?: any): Promise<T> {
try { try {
const res = await this._prisma[this._model].create({ const res = await this._prisma[this._model].create({
@ -101,7 +103,6 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
where: { uuid }, where: { uuid },
data: entity, data: entity,
}); });
return updatedEntity; return updatedEntity;
} catch (e) { } catch (e) {
if (e instanceof PrismaClientKnownRequestError) { if (e instanceof PrismaClientKnownRequestError) {
@ -147,6 +148,27 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
const entity = await this._prisma[this._model].delete({ const entity = await this._prisma[this._model].delete({
where: { uuid }, 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<void> {
try {
const entity = await this._prisma[this._model].deleteMany({
where: where,
});
return entity; return entity;
} catch (e) { } catch (e) {
if (e instanceof PrismaClientKnownRequestError) { if (e instanceof PrismaClientKnownRequestError) {

View File

@ -13,4 +13,5 @@ export interface IRepository<T> {
update(uuid: string, entity: Partial<T>, include?: any): Promise<T>; update(uuid: string, entity: Partial<T>, include?: any): Promise<T>;
updateWhere(where: any, entity: Partial<T> | any, include?: any): Promise<T>; updateWhere(where: any, entity: Partial<T> | any, include?: any): Promise<T>;
delete(uuid: string): Promise<T>; delete(uuid: string): Promise<T>;
deleteMany(where: any): Promise<void>;
} }

View File

@ -3,6 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from '../../src/adapters/secondaries/prisma-service'; import { PrismaService } from '../../src/adapters/secondaries/prisma-service';
import { PrismaRepository } from '../../src/adapters/secondaries/prisma-repository.abstract'; import { PrismaRepository } from '../../src/adapters/secondaries/prisma-repository.abstract';
import { DatabaseException } from '../../src/exceptions/database.exception'; import { DatabaseException } from '../../src/exceptions/database.exception';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime';
class FakeEntity { class FakeEntity {
uuid?: string; uuid?: string;
@ -57,7 +58,20 @@ const mockPrismaService = {
return Promise.resolve([fakeEntities, fakeEntities.length]); return Promise.resolve([fakeEntities, fakeEntities.length]);
}), }),
fake: { 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) => { findMany: jest.fn().mockImplementation((params?: any) => {
if (params?.where?.limit == 1) { if (params?.where?.limit == 1) {
@ -77,22 +91,66 @@ 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'); throw new Error('no entity');
} }
return entity; return entity;
}), }),
findFirst: jest.fn().mockImplementation((params?: any) => { findFirst: jest
.fn()
.mockImplementationOnce((params?: any) => {
if (params?.where?.name) { if (params?.where?.name) {
return Promise.resolve( return Promise.resolve(
fakeEntities.find((entity) => entity.name === params?.where?.name), 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');
}), }),
update: jest.fn().mockImplementation((params: any) => { 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( const entity = fakeEntities.find(
(entity) => entity.uuid === params.where.uuid, (entity) => entity.uuid === params.where.uuid,
); );
@ -103,7 +161,40 @@ const mockPrismaService = {
return Promise.resolve(entity); return Promise.resolve(entity);
}), }),
delete: jest.fn().mockImplementation((params: any) => { 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; let found = false;
fakeEntities.forEach((entity, index) => { fakeEntities.forEach((entity, index) => {
@ -180,14 +271,32 @@ describe('PrismaRepository', () => {
expect(newEntity).toBe(fakeEntityCreated); expect(newEntity).toBe(fakeEntityCreated);
expect(prisma.fake.create).toHaveBeenCalledTimes(1); expect(prisma.fake.create).toHaveBeenCalledTimes(1);
}); });
it('should throw a DatabaseException for client error', async () => {
await expect(
fakeRepository.create(fakeEntityToCreate),
).rejects.toBeInstanceOf(DatabaseException);
}); });
describe('findOne', () => { it('should throw a DatabaseException if uuid is not found', async () => {
await expect(
fakeRepository.create(fakeEntityToCreate),
).rejects.toBeInstanceOf(DatabaseException);
});
});
describe('findOneByUuid', () => {
it('should find an entity by uuid', async () => { it('should find an entity by uuid', async () => {
const entity = await fakeRepository.findOneByUuid(fakeEntities[0].uuid); const entity = await fakeRepository.findOneByUuid(fakeEntities[0].uuid);
expect(entity).toBe(fakeEntities[0]); 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 () => { it('should throw a DatabaseException if uuid is not found', async () => {
await expect( await expect(
fakeRepository.findOneByUuid('wrong-uuid'), 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', () => { 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'; const newName = 'random-name';
await fakeRepository.update(fakeEntities[0].uuid, { await fakeRepository.update(fakeEntities[0].uuid, {
@ -209,10 +365,19 @@ describe('PrismaRepository', () => {
await expect( await expect(
fakeRepository.update('fake-uuid', { name: 'error' }), fakeRepository.update('fake-uuid', { name: 'error' }),
).rejects.toBeInstanceOf(DatabaseException); ).rejects.toBeInstanceOf(DatabaseException);
await expect(
fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }),
).rejects.toBeInstanceOf(DatabaseException);
}); });
}); });
describe('delete', () => { 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 () => { it('should delete an entity', async () => {
const savedUuid = fakeEntities[0].uuid; const savedUuid = fakeEntities[0].uuid;
@ -232,13 +397,29 @@ describe('PrismaRepository', () => {
}); });
}); });
describe('findOne', () => { describe('deleteMany', () => {
it('should find one entity', async () => { it('should throw a DatabaseException for client error', async () => {
const entity = await fakeRepository.findOne({ await expect(
name: fakeEntities[0].name, 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);
}); });
}); });
}); });

View File

@ -16,7 +16,7 @@ import { FindAllUsersQuery } from '../../queries/find-all-users.query';
import { FindUserByUuidQuery } from '../../queries/find-user-by-uuid.query'; import { FindUserByUuidQuery } from '../../queries/find-user-by-uuid.query';
import { UserPresenter } from './user.presenter'; import { UserPresenter } from './user.presenter';
import { ICollection } from '../../../database/src/interfaces/collection.interface'; import { ICollection } from '../../../database/src/interfaces/collection.interface';
import { RpcValidationPipe } from '../../../../utils/rpc.validation-pipe'; import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
@UsePipes( @UsePipes(
new RpcValidationPipe({ new RpcValidationPipe({

View File

@ -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>(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);
});
});

View File

@ -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>(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);
});
});

View File

@ -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(<UpdateUserRequest>{}, metadata).catch((err) => {
expect(err.message).toEqual('Rpc Exception');
});
});
});