launchdarkly-js-sdk-common 3.5.0 → 4.0.3

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.
Files changed (68) hide show
  1. package/.circleci/config.yml +22 -0
  2. package/.eslintignore +4 -0
  3. package/.eslintrc.yaml +103 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  6. package/.github/pull_request_template.md +21 -0
  7. package/.ldrelease/config.yml +24 -0
  8. package/.prettierignore +1 -0
  9. package/.prettierrc +5 -0
  10. package/CHANGELOG.md +17 -0
  11. package/CONTRIBUTING.md +45 -0
  12. package/babel.config.js +18 -0
  13. package/docs/typedoc.js +11 -0
  14. package/jest.config.js +12 -0
  15. package/package.json +4 -32
  16. package/scripts/better-audit.sh +76 -0
  17. package/src/EventEmitter.js +60 -0
  18. package/src/EventProcessor.js +175 -0
  19. package/src/EventSender.js +87 -0
  20. package/src/EventSummarizer.js +84 -0
  21. package/src/Identity.js +26 -0
  22. package/src/InitializationState.js +83 -0
  23. package/src/PersistentFlagStore.js +50 -0
  24. package/src/PersistentStorage.js +81 -0
  25. package/src/Requestor.js +111 -0
  26. package/src/Stream.js +154 -0
  27. package/src/UserFilter.js +75 -0
  28. package/src/UserValidator.js +56 -0
  29. package/src/__tests__/.eslintrc.yaml +6 -0
  30. package/src/__tests__/EventProcessor-test.js +559 -0
  31. package/src/__tests__/EventSender-test.js +252 -0
  32. package/src/__tests__/EventSource-mock.js +61 -0
  33. package/src/__tests__/EventSummarizer-test.js +103 -0
  34. package/src/__tests__/LDClient-events-test.js +757 -0
  35. package/src/__tests__/LDClient-localstorage-test.js +179 -0
  36. package/src/__tests__/LDClient-streaming-test.js +716 -0
  37. package/src/__tests__/LDClient-test.js +753 -0
  38. package/src/__tests__/PersistentFlagStore-test.js +111 -0
  39. package/src/__tests__/Requestor-test.js +362 -0
  40. package/src/__tests__/Stream-test.js +299 -0
  41. package/src/__tests__/UserFilter-test.js +93 -0
  42. package/src/__tests__/UserValidator-test.js +57 -0
  43. package/src/__tests__/configuration-test.js +217 -0
  44. package/src/__tests__/diagnosticEvents-test.js +449 -0
  45. package/src/__tests__/loggers-test.js +149 -0
  46. package/src/__tests__/mockHttp.js +122 -0
  47. package/src/__tests__/promiseCoalescer-test.js +128 -0
  48. package/src/__tests__/stubPlatform.js +148 -0
  49. package/src/__tests__/testUtils.js +77 -0
  50. package/src/__tests__/utils-test.js +148 -0
  51. package/src/configuration.js +151 -0
  52. package/src/diagnosticEvents.js +269 -0
  53. package/src/errors.js +39 -0
  54. package/src/index.js +788 -0
  55. package/src/jest.setup.js +1 -0
  56. package/src/loggers.js +93 -0
  57. package/src/messages.js +222 -0
  58. package/src/promiseCoalescer.js +52 -0
  59. package/src/utils.js +214 -0
  60. package/test-types.ts +94 -0
  61. package/tsconfig.json +13 -0
  62. package/typings.d.ts +4 -43
  63. package/dist/ldclient-common.cjs.js +0 -2
  64. package/dist/ldclient-common.cjs.js.map +0 -1
  65. package/dist/ldclient-common.es.js +0 -2
  66. package/dist/ldclient-common.es.js.map +0 -1
  67. package/dist/ldclient-common.min.js +0 -2
  68. package/dist/ldclient-common.min.js.map +0 -1
@@ -0,0 +1,299 @@
1
+ import { DiagnosticsAccumulator } from '../diagnosticEvents';
2
+ import * as messages from '../messages';
3
+ import Stream from '../Stream';
4
+ import { getLDHeaders } from '../utils';
5
+
6
+ import { sleepAsync } from 'launchdarkly-js-test-helpers';
7
+ import EventSource from './EventSource-mock';
8
+ import * as stubPlatform from './stubPlatform';
9
+
10
+ const noop = () => {};
11
+
12
+ describe('Stream', () => {
13
+ const baseUrl = 'https://example.com';
14
+ const envName = 'testenv';
15
+ const user = { key: 'me' };
16
+ const encodedUser = 'eyJrZXkiOiJtZSJ9';
17
+ const hash = '012345789abcde';
18
+ const defaultConfig = { streamUrl: baseUrl, sendLDHeaders: true };
19
+ let logger;
20
+ let platform;
21
+ let baseHeaders;
22
+
23
+ beforeEach(() => {
24
+ logger = stubPlatform.logger();
25
+ defaultConfig.logger = logger;
26
+ platform = stubPlatform.defaults();
27
+ baseHeaders = getLDHeaders(platform, defaultConfig);
28
+ });
29
+
30
+ function makeExpectedStreamUrl(base64User, userHash, withReasons) {
31
+ const url = baseUrl + '/eval/' + envName + '/' + base64User;
32
+ const queryParams = [];
33
+ if (userHash) {
34
+ queryParams.push('h=' + userHash);
35
+ }
36
+ if (withReasons) {
37
+ queryParams.push('?withReasons=true');
38
+ }
39
+ return url + (queryParams.length ? '?' + queryParams.join('&') : '');
40
+ }
41
+
42
+ it('should not throw on EventSource when it does not exist', () => {
43
+ const platform1 = { ...platform };
44
+ delete platform1['eventSourceFactory'];
45
+
46
+ const stream = new Stream(platform1, defaultConfig, envName);
47
+
48
+ const connect = () => {
49
+ stream.connect(noop);
50
+ };
51
+
52
+ expect(connect).not.toThrow(TypeError);
53
+ });
54
+
55
+ it('should not throw when calling disconnect without first calling connect', () => {
56
+ const stream = new Stream(platform, defaultConfig, envName);
57
+ const disconnect = () => {
58
+ stream.disconnect(noop);
59
+ };
60
+
61
+ expect(disconnect).not.toThrow(TypeError);
62
+ });
63
+
64
+ it('connects to EventSource with eval stream URL by default', async () => {
65
+ const stream = new Stream(platform, defaultConfig, envName);
66
+ stream.connect(user, null, {});
67
+
68
+ await platform.testing.expectStream(makeExpectedStreamUrl(encodedUser));
69
+ });
70
+
71
+ it('adds secure mode hash to URL if provided', async () => {
72
+ const stream = new Stream(platform, defaultConfig, envName);
73
+ stream.connect(user, hash, {});
74
+
75
+ await platform.testing.expectStream(makeExpectedStreamUrl(encodedUser, hash));
76
+ });
77
+
78
+ it('falls back to ping stream URL if useReport is true and REPORT is not supported', async () => {
79
+ const config = { ...defaultConfig, useReport: true };
80
+ const stream = new Stream(platform, config, envName);
81
+ stream.connect(user, null, {});
82
+
83
+ await platform.testing.expectStream(baseUrl + '/ping/' + envName);
84
+ });
85
+
86
+ it('sends request body if useReport is true and REPORT is supported', async () => {
87
+ const platform1 = { ...platform, eventSourceAllowsReport: true };
88
+ const config = { ...defaultConfig, useReport: true };
89
+ const stream = new Stream(platform1, config, envName);
90
+ stream.connect(user, null, {});
91
+
92
+ const created = await platform.testing.expectStream(baseUrl + '/eval/' + envName);
93
+ expect(created.options.method).toEqual('REPORT');
94
+ expect(JSON.parse(created.options.body)).toEqual(user);
95
+ });
96
+
97
+ it('sends default SDK headers', async () => {
98
+ const stream = new Stream(platform, defaultConfig, envName);
99
+ stream.connect(user, null, {});
100
+
101
+ const created = await platform.testing.expectStream(makeExpectedStreamUrl(encodedUser));
102
+ expect(created.options.headers).toEqual(baseHeaders);
103
+ });
104
+
105
+ it('sends SDK headers with wrapper info', async () => {
106
+ const config = { ...defaultConfig, wrapperName: 'FakeSDK' };
107
+ const stream = new Stream(platform, config, envName);
108
+ stream.connect(user, null, {});
109
+
110
+ const created = await platform.testing.expectStream(makeExpectedStreamUrl(encodedUser));
111
+ expect(created.options.headers).toEqual({ ...baseHeaders, 'X-LaunchDarkly-Wrapper': 'FakeSDK' });
112
+ });
113
+
114
+ it('does not send SDK headers when sendLDHeaders is false', async () => {
115
+ const config = { ...defaultConfig, sendLDHeaders: false };
116
+ const stream = new Stream(platform, config, envName);
117
+ stream.connect(user, null, {});
118
+
119
+ const created = await platform.testing.expectStream(makeExpectedStreamUrl(encodedUser));
120
+ expect(created.options.headers).toEqual({});
121
+ });
122
+
123
+ it('sends transformed headers if requestHeaderTransform function is provided', async () => {
124
+ const headerTransform = input => {
125
+ const output = { ...input };
126
+ output['a'] = '10';
127
+ return output;
128
+ };
129
+ const config = { ...defaultConfig, requestHeaderTransform: headerTransform };
130
+ const stream = new Stream(platform, config, envName);
131
+ stream.connect(user, null, {});
132
+
133
+ const created = await platform.testing.expectStream(makeExpectedStreamUrl(encodedUser));
134
+ expect(created.options.headers).toEqual({ ...baseHeaders, a: '10' });
135
+ });
136
+
137
+ it('sets event listeners', async () => {
138
+ const stream = new Stream(platform, defaultConfig, envName);
139
+ const fn1 = jest.fn();
140
+ const fn2 = jest.fn();
141
+
142
+ stream.connect(user, null, {
143
+ birthday: fn1,
144
+ anniversary: fn2,
145
+ });
146
+
147
+ const created = await platform.testing.expectStream();
148
+ const es = created.eventSource;
149
+
150
+ es.mockEmit('birthday');
151
+ expect(fn1).toHaveBeenCalled();
152
+ expect(fn2).not.toHaveBeenCalled();
153
+
154
+ es.mockEmit('anniversary');
155
+ expect(fn2).toHaveBeenCalled();
156
+ });
157
+
158
+ it('reconnects after encountering an error', async () => {
159
+ const config = { ...defaultConfig, streamReconnectDelay: 1, useReport: false };
160
+ const stream = new Stream(platform, config, envName);
161
+ stream.connect(user);
162
+
163
+ const created = await platform.testing.expectStream();
164
+ let es = created.eventSource;
165
+
166
+ expect(es.readyState).toBe(EventSource.CONNECTING);
167
+ es.mockOpen();
168
+ expect(es.readyState).toBe(EventSource.OPEN);
169
+
170
+ const nAttempts = 5;
171
+ for (let i = 0; i < nAttempts; i++) {
172
+ es.mockError('test error');
173
+ const created1 = await platform.testing.expectStream();
174
+ const es1 = created1.eventSource;
175
+
176
+ expect(es.readyState).toBe(EventSource.CLOSED);
177
+ expect(es1.readyState).toBe(EventSource.CONNECTING);
178
+
179
+ es1.mockOpen();
180
+ await sleepAsync(0); // make sure the stream logic has a chance to catch up with the new EventSource state
181
+
182
+ expect(stream.isConnected()).toBe(true);
183
+
184
+ es = es1;
185
+ }
186
+ });
187
+
188
+ it('logs a warning for only the first failed connection attempt', async () => {
189
+ const config = { ...defaultConfig, streamReconnectDelay: 1 };
190
+ const stream = new Stream(platform, config, envName);
191
+ stream.connect(user);
192
+
193
+ const created = await platform.testing.expectStream();
194
+ let es = created.eventSource;
195
+ es.mockOpen();
196
+
197
+ const nAttempts = 5;
198
+ for (let i = 0; i < nAttempts; i++) {
199
+ es.mockError('test error');
200
+ const created1 = await platform.testing.expectStream();
201
+ es = created1.eventSource;
202
+ es.mockOpen();
203
+ }
204
+
205
+ // make sure there is just a single logged message rather than five (one per attempt)
206
+ expect(logger.output.warn).toEqual([messages.streamError('test error', 1)]);
207
+ });
208
+
209
+ it('logs a warning again after a successful connection', async () => {
210
+ const config = { ...defaultConfig, streamReconnectDelay: 1 };
211
+ const stream = new Stream(platform, config, envName);
212
+ const fakePut = jest.fn();
213
+ stream.connect(user, null, {
214
+ put: fakePut,
215
+ });
216
+
217
+ const created = await platform.testing.expectStream();
218
+ let es = created.eventSource;
219
+ es.mockOpen();
220
+
221
+ const nAttempts = 5;
222
+ for (let i = 0; i < nAttempts; i++) {
223
+ es.mockError('test error #1');
224
+ const created1 = await platform.testing.expectStream();
225
+ es = created1.eventSource;
226
+ es.mockOpen();
227
+ }
228
+
229
+ // simulate the re-establishment of a successful connection
230
+ es.mockEmit('put', 'something');
231
+ expect(fakePut).toHaveBeenCalled();
232
+
233
+ for (let i = 0; i < nAttempts; i++) {
234
+ es.mockError('test error #2');
235
+ const created1 = await platform.testing.expectStream();
236
+ es = created1.eventSource;
237
+ es.mockOpen();
238
+ }
239
+
240
+ // make sure there is just a single logged message rather than five (one per attempt)
241
+ expect(logger.output.warn).toEqual([
242
+ messages.streamError('test error #1', 1),
243
+ messages.streamError('test error #2', 1),
244
+ ]);
245
+ });
246
+
247
+ describe('interaction with diagnostic events', () => {
248
+ it('records successful stream initialization', async () => {
249
+ const startTime = new Date().getTime();
250
+ const acc = DiagnosticsAccumulator(startTime);
251
+ const config = { ...defaultConfig, streamReconnectDelay: 1 };
252
+ const stream = new Stream(platform, config, envName, acc);
253
+
254
+ expect(acc.getProps().streamInits.length).toEqual(0);
255
+
256
+ stream.connect(user, null, {
257
+ put: jest.fn(),
258
+ });
259
+
260
+ const created = await platform.testing.expectStream();
261
+ const es = created.eventSource;
262
+ es.mockOpen();
263
+
264
+ // streamInits should not be updated until we actually receive something
265
+ expect(acc.getProps().streamInits.length).toEqual(0);
266
+
267
+ es.mockEmit('put', 'something');
268
+
269
+ const streamInits = acc.getProps().streamInits;
270
+ expect(streamInits.length).toEqual(1);
271
+ expect(streamInits[0].timestamp).toBeGreaterThanOrEqual(startTime);
272
+ expect(streamInits[0].durationMillis).toBeGreaterThanOrEqual(0);
273
+ expect(streamInits[0].failed).toBeFalsy();
274
+ });
275
+
276
+ it('records failed stream initialization', async () => {
277
+ const startTime = new Date().getTime();
278
+ const acc = DiagnosticsAccumulator(startTime);
279
+ const config = { ...defaultConfig, streamReconnectDelay: 1 };
280
+ const stream = new Stream(platform, config, envName, acc);
281
+
282
+ expect(acc.getProps().streamInits.length).toEqual(0);
283
+
284
+ stream.connect(user, null, {
285
+ put: jest.fn(),
286
+ });
287
+
288
+ const created = await platform.testing.expectStream();
289
+ const es = created.eventSource;
290
+ es.mockError('test error');
291
+
292
+ const streamInits = acc.getProps().streamInits;
293
+ expect(streamInits.length).toEqual(1);
294
+ expect(streamInits[0].timestamp).toBeGreaterThanOrEqual(startTime);
295
+ expect(streamInits[0].durationMillis).toBeGreaterThanOrEqual(0);
296
+ expect(streamInits[0].failed).toBe(true);
297
+ });
298
+ });
299
+ });
@@ -0,0 +1,93 @@
1
+ import UserFilter from '../UserFilter';
2
+
3
+ describe('UserFilter', () => {
4
+ // users to serialize
5
+ const user = {
6
+ key: 'abc',
7
+ firstName: 'Sue',
8
+ custom: { bizzle: 'def', dizzle: 'ghi' },
9
+ };
10
+
11
+ const userSpecifyingOwnPrivateAttr = {
12
+ key: 'abc',
13
+ firstName: 'Sue',
14
+ custom: { bizzle: 'def', dizzle: 'ghi' },
15
+ privateAttributeNames: ['dizzle', 'unused'],
16
+ };
17
+
18
+ const userWithUnknownTopLevelAttrs = {
19
+ key: 'abc',
20
+ firstName: 'Sue',
21
+ species: 'human',
22
+ hatSize: 6,
23
+ custom: { bizzle: 'def', dizzle: 'ghi' },
24
+ };
25
+
26
+ const anonUser = {
27
+ key: 'abc',
28
+ anonymous: true,
29
+ custom: { bizzle: 'def', dizzle: 'ghi' },
30
+ };
31
+
32
+ // expected results from serializing user
33
+ const userWithAllAttrsHidden = {
34
+ key: 'abc',
35
+ custom: {},
36
+ privateAttrs: ['bizzle', 'dizzle', 'firstName'],
37
+ };
38
+
39
+ const userWithSomeAttrsHidden = {
40
+ key: 'abc',
41
+ custom: { dizzle: 'ghi' },
42
+ privateAttrs: ['bizzle', 'firstName'],
43
+ };
44
+
45
+ const userWithOwnSpecifiedAttrHidden = {
46
+ key: 'abc',
47
+ firstName: 'Sue',
48
+ custom: { bizzle: 'def' },
49
+ privateAttrs: ['dizzle'],
50
+ };
51
+
52
+ const anonUserWithAllAttrsHidden = {
53
+ key: 'abc',
54
+ anonymous: true,
55
+ custom: {},
56
+ privateAttrs: ['bizzle', 'dizzle'],
57
+ };
58
+
59
+ it('includes all user attributes by default', () => {
60
+ const uf = UserFilter({});
61
+ expect(uf.filterUser(user)).toEqual(user);
62
+ });
63
+
64
+ it('hides all except key if allAttributesPrivate is true', () => {
65
+ const uf = UserFilter({ allAttributesPrivate: true });
66
+ expect(uf.filterUser(user)).toEqual(userWithAllAttrsHidden);
67
+ });
68
+
69
+ it('hides some attributes if privateAttributeNames is set', () => {
70
+ const uf = UserFilter({ privateAttributeNames: ['firstName', 'bizzle'] });
71
+ expect(uf.filterUser(user)).toEqual(userWithSomeAttrsHidden);
72
+ });
73
+
74
+ it('hides attributes specified in per-user privateAttrs', () => {
75
+ const uf = UserFilter({});
76
+ expect(uf.filterUser(userSpecifyingOwnPrivateAttr)).toEqual(userWithOwnSpecifiedAttrHidden);
77
+ });
78
+
79
+ it('looks at both per-user privateAttrs and global config', () => {
80
+ const uf = UserFilter({ privateAttributeNames: ['firstName', 'bizzle'] });
81
+ expect(uf.filterUser(userSpecifyingOwnPrivateAttr)).toEqual(userWithAllAttrsHidden);
82
+ });
83
+
84
+ it('strips unknown top-level attributes', () => {
85
+ const uf = UserFilter({});
86
+ expect(uf.filterUser(userWithUnknownTopLevelAttrs)).toEqual(user);
87
+ });
88
+
89
+ it('leaves the "anonymous" attribute as is', () => {
90
+ const uf = UserFilter({ allAttributesPrivate: true });
91
+ expect(uf.filterUser(anonUser)).toEqual(anonUserWithAllAttrsHidden);
92
+ });
93
+ });
@@ -0,0 +1,57 @@
1
+ import UserValidator from '../UserValidator';
2
+
3
+ describe('UserValidator', () => {
4
+ let localStorage;
5
+ let logger;
6
+ let uv;
7
+
8
+ beforeEach(() => {
9
+ localStorage = {};
10
+ logger = {
11
+ warn: jest.fn(),
12
+ };
13
+ uv = UserValidator(localStorage, logger);
14
+ });
15
+
16
+ it('rejects null user', async () => {
17
+ await expect(uv.validateUser(null)).rejects.toThrow();
18
+ });
19
+
20
+ it('leaves user with string key unchanged', async () => {
21
+ const u = { key: 'someone', name: 'me' };
22
+ expect(await uv.validateUser(u)).toEqual(u);
23
+ });
24
+
25
+ it('stringifies non-string key', async () => {
26
+ const u0 = { key: 123, name: 'me' };
27
+ const u1 = { key: '123', name: 'me' };
28
+ expect(await uv.validateUser(u0)).toEqual(u1);
29
+ });
30
+
31
+ it('uses cached key for anonymous user', async () => {
32
+ const cachedKey = 'thing';
33
+ let storageKey;
34
+ localStorage.get = async key => {
35
+ storageKey = key;
36
+ return cachedKey;
37
+ };
38
+ const u = { anonymous: true };
39
+ expect(await uv.validateUser(u)).toEqual({ key: cachedKey, anonymous: true });
40
+ expect(storageKey).toEqual('ld:$anonUserId');
41
+ });
42
+
43
+ it('generates and stores key for anonymous user', async () => {
44
+ let storageKey;
45
+ let storedValue;
46
+ localStorage.get = async () => null;
47
+ localStorage.set = async (key, value) => {
48
+ storageKey = key;
49
+ storedValue = value;
50
+ };
51
+ const u0 = { anonymous: true };
52
+ const u1 = await uv.validateUser(u0);
53
+ expect(storedValue).toEqual(expect.anything());
54
+ expect(u1).toEqual({ key: storedValue, anonymous: true });
55
+ expect(storageKey).toEqual('ld:$anonUserId');
56
+ });
57
+ });
@@ -0,0 +1,217 @@
1
+ import { sleepAsync, eventSink } from 'launchdarkly-js-test-helpers';
2
+
3
+ import * as configuration from '../configuration';
4
+ import { LDInvalidArgumentError } from '../errors';
5
+ import * as messages from '../messages';
6
+ import EventEmitter from '../EventEmitter';
7
+
8
+ import * as stubPlatform from './stubPlatform';
9
+
10
+ describe('configuration', () => {
11
+ function errorListener() {
12
+ const logger = stubPlatform.logger();
13
+ const emitter = EventEmitter(logger);
14
+ const errorQueue = eventSink(emitter, 'error');
15
+ return {
16
+ emitter,
17
+ logger,
18
+ expectNoErrors: async () => {
19
+ await sleepAsync(0); // errors are dispatched on next tick
20
+ expect(errorQueue.length()).toEqual(0);
21
+ expect(logger.output.error).toEqual([]);
22
+ },
23
+ expectError: async message => {
24
+ await sleepAsync(0);
25
+ expect(errorQueue.length()).toEqual(1);
26
+ if (message) {
27
+ expect(await errorQueue.take()).toEqual(new LDInvalidArgumentError(message));
28
+ } else {
29
+ expect((await errorQueue.take()).constructor.prototype.name).toEqual('LaunchDarklyInvalidArgumentError');
30
+ }
31
+ },
32
+ expectWarningOnly: async message => {
33
+ await sleepAsync(0);
34
+ expect(errorQueue.length()).toEqual(0);
35
+ expect(logger.output.warn).toContain(message);
36
+ },
37
+ };
38
+ }
39
+
40
+ async function expectDefault(name) {
41
+ const listener = errorListener();
42
+ const config = configuration.validate({}, listener.emitter, null, listener.logger);
43
+ expect(config[name]).toBe(configuration.baseOptionDefs[name].default);
44
+ await listener.expectNoErrors();
45
+ }
46
+
47
+ // As of the latest major version, there are no deprecated options. This logic can be restored
48
+ // the next time we deprecate something.
49
+ // function checkDeprecated(oldName, newName, value) {
50
+ // const desc = newName
51
+ // ? 'allows "' + oldName + '" as a deprecated equivalent to "' + newName + '"'
52
+ // : 'warns that "' + oldName + '" is deprecated';
53
+ // it(desc, async () => {
54
+ // const listener = errorListener();
55
+ // const config0 = {};
56
+ // config0[oldName] = value;
57
+ // const config1 = configuration.validate(config0, listener.emitter, null, listener.logger);
58
+ // if (newName) {
59
+ // expect(config1[newName]).toBe(value);
60
+ // expect(config1[oldName]).toBeUndefined();
61
+ // } else {
62
+ // expect(config1[oldName]).toEqual(value);
63
+ // }
64
+ // await listener.expectWarningOnly(messages.deprecated(oldName, newName));
65
+ // });
66
+ // }
67
+
68
+ function checkBooleanProperty(name) {
69
+ it('enforces boolean type and default for "' + name + '"', async () => {
70
+ await expectDefault(name);
71
+
72
+ let listener = errorListener();
73
+ const configIn1 = {};
74
+ configIn1[name] = true;
75
+ const config1 = configuration.validate(configIn1, listener.emitter, null, listener.logger);
76
+ expect(config1[name]).toBe(true);
77
+ await listener.expectNoErrors();
78
+
79
+ listener = errorListener();
80
+ const configIn2 = {};
81
+ configIn2[name] = false;
82
+ const config2 = configuration.validate(configIn2, listener.emitter, null, listener.logger);
83
+ expect(config2[name]).toBe(false);
84
+ await listener.expectNoErrors();
85
+
86
+ listener = errorListener();
87
+ const configIn3 = {};
88
+ configIn3[name] = 'abc';
89
+ const config3 = configuration.validate(configIn3, listener.emitter, null, listener.logger);
90
+ expect(config3[name]).toBe(true);
91
+ await listener.expectError(messages.wrongOptionTypeBoolean(name, 'string'));
92
+
93
+ listener = errorListener();
94
+ const configIn4 = {};
95
+ configIn4[name] = 0;
96
+ const config4 = configuration.validate(configIn4, listener.emitter, null, listener.logger);
97
+ expect(config4[name]).toBe(false);
98
+ await listener.expectError(messages.wrongOptionTypeBoolean(name, 'number'));
99
+ });
100
+ }
101
+
102
+ checkBooleanProperty('sendEvents');
103
+ checkBooleanProperty('allAttributesPrivate');
104
+ checkBooleanProperty('sendLDHeaders');
105
+ checkBooleanProperty('inlineUsersInEvents');
106
+ checkBooleanProperty('allowFrequentDuplicateEvents');
107
+ checkBooleanProperty('sendEventsOnlyForVariation');
108
+ checkBooleanProperty('useReport');
109
+ checkBooleanProperty('evaluationReasons');
110
+ checkBooleanProperty('diagnosticOptOut');
111
+ checkBooleanProperty('streaming');
112
+
113
+ function checkNumericProperty(name, validValue) {
114
+ it('enforces numeric type and default for "' + name + '"', async () => {
115
+ await expectDefault(name);
116
+
117
+ let listener = errorListener();
118
+ const configIn1 = {};
119
+ configIn1[name] = validValue;
120
+ const config1 = configuration.validate(configIn1, listener.emitter, null, listener.logger);
121
+ expect(config1[name]).toBe(validValue);
122
+ await listener.expectNoErrors();
123
+
124
+ listener = errorListener();
125
+ const configIn2 = {};
126
+ configIn2[name] = 'no';
127
+ const config2 = configuration.validate(configIn2, listener.emitter, null, listener.logger);
128
+ expect(config2[name]).toBe(configuration.baseOptionDefs[name].default);
129
+ await listener.expectError(messages.wrongOptionType(name, 'number', 'string'));
130
+ });
131
+ }
132
+
133
+ checkNumericProperty('eventCapacity', 200);
134
+ checkNumericProperty('flushInterval', 3000);
135
+ checkNumericProperty('samplingInterval', 1);
136
+ checkNumericProperty('streamReconnectDelay', 2000);
137
+
138
+ function checkMinimumValue(name, minimum) {
139
+ it('disallows value below minimum of ' + minimum + ' for ' + name, async () => {
140
+ const listener = errorListener();
141
+ const configIn = {};
142
+ configIn[name] = minimum - 1;
143
+ const config = configuration.validate(configIn, listener.emitter, null, listener.logger);
144
+ await listener.expectError(messages.optionBelowMinimum(name, minimum - 1, minimum));
145
+ expect(config[name]).toBe(minimum);
146
+ });
147
+ }
148
+
149
+ checkMinimumValue('eventCapacity', 1);
150
+ checkMinimumValue('flushInterval', 2000);
151
+ checkMinimumValue('samplingInterval', 0);
152
+ checkMinimumValue('diagnosticRecordingInterval', 2000);
153
+
154
+ function checkValidValue(name, goodValue) {
155
+ it('allows value of ' + JSON.stringify(goodValue) + ' for ' + name, async () => {
156
+ const listener = errorListener();
157
+ const configIn = {};
158
+ configIn[name] = goodValue;
159
+ const config = configuration.validate(configIn, listener.emitter, null, listener.logger);
160
+ await listener.expectNoErrors();
161
+ expect(config[name]).toBe(goodValue);
162
+ });
163
+ }
164
+
165
+ checkValidValue('bootstrap', 'localstorage');
166
+ checkValidValue('bootstrap', { flag: 'value' });
167
+
168
+ it('validates custom logger methods', () => {
169
+ const badLogger = { debug: () => {}, info: () => {}, warn: () => {}, error: 'not a function' };
170
+ const listener = errorListener();
171
+ const configIn = { logger: badLogger };
172
+ expect(() => configuration.validate(configIn, listener.emitter, null, listener.logger)).toThrow();
173
+ });
174
+
175
+ it('allows custom logger with valid methods', async () => {
176
+ const goodLogger = { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} };
177
+ const listener = errorListener();
178
+ const configIn = { logger: goodLogger };
179
+ const config = configuration.validate(configIn, listener.emitter, null, listener.logger);
180
+ await listener.expectNoErrors();
181
+ expect(config.logger).toBe(goodLogger);
182
+ });
183
+
184
+ it('complains if you set an unknown property', async () => {
185
+ const listener = errorListener();
186
+ const configIn = { unsupportedThing: true };
187
+ const config = configuration.validate(configIn, listener.emitter, null, listener.logger);
188
+ await listener.expectError(messages.unknownOption('unsupportedThing'));
189
+ expect(config.unsupportedThing).toBe(true);
190
+ });
191
+
192
+ it('allows platform-specific SDK options whose defaults are specified by the SDK', async () => {
193
+ const listener = errorListener();
194
+ const fn = () => {};
195
+ const platformSpecificOptions = {
196
+ extraBooleanOption: { default: true },
197
+ extraNumericOption: { default: 2 },
198
+ extraNumericOptionWithoutDefault: { type: 'number' },
199
+ extraStringOption: { default: 'yes' },
200
+ extraStringOptionWithoutDefault: { type: 'string' },
201
+ extraFunctionOption: { type: 'function' },
202
+ };
203
+ const configIn = {
204
+ extraBooleanOption: false,
205
+ extraNumericOptionWithoutDefault: 'not a number',
206
+ extraStringOptionWithoutDefault: 'ok',
207
+ extraFunctionOption: fn,
208
+ };
209
+ const config = configuration.validate(configIn, listener.emitter, platformSpecificOptions, listener.logger);
210
+ expect(config.extraBooleanOption).toBe(false);
211
+ expect(config.extraNumericOption).toBe(2);
212
+ expect(config.extraStringOption).toBe('yes');
213
+ expect(config.extraStringOptionWithoutDefault).toBe('ok');
214
+ expect(config.extraFunctionOption).toBe(fn);
215
+ await listener.expectError(messages.wrongOptionType('extraNumericOptionWithoutDefault', 'number', 'string'));
216
+ });
217
+ });