-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: Add tests for scheduled handler
- Loading branch information
1 parent
6aa304f
commit a0a3666
Showing
3 changed files
with
219 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,49 +3,221 @@ | |
|
||
import { beforeEach, describe, expect, test, vi } from 'vitest'; | ||
|
||
import type { ScheduledController } from '@cloudflare/workers-types'; | ||
import * as SentryCore from '@sentry/core'; | ||
import type { Event } from '@sentry/types'; | ||
import { CloudflareClient } from '../src/client'; | ||
import { withSentry } from '../src/handler'; | ||
|
||
const MOCK_ENV = { | ||
SENTRY_DSN: 'https://[email protected]/1337', | ||
}; | ||
|
||
describe('sentryPagesPlugin', () => { | ||
describe('withSentry', () => { | ||
beforeEach(() => { | ||
vi.clearAllMocks(); | ||
}); | ||
|
||
test('gets env from handler', async () => { | ||
const handler = { | ||
fetch(_request, _env, _context) { | ||
return new Response('test'); | ||
}, | ||
} satisfies ExportedHandler; | ||
describe('fetch handler', () => { | ||
test('executes options callback with env', async () => { | ||
const handler = { | ||
fetch(_request, _env, _context) { | ||
return new Response('test'); | ||
}, | ||
} satisfies ExportedHandler<typeof MOCK_ENV>; | ||
|
||
const optionsCallback = vi.fn().mockReturnValue({}); | ||
const optionsCallback = vi.fn().mockReturnValue({}); | ||
|
||
const wrappedHandler = withSentry(optionsCallback, handler); | ||
await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext()); | ||
const wrappedHandler = withSentry(optionsCallback, handler); | ||
await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext()); | ||
|
||
expect(optionsCallback).toHaveBeenCalledTimes(1); | ||
expect(optionsCallback).toHaveBeenLastCalledWith(MOCK_ENV); | ||
expect(optionsCallback).toHaveBeenCalledTimes(1); | ||
expect(optionsCallback).toHaveBeenLastCalledWith(MOCK_ENV); | ||
}); | ||
|
||
test('passes through the handler response', async () => { | ||
const response = new Response('test'); | ||
const handler = { | ||
async fetch(_request, _env, _context) { | ||
return response; | ||
}, | ||
} satisfies ExportedHandler<typeof MOCK_ENV>; | ||
|
||
const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); | ||
const result = await wrappedHandler.fetch( | ||
new Request('https://example.com'), | ||
MOCK_ENV, | ||
createMockExecutionContext(), | ||
); | ||
|
||
expect(result).toBe(response); | ||
}); | ||
}); | ||
|
||
test('passes through the response from the handler', async () => { | ||
const response = new Response('test'); | ||
const handler = { | ||
async fetch(_request, _env, _context) { | ||
return response; | ||
}, | ||
} satisfies ExportedHandler; | ||
|
||
const wrappedHandler = withSentry(() => ({}), handler); | ||
const result = await wrappedHandler.fetch( | ||
new Request('https://example.com'), | ||
MOCK_ENV, | ||
createMockExecutionContext(), | ||
); | ||
|
||
expect(result).toBe(response); | ||
describe('scheduled handler', () => { | ||
test('executes options callback with env', async () => { | ||
const handler = { | ||
scheduled(_controller, _env, _context) { | ||
return; | ||
}, | ||
} satisfies ExportedHandler<typeof MOCK_ENV>; | ||
|
||
const optionsCallback = vi.fn().mockReturnValue({}); | ||
|
||
const wrappedHandler = withSentry(optionsCallback, handler); | ||
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); | ||
|
||
expect(optionsCallback).toHaveBeenCalledTimes(1); | ||
expect(optionsCallback).toHaveBeenLastCalledWith(MOCK_ENV); | ||
}); | ||
|
||
test('flushes the event after the handler is done using the cloudflare context.waitUntil', async () => { | ||
const handler = { | ||
scheduled(_controller, _env, _context) { | ||
return; | ||
}, | ||
} satisfies ExportedHandler<typeof MOCK_ENV>; | ||
|
||
const context = createMockExecutionContext(); | ||
const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); | ||
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, context); | ||
|
||
// eslint-disable-next-line @typescript-eslint/unbound-method | ||
expect(context.waitUntil).toHaveBeenCalledTimes(1); | ||
// eslint-disable-next-line @typescript-eslint/unbound-method | ||
expect(context.waitUntil).toHaveBeenLastCalledWith(expect.any(Promise)); | ||
}); | ||
|
||
test('creates a cloudflare client and sets it on the handler', async () => { | ||
const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind'); | ||
const handler = { | ||
scheduled(_controller, _env, _context) { | ||
return; | ||
}, | ||
} satisfies ExportedHandler<typeof MOCK_ENV>; | ||
|
||
const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); | ||
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); | ||
|
||
expect(initAndBindSpy).toHaveBeenCalledTimes(1); | ||
expect(initAndBindSpy).toHaveBeenLastCalledWith(CloudflareClient, expect.any(Object)); | ||
}); | ||
|
||
describe('scope instrumentation', () => { | ||
test('adds cloud resource context', async () => { | ||
const handler = { | ||
scheduled(_controller, _env, _context) { | ||
SentryCore.captureMessage('cloud_resource'); | ||
return; | ||
}, | ||
} satisfies ExportedHandler<typeof MOCK_ENV>; | ||
|
||
let sentryEvent: Event = {}; | ||
const wrappedHandler = withSentry( | ||
env => ({ | ||
dsn: env.SENTRY_DSN, | ||
beforeSend(event) { | ||
sentryEvent = event; | ||
return null; | ||
}, | ||
}), | ||
handler, | ||
); | ||
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); | ||
|
||
expect(sentryEvent.contexts?.cloud_resource).toEqual({ 'cloud.provider': 'cloudflare' }); | ||
}); | ||
}); | ||
|
||
describe('error instrumentation', () => { | ||
test('captures errors thrown by the handler', async () => { | ||
const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException'); | ||
const error = new Error('test'); | ||
|
||
expect(captureExceptionSpy).not.toHaveBeenCalled(); | ||
|
||
const handler = { | ||
scheduled(_controller, _env, _context) { | ||
throw error; | ||
}, | ||
} satisfies ExportedHandler<typeof MOCK_ENV>; | ||
|
||
const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); | ||
try { | ||
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); | ||
} catch { | ||
// ignore | ||
} | ||
|
||
expect(captureExceptionSpy).toHaveBeenCalledTimes(1); | ||
expect(captureExceptionSpy).toHaveBeenLastCalledWith(error, { | ||
mechanism: { handled: false, type: 'cloudflare' }, | ||
}); | ||
}); | ||
|
||
test('re-throws the error after capturing', async () => { | ||
const error = new Error('test'); | ||
const handler = { | ||
scheduled(_controller, _env, _context) { | ||
throw error; | ||
}, | ||
} satisfies ExportedHandler<typeof MOCK_ENV>; | ||
|
||
const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); | ||
|
||
let thrownError: Error | undefined; | ||
try { | ||
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); | ||
} catch (e: any) { | ||
thrownError = e; | ||
} | ||
|
||
expect(thrownError).toBe(error); | ||
}); | ||
}); | ||
|
||
describe('tracing instrumentation', () => { | ||
test('creates a span that wraps scheduled invocation', async () => { | ||
const handler = { | ||
scheduled(_controller, _env, _context) { | ||
return; | ||
}, | ||
} satisfies ExportedHandler<typeof MOCK_ENV>; | ||
|
||
let sentryEvent: Event = {}; | ||
const wrappedHandler = withSentry( | ||
env => ({ | ||
dsn: env.SENTRY_DSN, | ||
tracesSampleRate: 1, | ||
beforeSendTransaction(event) { | ||
sentryEvent = event; | ||
return null; | ||
}, | ||
}), | ||
handler, | ||
); | ||
|
||
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); | ||
|
||
expect(sentryEvent.transaction).toEqual('Scheduled Cron 0 0 0 * * *'); | ||
expect(sentryEvent.spans).toHaveLength(0); | ||
expect(sentryEvent.contexts?.trace).toEqual({ | ||
data: { | ||
'sentry.origin': 'auto.faas.cloudflare', | ||
'sentry.op': 'faas.cron', | ||
'faas.cron': '0 0 0 * * *', | ||
'faas.time': expect.any(String), | ||
'faas.trigger': 'timer', | ||
'sentry.sample_rate': 1, | ||
'sentry.source': 'task', | ||
}, | ||
op: 'faas.cron', | ||
origin: 'auto.faas.cloudflare', | ||
span_id: expect.any(String), | ||
trace_id: expect.any(String), | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
|
@@ -55,3 +227,11 @@ function createMockExecutionContext(): ExecutionContext { | |
passThroughOnException: vi.fn(), | ||
}; | ||
} | ||
|
||
function createMockScheduledController(): ScheduledController { | ||
return { | ||
scheduledTime: 123, | ||
cron: '0 0 0 * * *', | ||
noRetry: vi.fn(), | ||
}; | ||
} |