Skip to content

Commit a445510

Browse files
committed
test: adding more unit tests
1 parent b279ce9 commit a445510

6 files changed

Lines changed: 589 additions & 250 deletions

File tree

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import type { LDLogger } from '@launchdarkly/js-client-sdk-common';
2+
3+
import { createClient } from '../src';
4+
import NodeDataManager from '../src/NodeDataManager';
5+
import { makeMockPlatform, mockFetch } from './NodeClient.mocks';
6+
7+
// Replace NodePlatform's constructor with one that returns the mock platform. Lets us
8+
// inject deterministic fetch / EventSource without touching the real filesystem or network.
9+
jest.mock('../src/platform/NodePlatform', () => {
10+
const { makeMockPlatform: makePlatform } = jest.requireActual('./NodeClient.mocks');
11+
return {
12+
__esModule: true,
13+
default: jest.fn().mockImplementation(() => makePlatform()),
14+
};
15+
});
16+
17+
const NodePlatformMock = jest.requireMock('../src/platform/NodePlatform').default as jest.Mock;
18+
19+
const bootstrapData = {
20+
'string-flag': 'is bob',
21+
'my-boolean-flag': false,
22+
$flagsState: {
23+
'string-flag': { variation: 1, version: 3 },
24+
'my-boolean-flag': { variation: 1, version: 11 },
25+
},
26+
$valid: true,
27+
};
28+
29+
let logger: LDLogger;
30+
31+
beforeEach(() => {
32+
logger = {
33+
error: jest.fn(),
34+
warn: jest.fn(),
35+
info: jest.fn(),
36+
debug: jest.fn(),
37+
};
38+
NodePlatformMock.mockReset();
39+
NodePlatformMock.mockImplementation(() => makeMockPlatform());
40+
});
41+
42+
afterEach(() => {
43+
jest.restoreAllMocks();
44+
});
45+
46+
it('start with streaming + bootstrap resolves and opens streaming connection', async () => {
47+
const fakePlatform = makeMockPlatform();
48+
NodePlatformMock.mockImplementationOnce(() => fakePlatform);
49+
50+
const client = createClient(
51+
'client-side-id',
52+
{ kind: 'user', key: 'bob' },
53+
{
54+
initialConnectionMode: 'streaming',
55+
sendEvents: false,
56+
diagnosticOptOut: true,
57+
logger,
58+
},
59+
);
60+
61+
const result = await client.start({ bootstrap: bootstrapData });
62+
63+
expect(result.status).toBe('complete');
64+
expect(client.stringVariation('string-flag', 'default')).toBe('is bob');
65+
expect(client.boolVariation('my-boolean-flag', true)).toBe(false);
66+
// Streaming connection was opened for ongoing updates (the fix lets streaming start
67+
// alongside bootstrap; the previous bug just routed identify callbacks through it).
68+
expect(fakePlatform.requests.createEventSource).toHaveBeenCalled();
69+
70+
await client.close();
71+
});
72+
73+
it('bootstrap in streaming mode invokes _setupConnection without identify callbacks (regression guard)', async () => {
74+
const setupSpy = jest.spyOn(NodeDataManager.prototype as any, '_setupConnection');
75+
const fakePlatform = makeMockPlatform();
76+
NodePlatformMock.mockImplementationOnce(() => fakePlatform);
77+
78+
const client = createClient(
79+
'client-side-id',
80+
{ kind: 'user', key: 'bob' },
81+
{
82+
initialConnectionMode: 'streaming',
83+
sendEvents: false,
84+
diagnosticOptOut: true,
85+
logger,
86+
},
87+
);
88+
89+
await client.start({ bootstrap: bootstrapData });
90+
91+
// The fix: streaming setup happens without forwarding identify callbacks, since
92+
// bootstrap already resolved identify.
93+
expect(setupSpy).toHaveBeenCalled();
94+
const lastCallArgs = setupSpy.mock.calls[setupSpy.mock.calls.length - 1];
95+
expect(lastCallArgs[1]).toBeUndefined();
96+
expect(lastCallArgs[2]).toBeUndefined();
97+
98+
await client.close();
99+
});
100+
101+
it('polling mode without bootstrap uses identify callbacks on _setupConnection', async () => {
102+
const setupSpy = jest.spyOn(NodeDataManager.prototype as any, '_setupConnection');
103+
const fakePlatform = makeMockPlatform({
104+
requests: {
105+
fetch: mockFetch(JSON.stringify(bootstrapData), 200),
106+
createEventSource: jest.fn(),
107+
getEventSourceCapabilities: () => ({ readTimeout: false, headers: true, customMethod: false }),
108+
},
109+
});
110+
NodePlatformMock.mockImplementationOnce(() => fakePlatform);
111+
112+
const client = createClient(
113+
'client-side-id',
114+
{ kind: 'user', key: 'bob' },
115+
{
116+
initialConnectionMode: 'polling',
117+
sendEvents: false,
118+
diagnosticOptOut: true,
119+
logger,
120+
},
121+
);
122+
123+
await client.start({ timeout: 2 });
124+
125+
// Without bootstrap, identify is resolved via the network processor -- callbacks
126+
// must be forwarded to _setupConnection.
127+
expect(setupSpy).toHaveBeenCalled();
128+
const firstCallArgs = setupSpy.mock.calls[0];
129+
expect(typeof firstCallArgs[1]).toBe('function');
130+
expect(typeof firstCallArgs[2]).toBe('function');
131+
132+
await client.close();
133+
});
134+
135+
it('polling mode opens a fetch request to the polling endpoint', async () => {
136+
const fetchMock = mockFetch(JSON.stringify(bootstrapData), 200);
137+
const fakePlatform = makeMockPlatform({
138+
requests: {
139+
fetch: fetchMock,
140+
createEventSource: jest.fn(),
141+
getEventSourceCapabilities: () => ({ readTimeout: false, headers: true, customMethod: false }),
142+
},
143+
});
144+
NodePlatformMock.mockImplementationOnce(() => fakePlatform);
145+
146+
const client = createClient(
147+
'client-side-id',
148+
{ kind: 'user', key: 'bob' },
149+
{
150+
initialConnectionMode: 'polling',
151+
sendEvents: false,
152+
diagnosticOptOut: true,
153+
logger,
154+
},
155+
);
156+
157+
await client.start({ timeout: 2 });
158+
159+
const pollingCall = fetchMock.mock.calls.find(([url]: [string]) => url.includes('/sdk/evalx/'));
160+
expect(pollingCall).toBeDefined();
161+
162+
await client.close();
163+
});
164+
165+
it('streaming mode opens an EventSource to the streaming endpoint with authorization header', async () => {
166+
const createEventSource = jest.fn(() => ({
167+
addEventListener: jest.fn(),
168+
close: jest.fn(),
169+
onclose: jest.fn(),
170+
onerror: jest.fn(),
171+
onopen: jest.fn(),
172+
onretrying: jest.fn(),
173+
}));
174+
const fakePlatform = makeMockPlatform({
175+
requests: {
176+
fetch: jest.fn(),
177+
createEventSource: createEventSource as any,
178+
getEventSourceCapabilities: () => ({ readTimeout: false, headers: true, customMethod: false }),
179+
},
180+
});
181+
NodePlatformMock.mockImplementationOnce(() => fakePlatform);
182+
183+
const client = createClient(
184+
'client-side-id',
185+
{ kind: 'user', key: 'bob' },
186+
{
187+
initialConnectionMode: 'streaming',
188+
sendEvents: false,
189+
diagnosticOptOut: true,
190+
logger,
191+
},
192+
);
193+
194+
await client.start({ bootstrap: bootstrapData });
195+
196+
expect(createEventSource).toHaveBeenCalled();
197+
const firstCall = (createEventSource.mock.calls as unknown as [string, any][])[0];
198+
expect(firstCall[0]).toMatch(/\/eval\//);
199+
expect(firstCall[1].headers).toMatchObject({ authorization: 'client-side-id' });
200+
201+
await client.close();
202+
});
203+
204+
it('setConnectionMode offline -> streaming brings the data source back up', async () => {
205+
const fakePlatform = makeMockPlatform();
206+
NodePlatformMock.mockImplementationOnce(() => fakePlatform);
207+
208+
const client = createClient(
209+
'client-side-id',
210+
{ kind: 'user', key: 'bob' },
211+
{
212+
initialConnectionMode: 'offline',
213+
sendEvents: false,
214+
diagnosticOptOut: true,
215+
logger,
216+
},
217+
);
218+
219+
await client.start({ bootstrap: bootstrapData });
220+
expect(client.isOffline()).toBe(true);
221+
expect(fakePlatform.requests.createEventSource).not.toHaveBeenCalled();
222+
223+
await client.setConnectionMode('streaming');
224+
expect(client.isOffline()).toBe(false);
225+
expect(client.getConnectionMode()).toBe('streaming');
226+
expect(fakePlatform.requests.createEventSource).toHaveBeenCalled();
227+
228+
await client.close();
229+
});
230+
231+
it('streaming with useReport opens an EventSource using REPORT to the no-context path', async () => {
232+
const createEventSource = jest.fn(() => ({
233+
addEventListener: jest.fn(),
234+
close: jest.fn(),
235+
onclose: jest.fn(),
236+
onerror: jest.fn(),
237+
onopen: jest.fn(),
238+
onretrying: jest.fn(),
239+
}));
240+
const fakePlatform = makeMockPlatform({
241+
requests: {
242+
fetch: jest.fn(),
243+
createEventSource: createEventSource as any,
244+
getEventSourceCapabilities: () => ({ readTimeout: true, headers: true, customMethod: true }),
245+
},
246+
});
247+
NodePlatformMock.mockImplementationOnce(() => fakePlatform);
248+
249+
const client = createClient(
250+
'client-side-id',
251+
{ kind: 'user', key: 'bob' },
252+
{
253+
initialConnectionMode: 'streaming',
254+
sendEvents: false,
255+
diagnosticOptOut: true,
256+
useReport: true,
257+
logger,
258+
},
259+
);
260+
261+
await client.start({ bootstrap: bootstrapData });
262+
263+
expect(createEventSource).toHaveBeenCalled();
264+
const firstCall = (createEventSource.mock.calls as unknown as [string, any][])[0];
265+
// REPORT mode hits /eval/<env> without an encoded context segment in the path.
266+
expect(firstCall[0]).toMatch(/\/eval\/client-side-id(?:\?|$)/);
267+
268+
await client.close();
269+
});
270+
271+
it('setConnectionMode streaming -> offline tears down the EventSource', async () => {
272+
const eventSourceClose = jest.fn();
273+
const createEventSource = jest.fn(() => ({
274+
addEventListener: jest.fn(),
275+
close: eventSourceClose,
276+
onclose: jest.fn(),
277+
onerror: jest.fn(),
278+
onopen: jest.fn(),
279+
onretrying: jest.fn(),
280+
}));
281+
const fakePlatform = makeMockPlatform({
282+
requests: {
283+
fetch: jest.fn(),
284+
createEventSource: createEventSource as any,
285+
getEventSourceCapabilities: () => ({ readTimeout: false, headers: true, customMethod: false }),
286+
},
287+
});
288+
NodePlatformMock.mockImplementationOnce(() => fakePlatform);
289+
290+
const client = createClient(
291+
'client-side-id',
292+
{ kind: 'user', key: 'bob' },
293+
{
294+
initialConnectionMode: 'streaming',
295+
sendEvents: false,
296+
diagnosticOptOut: true,
297+
logger,
298+
},
299+
);
300+
301+
await client.start({ bootstrap: bootstrapData });
302+
expect(createEventSource).toHaveBeenCalledTimes(1);
303+
304+
await client.setConnectionMode('offline');
305+
expect(client.isOffline()).toBe(true);
306+
expect(eventSourceClose).toHaveBeenCalled();
307+
308+
await client.close();
309+
});

0 commit comments

Comments
 (0)