launchdarkly-js-sdk-common 3.4.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.circleci/config.yml +22 -0
- package/.eslintignore +4 -0
- package/.eslintrc.yaml +103 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/pull_request_template.md +21 -0
- package/.ldrelease/config.yml +24 -0
- package/.prettierignore +1 -0
- package/.prettierrc +5 -0
- package/CHANGELOG.md +24 -0
- package/CONTRIBUTING.md +45 -0
- package/babel.config.js +18 -0
- package/docs/typedoc.js +11 -0
- package/jest.config.js +12 -0
- package/package.json +5 -33
- package/scripts/better-audit.sh +76 -0
- package/src/EventEmitter.js +60 -0
- package/src/EventProcessor.js +175 -0
- package/src/EventSender.js +87 -0
- package/src/EventSummarizer.js +84 -0
- package/src/Identity.js +26 -0
- package/src/InitializationState.js +83 -0
- package/src/PersistentFlagStore.js +50 -0
- package/src/PersistentStorage.js +81 -0
- package/src/Requestor.js +111 -0
- package/src/Stream.js +154 -0
- package/src/UserFilter.js +75 -0
- package/src/UserValidator.js +56 -0
- package/src/__tests__/.eslintrc.yaml +6 -0
- package/src/__tests__/EventProcessor-test.js +559 -0
- package/src/__tests__/EventSender-test.js +252 -0
- package/src/__tests__/EventSource-mock.js +61 -0
- package/src/__tests__/EventSummarizer-test.js +103 -0
- package/src/__tests__/LDClient-events-test.js +757 -0
- package/src/__tests__/LDClient-localstorage-test.js +179 -0
- package/src/__tests__/LDClient-streaming-test.js +683 -0
- package/src/__tests__/LDClient-test.js +753 -0
- package/src/__tests__/PersistentFlagStore-test.js +111 -0
- package/src/__tests__/Requestor-test.js +362 -0
- package/src/__tests__/Stream-test.js +299 -0
- package/src/__tests__/UserFilter-test.js +93 -0
- package/src/__tests__/UserValidator-test.js +57 -0
- package/src/__tests__/configuration-test.js +217 -0
- package/src/__tests__/diagnosticEvents-test.js +449 -0
- package/src/__tests__/loggers-test.js +149 -0
- package/src/__tests__/mockHttp.js +122 -0
- package/src/__tests__/promiseCoalescer-test.js +128 -0
- package/src/__tests__/stubPlatform.js +148 -0
- package/src/__tests__/testUtils.js +77 -0
- package/src/__tests__/utils-test.js +148 -0
- package/src/configuration.js +151 -0
- package/src/diagnosticEvents.js +269 -0
- package/src/errors.js +37 -0
- package/src/index.js +769 -0
- package/src/jest.setup.js +1 -0
- package/src/loggers.js +93 -0
- package/src/messages.js +217 -0
- package/src/promiseCoalescer.js +52 -0
- package/src/utils.js +214 -0
- package/test-types.ts +94 -0
- package/tsconfig.json +13 -0
- package/typings.d.ts +98 -45
- package/dist/ldclient-common.cjs.js +0 -2
- package/dist/ldclient-common.cjs.js.map +0 -1
- package/dist/ldclient-common.es.js +0 -2
- package/dist/ldclient-common.es.js.map +0 -1
- package/dist/ldclient-common.min.js +0 -2
- package/dist/ldclient-common.min.js.map +0 -1
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
import * as LDClient from '../index';
|
|
2
|
+
import * as messages from '../messages';
|
|
3
|
+
import * as utils from '../utils';
|
|
4
|
+
|
|
5
|
+
import { eventSink, promisifySingle, sleepAsync, withCloseable, AsyncQueue } from 'launchdarkly-js-test-helpers';
|
|
6
|
+
|
|
7
|
+
import { respond, respondJson } from './mockHttp';
|
|
8
|
+
import * as stubPlatform from './stubPlatform';
|
|
9
|
+
import { makeBootstrap, numericUser, stringifiedNumericUser } from './testUtils';
|
|
10
|
+
|
|
11
|
+
describe('LDClient', () => {
|
|
12
|
+
const envName = 'UNKNOWN_ENVIRONMENT_ID';
|
|
13
|
+
const user = { key: 'user' };
|
|
14
|
+
let platform;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
platform = stubPlatform.defaults();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
async function withServers(asyncCallback) {
|
|
21
|
+
const pollServer = platform.testing.http.newServer();
|
|
22
|
+
const eventsServer = platform.testing.http.newServer();
|
|
23
|
+
pollServer.byDefault(respondJson({}));
|
|
24
|
+
eventsServer.byDefault(respond(202));
|
|
25
|
+
const baseConfig = { baseUrl: pollServer.url, eventsUrl: eventsServer.url };
|
|
26
|
+
return await asyncCallback(baseConfig, pollServer, eventsServer);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function withClient(user, extraConfig, asyncCallback) {
|
|
30
|
+
const client = platform.testing.makeClient(envName, user, { diagnosticOptOut: true, ...extraConfig });
|
|
31
|
+
return await withCloseable(client, asyncCallback);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function withDiagnosticsEnabledClient(user, extraConfig, asyncCallback) {
|
|
35
|
+
const client = platform.testing.makeClient(envName, user, { ...extraConfig });
|
|
36
|
+
return await withCloseable(client, asyncCallback);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
it('should exist', () => {
|
|
40
|
+
expect(LDClient).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('initialization', () => {
|
|
44
|
+
it('triggers "ready" event', async () => {
|
|
45
|
+
await withServers(async baseConfig => {
|
|
46
|
+
await withClient(user, baseConfig, async client => {
|
|
47
|
+
const gotReady = eventSink(client, 'ready');
|
|
48
|
+
await gotReady.take();
|
|
49
|
+
|
|
50
|
+
expect(platform.testing.logger.output.info).toEqual([messages.clientInitialized()]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('triggers "initialized" event', async () => {
|
|
56
|
+
await withServers(async baseConfig => {
|
|
57
|
+
await withClient(user, baseConfig, async client => {
|
|
58
|
+
const gotInited = eventSink(client, 'initialized');
|
|
59
|
+
await gotInited.take();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('resolves waitForInitialization promise', async () => {
|
|
65
|
+
await withServers(async baseConfig => {
|
|
66
|
+
await withClient(user, baseConfig, async client => {
|
|
67
|
+
await client.waitForInitialization();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('resolves waitUntilReady promise', async () => {
|
|
73
|
+
await withServers(async baseConfig => {
|
|
74
|
+
await withClient(user, baseConfig, async client => {
|
|
75
|
+
await client.waitUntilReady();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('fetches flag settings if bootstrap is not provided (without reasons)', async () => {
|
|
81
|
+
const flags = { flagKey: { value: true } };
|
|
82
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
83
|
+
pollServer.byDefault(respondJson(flags));
|
|
84
|
+
await withClient(user, baseConfig, async client => {
|
|
85
|
+
await client.waitForInitialization();
|
|
86
|
+
|
|
87
|
+
const req = await pollServer.nextRequest();
|
|
88
|
+
expect(req.path).toMatch(/sdk\/eval/);
|
|
89
|
+
expect(req.path).not.toMatch(/withReasons=true/);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('fetches flag settings if bootstrap is not provided (with reasons)', async () => {
|
|
95
|
+
const flags = { flagKey: { value: true, variation: 1, reason: { kind: 'OFF' } } };
|
|
96
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
97
|
+
pollServer.byDefault(respondJson(flags));
|
|
98
|
+
await withClient(user, { ...baseConfig, evaluationReasons: true }, async client => {
|
|
99
|
+
await client.waitForInitialization();
|
|
100
|
+
|
|
101
|
+
const req = await pollServer.nextRequest();
|
|
102
|
+
expect(req.path).toMatch(/sdk\/eval/);
|
|
103
|
+
expect(req.path).toMatch(/withReasons=true/);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
async function verifyCustomHeader(sendLDHeaders, shouldGetHeaders) {
|
|
109
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
110
|
+
await withClient(user, { ...baseConfig, sendLDHeaders }, async client => {
|
|
111
|
+
await client.waitForInitialization();
|
|
112
|
+
const request = await pollServer.nextRequest();
|
|
113
|
+
expect(request.headers['user-agent']).toEqual(
|
|
114
|
+
shouldGetHeaders ? utils.getLDUserAgentString(platform) : undefined
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
it('sends custom header by default', () => verifyCustomHeader(undefined, true));
|
|
121
|
+
|
|
122
|
+
it('sends custom header if sendLDHeaders is true', () => verifyCustomHeader(true, true));
|
|
123
|
+
|
|
124
|
+
it('does not send custom header if sendLDHeaders is false', () => verifyCustomHeader(undefined, true));
|
|
125
|
+
|
|
126
|
+
it('sanitizes the user', async () => {
|
|
127
|
+
await withServers(async baseConfig => {
|
|
128
|
+
await withClient(numericUser, baseConfig, async client => {
|
|
129
|
+
await client.waitForInitialization();
|
|
130
|
+
expect(client.getUser()).toEqual(stringifiedNumericUser);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('provides a persistent key for an anonymous user with no key', async () => {
|
|
136
|
+
const anonUser = { anonymous: true, country: 'US' };
|
|
137
|
+
await withServers(async baseConfig => {
|
|
138
|
+
let generatedUser;
|
|
139
|
+
await withClient(anonUser, baseConfig, async client0 => {
|
|
140
|
+
await client0.waitForInitialization();
|
|
141
|
+
|
|
142
|
+
generatedUser = client0.getUser();
|
|
143
|
+
expect(generatedUser.key).toEqual(expect.anything());
|
|
144
|
+
expect(generatedUser).toMatchObject(anonUser);
|
|
145
|
+
});
|
|
146
|
+
await withClient(anonUser, baseConfig, async client1 => {
|
|
147
|
+
await client1.waitForInitialization();
|
|
148
|
+
|
|
149
|
+
const newUser1 = client1.getUser();
|
|
150
|
+
expect(newUser1).toEqual(generatedUser);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('provides a key for an anonymous user with no key, even if local storage is unavailable', async () => {
|
|
156
|
+
platform.localStorage = null;
|
|
157
|
+
const anonUser = { anonymous: true, country: 'US' };
|
|
158
|
+
|
|
159
|
+
await withServers(async baseConfig => {
|
|
160
|
+
let generatedUser;
|
|
161
|
+
await withClient(anonUser, baseConfig, async client0 => {
|
|
162
|
+
await client0.waitForInitialization();
|
|
163
|
+
|
|
164
|
+
generatedUser = client0.getUser();
|
|
165
|
+
expect(generatedUser.key).toEqual(expect.anything());
|
|
166
|
+
expect(generatedUser).toMatchObject(anonUser);
|
|
167
|
+
});
|
|
168
|
+
await sleepAsync(100); // so that the time-based UUID algorithm will produce a different result below
|
|
169
|
+
await withClient(anonUser, baseConfig, async client1 => {
|
|
170
|
+
await client1.waitForInitialization();
|
|
171
|
+
|
|
172
|
+
const newUser1 = client1.getUser();
|
|
173
|
+
expect(newUser1.key).toEqual(expect.anything());
|
|
174
|
+
expect(newUser1.key).not.toEqual(generatedUser.key);
|
|
175
|
+
expect(newUser1).toMatchObject(anonUser);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('failed initialization', () => {
|
|
182
|
+
function doErrorTests(expectedMessage, doWithClientAsyncFn) {
|
|
183
|
+
async function runTest(asyncTest) {
|
|
184
|
+
try {
|
|
185
|
+
await doWithClientAsyncFn(asyncTest);
|
|
186
|
+
} finally {
|
|
187
|
+
// sleep briefly so any unhandled promise rejections will show up in this test, instead of
|
|
188
|
+
// in a later test
|
|
189
|
+
await sleepAsync(2);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
it('rejects waitForInitialization promise', async () => {
|
|
194
|
+
await runTest(async client => {
|
|
195
|
+
await expect(client.waitForInitialization()).rejects.toThrow();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('resolves waitUntilReady promise', async () => {
|
|
200
|
+
await runTest(async client => {
|
|
201
|
+
await client.waitUntilReady();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('emits "error" event', async () => {
|
|
206
|
+
await runTest(async client => {
|
|
207
|
+
const gotError = eventSink(client, 'error');
|
|
208
|
+
const err = await gotError.take();
|
|
209
|
+
expect(err.message).toEqual(expectedMessage);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('emits "failed" event', async () => {
|
|
214
|
+
await runTest(async client => {
|
|
215
|
+
const gotFailed = eventSink(client, 'failed');
|
|
216
|
+
const err = await gotFailed.take();
|
|
217
|
+
expect(err.message).toEqual(expectedMessage);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('emits "ready" event', async () => {
|
|
222
|
+
await runTest(async client => {
|
|
223
|
+
const gotReady = eventSink(client, 'ready');
|
|
224
|
+
await gotReady.take();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('returns default values', async () => {
|
|
229
|
+
await runTest(async client => {
|
|
230
|
+
await client.waitUntilReady();
|
|
231
|
+
expect(client.variation('flag-key', 1)).toEqual(1);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
describe('environment key not specified', () => {
|
|
237
|
+
doErrorTests(
|
|
238
|
+
messages.environmentNotSpecified(),
|
|
239
|
+
async callback => await withCloseable(platform.testing.makeClient('', user), callback)
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('invalid environment key (404 error)', () => {
|
|
244
|
+
doErrorTests(messages.environmentNotFound(), async callback => {
|
|
245
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
246
|
+
pollServer.byDefault(respond(404));
|
|
247
|
+
await withClient(user, baseConfig, callback);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('HTTP error other than 404 on initial poll', () => {
|
|
253
|
+
doErrorTests(messages.errorFetchingFlags(503), async callback => {
|
|
254
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
255
|
+
pollServer.byDefault(respond(503));
|
|
256
|
+
await withClient(user, baseConfig, callback);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('initialization with bootstrap object', () => {
|
|
263
|
+
it('should not fetch flag settings', async () => {
|
|
264
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
265
|
+
await withClient(user, { ...baseConfig, bootstrap: {} }, async client => {
|
|
266
|
+
await client.waitForInitialization();
|
|
267
|
+
|
|
268
|
+
expect(pollServer.requests.length()).toEqual(0);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('makes flags available immediately before ready event', async () => {
|
|
274
|
+
await withServers(async baseConfig => {
|
|
275
|
+
const initData = makeBootstrap({ foo: { value: 'bar', version: 1 } });
|
|
276
|
+
await withClient(user, { ...baseConfig, bootstrap: initData }, async client => {
|
|
277
|
+
expect(client.variation('foo')).toEqual('bar');
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('logs warning when bootstrap object uses old format', async () => {
|
|
283
|
+
const initData = { foo: 'bar' };
|
|
284
|
+
await withClient(user, { bootstrap: initData, sendEvents: false }, async client => {
|
|
285
|
+
await client.waitForInitialization();
|
|
286
|
+
|
|
287
|
+
expect(platform.testing.logger.output.warn).toEqual([messages.bootstrapOldFormat()]);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('does not log warning when bootstrap object uses new format', async () => {
|
|
292
|
+
const initData = makeBootstrap({ foo: { value: 'bar', version: 1 } });
|
|
293
|
+
await withClient(user, { bootstrap: initData, sendEvents: false }, async client => {
|
|
294
|
+
await client.waitForInitialization();
|
|
295
|
+
|
|
296
|
+
expect(platform.testing.logger.output.warn).toEqual([]);
|
|
297
|
+
expect(client.variation('foo')).toEqual('bar');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('variation', () => {
|
|
303
|
+
it('returns value for an existing flag - from bootstrap', async () => {
|
|
304
|
+
const config = {
|
|
305
|
+
bootstrap: makeBootstrap({ foo: { value: 'bar', version: 1 } }),
|
|
306
|
+
sendEvents: false,
|
|
307
|
+
};
|
|
308
|
+
await withClient(user, config, async client => {
|
|
309
|
+
await client.waitForInitialization();
|
|
310
|
+
|
|
311
|
+
expect(client.variation('foo')).toEqual('bar');
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('returns value for an existing flag - from bootstrap with old format', async () => {
|
|
316
|
+
const config = {
|
|
317
|
+
bootstrap: makeBootstrap({ foo: { value: 'bar', version: 1 } }),
|
|
318
|
+
sendEvents: false,
|
|
319
|
+
};
|
|
320
|
+
await withClient(user, config, async client => {
|
|
321
|
+
await client.waitForInitialization();
|
|
322
|
+
|
|
323
|
+
expect(client.variation('foo')).toEqual('bar');
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('returns value for an existing flag - from polling', async () => {
|
|
328
|
+
const flags = { 'enable-foo': { value: true, version: 1, variation: 2 } };
|
|
329
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
330
|
+
pollServer.byDefault(respondJson(flags));
|
|
331
|
+
await withClient(user, baseConfig, async client => {
|
|
332
|
+
await client.waitForInitialization();
|
|
333
|
+
|
|
334
|
+
expect(client.variation('enable-foo', 1)).toEqual(true);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('returns default value for flag that had null value', async () => {
|
|
340
|
+
const flags = { 'enable-foo': { value: null, version: 1, variation: 2 } };
|
|
341
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
342
|
+
pollServer.byDefault(respondJson(flags));
|
|
343
|
+
await withClient(user, baseConfig, async client => {
|
|
344
|
+
await client.waitForInitialization();
|
|
345
|
+
|
|
346
|
+
expect(client.variation('foo', 'default')).toEqual('default');
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('returns default value for unknown flag', async () => {
|
|
352
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
353
|
+
pollServer.byDefault(respondJson({}));
|
|
354
|
+
await withClient(user, baseConfig, async client => {
|
|
355
|
+
await client.waitForInitialization();
|
|
356
|
+
|
|
357
|
+
expect(client.variation('foo', 'default')).toEqual('default');
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe('variationDetail', () => {
|
|
364
|
+
const reason = { kind: 'FALLTHROUGH' };
|
|
365
|
+
it('returns details for an existing flag - from bootstrap', async () => {
|
|
366
|
+
const config = {
|
|
367
|
+
bootstrap: makeBootstrap({ foo: { value: 'bar', version: 1, variation: 2, reason: reason } }),
|
|
368
|
+
};
|
|
369
|
+
await withClient(user, config, async client => {
|
|
370
|
+
await client.waitForInitialization();
|
|
371
|
+
|
|
372
|
+
expect(client.variationDetail('foo')).toEqual({ value: 'bar', variationIndex: 2, reason: reason });
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('returns details for an existing flag - from bootstrap with old format', async () => {
|
|
377
|
+
const config = { bootstrap: { foo: 'bar' } };
|
|
378
|
+
await withClient(user, config, async client => {
|
|
379
|
+
await client.waitForInitialization();
|
|
380
|
+
|
|
381
|
+
expect(client.variationDetail('foo')).toEqual({ value: 'bar', variationIndex: null, reason: null });
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('returns details for an existing flag - from polling', async () => {
|
|
386
|
+
const flags = { foo: { value: 'bar', version: 1, variation: 2, reason: reason } };
|
|
387
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
388
|
+
pollServer.byDefault(respondJson(flags));
|
|
389
|
+
await withClient(user, baseConfig, async client => {
|
|
390
|
+
await client.waitForInitialization();
|
|
391
|
+
|
|
392
|
+
expect(client.variationDetail('foo', 'default')).toEqual({ value: 'bar', variationIndex: 2, reason: reason });
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('returns default value for flag that had null value', async () => {
|
|
398
|
+
const flags = { foo: { value: null, version: 1 } };
|
|
399
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
400
|
+
pollServer.byDefault(respondJson(flags));
|
|
401
|
+
await withClient(user, baseConfig, async client => {
|
|
402
|
+
await client.waitForInitialization();
|
|
403
|
+
|
|
404
|
+
expect(client.variationDetail('foo', 'default')).toEqual({
|
|
405
|
+
value: 'default',
|
|
406
|
+
variationIndex: null,
|
|
407
|
+
reason: null,
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('returns default value and error for unknown flag', async () => {
|
|
414
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
415
|
+
pollServer.byDefault(respondJson({}));
|
|
416
|
+
await withClient(user, baseConfig, async client => {
|
|
417
|
+
expect(client.variationDetail('foo', 'default')).toEqual({
|
|
418
|
+
value: 'default',
|
|
419
|
+
variationIndex: null,
|
|
420
|
+
reason: { kind: 'ERROR', errorKind: 'FLAG_NOT_FOUND' },
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
describe('allFlags', () => {
|
|
428
|
+
it('returns flag values', async () => {
|
|
429
|
+
const flags = { key1: { value: 'value1' }, key2: { value: 'value2' } };
|
|
430
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
431
|
+
pollServer.byDefault(respondJson(flags));
|
|
432
|
+
await withClient(user, baseConfig, async client => {
|
|
433
|
+
await client.waitForInitialization();
|
|
434
|
+
|
|
435
|
+
expect(client.allFlags()).toEqual({ key1: 'value1', key2: 'value2' });
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('returns empty map if client is not initialized', async () => {
|
|
441
|
+
const flags = { key1: { value: 'value1' }, key2: { value: 'value2' } };
|
|
442
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
443
|
+
pollServer.byDefault(respondJson(flags));
|
|
444
|
+
await withClient(user, baseConfig, async client => {
|
|
445
|
+
expect(client.allFlags()).toEqual({});
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe('identify', () => {
|
|
452
|
+
it('does not set user until the flag config has been updated', async () => {
|
|
453
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
454
|
+
pollServer.byDefault(respondJson({}));
|
|
455
|
+
await withClient(user, baseConfig, async client => {
|
|
456
|
+
const signal = new AsyncQueue();
|
|
457
|
+
const user2 = { key: 'user2' };
|
|
458
|
+
await client.waitForInitialization();
|
|
459
|
+
|
|
460
|
+
// Make the server wait until signaled to return the next response
|
|
461
|
+
pollServer.byDefault((req, res) => {
|
|
462
|
+
signal.take().then(() => {
|
|
463
|
+
respondJson({})(req, res);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const identifyPromise = client.identify(user2);
|
|
468
|
+
await sleepAsync(100); // sleep to jump some async ticks
|
|
469
|
+
expect(client.getUser()).toEqual(user);
|
|
470
|
+
|
|
471
|
+
signal.add();
|
|
472
|
+
await identifyPromise;
|
|
473
|
+
|
|
474
|
+
expect(client.getUser()).toEqual(user2);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('updates flag values when the user changes', async () => {
|
|
480
|
+
const flags0 = { 'enable-foo': { value: false } };
|
|
481
|
+
const flags1 = { 'enable-foo': { value: true } };
|
|
482
|
+
const user1 = { key: 'user1' };
|
|
483
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
484
|
+
pollServer.byDefault(respondJson(flags0));
|
|
485
|
+
await withClient(user, baseConfig, async client => {
|
|
486
|
+
await client.waitForInitialization();
|
|
487
|
+
|
|
488
|
+
expect(client.variation('enable-foo')).toBe(false);
|
|
489
|
+
|
|
490
|
+
pollServer.byDefault(respondJson(flags1));
|
|
491
|
+
|
|
492
|
+
const newFlagsMap = await client.identify(user1);
|
|
493
|
+
|
|
494
|
+
expect(client.variation('enable-foo')).toBe(true);
|
|
495
|
+
|
|
496
|
+
expect(newFlagsMap).toEqual({ 'enable-foo': true });
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('returns an error and does not update flags when identify is called with invalid user', async () => {
|
|
502
|
+
const flags0 = { 'enable-foo': { value: false } };
|
|
503
|
+
const flags1 = { 'enable-foo': { value: true } };
|
|
504
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
505
|
+
pollServer.byDefault(respondJson(flags0));
|
|
506
|
+
await withClient(user, baseConfig, async client => {
|
|
507
|
+
await client.waitForInitialization();
|
|
508
|
+
|
|
509
|
+
expect(client.variation('enable-foo')).toBe(false);
|
|
510
|
+
expect(pollServer.requests.length()).toEqual(1);
|
|
511
|
+
|
|
512
|
+
pollServer.byDefault(respondJson(flags1));
|
|
513
|
+
|
|
514
|
+
await expect(client.identify(null)).rejects.toThrow();
|
|
515
|
+
|
|
516
|
+
expect(client.variation('enable-foo')).toBe(false);
|
|
517
|
+
expect(pollServer.requests.length()).toEqual(1);
|
|
518
|
+
|
|
519
|
+
const userWithNoKey = { country: 'US' };
|
|
520
|
+
await expect(client.identify(userWithNoKey)).rejects.toThrow();
|
|
521
|
+
|
|
522
|
+
expect(client.variation('enable-foo')).toBe(false);
|
|
523
|
+
expect(pollServer.requests.length()).toEqual(1);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('provides a persistent key for an anonymous user with no key', async () => {
|
|
529
|
+
await withServers(async baseConfig => {
|
|
530
|
+
await withClient(user, baseConfig, async client => {
|
|
531
|
+
await client.waitForInitialization();
|
|
532
|
+
|
|
533
|
+
const anonUser = { anonymous: true, country: 'US' };
|
|
534
|
+
await client.identify(anonUser);
|
|
535
|
+
|
|
536
|
+
const newUser = client.getUser();
|
|
537
|
+
expect(newUser.key).toEqual(expect.anything());
|
|
538
|
+
expect(newUser).toMatchObject(anonUser);
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
describe('initializing with stateProvider', () => {
|
|
545
|
+
it('immediately uses initial state if available, and does not make an HTTP request', async () => {
|
|
546
|
+
const user = { key: 'user' };
|
|
547
|
+
const state = {
|
|
548
|
+
environment: 'env',
|
|
549
|
+
user: user,
|
|
550
|
+
flags: { flagkey: { value: 'value' } },
|
|
551
|
+
};
|
|
552
|
+
const sp = stubPlatform.mockStateProvider(state);
|
|
553
|
+
|
|
554
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
555
|
+
await withClient(null, { ...baseConfig, stateProvider: sp }, async client => {
|
|
556
|
+
await client.waitForInitialization();
|
|
557
|
+
|
|
558
|
+
expect(client.variation('flagkey')).toEqual('value');
|
|
559
|
+
expect(pollServer.requests.length()).toEqual(0);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('defers initialization if initial state not available, and does not make an HTTP request', async () => {
|
|
565
|
+
const sp = stubPlatform.mockStateProvider(null);
|
|
566
|
+
|
|
567
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
568
|
+
await withClient(null, { ...baseConfig, stateProvider: sp }, async () => {
|
|
569
|
+
expect(pollServer.requests.length()).toEqual(0);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('finishes initialization on receiving init event', async () => {
|
|
575
|
+
const user = { key: 'user' };
|
|
576
|
+
const state = {
|
|
577
|
+
environment: 'env',
|
|
578
|
+
user: user,
|
|
579
|
+
flags: { flagkey: { value: 'value' } },
|
|
580
|
+
};
|
|
581
|
+
const sp = stubPlatform.mockStateProvider(null);
|
|
582
|
+
|
|
583
|
+
await withClient(null, { stateProvider: sp, sendEvents: false }, async client => {
|
|
584
|
+
sp.emit('init', state);
|
|
585
|
+
|
|
586
|
+
await client.waitForInitialization();
|
|
587
|
+
expect(client.variation('flagkey')).toEqual('value');
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('updates flags on receiving update event', async () => {
|
|
592
|
+
const user = { key: 'user' };
|
|
593
|
+
const state0 = {
|
|
594
|
+
environment: 'env',
|
|
595
|
+
user: user,
|
|
596
|
+
flags: { flagkey: { value: 'value0' } },
|
|
597
|
+
};
|
|
598
|
+
const sp = stubPlatform.mockStateProvider(state0);
|
|
599
|
+
|
|
600
|
+
await withClient(null, { stateProvider: sp, sendEvents: false }, async client => {
|
|
601
|
+
await client.waitForInitialization();
|
|
602
|
+
|
|
603
|
+
expect(client.variation('flagkey')).toEqual('value0');
|
|
604
|
+
|
|
605
|
+
const state1 = {
|
|
606
|
+
flags: { flagkey: { value: 'value1' } },
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const gotChange = eventSink(client, 'change:flagkey');
|
|
610
|
+
|
|
611
|
+
sp.emit('update', state1);
|
|
612
|
+
|
|
613
|
+
const args = await gotChange.take();
|
|
614
|
+
expect(args).toEqual(['value1', 'value0']);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('disables identify()', async () => {
|
|
619
|
+
const user = { key: 'user' };
|
|
620
|
+
const user1 = { key: 'user1' };
|
|
621
|
+
const state = { environment: 'env', user: user, flags: { flagkey: { value: 'value' } } };
|
|
622
|
+
const sp = stubPlatform.mockStateProvider(state);
|
|
623
|
+
|
|
624
|
+
await withServers(async (baseConfig, pollServer) => {
|
|
625
|
+
await withClient(null, { ...baseConfig, stateProvider: sp }, async client => {
|
|
626
|
+
sp.emit('init', state);
|
|
627
|
+
|
|
628
|
+
await client.waitForInitialization();
|
|
629
|
+
const newFlags = await client.identify(user1);
|
|
630
|
+
|
|
631
|
+
expect(newFlags).toEqual({ flagkey: 'value' });
|
|
632
|
+
expect(pollServer.requests.length()).toEqual(0);
|
|
633
|
+
expect(platform.testing.logger.output.warn).toEqual([messages.identifyDisabled()]);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('copies data from state provider to avoid unintentional object-sharing', async () => {
|
|
639
|
+
const user = { key: 'user' };
|
|
640
|
+
const state = {
|
|
641
|
+
environment: 'env',
|
|
642
|
+
user: user,
|
|
643
|
+
flags: { flagkey: { value: 'value' } },
|
|
644
|
+
};
|
|
645
|
+
const sp = stubPlatform.mockStateProvider(null);
|
|
646
|
+
|
|
647
|
+
await withClient(null, { stateProvider: sp, sendEvents: false }, async client => {
|
|
648
|
+
sp.emit('init', state);
|
|
649
|
+
|
|
650
|
+
await client.waitForInitialization();
|
|
651
|
+
expect(client.variation('flagkey')).toEqual('value');
|
|
652
|
+
|
|
653
|
+
state.flags.flagkey = { value: 'secondValue' };
|
|
654
|
+
expect(client.variation('flagkey')).toEqual('value');
|
|
655
|
+
|
|
656
|
+
sp.emit('update', state);
|
|
657
|
+
expect(client.variation('flagkey')).toEqual('secondValue');
|
|
658
|
+
|
|
659
|
+
state.flags.flagkey = { value: 'thirdValue' };
|
|
660
|
+
expect(client.variation('flagkey')).toEqual('secondValue');
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
describe('close()', () => {
|
|
666
|
+
it('flushes events', async () => {
|
|
667
|
+
await withServers(async (baseConfig, pollServer, eventsServer) => {
|
|
668
|
+
await withClient(user, { ...baseConfig, flushInterval: 100000 }, async client => {
|
|
669
|
+
await client.waitForInitialization();
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
expect(eventsServer.requests.length()).toEqual(1);
|
|
673
|
+
const req = await eventsServer.nextRequest();
|
|
674
|
+
const data = JSON.parse(req.body);
|
|
675
|
+
expect(data.length).toEqual(1);
|
|
676
|
+
expect(data[0].kind).toEqual('identify');
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('does nothing if called twice', async () => {
|
|
681
|
+
await withServers(async (baseConfig, pollServer, eventsServer) => {
|
|
682
|
+
await withClient(user, { ...baseConfig, flushInterval: 100000 }, async client => {
|
|
683
|
+
await client.waitForInitialization();
|
|
684
|
+
|
|
685
|
+
await client.close();
|
|
686
|
+
|
|
687
|
+
expect(eventsServer.requests.length()).toEqual(1);
|
|
688
|
+
|
|
689
|
+
await client.close();
|
|
690
|
+
|
|
691
|
+
expect(eventsServer.requests.length()).toEqual(1);
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('is not rejected if flush fails', async () => {
|
|
697
|
+
await withServers(async (baseConfig, pollServer, eventsServer) => {
|
|
698
|
+
eventsServer.byDefault(respond(404));
|
|
699
|
+
await withClient(user, { ...baseConfig, flushInterval: 100000 }, async client => {
|
|
700
|
+
await client.waitForInitialization();
|
|
701
|
+
|
|
702
|
+
await client.close(); // shouldn't throw or have an unhandled rejection
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it('can take a callback instead of returning a promise', async () => {
|
|
708
|
+
await withServers(async (baseConfig, pollServer, eventsServer) => {
|
|
709
|
+
await withClient(user, { ...baseConfig }, async client => {
|
|
710
|
+
await client.waitForInitialization();
|
|
711
|
+
|
|
712
|
+
await promisifySingle(client.close)();
|
|
713
|
+
|
|
714
|
+
expect(eventsServer.requests.length()).toEqual(1);
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
describe('diagnostic events', () => {
|
|
721
|
+
// Note, the default configuration provided by withClient() sets { diagnosticOptOut: true } so that the
|
|
722
|
+
// diagnostic events won't interfere with the rest of the tests in this file. In this test group, we will
|
|
723
|
+
// deliberately enable diagnostic events. The details of DiagnosticManager's behavior are covered by
|
|
724
|
+
// diagnosticEvents-test.js, so here we're just verifying that the client starts up the DiagnosticsManager
|
|
725
|
+
// and gives it the right eventsUrl.
|
|
726
|
+
|
|
727
|
+
it('sends diagnostic init event if not opted out', async () => {
|
|
728
|
+
await withServers(async (baseConfig, pollServer, eventsServer) => {
|
|
729
|
+
await withDiagnosticsEnabledClient(user, baseConfig, async client => {
|
|
730
|
+
await client.waitForInitialization();
|
|
731
|
+
await client.flush();
|
|
732
|
+
|
|
733
|
+
// We can't be sure which will be posted first, the regular events or the diagnostic event
|
|
734
|
+
const requests = [];
|
|
735
|
+
const req1 = await eventsServer.requests.take();
|
|
736
|
+
requests.push({ path: req1.path, data: JSON.parse(req1.body) });
|
|
737
|
+
const req2 = await eventsServer.requests.take();
|
|
738
|
+
requests.push({ path: req2.path, data: JSON.parse(req2.body) });
|
|
739
|
+
|
|
740
|
+
expect(requests).toContainEqual({
|
|
741
|
+
path: '/events/bulk/' + envName,
|
|
742
|
+
data: expect.arrayContaining([expect.objectContaining({ kind: 'identify' })]),
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
expect(requests).toContainEqual({
|
|
746
|
+
path: '/events/diagnostic/' + envName,
|
|
747
|
+
data: expect.objectContaining({ kind: 'diagnostic-init' }),
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
});
|