import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import type { Queue } from 'bullmq'; import type { MongoMemoryServer } from 'mongodb-memory-server'; import type { Repos } from '../src/db.js'; import type { Config } from '../src/config.js'; import { createPoller } from '../src/poller.js'; import { pendingFilter } from '../src/poller.js'; import type { Channel, SendJob } from '../src/types.js'; import { aNotif, silentLog, startMongo } from './helpers.js'; let server: MongoMemoryServer; let repos: Repos; let baseCfg: Config; beforeAll(async () => { const s = await startMongo(); server = s.server; repos = s.repos; baseCfg = s.cfg; }); afterAll(async () => { await repos.close(); await server.stop(); }); beforeEach(async () => { await repos.notifications.deleteMany({}); }); /** Fake queue recording add() calls. */ function fakeQueues() { const added: Record = { fcm: [], email: [] }; const make = (ch: Channel) => ({ async add(_name: string, job: SendJob) { added[ch].push(job); }, }) as unknown as Queue; return { queues: { fcm: make('fcm'), email: make('email') } as Record>, added }; } describe('pendingFilter', () => { it('uses the CORRECT field names (old cron had a typo)', () => { expect(pendingFilter('fcm')).toEqual({ type: 'mobile', notified: { $ne: true } }); expect(pendingFilter('email')).toEqual({ type: 'web', emailNotified: { $ne: true } }); }); }); describe('poller.runOnce', () => { it('enqueues only pending docs of owned channels and claims them', async () => { await repos.notifications.insertMany([ aNotif({ type: 'mobile' }), // pending fcm aNotif({ type: 'mobile', notified: true }), // already done aNotif({ type: 'web' }), // pending email aNotif({ type: 'web', emailNotified: true }), // already done ]); const { queues, added } = fakeQueues(); const poller = createPoller(baseCfg, repos, queues, silentLog); const res = await poller.runOnce(); expect(res).toEqual({ fcm: 1, email: 1 }); expect(added.fcm).toHaveLength(1); expect(added.email).toHaveLength(1); // claimedBy provenance set expect(await repos.notifications.countDocuments({ claimedBy: baseCfg.serviceName })).toBe(2); }); it('only polls owned channels', async () => { await repos.notifications.insertMany([aNotif({ type: 'mobile' }), aNotif({ type: 'web' })]); const { queues, added } = fakeQueues(); const cfg = { ...baseCfg, ownedChannels: ['fcm'] as Channel[] }; const poller = createPoller(cfg, repos, queues, silentLog); const res = await poller.runOnce(); expect(res.fcm).toBe(1); expect(added.email).toHaveLength(0); }); it('circuit breaker halts on oversized batch', async () => { const many = Array.from({ length: 5 }, () => aNotif({ type: 'mobile' })); await repos.notifications.insertMany(many); const { queues, added } = fakeQueues(); const cfg = { ...baseCfg, ownedChannels: ['fcm'] as Channel[], limits: { ...baseCfg.limits, maxPerBatch: 3 } }; const poller = createPoller(cfg, repos, queues, silentLog); const res = await poller.runOnce(); // Batch of 5 > max 3 -> nothing enqueued, poller halted. expect(res.fcm).toBe(0); expect(added.fcm).toHaveLength(0); }); });