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 { CustomersRepository } from './customers.repository'; describe('CustomersRepository', () => { let repository: CustomersRepository; 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: 'cu-1', code: 'CUST_01', name: 'Acme Corp', phone: '+6281234567890', address: 'Jl Sudirman No 1', latitude: -6.2, longitude: 106.8, nfcId: 'NFC-001', status: 'draft', createdAt: 1_700_000_000_000, updatedAt: 1_700_000_000_000, createdBy: 'user-1', updatedBy: 'user-1', }; const contactRow = { id: 'ct-1', customerId: 'cu-1', name: 'Jean Luc', jobTitle: 'Buyer', phone: '+6281234567891', mobilePhone: null, notes: 'Primary', }; const createInput = { code: 'CUST_01', name: 'Acme Corp', phone: PhoneNumber.create('+6281234567890'), address: 'Jl Sudirman No 1', 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: [CustomersRepository, { provide: DRIZZLE, useValue: db }], }).compile(); repository = moduleRef.get(CustomersRepository); }); it('findById maps a row and contacts to domain', async () => { limit.mockResolvedValueOnce([row]); orderBy.mockResolvedValueOnce([contactRow]); const customer = await repository.findById('cu-1'); expect(customer).toMatchObject({ id: 'cu-1', code: 'CUST_01', createdBy: 'user-1', }); expect(customer?.phone.value).toBe('+6281234567890'); expect(customer?.status.value).toBe('draft'); expect(customer?.contacts[0].name).toBe('Jean Luc'); expect(customer?.contacts[0].phone?.value).toBe('+6281234567891'); }); it('findById returns null when missing', async () => { limit.mockResolvedValueOnce([]); await expect(repository.findById('missing')).resolves.toBeNull(); }); it('list returns mapped rows and total without loading contacts', 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: 'Acme', code: 'CUST', search: 'sudirman', limit: 10, offset: 0, }); expect(result.total).toBe(1); expect(result.data[0].code).toBe('CUST_01'); expect(result.data[0].contacts).toEqual([]); }); it('create inserts and maps unique violations', async () => { returning.mockResolvedValue([row]); orderBy.mockResolvedValueOnce([]); const created = await repository.create(createInput); expect(created.code).toBe('CUST_01'); transaction.mockRejectedValueOnce({ code: '23505' }); await expect(repository.create(createInput)).rejects.toBeInstanceOf( ConflictException, ); transaction.mockRejectedValueOnce({ code: '23505', constraint: 'customers_nfc_id_unique', }); await expect(repository.create(createInput)).rejects.toMatchObject({ message: 'Customer NFC ID already exists', }); transaction.mockRejectedValueOnce({ cause: { code: '23505', constraint_name: 'customers_code_unique', }, }); await expect(repository.create(createInput)).rejects.toMatchObject({ message: 'Customer code already exists', }); }); it('create rethrows unknown errors', async () => { transaction.mockRejectedValue(new Error('db down')); await expect(repository.create(createInput)).rejects.toThrow('db down'); }); it('createMany returns 0 for an empty batch', async () => { await expect(repository.createMany([])).resolves.toBe(0); }); it('update throws when missing', async () => { limit.mockResolvedValueOnce([]); await expect( repository.update('missing', { userId: 'user-1' }), ).rejects.toBeInstanceOf(NotFoundException); }); 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('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('addContact throws when customer is missing', async () => { limit.mockResolvedValueOnce([]); await expect( repository.addContact('missing', { name: 'Ada' }, 'user-1'), ).rejects.toBeInstanceOf(NotFoundException); }); it('deleteContact throws when contact is missing', async () => { returning.mockResolvedValueOnce([]); await expect( repository.deleteContact('cu-1', 'ct-1'), ).rejects.toBeInstanceOf(NotFoundException); }); it('extendListQuery is a passthrough hook', () => { const qb = { join: true }; expect(repository.extendListQuery(qb, { limit: 10, offset: 0 })).toBe(qb); }); });