"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const push_1 = require("./push"); const pruneTokens_1 = require("./pruneTokens"); // ── Lightweight Firestore/Messaging fakes ──────────────────────────────────── // Only the surface push.ts + pruneTokens.ts actually touch. const deadError = { code: 'messaging/registration-token-not-registered' }; const transientError = { code: 'messaging/server-unavailable' }; function makeFakeDb(userData, subTokens) { const deleted = []; const commits = { count: 0 }; const fcmDocs = subTokens.map((t, i) => ({ ref: { __token: t, __id: `tok${i}` }, data: () => ({ token: t }), })); const userRef = { get: async () => ({ exists: true, data: () => userData }), collection: (name) => { if (name !== 'fcmTokens') throw new Error(`unexpected subcollection ${name}`); return { get: async () => ({ docs: fcmDocs }) }; }, }; const db = { collection: (name) => { if (name !== 'users') throw new Error(`unexpected collection ${name}`); return { doc: () => userRef }; }, batch: () => ({ delete: (ref) => deleted.push(ref.__token), update: () => deleted.push('__legacy__'), commit: async () => { commits.count++; }, }), }; return { db, deleted, commits }; } function batch(responses) { return { responses, successCount: responses.filter((r) => r.success).length, failureCount: responses.filter((r) => !r.success).length, }; } function makeFakeMessaging(response) { return { sendEachForMulticast: async () => response }; } describe('getUserTokens', () => { it('merges the legacy field with the subcollection, deduped and order-stable', async () => { const { db } = makeFakeDb({ fcmToken: 'A' }, ['A', 'B']); expect(await (0, push_1.getUserTokens)(db, 'u1')).toEqual(['A', 'B']); }); it('returns only subcollection tokens when there is no legacy field', async () => { const { db } = makeFakeDb({}, ['B', 'C']); expect(await (0, push_1.getUserTokens)(db, 'u1')).toEqual(['B', 'C']); }); it('returns empty when the user has no tokens', async () => { const { db } = makeFakeDb({}, []); expect(await (0, push_1.getUserTokens)(db, 'u1')).toEqual([]); }); it('uses provided userData without re-reading the user doc', async () => { const { db } = makeFakeDb({}, ['B']); // Make the user-doc read blow up; passing userData must avoid it entirely. db.collection = (name) => { if (name !== 'users') throw new Error('unexpected'); return { doc: () => ({ get: async () => { throw new Error('should not read user doc'); }, collection: () => ({ get: async () => ({ docs: [{ data: () => ({ token: 'B' }) }] }) }), }), }; }; expect(await (0, push_1.getUserTokens)(db, 'u1', { fcmToken: 'A' })).toEqual(['A', 'B']); }); }); describe('batchResponseToResults', () => { it('maps a multicast BatchResponse to settled results that pick out dead tokens by index', () => { const results = (0, push_1.batchResponseToResults)(batch([ { success: true, messageId: 'm1' }, { success: false, error: deadError }, { success: false, error: transientError }, ])); expect(results.map((r) => r.status)).toEqual(['fulfilled', 'rejected', 'rejected']); // End-to-end: only the permanently-dead token (index 1) is selected for pruning. expect((0, pruneTokens_1.selectDeadTokens)(['A', 'B', 'C'], results)).toEqual(['B']); }); }); describe('sendPushToUser', () => { it('sends to every token and prunes exactly the permanently-dead ones', async () => { const { db, deleted, commits } = makeFakeDb({ fcmToken: 'LEG' }, ['LEG', 'DEAD', 'GOOD']); const messaging = makeFakeMessaging(batch([ { success: true, messageId: 'm1' }, // LEG { success: false, error: deadError }, // DEAD { success: true, messageId: 'm2' }, // GOOD ])); const res = await (0, push_1.sendPushToUser)(db, messaging, 'u1', { notification: { title: 't', body: 'b' } }); expect(res).toEqual({ sent: 2, failed: 1, pruned: 1 }); expect(deleted).toEqual(['DEAD']); expect(commits.count).toBe(1); }); it('does not prune on transient failures', async () => { const { db, deleted } = makeFakeDb({}, ['T1']); const messaging = makeFakeMessaging(batch([{ success: false, error: transientError }])); const res = await (0, push_1.sendPushToUser)(db, messaging, 'u1', { notification: { title: 't', body: 'b' } }); expect(res).toEqual({ sent: 0, failed: 1, pruned: 0 }); expect(deleted).toEqual([]); }); it('no-ops cleanly when the user has no tokens', async () => { const { db } = makeFakeDb({}, []); const messaging = makeFakeMessaging(batch([])); expect(await (0, push_1.sendPushToUser)(db, messaging, 'u1', { notification: { title: 't', body: 'b' } })).toEqual({ sent: 0, failed: 0, pruned: 0, }); }); it('treats a whole-batch send failure as non-fatal (no prune)', async () => { const { db, deleted } = makeFakeDb({}, ['X']); const messaging = { sendEachForMulticast: async () => Promise.reject(new Error('quota')) }; const res = await (0, push_1.sendPushToUser)(db, messaging, 'u1', { notification: { title: 't', body: 'b' } }); expect(res).toEqual({ sent: 0, failed: 1, pruned: 0 }); expect(deleted).toEqual([]); }); }); //# sourceMappingURL=push.test.js.map