84 lines
3.2 KiB
TypeScript
84 lines
3.2 KiB
TypeScript
// server/supabase/functions/send-push/dispatch-response.test.ts
|
|
// The dispatch presenter must not leak provider transport codes to end users.
|
|
|
|
import { presentDispatchOutcome, type DispatchOutcome } from './dispatch-response.ts'
|
|
|
|
function assert(condition: boolean, message: string): asserts condition {
|
|
if (!condition) throw new Error(message)
|
|
}
|
|
|
|
const EVENT = 'transcription.completed'
|
|
const RESOURCE = '11111111-2222-4333-8444-555555555555'
|
|
|
|
function outcome(overrides: Partial<DispatchOutcome> = {}): DispatchOutcome {
|
|
return {
|
|
status: 'succeeded',
|
|
complete: true,
|
|
attempted: 1,
|
|
delivered: 1,
|
|
stale: 0,
|
|
retryableFailed: 0,
|
|
permanentFailed: 0,
|
|
nextRetryAt: null,
|
|
transientError: null,
|
|
transientStatus: null,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
const TRANSIENT_CASES: ReadonlyArray<[string, number]> = [
|
|
['webpush_send_timeout', 504],
|
|
['webpush_send_failed', 502],
|
|
['webpush_payload_too_large', 500],
|
|
['webpush_not_configured', 503],
|
|
['fcm_send_failed', 502],
|
|
]
|
|
|
|
Deno.test('user callers see one generic failure regardless of the transport outcome', () => {
|
|
const seen = new Set<string>()
|
|
for (const [code, status] of TRANSIENT_CASES) {
|
|
const presented = presentDispatchOutcome('user', EVENT, RESOURCE, outcome({
|
|
status: 'pending',
|
|
complete: false,
|
|
delivered: 0,
|
|
retryableFailed: 1,
|
|
transientError: code,
|
|
transientStatus: status,
|
|
}))
|
|
assert(presented.body.error === 'push_delivery_failed', `${code} leaked as ${String(presented.body.error)}`)
|
|
assert(!JSON.stringify(presented.body).includes(code), `${code} must not appear in the body`)
|
|
seen.add(`${presented.status}:${String(presented.body.error)}`)
|
|
}
|
|
assert(seen.size === 1, `user responses must be indistinguishable, got ${[...seen].join(', ')}`)
|
|
})
|
|
|
|
Deno.test('system callers keep the detailed transport code and status', () => {
|
|
for (const [code, status] of TRANSIENT_CASES) {
|
|
const presented = presentDispatchOutcome('system', EVENT, RESOURCE, outcome({
|
|
status: 'pending',
|
|
complete: false,
|
|
delivered: 0,
|
|
retryableFailed: 1,
|
|
transientError: code,
|
|
transientStatus: status,
|
|
}))
|
|
assert(presented.body.error === code, `system caller expects ${code}`)
|
|
assert(presented.status === status, `system caller expects status ${status}`)
|
|
}
|
|
})
|
|
|
|
Deno.test('success, pending and partial responses keep their existing shape', () => {
|
|
const ok = presentDispatchOutcome('user', EVENT, RESOURCE, outcome())
|
|
assert(ok.status === 200, 'complete dispatch is 200')
|
|
assert(!('error' in ok.body), 'success carries no error')
|
|
assert(ok.body.sent === 1 && ok.body.event_type === EVENT && ok.body.resource_id === RESOURCE, 'body fields preserved')
|
|
assert(ok.body.duplicate === false, 'duplicate flag preserved')
|
|
|
|
const pending = presentDispatchOutcome('system', EVENT, RESOURCE, outcome({ status: 'processing', complete: false }))
|
|
assert(pending.status === 202, 'incomplete dispatch is 202')
|
|
|
|
for (const audience of ['user', 'system'] as const) {
|
|
const partial = presentDispatchOutcome(audience, EVENT, RESOURCE, outcome({ status: 'partial', permanentFailed: 1 }))
|
|
assert(partial.status === 502 && partial.body.error === 'push_delivery_failed', `${audience} partial is 502`)
|
|
}
|
|
})
|