import { describe, expect, it } from 'vitest'; import { ObjectId } from 'mongodb'; import { decideSend } from '../src/mode.js'; import { assertBatchWithinLimit, BatchTooLargeError } from '../src/limits.js'; import type { Config } from '../src/config.js'; import type { NotificationDoc } from '../src/types.js'; function baseCfg(over: Partial = {}): Config { return { mode: 'full', killSwitch: false, ownedChannels: ['fcm'], mongoUrl: 'mongodb://x', dbName: 'fuegos', redis: { host: 'x', port: 6379, db: 0 }, poll: { intervalMs: 1000, batchSize: 100 }, limits: { perUserPerDay: 20, maxPerBatch: 2000 }, canary: { userIds: [], subsIds: [] }, retries: { attempts: 3, backoffMs: 100 }, fcm: {}, email: { from: 'x' }, rootUrl: 'https://x/', gmaps: {}, logLevel: 'silent', serviceName: 'test', ...over, }; } const notif: NotificationDoc = { _id: new ObjectId(), userId: 'u1', subsId: new ObjectId('bbbbbbbbbbbbbbbbbbbbbbbb'), content: 'x', geo: { type: 'Point', coordinates: [0, 0] }, type: 'mobile', when: new Date(), sealed: 's', }; describe('decideSend', () => { it('kill switch overrides every mode', () => { for (const mode of ['dry-run', 'shadow', 'canary', 'full'] as const) { const d = decideSend(baseCfg({ mode, killSwitch: true }), notif); expect(d).toEqual({ send: false, reason: 'kill-switch' }); } }); it('dry-run never sends', () => { expect(decideSend(baseCfg({ mode: 'dry-run' }), notif)).toEqual({ send: false, reason: 'dry-run' }); }); it('shadow never sends', () => { expect(decideSend(baseCfg({ mode: 'shadow' }), notif)).toEqual({ send: false, reason: 'shadow' }); }); it('canary sends only to cohort by userId', () => { expect(decideSend(baseCfg({ mode: 'canary', canary: { userIds: ['u1'], subsIds: [] } }), notif)).toEqual({ send: true }); expect(decideSend(baseCfg({ mode: 'canary', canary: { userIds: ['other'], subsIds: [] } }), notif)).toEqual({ send: false, reason: 'not-in-canary' }); }); it('canary sends to cohort by subsId', () => { expect(decideSend(baseCfg({ mode: 'canary', canary: { userIds: [], subsIds: ['bbbbbbbbbbbbbbbbbbbbbbbb'] } }), notif)).toEqual({ send: true }); }); it('full always sends', () => { expect(decideSend(baseCfg({ mode: 'full' }), notif)).toEqual({ send: true }); }); }); describe('circuit breaker', () => { it('throws when batch exceeds max', () => { expect(() => assertBatchWithinLimit(2001, 2000)).toThrow(BatchTooLargeError); }); it('passes when within max', () => { expect(() => assertBatchWithinLimit(2000, 2000)).not.toThrow(); }); it('unlimited when max is 0', () => { expect(() => assertBatchWithinLimit(999999, 0)).not.toThrow(); }); });