import { BadRequestException, 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 { BranchesRepository } from './branches.repository'; describe('BranchesRepository', () => { let repository: BranchesRepository; 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 leftJoin = jest.fn(); const db = { select, insert, update, delete: del, transaction, }; const row = { id: 'br-1', code: 'JKT_01', name: 'Jakarta Pusat', phone: '+6281234567890', address: 'Jl Sudirman No 1', latitude: -6.2, longitude: 106.8, workingDaysStart: 'monday', workingDaysEnd: 'friday', workingHoursStart: '08:00', workingHoursEnd: '17:00', nfcId: 'NFC-001', divisionId: 'div-1', status: 'draft', createdAt: 1_700_000_000_000, updatedAt: 1_700_000_000_000, createdBy: 'user-1', updatedBy: 'user-1', }; const joinedRow = { branch: row, division: { id: 'div-1', code: 'JKT', name: 'Jakarta' }, createdByUser: { id: 'user-1', username: 'admin' }, updatedByUser: { id: 'user-1', username: 'admin' }, }; const createInput = { code: 'JKT_01', name: 'Jakarta Pusat', phone: PhoneNumber.create('+6281234567890'), address: 'Jl Sudirman No 1', workingDaysStart: 'monday', workingDaysEnd: 'friday', workingHoursStart: '08:00', workingHoursEnd: '17:00', userId: 'user-1', }; const joinChain = () => { const chain: { leftJoin: jest.Mock; where: typeof where; $dynamic: typeof $dynamic; } = { leftJoin: jest.fn(), where, $dynamic, }; chain.leftJoin.mockReturnValue(chain); return chain; }; beforeEach(async () => { jest.clearAllMocks(); where.mockImplementation(() => ({ limit, orderBy, returning })); orderBy.mockImplementation(() => ({ limit })); limit.mockImplementation(() => ({ offset, then: ( resolve: (value: (typeof joinedRow)[]) => unknown, reject?: (reason: unknown) => unknown, ) => Promise.resolve([joinedRow]).then(resolve, reject), })); offset.mockResolvedValue([joinedRow]); from.mockImplementation(() => joinChain()); leftJoin.mockImplementation(() => joinChain()); $dynamic.mockReturnValue({ where }); select.mockImplementation(() => ({ from })); values.mockReturnValue({ returning }); insert.mockReturnValue({ values }); set.mockReturnValue({ where }); update.mockReturnValue({ set }); del.mockReturnValue({ where }); returning.mockResolvedValue([row]); const moduleRef: TestingModule = await Test.createTestingModule({ providers: [BranchesRepository, { provide: DRIZZLE, useValue: db }], }).compile(); repository = moduleRef.get(BranchesRepository); }); it('findById maps a row to domain Branch', async () => { const branch = await repository.findById('br-1'); expect(branch).toMatchObject({ id: 'br-1', code: 'JKT_01', name: 'Jakarta Pusat', createdBy: 'user-1', division: { id: 'div-1', code: 'JKT', name: 'Jakarta' }, createdByUser: { id: 'user-1', username: 'admin' }, updatedByUser: { id: 'user-1', username: 'admin' }, }); expect(branch?.phone.value).toBe('+6281234567890'); expect(branch?.status.value).toBe('draft'); expect(branch?.createdAt.value).toBe(1_700_000_000_000); }); it('findById returns null when missing', async () => { limit.mockImplementationOnce(() => Promise.resolve([])); await expect(repository.findById('missing')).resolves.toBeNull(); }); it('findByCode maps a row', async () => { const branch = await repository.findByCode('JKT_01'); expect(branch?.code).toBe('JKT_01'); }); it('list returns mapped rows and total', async () => { select .mockImplementationOnce(() => ({ from: () => ({ where: () => Promise.resolve([{ total: 1 }]), }), })) .mockImplementationOnce(() => ({ from: () => { const chain = joinChain(); return chain; }, })); const result = await repository.list({ name: 'Jakarta', code: 'JKT', phone: '+628', address: 'Sudirman', divisionId: 'div-1', nfcId: 'NFC-001', status: 'draft', workingDaysStart: 'monday', workingDaysEnd: 'friday', workingHoursStart: '08:00', workingHoursEnd: '17:00', search: 'sudirman', limit: 10, offset: 0, }); expect(result.total).toBe(1); expect(result.data[0].code).toBe('JKT_01'); expect(result.data[0].division).toEqual({ id: 'div-1', code: 'JKT', name: 'Jakarta', }); }); it('create inserts and maps unique violations', async () => { returning.mockResolvedValueOnce([row]); const created = await repository.create(createInput); expect(created.code).toBe('JKT_01'); expect(created.createdByUser).toEqual({ id: 'user-1', username: 'admin' }); returning.mockRejectedValueOnce({ code: '23505' }); await expect(repository.create(createInput)).rejects.toBeInstanceOf( ConflictException, ); returning.mockRejectedValueOnce({ code: '23505', constraint: 'branches_nfc_id_unique', }); await expect(repository.create(createInput)).rejects.toMatchObject({ message: 'Branch NFC ID already exists', }); }); it('create maps missing division foreign keys', async () => { returning.mockRejectedValueOnce({ code: '23503' }); await expect(repository.create(createInput)).rejects.toBeInstanceOf( BadRequestException, ); }); 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('updateStatus returns the mapped row', async () => { returning.mockResolvedValueOnce([row]); const updated = await repository.updateStatus( 'br-1', Status.create('active'), 'user-1', ); expect(updated.id).toBe('br-1'); expect(updated.updatedByUser).toEqual({ id: 'user-1', username: 'admin' }); }); it('update throws when missing', async () => { limit.mockImplementationOnce(() => Promise.resolve([])); await expect( repository.update('missing', { userId: 'user-1' }), ).rejects.toBeInstanceOf(NotFoundException); }); it('update maps a row when present', async () => { returning.mockResolvedValueOnce([row]); const updated = await repository.update('br-1', { name: 'Jakarta Selatan', userId: 'user-1', }); expect(updated.code).toBe('JKT_01'); }); 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('bulkUpdateStatus and bulkDelete return affected counts', async () => { returning.mockResolvedValue([{ id: 'br-1' }, { id: 'br-2' }]); await expect( repository.bulkUpdateStatus( ['br-1', 'br-2'], Status.create('active'), 'user-1', ), ).resolves.toBe(2); returning.mockResolvedValue([{ id: 'br-1' }]); await expect(repository.bulkDelete(['br-1'])).resolves.toBe(1); }); it('extendListQuery is a passthrough hook', () => { const qb = { join: true }; expect(repository.extendListQuery(qb, { limit: 10, offset: 0 })).toBe(qb); }); });