import { ConflictException, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { PhoneNumber } from '../../../common/value-objects/phone-number/phone-number'; import { Status } from '../../../common/value-objects/status/status'; import { DRIZZLE } from '../../../database/database.module'; import { EmployeesRepository } from './employees.repository'; describe('EmployeesRepository', () => { let repository: EmployeesRepository; const limit = jest.fn(); const orderBy = jest.fn(); const offset = jest.fn(); const where = jest.fn(); const from = jest.fn(); const select = jest.fn(); const returning = jest.fn(); const values = jest.fn(); const insert = jest.fn(); const set = jest.fn(); const update = jest.fn(); const del = jest.fn(); const transaction = jest.fn(); const $dynamic = jest.fn(); const db = { select, insert, update, delete: del, transaction, }; const row = { id: 'emp-1', code: 'EMP_01', name: 'Ada Lovelace', phone: '+6281234567890', position: 'sales', status: 'draft', createdAt: 1_700_000_000_000, updatedAt: 1_700_000_000_000, createdBy: 'user-1', updatedBy: 'user-1', userId: null, }; const createInput = { code: 'EMP_01', name: 'Ada Lovelace', phone: PhoneNumber.create('+6281234567890'), position: 'sales' as const, userId: 'user-1', }; beforeEach(async () => { jest.clearAllMocks(); where.mockImplementation(() => Object.assign(Promise.resolve([{ id: 'user-1', username: 'admin' }]), { limit, orderBy, returning, }), ); orderBy.mockImplementation(() => ({ limit })); limit.mockImplementation(() => ({ offset })); offset.mockResolvedValue([row]); from.mockImplementation(() => ({ where, $dynamic, })); $dynamic.mockReturnValue({ where }); select.mockImplementation(() => ({ from })); values.mockReturnValue({ returning }); insert.mockReturnValue({ values }); set.mockReturnValue({ where }); update.mockReturnValue({ set }); del.mockReturnValue({ where }); returning.mockResolvedValue([row]); transaction.mockImplementation((fn: (tx: typeof db) => unknown) => fn(db)); const moduleRef: TestingModule = await Test.createTestingModule({ providers: [EmployeesRepository, { provide: DRIZZLE, useValue: db }], }).compile(); repository = moduleRef.get(EmployeesRepository); }); it('findById maps a row to domain Employee', async () => { limit.mockResolvedValueOnce([row]); const employee = await repository.findById('emp-1'); expect(employee).toMatchObject({ id: 'emp-1', code: 'EMP_01', name: 'Ada Lovelace', position: 'sales', createdBy: 'user-1', }); expect(employee?.phone.value).toBe('+6281234567890'); expect(employee?.status.value).toBe('draft'); expect(employee?.createdAt.value).toBe(1_700_000_000_000); }); it('findById returns null when missing', async () => { limit.mockResolvedValueOnce([]); await expect(repository.findById('missing')).resolves.toBeNull(); }); it('findByCode maps a row', async () => { limit.mockResolvedValueOnce([row]); const employee = await repository.findByCode('EMP_01'); expect(employee?.code).toBe('EMP_01'); }); it('findByUserId maps a row', async () => { limit.mockResolvedValueOnce([row]); const employee = await repository.findByUserId('user-1'); expect(employee?.id).toBe('emp-1'); }); it('list returns mapped rows and total', async () => { select .mockImplementationOnce(() => ({ from: () => ({ where: () => Promise.resolve([{ total: 1 }]), }), })) .mockImplementationOnce(() => ({ from: () => ({ $dynamic: () => ({ where: () => ({ orderBy: () => ({ limit: () => ({ offset: () => Promise.resolve([row]), }), }), }), }), }), })); const result = await repository.list({ name: 'Ada', code: 'EMP', phone: '+628', position: ['sales', 'driver'], status: 'draft', search: 'ada', limit: 10, offset: 0, }); expect(result.total).toBe(1); expect(result.data[0].code).toBe('EMP_01'); expect(result.data[0].phone.value).toBe('+6281234567890'); }); it('create inserts and maps unique violations', async () => { returning.mockResolvedValueOnce([row]); const created = await repository.create(createInput); expect(created.code).toBe('EMP_01'); returning.mockRejectedValueOnce({ code: '23505' }); await expect(repository.create(createInput)).rejects.toBeInstanceOf( ConflictException, ); returning.mockRejectedValueOnce({ cause: { code: '23505' }, }); await expect(repository.create(createInput)).rejects.toBeInstanceOf( ConflictException, ); }); it('create rethrows unknown errors', async () => { returning.mockRejectedValue(new Error('db down')); await expect(repository.create(createInput)).rejects.toThrow('db down'); }); it('createMany returns 0 for an empty batch and inserts otherwise', async () => { await expect(repository.createMany([])).resolves.toBe(0); transaction.mockImplementation( async (fn: (tx: typeof db) => Promise) => { await fn(db); }, ); await expect(repository.createMany([createInput])).resolves.toBe(1); }); it('createMany maps unique violations', async () => { transaction.mockRejectedValue({ code: '23505' }); await expect(repository.createMany([createInput])).rejects.toBeInstanceOf( ConflictException, ); }); it('update throws when missing and maps unique violations', async () => { limit.mockResolvedValueOnce([]); await expect( repository.update('missing', { userId: 'user-1' }), ).rejects.toBeInstanceOf(NotFoundException); limit.mockResolvedValueOnce([row]); returning.mockRejectedValueOnce({ code: '23505' }); await expect( repository.update('emp-1', { code: 'EMP_02', userId: 'user-1' }), ).rejects.toBeInstanceOf(ConflictException); }); it('updateStatus throws when missing', async () => { returning.mockResolvedValueOnce([]); await expect( repository.updateStatus('missing', Status.create('active'), 'user-1'), ).rejects.toBeInstanceOf(NotFoundException); }); it('delete throws when missing', async () => { returning.mockResolvedValueOnce([]); await expect(repository.delete('missing')).rejects.toBeInstanceOf( NotFoundException, ); }); it('delete maps foreign-key violations to ConflictException', async () => { returning.mockRejectedValueOnce({ code: '23503' }); await expect(repository.delete('emp-1')).rejects.toMatchObject({ constructor: ConflictException, message: 'Employee is referenced by other records', }); returning.mockRejectedValueOnce({ cause: { code: '23503' }, }); await expect(repository.delete('emp-1')).rejects.toBeInstanceOf( ConflictException, ); }); it('delete rethrows unknown errors', async () => { returning.mockRejectedValueOnce(new Error('db down')); await expect(repository.delete('emp-1')).rejects.toThrow('db down'); }); it('bulkUpdateStatus and bulkDelete return 0 for empty ids', async () => { await expect( repository.bulkUpdateStatus([], Status.create('active'), 'user-1'), ).resolves.toBe(0); await expect(repository.bulkDelete([])).resolves.toBe(0); }); it('bulkUpdateStatus and bulkDelete return affected counts', async () => { returning.mockResolvedValue([{ id: 'emp-1' }, { id: 'emp-2' }]); await expect( repository.bulkUpdateStatus( ['emp-1', 'emp-2'], Status.create('active'), 'user-1', ), ).resolves.toBe(2); returning.mockResolvedValue([{ id: 'emp-1' }]); await expect(repository.bulkDelete(['emp-1'])).resolves.toBe(1); }); it('bulkDelete maps foreign-key violations to ConflictException', async () => { returning.mockRejectedValueOnce({ cause: { code: '23503' }, }); await expect(repository.bulkDelete(['emp-1'])).rejects.toMatchObject({ constructor: ConflictException, message: 'Employee is referenced by other records', }); }); it('extendListQuery is a passthrough hook', () => { const qb = { join: true }; expect(repository.extendListQuery(qb, { limit: 10, offset: 0 })).toBe(qb); }); });