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/database.exception'; import { Prisma } from '@prisma/client'; class FakeEntity { uuid?: string; name: string; } let entityId = 2; const entityUuid = 'uuid-'; const entityName = 'name-'; const createRandomEntity = (): FakeEntity => { const entity: FakeEntity = { uuid: `${entityUuid}${entityId}`, name: `${entityName}${entityId}`, }; entityId++; return entity; }; const fakeEntityToCreate: FakeEntity = { name: 'test', }; const fakeEntityCreated: FakeEntity = { ...fakeEntityToCreate, uuid: 'some-uuid', }; const fakeEntities: FakeEntity[] = []; Array.from({ length: 10 }).forEach(() => { fakeEntities.push(createRandomEntity()); }); @Injectable() class FakePrismaRepository extends PrismaRepository { protected _model = 'fake'; } class FakePrismaService extends PrismaService { fake: any; } const mockPrismaService = { $transaction: jest.fn().mockImplementation(async (data: any) => { const entities = await data[0]; if (entities.length == 1) { return Promise.resolve([[fakeEntityCreated], 1]); } return Promise.resolve([fakeEntities, fakeEntities.length]); }), // eslint-disable-next-line @typescript-eslint/no-unused-vars $queryRawUnsafe: jest.fn().mockImplementation((query?: string) => { return Promise.resolve(fakeEntities); }), $executeRawUnsafe: jest .fn() .mockResolvedValueOnce(fakeEntityCreated) // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((fields: object) => { throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); }) // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((fields: object) => { throw new Error('an unknown error'); }) .mockResolvedValueOnce(fakeEntityCreated) // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((fields: object) => { throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); }) // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((fields: object) => { throw new Error('an unknown error'); }), $queryRaw: jest .fn() .mockImplementationOnce(() => { throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); }) .mockImplementationOnce(() => { return true; }) .mockImplementation(() => { throw new Prisma.PrismaClientKnownRequestError('Database unavailable', { code: 'code', clientVersion: 'version', }); }), fake: { create: jest .fn() .mockResolvedValueOnce(fakeEntityCreated) // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((params?: any) => { throw new Prisma.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) { return Promise.resolve([fakeEntityCreated]); } return Promise.resolve(fakeEntities); }), count: jest.fn().mockResolvedValue(fakeEntities.length), findUnique: jest.fn().mockImplementation(async (params?: any) => { let entity; if (params?.where?.uuid) { entity = fakeEntities.find( (entity) => entity.uuid === params?.where?.uuid, ); } if (!entity && params?.where?.uuid == 'unknown') { throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); } else if (!entity) { throw new Error('no entity'); } return entity; }), 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 Prisma.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() // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((params?: any) => { throw new Prisma.PrismaClientKnownRequestError('unknown request', { code: 'code', clientVersion: 'version', }); }) // eslint-disable-next-line @typescript-eslint/no-unused-vars .mockImplementationOnce((params?: any) => { throw new Prisma.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 Prisma.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 Prisma.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(); } }), }, }; describe('PrismaRepository', () => { let fakeRepository: FakePrismaRepository; let prisma: FakePrismaService; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ FakePrismaRepository, { provide: PrismaService, useValue: mockPrismaService, }, ], }).compile(); fakeRepository = module.get(FakePrismaRepository); prisma = module.get(PrismaService) as FakePrismaService; }); it('should be defined', () => { expect(fakeRepository).toBeDefined(); expect(prisma).toBeDefined(); }); describe('findAll', () => { it('should return an array of entities', async () => { jest.spyOn(prisma.fake, 'findMany'); jest.spyOn(prisma.fake, 'count'); jest.spyOn(prisma, '$transaction'); const entities = await fakeRepository.findAll(); expect(entities).toStrictEqual({ data: fakeEntities, total: fakeEntities.length, }); }); it('should return an array containing only one entity', async () => { const entities = await fakeRepository.findAll(1, 10, { limit: 1 }); expect(prisma.fake.findMany).toHaveBeenCalledWith({ skip: 0, take: 10, where: { limit: 1 }, }); expect(entities).toEqual({ data: [fakeEntityCreated], total: 1, }); }); }); describe('create', () => { it('should create an entity', async () => { jest.spyOn(prisma.fake, 'create'); const newEntity = await fakeRepository.create(fakeEntityToCreate); 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('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'), ).rejects.toBeInstanceOf(DatabaseException); }); }); 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 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, { name: newName, }); expect(fakeEntities[0].name).toBe(newName); }); it("should throw an exception if an entity doesn't exist", async () => { 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; // eslint-disable-next-line @typescript-eslint/no-unused-vars const res = await fakeRepository.delete(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.delete('fake-uuid')).rejects.toBeInstanceOf( DatabaseException, ); }); }); describe('deleteMany', () => { 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({ uuid: 'fake-uuid' }), ).rejects.toBeInstanceOf(DatabaseException); }); }); describe('findAllByquery', () => { it('should return an array of entities', async () => { const entities = await fakeRepository.findAllByQuery( ['uuid', 'name'], ['name is not null'], ); expect(entities).toStrictEqual({ data: fakeEntities, total: fakeEntities.length, }); }); }); describe('createWithFields', () => { it('should create an entity', async () => { jest.spyOn(prisma, '$queryRawUnsafe'); const newEntity = await fakeRepository.createWithFields({ uuid: '804319b3-a09b-4491-9f82-7976bfce0aff', name: 'my-name', }); expect(newEntity).toBe(fakeEntityCreated); expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1); }); it('should throw a DatabaseException for client error', async () => { await expect( fakeRepository.createWithFields({ uuid: '804319b3-a09b-4491-9f82-7976bfce0aff', name: 'my-name', }), ).rejects.toBeInstanceOf(DatabaseException); }); it('should throw a DatabaseException if uuid is not found', async () => { await expect( fakeRepository.createWithFields({ name: 'my-name', }), ).rejects.toBeInstanceOf(DatabaseException); }); }); describe('updateWithFields', () => { it('should update an entity', async () => { jest.spyOn(prisma, '$queryRawUnsafe'); const updatedEntity = await fakeRepository.updateWithFields( '804319b3-a09b-4491-9f82-7976bfce0aff', { name: 'my-name', }, ); expect(updatedEntity).toBe(fakeEntityCreated); expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1); }); it('should throw a DatabaseException for client error', async () => { await expect( fakeRepository.updateWithFields( '804319b3-a09b-4491-9f82-7976bfce0aff', { name: 'my-name', }, ), ).rejects.toBeInstanceOf(DatabaseException); }); it('should throw a DatabaseException if uuid is not found', async () => { await expect( fakeRepository.updateWithFields( '804319b3-a09b-4491-9f82-7976bfce0aff', { name: 'my-name', }, ), ).rejects.toBeInstanceOf(DatabaseException); }); }); describe('healthCheck', () => { it('should throw a DatabaseException for client error', async () => { await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf( DatabaseException, ); }); it('should return a healthy result', async () => { const res = await fakeRepository.healthCheck(); expect(res).toBeTruthy(); }); it('should throw an exception if database is not available', async () => { await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf( DatabaseException, ); }); }); });