Å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.
127 lines
4.0 KiB
TypeScript
127 lines
4.0 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
vi.mock('@/lib/auth/api-keys', () => ({
|
|
createServiceClientNoCookies: vi.fn(() => ({ __client: true })),
|
|
}))
|
|
|
|
// `after` is a no-op outside a request scope in these tests; the module falls
|
|
// back to a floating promise, which is the path a plain node server takes too.
|
|
vi.mock('next/server', () => ({
|
|
after: () => {
|
|
throw new Error('after() called outside a request scope')
|
|
},
|
|
}))
|
|
|
|
import { kickWebhookDispatch, resetKickStateForTests, KICK_BATCH_SIZE } from '../dispatch-kick'
|
|
|
|
/**
|
|
* A zero DispatchSummary. Spread rather than repeated so adding a counter to
|
|
* the summary does not mean re-editing every case in this file; these tests
|
|
* are about scheduling and coalescing, never about the counts.
|
|
*/
|
|
const EMPTY_SUMMARY = {
|
|
picked: 0,
|
|
delivered: 0,
|
|
failed: 0,
|
|
dead: 0,
|
|
skipped: 0,
|
|
released: 0,
|
|
recovered: 0,
|
|
recoveredDead: 0,
|
|
}
|
|
|
|
/** Resolves after the microtask queue drains, so floating work has run. */
|
|
const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
resetKickStateForTests()
|
|
})
|
|
|
|
describe('kickWebhookDispatch', () => {
|
|
it('runs one dispatch cycle with the small kick batch size', async () => {
|
|
const dispatch = vi.fn().mockResolvedValue({ ...EMPTY_SUMMARY, picked: 1, delivered: 1 })
|
|
|
|
kickWebhookDispatch(dispatch)
|
|
await flush()
|
|
|
|
expect(dispatch).toHaveBeenCalledTimes(1)
|
|
expect(dispatch.mock.calls[0][0]).toMatchObject({ batchSize: KICK_BATCH_SIZE })
|
|
})
|
|
|
|
it('claims far fewer rows than the cron, so a backlog cannot own a request tail', () => {
|
|
// Each delivery can burn the full 10 s receiver timeout; the cron's 50 is
|
|
// fine on a dedicated invocation, not on the tail of a user request.
|
|
expect(KICK_BATCH_SIZE).toBeLessThan(50)
|
|
})
|
|
|
|
it('returns synchronously: the emitter never waits on receiver HTTP', async () => {
|
|
let settled = false
|
|
const dispatch = vi.fn(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
setTimeout(() => {
|
|
settled = true
|
|
resolve({ ...EMPTY_SUMMARY })
|
|
}, 20)
|
|
}),
|
|
)
|
|
|
|
const returned = kickWebhookDispatch(dispatch as never)
|
|
|
|
// eventBus.emit awaits its subscribers, so a kick that blocked here would
|
|
// put a stranger's slow endpoint on the critical path of committing a
|
|
// verifikat.
|
|
expect(returned).toBeUndefined()
|
|
expect(settled).toBe(false)
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 40))
|
|
expect(settled).toBe(true)
|
|
})
|
|
|
|
it('coalesces a burst of kicks into a single cycle', async () => {
|
|
// A bulk booking emits once per row. Without coalescing, 100 rows would
|
|
// mean 100 claim round trips against the same handful of due deliveries.
|
|
const dispatch = vi.fn().mockResolvedValue({ ...EMPTY_SUMMARY })
|
|
|
|
for (let i = 0; i < 100; i++) kickWebhookDispatch(dispatch)
|
|
await flush()
|
|
|
|
expect(dispatch).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('accepts a new kick once the previous cycle has started', async () => {
|
|
const dispatch = vi.fn().mockResolvedValue({ ...EMPTY_SUMMARY })
|
|
|
|
kickWebhookDispatch(dispatch)
|
|
await flush()
|
|
kickWebhookDispatch(dispatch)
|
|
await flush()
|
|
|
|
expect(dispatch).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('swallows a dispatch failure: a failed kick is latency, not a lost delivery', async () => {
|
|
const dispatch = vi.fn().mockRejectedValue(new Error('receiver unreachable'))
|
|
|
|
expect(() => kickWebhookDispatch(dispatch)).not.toThrow()
|
|
await flush()
|
|
|
|
expect(dispatch).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('does not wedge the coalescing flag when a cycle fails', async () => {
|
|
// The cron still covers every row, but a stuck flag would silently
|
|
// disable the fast path for the life of the function instance.
|
|
const failing = vi.fn().mockRejectedValue(new Error('boom'))
|
|
kickWebhookDispatch(failing)
|
|
await flush()
|
|
|
|
const ok = vi.fn().mockResolvedValue({ ...EMPTY_SUMMARY })
|
|
kickWebhookDispatch(ok)
|
|
await flush()
|
|
|
|
expect(ok).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|