81 lines
3.7 KiB
TypeScript
81 lines
3.7 KiB
TypeScript
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<string, Array<{ _id: Types.ObjectId; count: number }>>;
|
|
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<void>;
|
|
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);
|
|
});
|
|
});
|