import { describe, it, expect, vi, beforeEach } from 'vitest'; const { mockFind, mockFindOne, mockFindOneAndUpdate, mockFindOneAndDelete, mockSave, mockAggregate, mockUpdateMany, mockFindByIdAndUpdate, } = vi.hoisted(() => ({ mockFind: vi.fn(), mockFindOne: vi.fn(), mockFindOneAndUpdate: vi.fn(), mockFindOneAndDelete: vi.fn(), mockSave: vi.fn(), mockAggregate: vi.fn(), mockUpdateMany: vi.fn(), mockFindByIdAndUpdate: vi.fn(), })); vi.mock('../../schemas/pantry-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 = () => ({ lean: vi.fn().mockReturnThis(), exec: mockFindOneAndUpdate, }); const deleteChain = () => ({ exec: mockFindOneAndDelete, }); const updateByIdChain = () => ({ exec: mockFindByIdAndUpdate, }); const updateManyChain = () => ({ exec: mockUpdateMany, }); const aggChain = () => ({ exec: mockAggregate, }); class FakeModel { data: unknown; constructor(data: unknown) { this.data = data; } save() { mockSave(this.data); return Promise.resolve({ toObject: () => this.data }); } static find = vi.fn(() => chain()); static findOne = vi.fn(() => findOneChain()); static findOneAndUpdate = vi.fn(() => updateChain()); static findOneAndDelete = vi.fn(() => deleteChain()); static findByIdAndUpdate = vi.fn(() => updateByIdChain()); static updateMany = vi.fn(() => updateManyChain()); static aggregate = vi.fn(() => aggChain()); } return { PantryItemModel: FakeModel }; }); import { PantryRepository } from './pantry.repository.js'; describe(PantryRepository.name, () => { let repo: PantryRepository; beforeEach(() => { vi.clearAllMocks(); repo = new PantryRepository(); }); describe('findByHousehold', () => { it('returns paginated results', async () => { const items = [{ _id: { toString: () => 'id1' } }]; mockFind.mockResolvedValue(items); const result = await repo.findByHousehold('hh1', { limit: 20 }); expect(result.data).toHaveLength(1); expect(result.pagination.hasMore).toBe(false); }); it('handles hasMore', async () => { const items = Array.from({ length: 3 }, (_, i) => ({ _id: { toString: () => `id${i}` }, })); mockFind.mockResolvedValue(items); const result = await repo.findByHousehold('hh1', { limit: 2 }); expect(result.data).toHaveLength(2); expect(result.pagination.hasMore).toBe(true); }); it('applies storageLocation filter', async () => { mockFind.mockResolvedValue([]); await repo.findByHousehold('hh1', { storageLocation: 'fridge', limit: 20 }); expect(mockFind).toHaveBeenCalled(); }); it('applies status filter with single value', async () => { mockFind.mockResolvedValue([]); await repo.findByHousehold('hh1', { status: 'sealed', limit: 20 }); expect(mockFind).toHaveBeenCalled(); }); it('applies status filter with multiple values', async () => { mockFind.mockResolvedValue([]); await repo.findByHousehold('hh1', { status: 'sealed,opened', limit: 20 }); expect(mockFind).toHaveBeenCalled(); }); it('applies urgency filter', async () => { mockFind.mockResolvedValue([]); await repo.findByHousehold('hh1', { urgency: 'urgent,check', limit: 20 }); expect(mockFind).toHaveBeenCalled(); }); it('applies single urgency filter', async () => { mockFind.mockResolvedValue([]); await repo.findByHousehold('hh1', { urgency: 'urgent', limit: 20 }); expect(mockFind).toHaveBeenCalled(); }); it('applies productId filter', async () => { mockFind.mockResolvedValue([]); await repo.findByHousehold('hh1', { productId: 'p1', limit: 20 }); expect(mockFind).toHaveBeenCalled(); }); it('applies cursor', async () => { const cursor = Buffer.from('abc').toString('base64'); mockFind.mockResolvedValue([]); await repo.findByHousehold('hh1', { cursor, limit: 20 }); expect(mockFind).toHaveBeenCalled(); }); }); describe('findById', () => { it('returns item', async () => { mockFindOne.mockResolvedValue({ _id: 'id1' }); const result = await repo.findById('id1', 'hh1'); expect(result).toEqual({ _id: 'id1' }); }); }); describe('findExpiringSoon', () => { it('returns items expiring within days', async () => { mockFind.mockResolvedValue([]); const result = await repo.findExpiringSoon('hh1', 7); expect(result.pagination.hasMore).toBe(false); }); it('supports cursor', async () => { const cursor = Buffer.from('abc').toString('base64'); mockFind.mockResolvedValue([]); const result = await repo.findExpiringSoon('hh1', 7, cursor, 20); expect(result.pagination.hasMore).toBe(false); }); }); describe('findActiveByHousehold', () => { it('returns active items', async () => { mockFind.mockResolvedValue([{ _id: 'id1' }]); const result = await repo.findActiveByHousehold('hh1'); expect(result).toHaveLength(1); }); }); describe('create', () => { it('saves and returns item', async () => { const data = { name: 'test' }; const result = await repo.create(data); expect(mockSave).toHaveBeenCalled(); expect(result).toEqual(data); }); }); describe('update', () => { it('updates and returns item', async () => { mockFindOneAndUpdate.mockResolvedValue({ _id: 'id1' }); const result = await repo.update('id1', 'hh1', { quantity: 3 }); expect(result).toEqual({ _id: 'id1' }); }); }); describe('updateFreshness', () => { it('updates freshness estimate', async () => { mockFindByIdAndUpdate.mockResolvedValue(undefined); await repo.updateFreshness('id1', { urgency: 'fresh' }); expect(mockFindByIdAndUpdate).toHaveBeenCalled(); }); it('updates freshness and status', async () => { mockFindByIdAndUpdate.mockResolvedValue(undefined); await repo.updateFreshness('id1', { urgency: 'expired' }, 'expired'); expect(mockFindByIdAndUpdate).toHaveBeenCalled(); }); }); describe('delete', () => { it('deletes item', async () => { mockFindOneAndDelete.mockResolvedValue({ _id: 'id1' }); await repo.delete('id1', 'hh1'); expect(mockFindOneAndDelete).toHaveBeenCalled(); }); }); describe('getWasteStats', () => { it('returns aggregation result', async () => { mockAggregate.mockResolvedValue([{ totalConsumed: 5, totalDiscarded: 2 }]); const result = await repo.getWasteStats('hh1', new Date(), new Date()); expect(result).toHaveLength(1); }); }); describe('getTopWastedProducts', () => { it('returns top wasted products', async () => { mockAggregate.mockResolvedValue([{ productId: 'p1', productName: 'Milk', count: 3 }]); const result = await repo.getTopWastedProducts('hh1', new Date(), new Date()); expect(result).toHaveLength(1); }); }); describe('findByIds', () => { it('returns items by ids', async () => { mockFind.mockResolvedValue([{ _id: 'id1' }]); const result = await repo.findByIds(['id1'], 'hh1'); expect(result).toHaveLength(1); }); }); describe('bulkUpdateStatus', () => { it('returns modified count', async () => { mockUpdateMany.mockResolvedValue({ modifiedCount: 2 }); const result = await repo.bulkUpdateStatus(['id1', 'id2'], 'hh1', 'consumed' as never); expect(result).toBe(2); }); }); });