Files
accounted/app/api/webhooks/dispatch/cron/__tests__/route.test.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

95 lines
3.3 KiB
TypeScript

/**
* Tests for the per-minute webhook dispatch cron (#1257).
*
* The route is a thin wrapper, so there is exactly one thing here that is not
* covered by lib/webhooks/__tests__: the function budget. The dispatcher's
* CYCLE_BUDGET_MS is what hands unattempted claims back before a cycle ends,
* and the 160 s stuck-recovery window is derived from it. Both are only real
* if the platform grants the invocation more wall time than the budget spends,
* which is what `export const maxDuration` buys. Without it the route runs on
* the platform default, the release path can be killed before it fires, and
* rows stay stranded in in_flight carrying their claim-time updated_at.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { NextResponse } from 'next/server'
import { verifyCronSecret } from '@/lib/auth/cron'
import { __TESTING__ } from '@/lib/webhooks/dispatcher'
vi.mock('@/lib/auth/cron', () => ({
verifyCronSecret: vi.fn(() => null),
}))
vi.mock('@/lib/auth/api-keys', () => ({
createServiceClientNoCookies: vi.fn(() => ({ __client: true })),
}))
const dispatchDueDeliveries = vi.fn()
vi.mock('@/lib/webhooks/dispatcher', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/webhooks/dispatcher')>()
return {
...actual,
dispatchDueDeliveries: (...args: unknown[]) => dispatchDueDeliveries(...args),
}
})
import { GET, maxDuration } from '../route'
function cronRequest(): Request {
return new Request('http://localhost:3000/api/webhooks/dispatch/cron')
}
const SUMMARY = {
picked: 3,
delivered: 1,
failed: 1,
dead: 1,
skipped: 0,
released: 0,
recovered: 2,
recoveredDead: 1,
}
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(verifyCronSecret).mockReturnValue(null)
dispatchDueDeliveries.mockResolvedValue(SUMMARY)
})
describe('GET /api/webhooks/dispatch/cron', () => {
it('declares a function budget above the dispatcher cycle budget', () => {
expect(maxDuration).toBe(300)
// The load-bearing relation, not just the literal: the in-code budget can
// only hand claims back if the invocation is still alive when it expires.
expect(maxDuration * 1000).toBeGreaterThan(__TESTING__.CYCLE_BUDGET_MS)
// And the sweep window has to fit inside the invocation too, otherwise the
// window is derived from a bound nothing enforces.
expect(maxDuration * 1000).toBeGreaterThan(
__TESTING__.CYCLE_BUDGET_MS +
__TESTING__.REQUEST_TIMEOUT_MS +
__TESTING__.STUCK_RECOVERY_SLACK_MS,
)
})
it('returns 401 and dispatches nothing when the cron secret is invalid', async () => {
vi.mocked(verifyCronSecret).mockReturnValueOnce(
NextResponse.json({ error: 'Unauthorized' }, { status: 401 }),
)
const response = await GET(cronRequest())
expect(response.status).toBe(401)
expect(dispatchDueDeliveries).not.toHaveBeenCalled()
})
it('returns the full dispatch summary, including the sweep outcome', async () => {
const response = await GET(cronRequest())
expect(response.status).toBe(200)
const body = (await response.json()) as { data: typeof SUMMARY }
// recovered / recoveredDead are the counts an operator needs to see that a
// tick took deliveries to the terminal, immutable 'dead' state.
expect(body.data).toEqual(SUMMARY)
expect(dispatchDueDeliveries).toHaveBeenCalledTimes(1)
})
})