import { describe, it, expect, vi, beforeEach } from 'vitest'; const { mockFind, mockFindOne, mockFindOneAndUpdate, mockSave, mockCountDocuments, mockAggregate } = vi.hoisted(() => ({ mockFind: vi.fn(), mockFindOne: vi.fn(), mockFindOneAndUpdate: vi.fn(), mockSave: vi.fn(), mockCountDocuments: vi.fn(), mockAggregate: vi.fn(), })); vi.mock('../../schemas/cabinet-item.schema.js', () => { const chain = () => ({ sort: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), lean: vi.fn().mockReturnThis(), exec: mockFind, }); const findOneChain = () => ({ lean: vi.fn().mockReturnThis(), exec: mockFindOne, }); const updateChain = () => ({ exec: mockFindOneAndUpdate, }); const countChain = () => ({ exec: mockCountDocuments, }); const aggChain = () => ({ exec: mockAggregate, }); class FakeModel { data: unknown; constructor(data: unknown) { this.data = data; } save = mockSave; toObject() { return this.data; } static find = vi.fn(() => chain()); static findOne = vi.fn(() => findOneChain()); static findOneAndUpdate = vi.fn(() => updateChain()); static countDocuments = vi.fn(() => countChain()); static aggregate = vi.fn(() => aggChain()); } return { CabinetItemModel: FakeModel }; }); import { CabinetRepository } from './cabinet.repository.js'; describe(CabinetRepository.name, () => { let repo: CabinetRepository; beforeEach(() => { vi.clearAllMocks(); repo = new CabinetRepository(); }); describe('findByHousehold', () => { it('returns paginated items', async () => { const items = [{ _id: 'ci-1', quantity: 30 }]; mockFind.mockResolvedValue(items); const result = await repo.findByHousehold('hh1', { limit: 20 }); expect(result.data).toEqual(items); expect(result.pagination.hasMore).toBe(false); }); it('handles cursor-based pagination', async () => { const items = [{ _id: 'ci-2', quantity: 10 }]; mockFind.mockResolvedValue(items); const cursor = Buffer.from('ci-1').toString('base64'); const result = await repo.findByHousehold('hh1', { cursor, limit: 20 }); expect(result.data).toEqual(items); expect(result.pagination.hasMore).toBe(false); }); it('sets hasMore when more items exist', async () => { const items = Array.from({ length: 3 }, (_, i) => ({ _id: `ci-${i}`, quantity: i })); mockFind.mockResolvedValue(items); const result = await repo.findByHousehold('hh1', { limit: 2 }); expect(result.data).toHaveLength(2); expect(result.pagination.hasMore).toBe(true); expect(result.pagination.cursor).toBeTruthy(); }); it('returns null cursor when no data', async () => { mockFind.mockResolvedValue([]); const result = await repo.findByHousehold('hh1', { limit: 20 }); expect(result.pagination.cursor).toBeNull(); }); it('filters by medicineId', async () => { mockFind.mockResolvedValue([]); await repo.findByHousehold('hh1', { medicineId: 'med-1', limit: 20 }); expect(mockFind).toHaveBeenCalled(); }); it('filters by status', async () => { mockFind.mockResolvedValue([]); await repo.findByHousehold('hh1', { status: 'active' as never, limit: 20, }); expect(mockFind).toHaveBeenCalled(); }); it('filters by expiringWithin', async () => { mockFind.mockResolvedValue([]); await repo.findByHousehold('hh1', { expiringWithin: 30, limit: 20 }); expect(mockFind).toHaveBeenCalled(); }); }); describe('findById', () => { it('returns item by id and householdId', async () => { const item = { _id: 'ci-1', householdId: 'hh1', quantity: 30 }; mockFindOne.mockResolvedValue(item); const result = await repo.findById('ci-1', 'hh1'); expect(result).toEqual(item); }); }); describe('getAggregateSummary', () => { it('returns aggregate data', async () => { const aggregated = [{ _id: 'med-1', totalQuantity: 60, itemCount: 2 }]; mockAggregate.mockResolvedValue(aggregated); const result = await repo.getAggregateSummary('hh1'); expect(result).toEqual(aggregated); }); }); describe('create', () => { it('creates and returns cabinet item', async () => { const data = { medicineId: 'med-1', medicineName: 'Metformin', medicineStrength: 500, medicineStrengthUnit: 'mg', medicineForm: 'tablet', quantity: 30, unit: 'tablet' as const, }; mockSave.mockImplementation(function (this: { toObject: () => unknown }) { return Promise.resolve(this); }); const result = await repo.create(data, 'hh1', 'user-1'); expect(result).toBeTruthy(); expect(mockSave).toHaveBeenCalled(); }); }); describe('update', () => { it('updates and returns item', async () => { const updated = { _id: 'ci-1', quantity: 25 }; mockFindOneAndUpdate.mockResolvedValue(updated); const result = await repo.update('ci-1', 'hh1', { quantity: 25 }); expect(result).toEqual(updated); }); }); describe('adjustQuantity', () => { it('adjusts quantity and returns updated item', async () => { mockFindOne.mockResolvedValue({ _id: 'ci-1', quantity: 30, status: 'active' }); const updated = { _id: 'ci-1', quantity: 27, status: 'active' }; mockFindOneAndUpdate.mockResolvedValue(updated); const result = await repo.adjustQuantity('ci-1', 'hh1', -3); expect(result).toEqual(updated); }); it('floors quantity at 0 and sets depleted status', async () => { mockFindOne.mockResolvedValue({ _id: 'ci-1', quantity: 2, status: 'active' }); const updated = { _id: 'ci-1', quantity: 0, status: 'depleted' }; mockFindOneAndUpdate.mockResolvedValue(updated); const result = await repo.adjustQuantity('ci-1', 'hh1', -5); expect(result).toEqual(updated); }); it('re-activates depleted item when adding stock', async () => { mockFindOne.mockResolvedValue({ _id: 'ci-1', quantity: 0, status: 'depleted' }); const updated = { _id: 'ci-1', quantity: 10, status: 'active' }; mockFindOneAndUpdate.mockResolvedValue(updated); const result = await repo.adjustQuantity('ci-1', 'hh1', 10); expect(result).toEqual(updated); }); it('returns null if item not found', async () => { mockFindOne.mockResolvedValue(null); const result = await repo.adjustQuantity('ci-missing', 'hh1', 5); expect(result).toBeNull(); }); }); describe('findExpiringSoon', () => { it('returns items expiring within N days', async () => { const items = [{ _id: 'ci-1', expirationDate: new Date() }]; mockFind.mockResolvedValue(items); const result = await repo.findExpiringSoon('hh1', 30); expect(result).toEqual(items); }); }); describe('countByMedicineId', () => { it('returns count', async () => { mockCountDocuments.mockResolvedValue(3); const result = await repo.countByMedicineId('med-1'); expect(result).toBe(3); }); }); describe('softDelete', () => { it('soft deletes and returns item', async () => { const deleted = { _id: 'ci-1', isDeleted: true }; mockFindOneAndUpdate.mockResolvedValue(deleted); const result = await repo.softDelete('ci-1', 'hh1'); expect(result).toEqual(deleted); }); }); });