Every notification depended on Firebase Cloud Messaging, so a missing Firebase project, which is the current state, meant no notification could be delivered on any platform. Web Push and token-based Apple Push are now first class transports alongside FCM, chosen per registered device, and a scheduled Cloudflare Worker drain retries an outbox so a provider outage delays rather than drops a message.
69 lines
3.3 KiB
TypeScript
69 lines
3.3 KiB
TypeScript
// server/cloudflare-worker/src/push-drain.test.ts
|
|
import { drainPushDispatches, resolveBatchLimit } from './push-drain.ts'
|
|
|
|
function assert(condition: boolean, message: string): asserts condition {
|
|
if (!condition) throw new Error(message)
|
|
}
|
|
|
|
const env = { SUPABASE_URL: 'https://example.supabase.co', SUPABASE_SERVICE_ROLE_KEY: 'x'.repeat(40) }
|
|
|
|
function jsonResponse(status: number, body: unknown): Response {
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
}
|
|
|
|
Deno.test('batch limit is clamped to the edge function contract', () => {
|
|
assert(resolveBatchLimit(undefined) === 20, 'default is 20')
|
|
assert(resolveBatchLimit('5') === 5, 'explicit value is used')
|
|
assert(resolveBatchLimit('9999') === 100, 'upper bound is 100')
|
|
assert(resolveBatchLimit('0') === 20, 'zero falls back to the default')
|
|
assert(resolveBatchLimit('abc') === 20, 'garbage falls back to the default')
|
|
})
|
|
|
|
Deno.test('drain fails closed without configuration', async () => {
|
|
const result = await drainPushDispatches({}, () => {
|
|
throw new Error('must not be called')
|
|
})
|
|
assert(result.ok === false, 'not ok')
|
|
assert(result.error === 'push_drain_not_configured', 'clear configuration error')
|
|
assert(result.status === 503, 'service unavailable')
|
|
})
|
|
|
|
Deno.test('drain posts to the drain endpoint with the service role bearer', async () => {
|
|
const calls: Array<{ url: string; init: RequestInit }> = []
|
|
const fetchImpl = ((url: string | URL | Request, init?: RequestInit) => {
|
|
calls.push({ url: String(url), init: init ?? {} })
|
|
return Promise.resolve(jsonResponse(200, { claimed: 3, completed: 2, pending: 1 }))
|
|
}) as unknown as typeof fetch
|
|
|
|
const result = await drainPushDispatches(env, fetchImpl)
|
|
assert(result.ok === true, 'ok')
|
|
assert(result.claimed === 3 && result.completed === 2 && result.pending === 1, 'summary parsed')
|
|
assert(calls[0].url === 'https://example.supabase.co/functions/v1/send-push?mode=drain&limit=20', 'drain url')
|
|
const headers = calls[0].init.headers as Record<string, string>
|
|
assert(headers.Authorization === `Bearer ${env.SUPABASE_SERVICE_ROLE_KEY}`, 'service role bearer')
|
|
assert(headers['Content-Type'] === 'application/json', 'json content type')
|
|
assert(calls[0].init.body === '{}', 'empty json body')
|
|
assert(calls[0].init.method === 'POST', 'post method')
|
|
})
|
|
|
|
Deno.test('drain reports upstream failures without throwing', async () => {
|
|
const httpFail = ((() => Promise.resolve(new Response('nope', { status: 500 }))) as unknown as typeof fetch)
|
|
const result = await drainPushDispatches(env, httpFail)
|
|
assert(result.ok === false, 'not ok')
|
|
assert(result.error === 'push_drain_http_500', 'http error surfaced')
|
|
assert(result.status === 500, 'status surfaced')
|
|
|
|
const unreachable = (() => Promise.reject(new Error('network'))) as unknown as typeof fetch
|
|
const unreachableResult = await drainPushDispatches(env, unreachable)
|
|
assert(unreachableResult.error === 'push_drain_unreachable', 'network error surfaced')
|
|
})
|
|
|
|
Deno.test('drain tolerates a malformed success body', async () => {
|
|
const fetchImpl = ((() => Promise.resolve(new Response('not-json', { status: 200 }))) as unknown as typeof fetch)
|
|
const result = await drainPushDispatches(env, fetchImpl)
|
|
assert(result.ok === true, 'ok')
|
|
assert(result.claimed === undefined, 'summary left undefined')
|
|
})
|