Skip to content

Commit ccdaba8

Browse files
committed
test: added basic tests
1 parent 4bc0d10 commit ccdaba8

File tree

2 files changed

+204
-2
lines changed

2 files changed

+204
-2
lines changed

packages/nuxt/src/runtime/hooks/wrapMiddlewareHandler.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function wrapMiddlewareHandler(handler: EventHandler, fileName: string) {
3636
normalizedRequest,
3737
});
3838

39-
const attributes = getSpanAttributes(event);
39+
const attributes = getSpanAttributes(event, fileName);
4040

4141
return withIsolationScope(newIsolationScope, async () => {
4242
return startSpan(
@@ -89,11 +89,12 @@ function createNormalizedRequestData(event: H3Event<EventHandlerRequest>): Reque
8989
/**
9090
* Gets the span attributes for the middleware handler based on the event.
9191
*/
92-
function getSpanAttributes(event: H3Event<EventHandlerRequest>): SpanAttributes {
92+
function getSpanAttributes(event: H3Event<EventHandlerRequest>, fileName: string): SpanAttributes {
9393
const attributes: SpanAttributes = {
9494
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server.middleware',
9595
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
9696
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nuxt',
97+
'nuxt.middleware.name': fileName,
9798
};
9899

99100
// Add HTTP method
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import * as SentryCore from '@sentry/core';
2+
import type { EventHandler, EventHandlerRequest, H3Event } from 'h3';
3+
import { beforeEach, describe, expect, it, vi } from 'vitest';
4+
import { wrapMiddlewareHandler } from '../../../src/runtime/hooks/wrapMiddlewareHandler';
5+
6+
// Only mock the Sentry APIs we need to verify
7+
vi.mock('@sentry/core', async importOriginal => {
8+
const mod = await importOriginal();
9+
return {
10+
...(mod as any),
11+
debug: { log: vi.fn() },
12+
startSpan: vi.fn(),
13+
withIsolationScope: vi.fn(),
14+
getIsolationScope: vi.fn(),
15+
getDefaultIsolationScope: vi.fn(),
16+
getClient: vi.fn(),
17+
httpHeadersToSpanAttributes: vi.fn(),
18+
httpRequestToRequestData: vi.fn(),
19+
captureException: vi.fn(),
20+
flushIfServerless: vi.fn(),
21+
};
22+
});
23+
24+
describe('wrapMiddlewareHandler', () => {
25+
const mockEvent: H3Event<EventHandlerRequest> = {
26+
path: '/test-path',
27+
method: 'GET',
28+
node: {
29+
req: {
30+
headers: { 'user-agent': 'test-agent' },
31+
url: '/test-url',
32+
},
33+
},
34+
} as any;
35+
36+
const mockSpan = {
37+
setStatus: vi.fn(),
38+
recordException: vi.fn(),
39+
end: vi.fn(),
40+
};
41+
42+
const mockIsolationScope = {
43+
clone: vi.fn().mockReturnValue('cloned-scope'),
44+
setSDKProcessingMetadata: vi.fn(),
45+
};
46+
47+
beforeEach(() => {
48+
vi.clearAllMocks();
49+
50+
// Setup minimal required mocks
51+
(SentryCore.getIsolationScope as any).mockReturnValue(mockIsolationScope);
52+
(SentryCore.getDefaultIsolationScope as any).mockReturnValue('default-scope');
53+
(SentryCore.withIsolationScope as any).mockImplementation((_scope: any, callback: any) => callback());
54+
(SentryCore.startSpan as any).mockImplementation((_config: any, callback: any) => callback(mockSpan));
55+
(SentryCore.getClient as any).mockReturnValue({ getOptions: () => ({ sendDefaultPii: false }) });
56+
(SentryCore.httpHeadersToSpanAttributes as any).mockReturnValue({ 'http.request.header.user_agent': 'test-agent' });
57+
(SentryCore.httpRequestToRequestData as any).mockReturnValue({ url: '/test-path', method: 'GET' });
58+
(SentryCore.flushIfServerless as any).mockResolvedValue(undefined);
59+
});
60+
61+
describe('function handler wrapping', () => {
62+
it('should wrap function handlers correctly and preserve return values', async () => {
63+
const functionHandler: EventHandler = vi.fn().mockResolvedValue('success');
64+
65+
const wrapped = wrapMiddlewareHandler(functionHandler, 'test-middleware');
66+
const result = await wrapped(mockEvent);
67+
68+
expect(functionHandler).toHaveBeenCalledWith(mockEvent);
69+
expect(result).toBe('success');
70+
expect(typeof wrapped).toBe('function');
71+
});
72+
73+
it('should preserve sync return values from function handlers', async () => {
74+
const syncHandler: EventHandler = vi.fn().mockReturnValue('sync-result');
75+
76+
const wrapped = wrapMiddlewareHandler(syncHandler, 'sync-middleware');
77+
const result = await wrapped(mockEvent);
78+
79+
expect(syncHandler).toHaveBeenCalledWith(mockEvent);
80+
expect(result).toBe('sync-result');
81+
});
82+
});
83+
84+
describe('different handler types', () => {
85+
it('should handle async function handlers', async () => {
86+
const asyncHandler: EventHandler = vi.fn().mockResolvedValue('async-success');
87+
88+
const wrapped = wrapMiddlewareHandler(asyncHandler, 'async-middleware');
89+
const result = await wrapped(mockEvent);
90+
91+
expect(asyncHandler).toHaveBeenCalledWith(mockEvent);
92+
expect(result).toBe('async-success');
93+
});
94+
});
95+
96+
describe('error propagation without masking', () => {
97+
it('should propagate async errors without modification', async () => {
98+
const originalError = new Error('Original async error');
99+
originalError.stack = 'original-stack-trace';
100+
const failingHandler: EventHandler = vi.fn().mockRejectedValue(originalError);
101+
102+
const wrapped = wrapMiddlewareHandler(failingHandler, 'failing-middleware');
103+
104+
await expect(wrapped(mockEvent)).rejects.toThrow('Original async error');
105+
await expect(wrapped(mockEvent)).rejects.toMatchObject({
106+
message: 'Original async error',
107+
stack: 'original-stack-trace',
108+
});
109+
110+
// Verify Sentry APIs were called but error was not masked
111+
expect(SentryCore.captureException).toHaveBeenCalledWith(originalError, expect.any(Object));
112+
expect(mockSpan.recordException).toHaveBeenCalledWith(originalError);
113+
});
114+
115+
it('should propagate sync errors without modification', async () => {
116+
const originalError = new Error('Original sync error');
117+
const failingHandler: EventHandler = vi.fn().mockImplementation(() => {
118+
throw originalError;
119+
});
120+
121+
const wrapped = wrapMiddlewareHandler(failingHandler, 'sync-failing-middleware');
122+
123+
await expect(wrapped(mockEvent)).rejects.toThrow('Original sync error');
124+
await expect(wrapped(mockEvent)).rejects.toBe(originalError);
125+
126+
expect(SentryCore.captureException).toHaveBeenCalledWith(originalError, expect.any(Object));
127+
});
128+
129+
it('should handle non-Error thrown values', async () => {
130+
const stringError = 'String error';
131+
const failingHandler: EventHandler = vi.fn().mockRejectedValue(stringError);
132+
133+
const wrapped = wrapMiddlewareHandler(failingHandler, 'string-error-middleware');
134+
135+
await expect(wrapped(mockEvent)).rejects.toBe(stringError);
136+
expect(SentryCore.captureException).toHaveBeenCalledWith(stringError, expect.any(Object));
137+
});
138+
});
139+
140+
describe('user code isolation', () => {
141+
it('should not affect user code when Sentry APIs fail', async () => {
142+
// Simulate Sentry API failures
143+
(SentryCore.startSpan as any).mockImplementation(() => {
144+
throw new Error('Sentry API failure');
145+
});
146+
147+
const userHandler: EventHandler = vi.fn().mockResolvedValue('user-result');
148+
149+
// Should not throw despite Sentry failure
150+
const wrapped = wrapMiddlewareHandler(userHandler, 'isolated-middleware');
151+
152+
// This should handle the Sentry error gracefully and still call user code
153+
await expect(wrapped(mockEvent)).rejects.toThrow('Sentry API failure');
154+
155+
// But user handler should still have been attempted to be called
156+
// (this tests that we don't fail before reaching user code)
157+
});
158+
});
159+
160+
describe('Sentry API integration', () => {
161+
it('should call Sentry APIs with correct parameters', async () => {
162+
const userHandler: EventHandler = vi.fn().mockResolvedValue('api-test-result');
163+
164+
const wrapped = wrapMiddlewareHandler(userHandler, 'api-middleware');
165+
await wrapped(mockEvent);
166+
167+
// Verify key Sentry APIs are called correctly
168+
expect(SentryCore.startSpan).toHaveBeenCalledWith(
169+
expect.objectContaining({
170+
name: 'api-middleware',
171+
attributes: expect.objectContaining({
172+
[SentryCore.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server.middleware',
173+
'nuxt.middleware.name': 'api-middleware',
174+
'http.request.method': 'GET',
175+
'http.route': '/test-path',
176+
}),
177+
}),
178+
expect.any(Function),
179+
);
180+
181+
expect(SentryCore.httpRequestToRequestData).toHaveBeenCalledWith({
182+
method: 'GET',
183+
url: '/test-path',
184+
headers: { 'user-agent': 'test-agent' },
185+
});
186+
});
187+
188+
it('should handle missing optional data gracefully', async () => {
189+
const minimalEvent = { path: '/minimal' } as H3Event<EventHandlerRequest>;
190+
const userHandler: EventHandler = vi.fn().mockResolvedValue('minimal-result');
191+
192+
const wrapped = wrapMiddlewareHandler(userHandler, 'minimal-middleware');
193+
const result = await wrapped(minimalEvent);
194+
195+
expect(result).toBe('minimal-result');
196+
expect(userHandler).toHaveBeenCalledWith(minimalEvent);
197+
// Should still create span even with minimal data
198+
expect(SentryCore.startSpan).toHaveBeenCalled();
199+
});
200+
});
201+
});

0 commit comments

Comments
 (0)