import { describe, it, expect, vi, beforeEach } from 'vitest'; import { CabinetService } from './cabinet.service.js'; describe(CabinetService.name, () => { const mockCabinetRepo = { findByHousehold: vi.fn(), findById: vi.fn(), getAggregateSummary: vi.fn(), create: vi.fn(), update: vi.fn(), adjustQuantity: vi.fn(), findExpiringSoon: vi.fn(), softDelete: vi.fn(), countByMedicineId: vi.fn(), }; const mockMedicinesRepo = { findById: vi.fn(), findByHousehold: vi.fn(), findDuplicate: vi.fn(), create: vi.fn(), update: vi.fn(), softDelete: vi.fn(), }; const mockProductsRepo = { findById: vi.fn(), findByMedicine: vi.fn(), create: vi.fn(), update: vi.fn(), softDelete: vi.fn(), countByMedicineId: vi.fn(), }; let service: CabinetService; beforeEach(() => { vi.clearAllMocks(); service = new CabinetService({ cabinetRepository: mockCabinetRepo as never, medicinesRepository: mockMedicinesRepo as never, medicineProductsRepository: mockProductsRepo as never, }); }); describe('list', () => { it('delegates to repository', async () => { const result = { data: [], pagination: { cursor: null, hasMore: false } }; mockCabinetRepo.findByHousehold.mockResolvedValue(result); const response = await service.list('hh1', { limit: 20 }); expect(response).toEqual(result); expect(mockCabinetRepo.findByHousehold).toHaveBeenCalledWith('hh1', { limit: 20 }); }); }); describe('getById', () => { it('returns item when found', async () => { const item = { _id: 'ci-1', quantity: 30 }; mockCabinetRepo.findById.mockResolvedValue(item); const result = await service.getById('ci-1', 'hh1'); expect(result).toEqual(item); }); it('throws NotFoundError when not found', async () => { mockCabinetRepo.findById.mockResolvedValue(null); await expect(service.getById('ci-missing', 'hh1')).rejects.toThrow('Cabinet item not found'); }); }); describe('getSummary', () => { it('returns aggregated summary with formatted dates', async () => { const expiryDate = new Date('2026-06-01T00:00:00.000Z'); mockCabinetRepo.getAggregateSummary.mockResolvedValue([ { _id: 'med-1', medicineName: 'Metformin', medicineStrength: 500, medicineStrengthUnit: 'mg', medicineForm: 'tablet', totalQuantity: 60, unit: 'tablet', earliestExpiry: expiryDate, itemCount: 2, }, ]); const result = await service.getSummary('hh1'); expect(result).toHaveLength(1); expect(result[0].medicineId).toBe('med-1'); expect(result[0].earliestExpiry).toBe('2026-06-01T00:00:00.000Z'); }); it('handles null expiry dates', async () => { mockCabinetRepo.getAggregateSummary.mockResolvedValue([ { _id: 'med-1', medicineName: 'Test', medicineStrength: 10, medicineStrengthUnit: 'mg', medicineForm: 'tablet', totalQuantity: 30, unit: 'tablet', earliestExpiry: null, itemCount: 1, }, ]); const result = await service.getSummary('hh1'); expect(result[0].earliestExpiry).toBeNull(); }); }); describe('addItem', () => { const createInput = { medicineId: 'med-1', quantity: 30, unit: 'tablet' as const, }; it('creates item with denormalized medicine fields', async () => { mockMedicinesRepo.findById.mockResolvedValue({ _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet', }); const created = { _id: 'ci-1', ...createInput, medicineName: 'Metformin' }; mockCabinetRepo.create.mockResolvedValue(created); const result = await service.addItem(createInput, 'hh1', 'user-1'); expect(result).toEqual(created); expect(mockCabinetRepo.create).toHaveBeenCalledWith( expect.objectContaining({ medicineName: 'Metformin', medicineStrength: 500, medicineStrengthUnit: 'mg', medicineForm: 'tablet', }), 'hh1', 'user-1', ); }); it('throws NotFoundError when medicine not found', async () => { mockMedicinesRepo.findById.mockResolvedValue(null); await expect(service.addItem(createInput, 'hh1', 'user-1')).rejects.toThrow( 'Medicine not found', ); }); it('denormalizes product brand when medicineProductId given', async () => { mockMedicinesRepo.findById.mockResolvedValue({ _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet', }); mockProductsRepo.findById.mockResolvedValue({ _id: 'prod-1', brand: 'Glucophage', }); mockCabinetRepo.create.mockResolvedValue({ _id: 'ci-1' }); await service.addItem({ ...createInput, medicineProductId: 'prod-1' }, 'hh1', 'user-1'); expect(mockCabinetRepo.create).toHaveBeenCalledWith( expect.objectContaining({ medicineProductBrand: 'Glucophage' }), 'hh1', 'user-1', ); }); it('throws NotFoundError when product not found', async () => { mockMedicinesRepo.findById.mockResolvedValue({ _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet', }); mockProductsRepo.findById.mockResolvedValue(null); await expect( service.addItem({ ...createInput, medicineProductId: 'prod-missing' }, 'hh1', 'user-1'), ).rejects.toThrow('Medicine product not found'); }); }); describe('update', () => { it('updates and returns item', async () => { mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1' }); const updated = { _id: 'ci-1', quantity: 25 }; mockCabinetRepo.update.mockResolvedValue(updated); const result = await service.update('ci-1', 'hh1', { quantity: 25 }); expect(result).toEqual(updated); }); it('throws NotFoundError when item does not exist', async () => { mockCabinetRepo.findById.mockResolvedValue(null); await expect(service.update('ci-missing', 'hh1', { quantity: 25 })).rejects.toThrow( 'Cabinet item not found', ); }); it('throws NotFoundError when update returns null', async () => { mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1' }); mockCabinetRepo.update.mockResolvedValue(null); await expect(service.update('ci-1', 'hh1', { quantity: 25 })).rejects.toThrow( 'Cabinet item not found', ); }); }); describe('adjustQuantity', () => { it('adjusts quantity and returns item', async () => { mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 30 }); const updated = { _id: 'ci-1', quantity: 27 }; mockCabinetRepo.adjustQuantity.mockResolvedValue(updated); const result = await service.adjustQuantity('ci-1', 'hh1', -3); expect(result).toEqual(updated); }); it('throws BadRequestError when delta is 0', async () => { await expect(service.adjustQuantity('ci-1', 'hh1', 0)).rejects.toThrow( 'Delta must be non-zero', ); }); it('throws NotFoundError when item does not exist', async () => { mockCabinetRepo.findById.mockResolvedValue(null); await expect(service.adjustQuantity('ci-missing', 'hh1', 5)).rejects.toThrow( 'Cabinet item not found', ); }); it('throws NotFoundError when adjust returns null', async () => { mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1' }); mockCabinetRepo.adjustQuantity.mockResolvedValue(null); await expect(service.adjustQuantity('ci-1', 'hh1', 5)).rejects.toThrow( 'Cabinet item not found', ); }); }); describe('getExpiringSoon', () => { it('delegates to repository', async () => { const items = [{ _id: 'ci-1' }]; mockCabinetRepo.findExpiringSoon.mockResolvedValue(items); const result = await service.getExpiringSoon('hh1', 30); expect(result).toEqual(items); expect(mockCabinetRepo.findExpiringSoon).toHaveBeenCalledWith('hh1', 30); }); }); describe('delete', () => { it('soft deletes item', async () => { mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1' }); mockCabinetRepo.softDelete.mockResolvedValue({ _id: 'ci-1', isDeleted: true }); const result = await service.delete('ci-1', 'hh1'); expect(result.isDeleted).toBe(true); }); it('throws NotFoundError when item does not exist', async () => { mockCabinetRepo.findById.mockResolvedValue(null); await expect(service.delete('ci-missing', 'hh1')).rejects.toThrow('Cabinet item not found'); }); it('throws NotFoundError when softDelete returns null', async () => { mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1' }); mockCabinetRepo.softDelete.mockResolvedValue(null); await expect(service.delete('ci-1', 'hh1')).rejects.toThrow('Cabinet item not found'); }); }); });