Skip to content

Commit 2767d76

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

3 files changed

Lines changed: 381 additions & 0 deletions

File tree

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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('setConnectionMode streaming -> offline tears down the EventSource', async () => {
232+
const eventSourceClose = jest.fn();
233+
const createEventSource = jest.fn(() => ({
234+
addEventListener: jest.fn(),
235+
close: eventSourceClose,
236+
onclose: jest.fn(),
237+
onerror: jest.fn(),
238+
onopen: jest.fn(),
239+
onretrying: jest.fn(),
240+
}));
241+
const fakePlatform = makeMockPlatform({
242+
requests: {
243+
fetch: jest.fn(),
244+
createEventSource: createEventSource as any,
245+
getEventSourceCapabilities: () => ({ readTimeout: false, headers: true, customMethod: false }),
246+
},
247+
});
248+
NodePlatformMock.mockImplementationOnce(() => fakePlatform);
249+
250+
const client = createClient(
251+
'client-side-id',
252+
{ kind: 'user', key: 'bob' },
253+
{
254+
initialConnectionMode: 'streaming',
255+
sendEvents: false,
256+
diagnosticOptOut: true,
257+
logger,
258+
},
259+
);
260+
261+
await client.start({ bootstrap: bootstrapData });
262+
expect(createEventSource).toHaveBeenCalledTimes(1);
263+
264+
await client.setConnectionMode('offline');
265+
expect(client.isOffline()).toBe(true);
266+
expect(eventSourceClose).toHaveBeenCalled();
267+
268+
await client.close();
269+
});
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import type {
2+
EventSource,
3+
EventSourceCapabilities,
4+
EventSourceInitDict,
5+
Platform,
6+
PlatformData,
7+
Requests,
8+
Response,
9+
SdkData,
10+
} from '@launchdarkly/js-client-sdk-common';
11+
12+
function mockResponse(value: string, statusCode: number): Promise<Response> {
13+
const response: Response = {
14+
headers: {
15+
get: jest.fn(() => null),
16+
keys: jest.fn(),
17+
values: jest.fn(),
18+
entries: jest.fn(),
19+
has: jest.fn(),
20+
},
21+
status: statusCode,
22+
text: () => Promise.resolve(value),
23+
json: () => Promise.resolve(JSON.parse(value)),
24+
};
25+
return Promise.resolve(response);
26+
}
27+
28+
export function mockFetch(value: string, statusCode: number = 200): jest.Mock {
29+
const f = jest.fn();
30+
f.mockResolvedValue(mockResponse(value, statusCode));
31+
return f;
32+
}
33+
34+
export interface MockEventSource extends EventSource {
35+
streamUri?: string;
36+
options?: EventSourceInitDict;
37+
}
38+
39+
export function makeMockEventSource(streamUri: string = '', options?: EventSourceInitDict): MockEventSource {
40+
return {
41+
streamUri,
42+
options,
43+
addEventListener: jest.fn(),
44+
close: jest.fn(),
45+
onclose: jest.fn(),
46+
onerror: jest.fn(),
47+
onopen: jest.fn(),
48+
onretrying: jest.fn(),
49+
} as unknown as MockEventSource;
50+
}
51+
52+
export function makeMockRequests(): Requests {
53+
return {
54+
fetch: mockFetch('{"flagA": true}', 200),
55+
createEventSource: jest.fn((streamUri: string, options: EventSourceInitDict) =>
56+
makeMockEventSource(streamUri, options),
57+
),
58+
getEventSourceCapabilities: (): EventSourceCapabilities => ({
59+
readTimeout: false,
60+
headers: true,
61+
customMethod: false,
62+
}),
63+
};
64+
}
65+
66+
export interface MockPlatformOptions {
67+
wrapperName?: string;
68+
wrapperVersion?: string;
69+
requests?: Requests;
70+
}
71+
72+
export function makeMockPlatform(options: MockPlatformOptions = {}): Platform {
73+
const requests = options.requests ?? makeMockRequests();
74+
return {
75+
requests,
76+
info: {
77+
platformData(): PlatformData {
78+
return { name: 'Node' };
79+
},
80+
sdkData(): SdkData {
81+
const sdkData: SdkData = {
82+
name: 'node-client-sdk',
83+
version: '0.0.1',
84+
userAgentBase: 'NodeClient',
85+
};
86+
if (options.wrapperName) {
87+
sdkData.wrapperName = options.wrapperName;
88+
}
89+
if (options.wrapperVersion) {
90+
sdkData.wrapperVersion = options.wrapperVersion;
91+
}
92+
return sdkData;
93+
},
94+
},
95+
crypto: {
96+
createHash: () => ({
97+
update: () => ({ digest: () => 'mock-digest' }),
98+
digest: () => 'mock-digest',
99+
}),
100+
randomUUID: () => 'mock-uuid',
101+
},
102+
storage: {
103+
get: jest.fn(async (_key: string) => null),
104+
set: jest.fn(async (_key: string, _value: string) => {}),
105+
clear: jest.fn(async (_key: string) => {}),
106+
},
107+
encoding: {
108+
btoa: (str: string) => Buffer.from(str).toString('base64'),
109+
},
110+
} as unknown as Platform;
111+
}

packages/sdk/node-client/__tests__/NodeClient.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ it('createClient returns the documented LDClient surface', () => {
5252
expect(typeof client.setConnectionMode).toBe('function');
5353
expect(typeof client.getConnectionMode).toBe('function');
5454
expect(typeof client.isOffline).toBe('function');
55+
expect(typeof client.getContext).toBe('function');
5556
expect(client.logger).toBeDefined();
5657
});
5758

0 commit comments

Comments
 (0)