Closer/functions/src/notifications/pruneTokens.test.ts

64 lines
2.8 KiB
TypeScript

import { isDeadTokenError, selectDeadTokens } from './pruneTokens'
// Shapes mirror what firebase-admin's FirebaseMessagingError actually throws (see errorInfo.code),
// plus the `code` getter form. Both must be recognised; transient/server errors must NOT be.
const deadNotRegistered = { errorInfo: { code: 'messaging/registration-token-not-registered' }, codePrefix: 'messaging' }
const deadInvalidToken = { code: 'messaging/invalid-registration-token' }
const transientUnavailable = { errorInfo: { code: 'messaging/server-unavailable' } }
const transientInternal = { code: 'messaging/internal-error' }
const badPayload = { errorInfo: { code: 'messaging/invalid-argument' } } // payload bug — must NOT prune
const rej = (reason: unknown): PromiseSettledResult<unknown> => ({ status: 'rejected', reason })
const ful = (): PromiseSettledResult<unknown> => ({ status: 'fulfilled', value: 'id' })
describe('isDeadTokenError', () => {
it('is true for permanently-dead token codes (both errorInfo.code and code shapes)', () => {
expect(isDeadTokenError(deadNotRegistered)).toBe(true)
expect(isDeadTokenError(deadInvalidToken)).toBe(true)
})
it('is false for transient/server errors — never prune on these', () => {
expect(isDeadTokenError(transientUnavailable)).toBe(false)
expect(isDeadTokenError(transientInternal)).toBe(false)
})
it('is false for invalid-argument — that blames the payload, not the token', () => {
expect(isDeadTokenError(badPayload)).toBe(false)
})
it('is false for missing/garbage reasons (fail safe)', () => {
expect(isDeadTokenError(undefined)).toBe(false)
expect(isDeadTokenError(null)).toBe(false)
expect(isDeadTokenError({})).toBe(false)
expect(isDeadTokenError('boom')).toBe(false)
expect(isDeadTokenError({ code: 42 })).toBe(false)
})
})
describe('selectDeadTokens', () => {
it('maps rejected-dead results to their token by index', () => {
const tokens = ['A', 'B', 'C']
const results = [ful(), rej(deadNotRegistered), rej(transientUnavailable)]
expect(selectDeadTokens(tokens, results)).toEqual(['B'])
})
it('ignores fulfilled sends and transient failures', () => {
const tokens = ['A', 'B']
expect(selectDeadTokens(tokens, [ful(), rej(transientInternal)])).toEqual([])
})
it('dedupes when the same dead token appears twice', () => {
const tokens = ['A', 'A']
expect(selectDeadTokens(tokens, [rej(deadNotRegistered), rej(deadInvalidToken)])).toEqual(['A'])
})
it('returns empty when there are no failures', () => {
expect(selectDeadTokens(['A', 'B'], [ful(), ful()])).toEqual([])
})
it('never selects a token whose index is missing', () => {
// more results than tokens (defensive) — no crash, no phantom token
expect(selectDeadTokens(['A'], [rej(deadNotRegistered), rej(deadNotRegistered)])).toEqual(['A'])
})
})