d3ro-voice/server/cloudflare-worker/src/push-drain.test.ts
Yun Chan bb0e54dcee feat(server): deliver push without a Firebase project
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.
2026-09-16 23:24:44 +09:00

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')
})