Skip to content

Commit 16b7732

Browse files
committed
More tests
1 parent 2bdc298 commit 16b7732

File tree

4 files changed

+186
-63
lines changed

4 files changed

+186
-63
lines changed

packages/sdk/browser/__tests__/BrowserClient.test.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import {
44
AutoEnvAttributes,
55
EventSourceCapabilities,
66
EventSourceInitDict,
7-
Hasher,
87
LDLogger,
98
PlatformData,
109
Requests,
1110
SdkData,
1211
} from '@launchdarkly/js-client-sdk-common';
1312

1413
import { BrowserClient } from '../src/BrowserClient';
14+
import { MockHasher } from './MockHasher';
1515

1616
function mockResponse(value: string, statusCode: number) {
1717
const response: Response = {
@@ -79,18 +79,6 @@ function makeRequests(): Requests {
7979
};
8080
}
8181

82-
class MockHasher implements Hasher {
83-
update(_data: string): Hasher {
84-
return this;
85-
}
86-
digest?(_encoding: string): string {
87-
return 'hashed';
88-
}
89-
async asyncDigest?(_encoding: string): Promise<string> {
90-
return 'hashed';
91-
}
92-
}
93-
9482
describe('given a mock platform for a BrowserClient', () => {
9583
const logger: LDLogger = {
9684
debug: jest.fn(),
Lines changed: 154 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import { jest } from '@jest/globals';
2+
import { TextEncoder } from 'node:util';
3+
14
import {
25
ApplicationTags,
6+
base64UrlEncode,
37
Configuration,
48
Context,
9+
Encoding,
510
FlagManager,
611
internal,
712
LDEmitter,
@@ -14,15 +19,26 @@ import {
1419
} from '@launchdarkly/js-client-sdk-common';
1520

1621
import BrowserDataManager from '../src/BrowserDataManager';
17-
import { ValidatedOptions } from '../src/options';
22+
import validateOptions, { ValidatedOptions } from '../src/options';
23+
import BrowserEncoding from '../src/platform/BrowserEncoding';
24+
import BrowserInfo from '../src/platform/BrowserInfo';
25+
import LocalStorage from '../src/platform/LocalStorage';
26+
import { MockHasher } from './MockHasher';
27+
28+
global.TextEncoder = TextEncoder;
1829

1930
function mockResponse(value: string, statusCode: number) {
2031
const response: Response = {
2132
headers: {
33+
// @ts-ignore
2234
get: jest.fn(),
35+
// @ts-ignore
2336
keys: jest.fn(),
37+
// @ts-ignore
2438
values: jest.fn(),
39+
// @ts-ignore
2540
entries: jest.fn(),
41+
// @ts-ignore
2642
has: jest.fn(),
2743
},
2844
status: statusCode,
@@ -32,8 +48,14 @@ function mockResponse(value: string, statusCode: number) {
3248
return Promise.resolve(response);
3349
}
3450

51+
/**
52+
* Mocks fetch. Returns the fetch jest.Mock object.
53+
* @param remoteJson
54+
* @param statusCode
55+
*/
3556
function mockFetch(value: string, statusCode: number = 200) {
3657
const f = jest.fn();
58+
// @ts-ignore
3759
f.mockResolvedValue(mockResponse(value, statusCode));
3860
return f;
3961
}
@@ -46,9 +68,8 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
4668
let baseHeaders: LDHeaders;
4769
let emitter: jest.Mocked<LDEmitter>;
4870
let diagnosticsManager: jest.Mocked<internal.DiagnosticsManager>;
49-
let browserDataManager: BrowserDataManager;
71+
let dataManager: BrowserDataManager;
5072
let logger: LDLogger;
51-
5273
beforeEach(() => {
5374
logger = {
5475
error: jest.fn(),
@@ -79,40 +100,67 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
79100
pollInterval: 1000,
80101
userAgentHeaderName: 'user-agent',
81102
trackEventModifier: (event) => event,
82-
}
103+
};
83104
const mockedFetch = mockFetch('{"flagA": true}', 200);
84105
platform = {
106+
crypto: {
107+
createHash: () => new MockHasher(),
108+
randomUUID: () => '123',
109+
},
110+
info: new BrowserInfo(),
85111
requests: {
112+
createEventSource: jest.fn((streamUri: string = '', options: any = {}) => ({
113+
streamUri,
114+
options,
115+
onclose: jest.fn(),
116+
addEventListener: jest.fn(),
117+
close: jest.fn(),
118+
})),
86119
fetch: mockedFetch,
87-
createEventSource: jest.fn(),
88120
getEventSourceCapabilities: jest.fn(),
89121
},
122+
storage: new LocalStorage(config.logger),
123+
encoding: new BrowserEncoding(),
90124
} as unknown as jest.Mocked<Platform>;
91125

92126
flagManager = {
93127
loadCached: jest.fn(),
128+
get: jest.fn(),
129+
getAll: jest.fn(),
130+
init: jest.fn(),
131+
upsert: jest.fn(),
132+
on: jest.fn(),
133+
off: jest.fn(),
94134
} as unknown as jest.Mocked<FlagManager>;
95135

96-
browserConfig = { stream: true } as ValidatedOptions;
136+
browserConfig = validateOptions({ stream: false }, logger);
97137
baseHeaders = {};
98138
emitter = {
99139
emit: jest.fn(),
100140
} as unknown as jest.Mocked<LDEmitter>;
101141
diagnosticsManager = {} as unknown as jest.Mocked<internal.DiagnosticsManager>;
102142

103-
browserDataManager = new BrowserDataManager(
143+
dataManager = new BrowserDataManager(
104144
platform,
105145
flagManager,
106146
'test-credential',
107147
config,
108148
browserConfig,
109149
() => ({
110-
pathGet: jest.fn(),
111-
pathReport: jest.fn(),
150+
pathGet(encoding: Encoding, _plainContextString: string): string {
151+
return `/msdk/evalx/contexts/${base64UrlEncode(_plainContextString, encoding)}`;
152+
},
153+
pathReport(_encoding: Encoding, _plainContextString: string): string {
154+
return `/msdk/evalx/context`;
155+
},
112156
}),
113157
() => ({
114-
pathGet: jest.fn(),
115-
pathReport: jest.fn(),
158+
pathGet(encoding: Encoding, _plainContextString: string): string {
159+
return `/meval/${base64UrlEncode(_plainContextString, encoding)}`;
160+
},
161+
pathReport(_encoding: Encoding, _plainContextString: string): string {
162+
return `/meval`;
163+
},
116164
}),
117165
baseHeaders,
118166
emitter,
@@ -124,71 +172,130 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
124172
jest.resetAllMocks();
125173
});
126174

127-
it('should load cached flags and continue to initialize via a poll', async () => {
175+
it('creates an event source when stream is true', async () => {
176+
dataManager = new BrowserDataManager(
177+
platform,
178+
flagManager,
179+
'test-credential',
180+
config,
181+
validateOptions({ stream: true }, logger),
182+
() => ({
183+
pathGet(encoding: Encoding, _plainContextString: string): string {
184+
return `/msdk/evalx/contexts/${base64UrlEncode(_plainContextString, encoding)}`;
185+
},
186+
pathReport(_encoding: Encoding, _plainContextString: string): string {
187+
return `/msdk/evalx/context`;
188+
},
189+
}),
190+
() => ({
191+
pathGet(encoding: Encoding, _plainContextString: string): string {
192+
return `/meval/${base64UrlEncode(_plainContextString, encoding)}`;
193+
},
194+
pathReport(_encoding: Encoding, _plainContextString: string): string {
195+
return `/meval`;
196+
},
197+
}),
198+
baseHeaders,
199+
emitter,
200+
diagnosticsManager,
201+
);
202+
203+
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
204+
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
205+
const identifyResolve = jest.fn();
206+
const identifyReject = jest.fn();
207+
208+
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
209+
210+
expect(platform.requests.createEventSource).toHaveBeenCalled();
211+
});
212+
213+
it('should load cached flags and continue to poll to complete identify', async () => {
128214
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
129-
const identifyOptions: LDIdentifyOptions = {};
215+
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
130216
const identifyResolve = jest.fn();
131217
const identifyReject = jest.fn();
132218

133219
flagManager.loadCached.mockResolvedValue(true);
134220

135-
await browserDataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
221+
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
136222

137223
expect(logger.debug).toHaveBeenCalledWith(
138-
'Identify - Flags loaded from cache. Continuing to initialize via a poll.',
224+
'[BrowserDataManager] Identify - Flags loaded from cache. Continuing to initialize via a poll.',
139225
);
226+
140227
expect(flagManager.loadCached).toHaveBeenCalledWith(context);
141-
expect(platform.requests.fetch).toHaveBeenCalled();
228+
expect(identifyResolve).toHaveBeenCalled();
229+
expect(flagManager.init).toHaveBeenCalledWith(
230+
expect.anything(),
231+
expect.objectContaining({ flagA: { flag: true, version: undefined } }),
232+
);
233+
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
142234
});
143235

144-
it('should set up streaming connection if stream is enabled', async () => {
236+
it('should identify from polling when there are no cached flags', async () => {
145237
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
146-
const identifyOptions: LDIdentifyOptions = {};
238+
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
147239
const identifyResolve = jest.fn();
148240
const identifyReject = jest.fn();
149241

150-
await browserDataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
242+
flagManager.loadCached.mockResolvedValue(false);
151243

152-
expect(platform.requests.createEventSource).toHaveBeenCalled();
244+
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
245+
246+
expect(logger.debug).not.toHaveBeenCalledWith(
247+
'Identify - Flags loaded from cache. Continuing to initialize via a poll.',
248+
);
249+
250+
expect(flagManager.loadCached).toHaveBeenCalledWith(context);
251+
expect(identifyResolve).toHaveBeenCalled();
252+
expect(flagManager.init).toHaveBeenCalledWith(
253+
expect.anything(),
254+
expect.objectContaining({ flagA: { flag: true, version: undefined } }),
255+
);
256+
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
153257
});
154258

155-
it('should not set up streaming connection if stream is disabled', async () => {
156-
browserConfig.stream = false;
259+
it('creates a stream when streaming is enabled after construction', async () => {
157260
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
158-
const identifyOptions: LDIdentifyOptions = {};
261+
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
159262
const identifyResolve = jest.fn();
160263
const identifyReject = jest.fn();
161264

162-
await browserDataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
265+
flagManager.loadCached.mockResolvedValue(false);
266+
267+
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
163268

164269
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
270+
dataManager.startDataSource();
271+
expect(platform.requests.createEventSource).toHaveBeenCalled();
165272
});
166273

167-
// it('should stop the data source', () => {
168-
// const mockClose = jest.fn();
169-
// browserDataManager.updateProcessor = { close: mockClose } as any;
170-
171-
// browserDataManager.stopDataSource();
172-
173-
// expect(mockClose).toHaveBeenCalled();
174-
// expect(browserDataManager.updateProcessor).toBeUndefined();
175-
// });
176-
177-
// it('should start the data source if context exists', () => {
178-
// const mockSetupConnection = jest.spyOn(browserDataManager as any, 'setupConnection');
179-
// browserDataManager.context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
180-
181-
// browserDataManager.startDataSource();
274+
it('does not re-create the stream if it already running', async () => {
275+
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
276+
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
277+
const identifyResolve = jest.fn();
278+
const identifyReject = jest.fn();
182279

183-
// expect(mockSetupConnection).toHaveBeenCalled();
184-
// });
280+
flagManager.loadCached.mockResolvedValue(false);
185281

186-
// it('should not start the data source if context does not exist', () => {
187-
// const mockSetupConnection = jest.spyOn(browserDataManager as any, 'setupConnection');
188-
// browserDataManager.context = undefined;
282+
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
189283

190-
// browserDataManager.startDataSource();
284+
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
285+
dataManager.startDataSource();
286+
dataManager.startDataSource();
287+
expect(platform.requests.createEventSource).toHaveBeenCalledTimes(1);
288+
expect(logger.debug).toHaveBeenCalledWith(
289+
'[BrowserDataManager] Update processor already active. Not changing state.',
290+
);
291+
});
191292

192-
// expect(mockSetupConnection).not.toHaveBeenCalled();
193-
// });
293+
it('does not start a stream if identify has not been called', async () => {
294+
expect(platform.requests.createEventSource).not.toHaveBeenCalled();
295+
dataManager.startDataSource();
296+
expect(platform.requests.createEventSource).not.toHaveBeenCalledTimes(1);
297+
expect(logger.debug).toHaveBeenCalledWith(
298+
'[BrowserDataManager] Context not set, not starting update processor.',
299+
);
300+
});
194301
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Hasher } from '@launchdarkly/js-client-sdk-common';
2+
3+
export class MockHasher implements Hasher {
4+
update(_data: string): Hasher {
5+
return this;
6+
}
7+
digest?(_encoding: string): string {
8+
return 'hashed';
9+
}
10+
async asyncDigest?(_encoding: string): Promise<string> {
11+
return 'hashed';
12+
}
13+
}

0 commit comments

Comments
 (0)