diff --git a/packages/cloudflare/test/executionContext.test-d.ts b/packages/cloudflare/test/executionContext.test-d.ts new file mode 100644 index 000000000000..77ec388beb03 --- /dev/null +++ b/packages/cloudflare/test/executionContext.test-d.ts @@ -0,0 +1,28 @@ +import type { ExecutionContext } from '@cloudflare/workers-types'; +import { describe, expectTypeOf, it } from 'vitest'; +import type { ExecutionContextCompat } from '../src/executionContext'; + +// The shape of `ExecutionContext` in `@cloudflare/workers-types` v4, which has no `exports` and an optional `tracing`. +interface ExecutionContextV4 { + waitUntil(promise: Promise): void; + passThroughOnException(): void; + readonly props: unknown; +} + +describe('ExecutionContextCompat', () => { + it('accepts a v5 ExecutionContext', () => { + expectTypeOf().toExtend(); + }); + + it('accepts a v4 ExecutionContext without the members v5 made required', () => { + expectTypeOf().toExtend(); + }); + + it('rejects a context without waitUntil', () => { + expectTypeOf>().not.toExtend(); + }); + + it('exposes waitUntil from both majors', () => { + expectTypeOf().toEqualTypeOf(); + }); +}); diff --git a/packages/cloudflare/test/withSentry.test.ts b/packages/cloudflare/test/withSentry.test.ts new file mode 100644 index 000000000000..c2282ba1f72b --- /dev/null +++ b/packages/cloudflare/test/withSentry.test.ts @@ -0,0 +1,73 @@ +import type { ExecutionContext } from '@cloudflare/workers-types'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { withSentry } from '../src/withSentry'; +import { resetSdk } from './testUtils'; + +const MOCK_ENV = { + SENTRY_DSN: 'https://public@dsn.ingest.sentry.io/1337', +}; + +function createMockExecutionContext(): ExecutionContext { + return { + waitUntil: vi.fn(), + passThroughOnException: vi.fn(), + props: {}, + } as unknown as ExecutionContext; +} + +class WorkerEntrypoint { + public constructor( + public ctx: ExecutionContext, + public env: unknown, + ) {} +} + +describe('withSentry', () => { + afterEach(() => { + vi.restoreAllMocks(); + resetSdk(); + }); + + it('returns the same handler object with its methods wrapped', () => { + const fetch = vi.fn(); + const handler = { fetch }; + + const wrapped = withSentry(() => ({}), handler); + + expect(wrapped).toBe(handler); + expect(wrapped.fetch).not.toBe(fetch); + }); + + it('instruments a WorkerEntrypoint class instead of treating it as a handler object', () => { + class MyEntrypoint extends WorkerEntrypoint { + public ping(): string { + return 'pong'; + } + } + + const optionsCallback = vi.fn().mockReturnValue({ dsn: MOCK_ENV.SENTRY_DSN }); + const context = createMockExecutionContext(); + + const Wrapped = withSentry(optionsCallback, MyEntrypoint as never) as unknown as typeof MyEntrypoint; + const instance = new Wrapped(context, MOCK_ENV); + + expect(Wrapped).not.toBe(MyEntrypoint); + expect(optionsCallback).toHaveBeenCalledWith(MOCK_ENV); + expect(instance).toBeInstanceOf(MyEntrypoint); + expect(instance.ctx).not.toBe(context); + expect(instance.ping()).toBe('pong'); + }); + + it('returns a handler it cannot instrument unchanged instead of throwing', () => { + const fetch = vi.fn(); + const handler = Object.freeze({ fetch }); + + let wrapped: typeof handler | undefined; + expect(() => { + wrapped = withSentry(() => ({}), handler); + }).not.toThrow(); + + expect(wrapped).toBe(handler); + expect(wrapped?.fetch).toBe(fetch); + }); +});