From dacf2971ff795c5784d0c9a75749ff1bb40a5158 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Tue, 10 Mar 2026 15:43:59 +0100 Subject: [PATCH] move event unit tests into separate test file --- .../sentry-nest-event-instrumentation.ts | 3 + .../test/integrations/nest-event.test.ts | 166 +++++++++++++++++ .../nestjs/test/integrations/nest.test.ts | 168 +----------------- 3 files changed, 171 insertions(+), 166 deletions(-) create mode 100644 packages/nestjs/test/integrations/nest-event.test.ts diff --git a/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts index d4ef20dcae01..b4f8784eea05 100644 --- a/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts +++ b/packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts @@ -16,6 +16,9 @@ const COMPONENT = '@nestjs/event-emitter'; * Custom instrumentation for nestjs event-emitter * * This hooks into the `OnEvent` decorator, which is applied on event handlers. + * Wrapped handlers run inside a forked isolation scope to ensure event-scoped data + * (breadcrumbs, tags, etc.) does not leak between concurrent event invocations + * or into subsequent HTTP requests. */ export class SentryNestEventInstrumentation extends InstrumentationBase { public constructor(config: InstrumentationConfig = {}) { diff --git a/packages/nestjs/test/integrations/nest-event.test.ts b/packages/nestjs/test/integrations/nest-event.test.ts new file mode 100644 index 000000000000..debf5bc8e34a --- /dev/null +++ b/packages/nestjs/test/integrations/nest-event.test.ts @@ -0,0 +1,166 @@ +import 'reflect-metadata'; +import * as core from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { SentryNestEventInstrumentation } from '../../src/integrations/sentry-nest-event-instrumentation'; +import type { OnEventTarget } from '../../src/integrations/types'; + +describe('EventInstrumentation', () => { + let instrumentation: SentryNestEventInstrumentation; + let mockOnEvent: vi.Mock; + let mockTarget: OnEventTarget; + + beforeEach(() => { + instrumentation = new SentryNestEventInstrumentation(); + // Mock OnEvent to return a function that applies the descriptor + mockOnEvent = vi.fn().mockImplementation(() => { + return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { + return descriptor; + }; + }); + mockTarget = { + name: 'TestClass', + prototype: {}, + } as OnEventTarget; + vi.spyOn(core, 'startSpan'); + vi.spyOn(core, 'captureException'); + vi.spyOn(core, 'withIsolationScope'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('init()', () => { + it('should return module definition with correct component name', () => { + const moduleDef = instrumentation.init(); + expect(moduleDef.name).toBe('@nestjs/event-emitter'); + }); + }); + + describe('OnEvent decorator wrapping', () => { + let wrappedOnEvent: any; + let descriptor: PropertyDescriptor; + let originalHandler: vi.Mock; + + beforeEach(() => { + originalHandler = vi.fn().mockResolvedValue('result'); + descriptor = { + value: originalHandler, + }; + + const moduleDef = instrumentation.init(); + const onEventFile = moduleDef.files[0]; + const moduleExports = { OnEvent: mockOnEvent }; + onEventFile?.patch(moduleExports); + wrappedOnEvent = moduleExports.OnEvent; + }); + + it('should wrap string event handlers', async () => { + const decorated = wrappedOnEvent('test.event'); + decorated(mockTarget, 'testMethod', descriptor); + + await descriptor.value(); + + expect(core.withIsolationScope).toHaveBeenCalled(); + expect(core.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'event test.event', + }), + expect.any(Function), + ); + expect(originalHandler).toHaveBeenCalled(); + }); + + it('should wrap symbol event handlers', async () => { + const decorated = wrappedOnEvent(Symbol('test.event')); + decorated(mockTarget, 'testMethod', descriptor); + + await descriptor.value(); + + expect(core.withIsolationScope).toHaveBeenCalled(); + expect(core.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'event Symbol(test.event)', + }), + expect.any(Function), + ); + expect(originalHandler).toHaveBeenCalled(); + }); + + it('should wrap string array event handlers', async () => { + const decorated = wrappedOnEvent(['test.event1', 'test.event2']); + decorated(mockTarget, 'testMethod', descriptor); + + await descriptor.value(); + + expect(core.withIsolationScope).toHaveBeenCalled(); + expect(core.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'event test.event1,test.event2', + }), + expect.any(Function), + ); + expect(originalHandler).toHaveBeenCalled(); + }); + + it('should wrap symbol array event handlers', async () => { + const decorated = wrappedOnEvent([Symbol('test.event1'), Symbol('test.event2')]); + decorated(mockTarget, 'testMethod', descriptor); + + await descriptor.value(); + + expect(core.withIsolationScope).toHaveBeenCalled(); + expect(core.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'event Symbol(test.event1),Symbol(test.event2)', + }), + expect.any(Function), + ); + expect(originalHandler).toHaveBeenCalled(); + }); + + it('should wrap mixed type array event handlers', async () => { + const decorated = wrappedOnEvent([Symbol('test.event1'), 'test.event2', Symbol('test.event3')]); + decorated(mockTarget, 'testMethod', descriptor); + + await descriptor.value(); + + expect(core.withIsolationScope).toHaveBeenCalled(); + expect(core.startSpan).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'event Symbol(test.event1),test.event2,Symbol(test.event3)', + }), + expect.any(Function), + ); + expect(originalHandler).toHaveBeenCalled(); + }); + + it('should capture exceptions and rethrow', async () => { + const error = new Error('Test error'); + originalHandler.mockRejectedValue(error); + + const decorated = wrappedOnEvent('test.event'); + decorated(mockTarget, 'testMethod', descriptor); + + await expect(descriptor.value()).rejects.toThrow(error); + expect(core.captureException).toHaveBeenCalledWith(error, { + mechanism: { + handled: false, + type: 'auto.event.nestjs', + }, + }); + }); + + it('should skip wrapping for internal Sentry handlers', () => { + const internalTarget = { + ...mockTarget, + __SENTRY_INTERNAL__: true, + }; + + const decorated = wrappedOnEvent('test.event'); + decorated(internalTarget, 'testMethod', descriptor); + + expect(descriptor.value).toBe(originalHandler); + }); + }); +}); diff --git a/packages/nestjs/test/integrations/nest.test.ts b/packages/nestjs/test/integrations/nest.test.ts index bebe32b915aa..6b758d44c982 100644 --- a/packages/nestjs/test/integrations/nest.test.ts +++ b/packages/nestjs/test/integrations/nest.test.ts @@ -1,9 +1,6 @@ -import 'reflect-metadata'; -import * as core from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { isPatched } from '../../src/integrations/helpers'; -import { SentryNestEventInstrumentation } from '../../src/integrations/sentry-nest-event-instrumentation'; -import type { InjectableTarget, OnEventTarget } from '../../src/integrations/types'; +import type { InjectableTarget } from '../../src/integrations/types'; describe('Nest', () => { describe('isPatched', () => { @@ -18,165 +15,4 @@ describe('Nest', () => { expect(target.sentryPatched).toBe(true); }); }); - - describe('EventInstrumentation', () => { - let instrumentation: SentryNestEventInstrumentation; - let mockOnEvent: vi.Mock; - let mockTarget: OnEventTarget; - - beforeEach(() => { - instrumentation = new SentryNestEventInstrumentation(); - // Mock OnEvent to return a function that applies the descriptor - mockOnEvent = vi.fn().mockImplementation(() => { - return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { - return descriptor; - }; - }); - mockTarget = { - name: 'TestClass', - prototype: {}, - } as OnEventTarget; - vi.spyOn(core, 'startSpan'); - vi.spyOn(core, 'captureException'); - vi.spyOn(core, 'withIsolationScope'); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('init()', () => { - it('should return module definition with correct component name', () => { - const moduleDef = instrumentation.init(); - expect(moduleDef.name).toBe('@nestjs/event-emitter'); - }); - }); - - describe('OnEvent decorator wrapping', () => { - let wrappedOnEvent: any; - let descriptor: PropertyDescriptor; - let originalHandler: vi.Mock; - - beforeEach(() => { - originalHandler = vi.fn().mockResolvedValue('result'); - descriptor = { - value: originalHandler, - }; - - const moduleDef = instrumentation.init(); - const onEventFile = moduleDef.files[0]; - const moduleExports = { OnEvent: mockOnEvent }; - onEventFile?.patch(moduleExports); - wrappedOnEvent = moduleExports.OnEvent; - }); - - it('should wrap string event handlers', async () => { - const decorated = wrappedOnEvent('test.event'); - decorated(mockTarget, 'testMethod', descriptor); - - await descriptor.value(); - - expect(core.withIsolationScope).toHaveBeenCalled(); - expect(core.startSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'event test.event', - }), - expect.any(Function), - ); - expect(originalHandler).toHaveBeenCalled(); - }); - - it('should wrap symbol event handlers', async () => { - const decorated = wrappedOnEvent(Symbol('test.event')); - decorated(mockTarget, 'testMethod', descriptor); - - await descriptor.value(); - - expect(core.withIsolationScope).toHaveBeenCalled(); - expect(core.startSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'event Symbol(test.event)', - }), - expect.any(Function), - ); - expect(originalHandler).toHaveBeenCalled(); - }); - - it('should wrap string array event handlers', async () => { - const decorated = wrappedOnEvent(['test.event1', 'test.event2']); - decorated(mockTarget, 'testMethod', descriptor); - - await descriptor.value(); - - expect(core.withIsolationScope).toHaveBeenCalled(); - expect(core.startSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'event test.event1,test.event2', - }), - expect.any(Function), - ); - expect(originalHandler).toHaveBeenCalled(); - }); - - it('should wrap symbol array event handlers', async () => { - const decorated = wrappedOnEvent([Symbol('test.event1'), Symbol('test.event2')]); - decorated(mockTarget, 'testMethod', descriptor); - - await descriptor.value(); - - expect(core.withIsolationScope).toHaveBeenCalled(); - expect(core.startSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'event Symbol(test.event1),Symbol(test.event2)', - }), - expect.any(Function), - ); - expect(originalHandler).toHaveBeenCalled(); - }); - - it('should wrap mixed type array event handlers', async () => { - const decorated = wrappedOnEvent([Symbol('test.event1'), 'test.event2', Symbol('test.event3')]); - decorated(mockTarget, 'testMethod', descriptor); - - await descriptor.value(); - - expect(core.withIsolationScope).toHaveBeenCalled(); - expect(core.startSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'event Symbol(test.event1),test.event2,Symbol(test.event3)', - }), - expect.any(Function), - ); - expect(originalHandler).toHaveBeenCalled(); - }); - - it('should capture exceptions and rethrow', async () => { - const error = new Error('Test error'); - originalHandler.mockRejectedValue(error); - - const decorated = wrappedOnEvent('test.event'); - decorated(mockTarget, 'testMethod', descriptor); - - await expect(descriptor.value()).rejects.toThrow(error); - expect(core.captureException).toHaveBeenCalledWith(error, { - mechanism: { - handled: false, - type: 'auto.event.nestjs', - }, - }); - }); - - it('should skip wrapping for internal Sentry handlers', () => { - const internalTarget = { - ...mockTarget, - __SENTRY_INTERNAL__: true, - }; - - const decorated = wrappedOnEvent('test.event'); - decorated(internalTarget, 'testMethod', descriptor); - - expect(descriptor.value).toBe(originalHandler); - }); - }); - }); });