Skip to content

Commit 78b2244

Browse files
committed
fix(test): add unit test for redis-dc
1 parent 6fd04e5 commit 78b2244

1 file changed

Lines changed: 214 additions & 0 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { SPAN_STATUS_ERROR } from '@sentry/core';
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3+
4+
// channels registry must be created before the vi.mock factory runs
5+
const channels = vi.hoisted(() => ({}) as Record<string, { subs: Record<string, (data: any) => void> }>);
6+
7+
vi.mock('@sentry/opentelemetry/tracing-channel', () => ({
8+
tracingChannel: (name: string, _transform: unknown) => {
9+
const subs: Record<string, (data: any) => void> = {};
10+
channels[name] = { subs };
11+
return { subscribe: (s: Record<string, (data: any) => void>) => Object.assign(subs, s) };
12+
},
13+
}));
14+
15+
import {
16+
_resetRedisDiagnosticChannelsForTesting,
17+
subscribeRedisDiagnosticChannels,
18+
} from '../../../../src/integrations/tracing/redis/redis-dc-subscriber';
19+
20+
const CHANNEL_COMMAND = 'node-redis:command';
21+
const CHANNEL_BATCH = 'node-redis:batch';
22+
const CHANNEL_CONNECT = 'node-redis:connect';
23+
24+
const subs = (name: string) =>
25+
channels[name]?.subs as {
26+
asyncEnd: (data: any) => void;
27+
error: (data: any) => void;
28+
};
29+
30+
function makeSpan() {
31+
return {
32+
end: vi.fn(),
33+
setStatus: vi.fn(),
34+
setAttribute: vi.fn(),
35+
setAttributes: vi.fn(),
36+
updateName: vi.fn(),
37+
spanContext: () => ({ spanId: 'test-span-id', traceId: 'test-trace-id', traceFlags: 1 }),
38+
};
39+
}
40+
41+
describe('redis-dc-subscriber', () => {
42+
let mockSpan: ReturnType<typeof makeSpan>;
43+
let responseHook: ReturnType<typeof vi.fn>;
44+
45+
beforeEach(() => {
46+
_resetRedisDiagnosticChannelsForTesting();
47+
mockSpan = makeSpan();
48+
responseHook = vi.fn();
49+
subscribeRedisDiagnosticChannels(responseHook);
50+
});
51+
52+
afterEach(() => {
53+
vi.clearAllMocks();
54+
});
55+
56+
describe('command channel', () => {
57+
describe('asyncEnd (success path)', () => {
58+
it('calls the response hook with sliced args and ends the span', () => {
59+
const data = {
60+
command: 'GET',
61+
args: ['GET', 'cache:key'],
62+
result: 'hit-value',
63+
_sentrySpan: mockSpan,
64+
};
65+
subs(CHANNEL_COMMAND).asyncEnd(data);
66+
67+
expect(responseHook).toHaveBeenCalledWith(mockSpan, 'GET', ['cache:key'], 'hit-value');
68+
expect(mockSpan.end).toHaveBeenCalledTimes(1);
69+
});
70+
71+
it('strips the command name from args before passing to the response hook', () => {
72+
const data = {
73+
command: 'MGET',
74+
args: ['MGET', 'key1', 'key2', 'key3'],
75+
result: ['v1', 'v2', 'v3'],
76+
_sentrySpan: mockSpan,
77+
};
78+
subs(CHANNEL_COMMAND).asyncEnd(data);
79+
80+
expect(responseHook).toHaveBeenCalledWith(mockSpan, 'MGET', ['key1', 'key2', 'key3'], ['v1', 'v2', 'v3']);
81+
});
82+
83+
it('bails early when _sentrySpan is absent', () => {
84+
subs(CHANNEL_COMMAND).asyncEnd({ command: 'GET', args: ['GET', 'k'], result: 'v' });
85+
86+
expect(responseHook).not.toHaveBeenCalled();
87+
expect(mockSpan.end).not.toHaveBeenCalled();
88+
});
89+
});
90+
91+
describe('error path', () => {
92+
it('sets error status and ends the span in the error handler', () => {
93+
const error = new Error('ECONNREFUSED');
94+
const data = { command: 'SET', args: ['SET', 'k', 'v'], error, _sentrySpan: mockSpan };
95+
subs(CHANNEL_COMMAND).error(data);
96+
97+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SPAN_STATUS_ERROR, message: 'ECONNREFUSED' });
98+
expect(mockSpan.end).toHaveBeenCalledTimes(1);
99+
});
100+
101+
it('does not call the response hook or end the span a second time in asyncEnd when error is set', () => {
102+
const error = new Error('ECONNREFUSED');
103+
const data = { command: 'GET', args: ['GET', 'k'], error, _sentrySpan: mockSpan };
104+
105+
// TracingChannel fires error first, then asyncEnd, on the same data object
106+
subs(CHANNEL_COMMAND).error(data);
107+
subs(CHANNEL_COMMAND).asyncEnd(data);
108+
109+
expect(responseHook).not.toHaveBeenCalled();
110+
expect(mockSpan.end).toHaveBeenCalledTimes(1);
111+
});
112+
113+
it('bails early in error handler when _sentrySpan is absent', () => {
114+
subs(CHANNEL_COMMAND).error({ command: 'GET', args: ['GET', 'k'], error: new Error('x') });
115+
116+
expect(mockSpan.setStatus).not.toHaveBeenCalled();
117+
expect(mockSpan.end).not.toHaveBeenCalled();
118+
});
119+
});
120+
});
121+
122+
describe('batch channel', () => {
123+
describe('asyncEnd (success path)', () => {
124+
it('ends the span', () => {
125+
const data = { batchMode: 'PIPELINE', batchSize: 3, _sentrySpan: mockSpan };
126+
subs(CHANNEL_BATCH).asyncEnd(data);
127+
128+
expect(mockSpan.end).toHaveBeenCalledTimes(1);
129+
});
130+
131+
it('bails early when _sentrySpan is absent', () => {
132+
subs(CHANNEL_BATCH).asyncEnd({ batchMode: 'MULTI' });
133+
134+
expect(mockSpan.end).not.toHaveBeenCalled();
135+
});
136+
});
137+
138+
describe('error path', () => {
139+
it('sets error status and ends the span in the error handler', () => {
140+
const error = new Error('MULTI aborted');
141+
const data = { batchMode: 'MULTI', error, _sentrySpan: mockSpan };
142+
subs(CHANNEL_BATCH).error(data);
143+
144+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SPAN_STATUS_ERROR, message: 'MULTI aborted' });
145+
expect(mockSpan.end).toHaveBeenCalledTimes(1);
146+
});
147+
148+
it('does not end the span a second time in asyncEnd when error is set', () => {
149+
const error = new Error('MULTI aborted');
150+
const data = { batchMode: 'MULTI', error, _sentrySpan: mockSpan };
151+
152+
subs(CHANNEL_BATCH).error(data);
153+
subs(CHANNEL_BATCH).asyncEnd(data);
154+
155+
expect(mockSpan.end).toHaveBeenCalledTimes(1);
156+
});
157+
});
158+
});
159+
160+
describe('connect channel', () => {
161+
describe('asyncEnd (success path)', () => {
162+
it('ends the span', () => {
163+
const data = { serverAddress: '127.0.0.1', serverPort: 6379, _sentrySpan: mockSpan };
164+
subs(CHANNEL_CONNECT).asyncEnd(data);
165+
166+
expect(mockSpan.end).toHaveBeenCalledTimes(1);
167+
});
168+
169+
it('bails early when _sentrySpan is absent', () => {
170+
subs(CHANNEL_CONNECT).asyncEnd({ serverAddress: '127.0.0.1' });
171+
172+
expect(mockSpan.end).not.toHaveBeenCalled();
173+
});
174+
});
175+
176+
describe('error path', () => {
177+
it('sets error status and ends the span in the error handler', () => {
178+
const error = new Error('connect ECONNREFUSED');
179+
const data = { serverAddress: '127.0.0.1', serverPort: 6379, error, _sentrySpan: mockSpan };
180+
subs(CHANNEL_CONNECT).error(data);
181+
182+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SPAN_STATUS_ERROR, message: 'connect ECONNREFUSED' });
183+
expect(mockSpan.end).toHaveBeenCalledTimes(1);
184+
});
185+
186+
it('does not end the span a second time in asyncEnd when error is set', () => {
187+
const error = new Error('connect ECONNREFUSED');
188+
const data = { serverAddress: '127.0.0.1', error, _sentrySpan: mockSpan };
189+
190+
subs(CHANNEL_CONNECT).error(data);
191+
subs(CHANNEL_CONNECT).asyncEnd(data);
192+
193+
expect(mockSpan.end).toHaveBeenCalledTimes(1);
194+
});
195+
});
196+
});
197+
198+
describe('subscribeRedisDiagnosticChannels', () => {
199+
it('is idempotent — does not re-subscribe if called again', () => {
200+
// subscribeRedisDiagnosticChannels was already called in beforeEach.
201+
// Calling again should not throw or overwrite subscribers.
202+
const secondHook = vi.fn();
203+
subscribeRedisDiagnosticChannels(secondHook);
204+
205+
// The second hook should still be active (currentResponseHook is updated regardless)
206+
// but no new channel setup should occur.
207+
const data = { command: 'GET', args: ['GET', 'k'], result: 'v', _sentrySpan: mockSpan };
208+
subs(CHANNEL_COMMAND).asyncEnd(data);
209+
210+
expect(secondHook).toHaveBeenCalledTimes(1);
211+
expect(responseHook).not.toHaveBeenCalled();
212+
});
213+
});
214+
});

0 commit comments

Comments
 (0)