import { Types } from 'mongoose'; import { AppQueueService } from '../queue/app-queue.service'; import { CounterReconciliationService } from './counter-reconciliation.service'; import { MaintenanceController } from './maintenance.controller'; describe('CounterReconciliationService', () => { const postOne = new Types.ObjectId(); const postTwo = new Types.ObjectId(); let rows: Record>; let postsBulkWrite: jest.Mock; let collection: jest.Mock; const queue = { registerProcessor: jest.fn(), enqueue: jest.fn() } as unknown as AppQueueService; beforeEach(() => { jest.clearAllMocks(); rows = { likes: [{ _id: postOne, count: 3 }], comments: [{ _id: postOne, count: 2 }], saves: [{ _id: postTwo, count: 5 }], postshares: [{ _id: postOne, count: 1 }], }; postsBulkWrite = jest.fn(); collection = jest.fn((name: string) => name === 'posts' ? { find: jest.fn(() => ({ toArray: jest.fn().mockResolvedValue([{ _id: postOne }, { _id: postTwo }]) })), bulkWrite: postsBulkWrite, } : { aggregate: jest.fn(() => ({ toArray: jest.fn().mockResolvedValue(rows[name]) })) }, ); }); it('registers and enqueues reconciliation jobs', async () => { const service = new CounterReconciliationService({ collection } as never, queue); service.onModuleInit(); expect(queue.registerProcessor).toHaveBeenCalledWith( 'maintenance.reconcile-post-counters', expect.any(Function), ); const callback = (queue.registerProcessor as jest.Mock).mock.calls[0][1] as () => Promise; const reconcile = jest.spyOn(service, 'reconcile').mockResolvedValue({ dryRun: false, scannedPosts: 0, updates: 0 }); await callback(); expect(reconcile).toHaveBeenCalledWith(false); await expect(service.enqueue()).resolves.toEqual({ queued: true }); expect(queue.enqueue).toHaveBeenCalledWith('maintenance.reconcile-post-counters', {}); }); it('computes every counter without writes during a dry run', async () => { const service = new CounterReconciliationService({ collection } as never, queue); await expect(service.reconcile()).resolves.toEqual({ dryRun: true, scannedPosts: 2, updates: 2 }); expect(postsBulkWrite).not.toHaveBeenCalled(); const likesAggregate = collection.mock.results.find((result) => result.value.aggregate)?.value.aggregate; expect(likesAggregate).toHaveBeenCalledWith([ { $match: { targetType: 'post' } }, { $group: { _id: '$targetId', count: { $sum: 1 } } }, { $match: { _id: { $type: 'objectId' } } }, ]); }); it('writes reconciled values including zeros', async () => { const service = new CounterReconciliationService({ collection } as never, queue); await service.reconcile(false); expect(postsBulkWrite).toHaveBeenCalledWith( [ expect.objectContaining({ updateOne: expect.objectContaining({ update: { $set: { likesCount: 3, commentsCount: 2, savesCount: 0, shareCount: 1 } } }) }), expect.objectContaining({ updateOne: expect.objectContaining({ update: { $set: { likesCount: 0, commentsCount: 0, savesCount: 5, shareCount: 0 } } }) }), ], { ordered: false }, ); }); it('controller executes only on an explicit true flag', async () => { const counters = { enqueue: jest.fn().mockResolvedValue({ queued: true }), reconcile: jest.fn().mockResolvedValue({ dryRun: true }) }; const controller = new MaintenanceController(counters as never); await controller.reconcile('true'); await controller.reconcile('TRUE'); expect(counters.enqueue).toHaveBeenCalledTimes(1); expect(counters.reconcile).toHaveBeenCalledWith(true); }); });