Closer/functions/dist/notifications/push.test.js

130 lines
5.9 KiB
JavaScript
Raw Normal View History

"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