import { describe, it, expect, vi, beforeEach } from 'vitest'; import { PricesRepository } from '../../../src/modules/prices/prices.repository.js'; const { mockSave, MockPriceRecordModel } = vi.hoisted(() => { const mockSave = vi.fn(); function MockModel(this: { save: typeof mockSave }, data: unknown) { Object.assign(this, data); this.save = mockSave; } Object.assign(MockModel, { findOne: vi.fn(), find: vi.fn(), findOneAndUpdate: vi.fn(), insertMany: vi.fn(), aggregate: vi.fn(), }); return { mockSave, MockPriceRecordModel: MockModel }; }); vi.mock('../../../src/schemas/price-record.schema.js', () => ({ PriceRecordModel: MockPriceRecordModel, })); const { PriceRecordModel } = await import('../../../src/schemas/price-record.schema.js'); function makeChain(result: unknown = null) { return { sort: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), lean: vi.fn().mockReturnThis(), exec: vi.fn().mockResolvedValue(result), }; } describe(PricesRepository.name, () => { let repo: PricesRepository; beforeEach(() => { vi.clearAllMocks(); repo = new PricesRepository(); }); describe('create', () => { it('saves and returns new document toObject', async () => { const data = { householdId: 'h1', productId: 'p1', productName: 'Apple', storeId: 's1', storeName: 'Store', price: 1, currency: 'USD', quantity: 1, unit: 'g', pricePerUnit: 1, date: new Date(), createdBy: 'u1' }; mockSave.mockResolvedValue({ toObject: () => ({ ...data, _id: 'id1' }) }); const result = await repo.create(data); expect(mockSave).toHaveBeenCalled(); expect(result._id).toBe('id1'); }); }); describe('createMany', () => { it('inserts multiple records and returns mapped toObjects', async () => { const inputs = [{ price: 1 }, { price: 2 }]; const returns = inputs.map((x, idx) => ({ ...x, _id: `id${idx}`, toObject: function() { return this; } })); vi.mocked(PriceRecordModel.insertMany).mockResolvedValue(returns as any); const result = await repo.createMany(inputs as any); expect(PriceRecordModel.insertMany).toHaveBeenCalledWith(inputs); expect(result).toHaveLength(2); expect(result[0]._id).toBe('id0'); }); }); describe('findByProduct', () => { it('applies complex filters and pagination cursor decoding/encoding', async () => { const baseFilter = { householdId: 'h1', productId: 'prod1' }; const startDate = new Date('2026-01-01').toISOString(); const endDate = new Date('2026-01-10').toISOString(); const cursorId = '507f1f77bcf86cd799439011'; const cursorStr = Buffer.from(cursorId).toString('base64'); const mockItems = [ { _id: '607f1f77bcf86cd799439012', price: 10 }, { _id: '607f1f77bcf86cd799439013', price: 12 } ]; const chain = makeChain(mockItems); vi.mocked(PriceRecordModel.find).mockReturnValue(chain as any); const result = await repo.findByProduct('h1', 'prod1', { storeId: 'st1', startDate, endDate, cursor: cursorStr, limit: 2 }); expect(PriceRecordModel.find).toHaveBeenCalledWith({ householdId: 'h1', productId: 'prod1', storeId: 'st1', date: { $gte: new Date(startDate), $lte: new Date(endDate), }, _id: { $lt: cursorId } }); expect(result.data).toHaveLength(2); expect(result.pagination.hasMore).toBe(false); }); it('correctly indicates hasMore and generates next base64 cursor', async () => { const mockItems = [ { _id: '607f1f77bcf86cd799439011', price: 10 }, { _id: '607f1f77bcf86cd799439012', price: 11 }, { _id: '607f1f77bcf86cd799439013', price: 12 } ]; const chain = makeChain(mockItems); vi.mocked(PriceRecordModel.find).mockReturnValue(chain as any); const result = await repo.findByProduct('h1', 'prod1', { limit: 2 }); expect(result.data).toHaveLength(2); expect(result.pagination.hasMore).toBe(true); expect(result.pagination.cursor).toBe(Buffer.from('607f1f77bcf86cd799439012').toString('base64')); }); }); describe('compareStores', () => { it('runs group/aggregate queries ordered by deviance', async () => { const mockAggResult = [ { _id: 's1', storeName: 'Cheap', latestPrice: 10, latestPricePerUnit: 1, currency: 'USD', date: new Date() } ]; vi.mocked(PriceRecordModel.aggregate).mockReturnValue({ exec: vi.fn().mockResolvedValue(mockAggResult) } as any); const result = await repo.compareStores('h1', 'p1'); expect(PriceRecordModel.aggregate).toHaveBeenCalled(); expect(result).toHaveLength(1); expect(result[0].storeId).toBe('s1'); expect(result[0].latestPricePerUnit).toBe(1); }); }); describe('getLatestForProduct', () => { it('queries latest pricing document ordered by date descending', async () => { const chain = makeChain({ _id: 'pr1' }); vi.mocked(PriceRecordModel.findOne).mockReturnValue(chain as any); await repo.getLatestForProduct('h1', 'p1', 's1'); expect(PriceRecordModel.findOne).toHaveBeenCalledWith({ householdId: 'h1', productId: 'p1', storeId: 's1' }); expect(chain.sort).toHaveBeenCalledWith({ date: -1 }); }); }); describe('getAnalytics', () => { it('executes Promise.all parallel pipeline aggregations for periods, buckets, categories, and inflation', async () => { const mockExec = vi.fn().mockResolvedValue([]); vi.mocked(PriceRecordModel.aggregate).mockReturnValue({ exec: mockExec } as any); await repo.getAnalytics('h1'); // 4 explicit pipeline calls should have fired in Promise.all + inflation alert expect(PriceRecordModel.aggregate).toHaveBeenCalledTimes(4); }); }); });