130 lines
5.9 KiB
JavaScript
130 lines
5.9 KiB
JavaScript
"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
|