launchdarkly-js-sdk-common 4.0.0 → 4.0.1

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 (67) hide show
  1. package/.circleci/config.yml +22 -0
  2. package/.eslintignore +4 -0
  3. package/.eslintrc.yaml +104 -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 +4 -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 +15 -0
  15. package/package.json +3 -29
  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 +7 -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 +683 -0
  37. package/src/__tests__/LDClient-test.js +761 -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 +37 -0
  54. package/src/index.js +772 -0
  55. package/src/jest.setup.js +1 -0
  56. package/src/loggers.js +93 -0
  57. package/src/messages.js +217 -0
  58. package/src/promiseCoalescer.js +52 -0
  59. package/src/utils.js +214 -0
  60. package/test-types.ts +96 -0
  61. package/tsconfig.json +13 -0
  62. package/dist/ldclient-common.cjs.js +0 -2
  63. package/dist/ldclient-common.cjs.js.map +0 -1
  64. package/dist/ldclient-common.es.js +0 -2
  65. package/dist/ldclient-common.es.js.map +0 -1
  66. package/dist/ldclient-common.min.js +0 -2
  67. package/dist/ldclient-common.min.js.map +0 -1
@@ -0,0 +1,122 @@
1
+ import * as url from 'url';
2
+ import { AsyncQueue } from 'launchdarkly-js-test-helpers';
3
+
4
+ // The js-sdk-common package does not do any HTTP requests itself, because the implementation of
5
+ // HTTP is platform-dependent and must be provided by the individual SDKs (e.g. the browser SDK,
6
+ // which uses XMLHttpRequest, versus the Electron SDK, which uses Node HTTP). So, for testing
7
+ // this package, there is no point in using an HTTP capture tool like Sinon or a real embedded
8
+ // HTTP server. Instead we use this simple implementation of the abstraction, which lets us set
9
+ // up test handlers with a syntax that imitates our launchdarkly-js-test-helpers HTTP server.
10
+
11
+ let lastServerId = 0;
12
+
13
+ export function MockHttpState() {
14
+ const servers = {};
15
+
16
+ return {
17
+ newServer: () => {
18
+ lastServerId++;
19
+ const hostname = 'mock-server-' + lastServerId;
20
+ const server = newMockServer(hostname);
21
+ servers[hostname] = server;
22
+ return server;
23
+ },
24
+
25
+ doRequest: (method, requestUrl, headers, body, synchronous) => {
26
+ const urlParams = url.parse(requestUrl);
27
+ const server = servers[urlParams.host];
28
+ if (!server) {
29
+ return { promise: Promise.reject('unknown host: ' + urlParams.host) };
30
+ }
31
+ return server._doRequest(method, urlParams, headers, body, synchronous);
32
+ },
33
+ };
34
+ }
35
+
36
+ function newMockServer(hostname) {
37
+ let defaultHandler = respond(404);
38
+ const matchers = [];
39
+ const requests = new AsyncQueue();
40
+
41
+ function dispatch(req, resp) {
42
+ for (const i in matchers) {
43
+ if (matchers[i](req, resp)) {
44
+ return;
45
+ }
46
+ }
47
+ defaultHandler(req, resp);
48
+ }
49
+
50
+ const server = {
51
+ url: 'http://' + hostname,
52
+
53
+ requests,
54
+
55
+ nextRequest: async () => await requests.take(),
56
+
57
+ byDefault: handler => {
58
+ defaultHandler = handler;
59
+ return server;
60
+ },
61
+
62
+ forMethodAndPath: (method, path, handler) => {
63
+ const matcher = (req, resp) => {
64
+ if (req.method === method.toLowerCase() && req.path === path) {
65
+ handler(req, resp);
66
+ return true;
67
+ }
68
+ return false;
69
+ };
70
+ matchers.push(matcher);
71
+ return server;
72
+ },
73
+
74
+ close: () => {}, // currently we don't need to clean up the server state
75
+
76
+ _doRequest: (method, urlParams, headers, body) => {
77
+ const transformedHeaders = {};
78
+ Object.keys(headers || {}).forEach(key => {
79
+ transformedHeaders[key.toLowerCase()] = headers[key];
80
+ });
81
+ const req = {
82
+ method: method.toLowerCase(),
83
+ path: urlParams.path,
84
+ headers: transformedHeaders,
85
+ body,
86
+ };
87
+ requests.add(req);
88
+ const ret = {};
89
+ ret.promise = new Promise((resolve, reject) => {
90
+ const resp = { resolve, reject };
91
+ dispatch(req, resp);
92
+ });
93
+ return ret;
94
+ },
95
+ };
96
+
97
+ return server;
98
+ }
99
+
100
+ export function respond(status, headers, body) {
101
+ return (req, resp) => {
102
+ const respProps = {
103
+ // these are the properties our HTTP abstraction expects
104
+ status,
105
+ header: name => headers && headers[name.toLowerCase()],
106
+ body,
107
+ };
108
+ resp.resolve(respProps);
109
+ };
110
+ }
111
+
112
+ export function respondJson(data) {
113
+ return respond(200, { 'content-type': 'application/json' }, JSON.stringify(data));
114
+ }
115
+
116
+ export const fakeNetworkErrorValue = new Error('fake network error');
117
+
118
+ export function networkError() {
119
+ return (req, resp) => {
120
+ resp.reject(fakeNetworkErrorValue);
121
+ };
122
+ }
@@ -0,0 +1,128 @@
1
+ import promiseCoalescer from '../promiseCoalescer';
2
+
3
+ describe('promiseCoalescer', () => {
4
+ function instrumentedPromise() {
5
+ let resolveFn, rejectFn;
6
+ const p = new Promise((resolve, reject) => {
7
+ resolveFn = resolve;
8
+ rejectFn = reject;
9
+ });
10
+ p.resolve = resolveFn;
11
+ p.reject = rejectFn;
12
+ return p;
13
+ }
14
+
15
+ describe('with a single promise', () => {
16
+ it('resolves', async () => {
17
+ const c = promiseCoalescer();
18
+ const p = instrumentedPromise();
19
+ c.addPromise(p);
20
+ p.resolve(3);
21
+ const result = await c.resultPromise;
22
+ expect(result).toEqual(3);
23
+ });
24
+
25
+ it('rejects', async () => {
26
+ const c = promiseCoalescer();
27
+ const p = instrumentedPromise();
28
+ c.addPromise(p);
29
+ p.reject(new Error('no'));
30
+ await expect(c.resultPromise).rejects.toThrow('no');
31
+ });
32
+
33
+ it('does not call cancelFn', async () => {
34
+ const fn = jest.fn();
35
+ const c = promiseCoalescer();
36
+ const p = instrumentedPromise();
37
+ c.addPromise(p, fn);
38
+ p.resolve(3);
39
+ await c.resultPromise;
40
+ expect(fn).not.toHaveBeenCalled();
41
+ });
42
+
43
+ it('calls finallyFn', async () => {
44
+ const fn = jest.fn();
45
+ const c = promiseCoalescer(fn);
46
+ const p = instrumentedPromise();
47
+ c.addPromise(p, fn);
48
+ expect(fn).not.toHaveBeenCalled();
49
+ p.resolve(3);
50
+ await c.resultPromise;
51
+ expect(fn).toHaveBeenCalledTimes(1);
52
+ });
53
+ });
54
+
55
+ describe('with multiple promises', () => {
56
+ it('resolves only from the last one', async () => {
57
+ const c = promiseCoalescer();
58
+ const p1 = instrumentedPromise();
59
+ const p2 = instrumentedPromise();
60
+ const p3 = instrumentedPromise();
61
+ c.addPromise(p1);
62
+ c.addPromise(p2);
63
+ c.addPromise(p3);
64
+ p2.resolve(2);
65
+ p3.resolve(3);
66
+ p1.resolve(1);
67
+ const result = await c.resultPromise;
68
+ expect(result).toEqual(3);
69
+ });
70
+
71
+ it('rejects only from the last one', async () => {
72
+ const c = promiseCoalescer();
73
+ const p1 = instrumentedPromise();
74
+ const p2 = instrumentedPromise();
75
+ const p3 = instrumentedPromise();
76
+ c.addPromise(p1);
77
+ c.addPromise(p2);
78
+ c.addPromise(p3);
79
+ p2.resolve(2);
80
+ p3.reject(new Error('no'));
81
+ p1.resolve(new Error('maybe'));
82
+ await expect(c.resultPromise).rejects.toThrow('no');
83
+ });
84
+
85
+ it('calls cancelFn on all but the last', async () => {
86
+ const fn1 = jest.fn();
87
+ const fn2 = jest.fn();
88
+ const fn3 = jest.fn();
89
+ const c = promiseCoalescer();
90
+ const p1 = instrumentedPromise();
91
+ const p2 = instrumentedPromise();
92
+ const p3 = instrumentedPromise();
93
+ c.addPromise(p1, fn1);
94
+ expect(fn1).not.toHaveBeenCalled();
95
+ c.addPromise(p2, fn2);
96
+ expect(fn1).toHaveBeenCalledTimes(1);
97
+ expect(fn2).not.toHaveBeenCalled();
98
+ c.addPromise(p3, fn3);
99
+ expect(fn1).toHaveBeenCalledTimes(1);
100
+ expect(fn2).toHaveBeenCalledTimes(1);
101
+ expect(fn3).not.toHaveBeenCalled();
102
+ p2.resolve(2);
103
+ p3.resolve(3);
104
+ p1.resolve(1);
105
+ await c.resultPromise;
106
+ expect(fn3).not.toHaveBeenCalled();
107
+ });
108
+
109
+ it('calls finallyFn', async () => {
110
+ const fn = jest.fn();
111
+ const c = promiseCoalescer(fn);
112
+ const p1 = instrumentedPromise();
113
+ const p2 = instrumentedPromise();
114
+ const p3 = instrumentedPromise();
115
+ c.addPromise(p1);
116
+ expect(fn).not.toHaveBeenCalled();
117
+ c.addPromise(p2);
118
+ expect(fn).not.toHaveBeenCalled();
119
+ c.addPromise(p3);
120
+ expect(fn).not.toHaveBeenCalled();
121
+ p2.resolve(2);
122
+ p3.resolve(3);
123
+ p1.resolve(1);
124
+ await c.resultPromise;
125
+ expect(fn).toHaveBeenCalledTimes(1);
126
+ });
127
+ });
128
+ });
@@ -0,0 +1,148 @@
1
+ import * as LDClient from '../index';
2
+ import EventEmitter from '../EventEmitter';
3
+
4
+ import { AsyncQueue, sleepAsync } from 'launchdarkly-js-test-helpers';
5
+
6
+ import EventSource from './EventSource-mock';
7
+ import { MockHttpState } from './mockHttp';
8
+
9
+ // This file provides a stub implementation of the internal platform API for use in tests.
10
+ //
11
+ // The SDK expects the platform object to have the following properties and methods:
12
+ //
13
+ // httpRequest?: (method, url, headers, body, sync) => requestProperties
14
+ // requestProperties.promise: Promise // resolves to { status, header: (name) => value, body } or rejects for a network error
15
+ // requestProperties.cancel?: () => void // provided if it's possible to cancel requests in this implementation
16
+ // httpAllowsPost: boolean // true if we can do cross-origin POST requests
17
+ // httpFallbackPing?: (url) => {} // method for doing an HTTP GET without awaiting the result (i.e. browser image mechanism)
18
+ // getCurrentUrl: () => string // returns null if we're not in a browser
19
+ // isDoNotTrack: () => boolean
20
+ // localStorage: {
21
+ // get: (key: string, callback: (err: Error, data: string) => void) => void
22
+ // set: (key: string, data: string, callback: (err: Error) => void) => void
23
+ // clear: (key: string, callback: (err: Error) => void) => void
24
+ // }
25
+ // eventSourceFactory?: (url: string, options: object) => EventSource
26
+ // // note that the options are ignored by the browser's built-in EventSource; they only work with polyfills
27
+ // eventSourceIsActive?: (es: EventSource) => boolean // returns true if it's open or connecting
28
+ // eventSourceAllowsReport?: boolean // returns true if we can set { method: 'REPORT' } in the options
29
+ // diagnosticSdkData: object // provides the "sdk" property in diagnostic events
30
+ // diagnosticPlatformData: object // provides the "platform" property in diagnostic events
31
+ // diagnosticUseCombinedEvent: boolean // true if diagnostic events should use the combined model (browser SDK)
32
+ // uuid: () => string // function to generate a UUID (this is done differently in browsers vs. Node)
33
+ // userAgent: string
34
+ // userAgentHeaderName?: string // custom header name to use instead of User-Agent
35
+ // version?: string // the SDK version for the User-Agent header, if that is *not* the same as the version of launchdarkly-js-sdk-common
36
+
37
+ export function defaults() {
38
+ const localStore = {};
39
+ const mockHttpState = MockHttpState();
40
+ const eventSourcesCreated = new AsyncQueue();
41
+ let currentUrl = null;
42
+ let doNotTrack = false;
43
+
44
+ const p = {
45
+ httpRequest: mockHttpState.doRequest,
46
+ diagnosticSdkData: { name: 'stub-sdk' },
47
+ diagnosticPlatformData: { name: 'stub-platform' },
48
+ httpAllowsPost: () => true,
49
+ httpAllowsSync: () => true,
50
+ getCurrentUrl: () => currentUrl,
51
+ isDoNotTrack: () => doNotTrack,
52
+ eventSourceFactory: (url, options) => {
53
+ const es = new EventSource(url);
54
+ es.options = options;
55
+ eventSourcesCreated.add({ eventSource: es, url, options });
56
+ return es;
57
+ },
58
+ eventSourceIsActive: es => es.readyState === EventSource.OPEN || es.readyState === EventSource.CONNECTING,
59
+ localStorage: {
60
+ get: key =>
61
+ new Promise(resolve => {
62
+ resolve(localStore[key]);
63
+ }),
64
+ set: (key, value) =>
65
+ new Promise(resolve => {
66
+ localStore[key] = value;
67
+ resolve();
68
+ }),
69
+ clear: key =>
70
+ new Promise(resolve => {
71
+ delete localStore[key];
72
+ resolve();
73
+ }),
74
+ },
75
+ userAgent: 'stubClient',
76
+ version: '1.2.3',
77
+
78
+ // extra methods used for testing
79
+ testing: {
80
+ logger: logger(),
81
+
82
+ http: mockHttpState,
83
+
84
+ eventSourcesCreated,
85
+
86
+ makeClient: (env, user, options = {}) => {
87
+ const config = { logger: p.testing.logger, ...options };
88
+ // We want to simulate what the platform-specific SDKs will do in their own initialization functions.
89
+ // They will call the common package's LDClient.initialize() and receive the clientVars object which
90
+ // contains both the underlying client (in its "client" property) and some internal methods that the
91
+ // platform-specific SDKs can use to do internal stuff. One of those is start(), which they will
92
+ // call after doing any other initialization things they may need to do.
93
+ const clientVars = LDClient.initialize(env, user, config, p);
94
+ clientVars.start();
95
+ return clientVars.client;
96
+ },
97
+
98
+ setCurrentUrl: url => {
99
+ currentUrl = url;
100
+ },
101
+
102
+ setDoNotTrack: value => {
103
+ doNotTrack = value;
104
+ },
105
+
106
+ getLocalStorageImmediately: key => localStore[key],
107
+
108
+ setLocalStorageImmediately: (key, value) => {
109
+ localStore[key] = value;
110
+ },
111
+
112
+ expectStream: async url => {
113
+ await sleepAsync(0); // in case the stream is created by a deferred task
114
+ expect(eventSourcesCreated.length()).toBeGreaterThanOrEqual(1);
115
+ const created = await eventSourcesCreated.take();
116
+ if (url) {
117
+ expect(created.url).toEqual(url);
118
+ }
119
+ return created;
120
+ },
121
+ },
122
+ };
123
+ return p;
124
+ }
125
+
126
+ export function withoutHttp() {
127
+ const e = defaults();
128
+ delete e.httpRequest;
129
+ return e;
130
+ }
131
+
132
+ export function logger() {
133
+ const logger = {};
134
+ ['debug', 'info', 'warn', 'error'].forEach(level => {
135
+ logger[level] = msg => logger.output[level].push(typeof msg === 'function' ? msg() : msg);
136
+ });
137
+ logger.reset = () => {
138
+ logger.output = { debug: [], info: [], warn: [], error: [] };
139
+ };
140
+ logger.reset();
141
+ return logger;
142
+ }
143
+
144
+ export function mockStateProvider(initialState) {
145
+ const sp = EventEmitter();
146
+ sp.getInitialState = () => initialState;
147
+ return sp;
148
+ }
@@ -0,0 +1,77 @@
1
+ import { AsyncQueue } from 'launchdarkly-js-test-helpers';
2
+
3
+ export const numericUser = {
4
+ key: 1,
5
+ secondary: 2,
6
+ ip: 3,
7
+ country: 4,
8
+ email: 5,
9
+ firstName: 6,
10
+ lastName: 7,
11
+ avatar: 8,
12
+ name: 9,
13
+ anonymous: false,
14
+ custom: { age: 99 },
15
+ };
16
+
17
+ // This returns a Promise with a .callback property that is a plain callback function; when
18
+ // called, it will resolve the promise with either a single value or an array of arguments.
19
+ export function promiseListener() {
20
+ let cb;
21
+ const p = new Promise(resolve => {
22
+ cb = function(value) {
23
+ if (arguments.length > 1) {
24
+ resolve(Array.prototype.slice.call(arguments));
25
+ } else {
26
+ resolve(value);
27
+ }
28
+ };
29
+ });
30
+ p.callback = cb;
31
+ return p;
32
+ }
33
+
34
+ export const stringifiedNumericUser = {
35
+ key: '1',
36
+ secondary: '2',
37
+ ip: '3',
38
+ country: '4',
39
+ email: '5',
40
+ firstName: '6',
41
+ lastName: '7',
42
+ avatar: '8',
43
+ name: '9',
44
+ anonymous: false,
45
+ custom: { age: 99 },
46
+ };
47
+
48
+ export function makeBootstrap(flagsData) {
49
+ const ret = { $flagsState: {} };
50
+ for (const key in flagsData) {
51
+ const state = { ...flagsData[key] };
52
+ ret[key] = state.value;
53
+ delete state.value;
54
+ ret.$flagsState[key] = state;
55
+ }
56
+ return ret;
57
+ }
58
+
59
+ export function MockEventSender() {
60
+ const calls = new AsyncQueue();
61
+ let serverTime = null;
62
+ let status = 200;
63
+ const sender = {
64
+ calls,
65
+ sendEvents: (events, url) => {
66
+ calls.add({ events, url });
67
+ return Promise.resolve({ serverTime, status });
68
+ },
69
+ setServerTime: time => {
70
+ serverTime = time;
71
+ },
72
+ setStatus: respStatus => {
73
+ status = respStatus;
74
+ },
75
+ };
76
+ return sender;
77
+ }
@@ -0,0 +1,148 @@
1
+ import {
2
+ base64URLEncode,
3
+ getLDHeaders,
4
+ transformHeaders,
5
+ getLDUserAgentString,
6
+ wrapPromiseCallback,
7
+ chunkUserEventsForUrl,
8
+ } from '../utils';
9
+
10
+ import * as stubPlatform from './stubPlatform';
11
+
12
+ describe('utils', () => {
13
+ describe('wrapPromiseCallback', () => {
14
+ it('should resolve to the value', done => {
15
+ const promise = wrapPromiseCallback(Promise.resolve('woohoo'));
16
+ promise.then(value => {
17
+ expect(value).toEqual('woohoo');
18
+ done();
19
+ });
20
+ });
21
+
22
+ it('should reject with the error', done => {
23
+ const error = new Error('something went wrong');
24
+ const promise = wrapPromiseCallback(Promise.reject(error));
25
+ promise.catch(error => {
26
+ expect(error).toEqual(error);
27
+ done();
28
+ });
29
+ });
30
+
31
+ it('should call the callback with a value if the promise resolves', done => {
32
+ const promise = wrapPromiseCallback(Promise.resolve('woohoo'), (error, value) => {
33
+ expect(promise).toBeUndefined();
34
+ expect(error).toBeNull();
35
+ expect(value).toEqual('woohoo');
36
+ done();
37
+ });
38
+ });
39
+
40
+ it('should call the callback with an error if the promise rejects', done => {
41
+ const actualError = new Error('something went wrong');
42
+ const promise = wrapPromiseCallback(Promise.reject(actualError), (error, value) => {
43
+ expect(promise).toBeUndefined();
44
+ expect(error).toEqual(actualError);
45
+ expect(value).toBeNull();
46
+ done();
47
+ });
48
+ });
49
+ });
50
+
51
+ describe('getLDHeaders', () => {
52
+ it('sends no headers unless sendLDHeaders is true', () => {
53
+ const platform = stubPlatform.defaults();
54
+ const headers = getLDHeaders(platform, {});
55
+ expect(headers).toEqual({});
56
+ });
57
+
58
+ it('adds user-agent header', () => {
59
+ const platform = stubPlatform.defaults();
60
+ const headers = getLDHeaders(platform, { sendLDHeaders: true });
61
+ expect(headers).toMatchObject({ 'User-Agent': getLDUserAgentString(platform) });
62
+ });
63
+
64
+ it('adds user-agent header with custom name', () => {
65
+ const platform = stubPlatform.defaults();
66
+ platform.userAgentHeaderName = 'X-Fake-User-Agent';
67
+ const headers = getLDHeaders(platform, { sendLDHeaders: true });
68
+ expect(headers).toMatchObject({ 'X-Fake-User-Agent': getLDUserAgentString(platform) });
69
+ });
70
+
71
+ it('adds wrapper info if specified, without version', () => {
72
+ const platform = stubPlatform.defaults();
73
+ const headers = getLDHeaders(platform, { sendLDHeaders: true, wrapperName: 'FakeSDK' });
74
+ expect(headers).toMatchObject({
75
+ 'User-Agent': getLDUserAgentString(platform),
76
+ 'X-LaunchDarkly-Wrapper': 'FakeSDK',
77
+ });
78
+ });
79
+
80
+ it('adds wrapper info if specified, with version', () => {
81
+ const platform = stubPlatform.defaults();
82
+ const headers = getLDHeaders(platform, { sendLDHeaders: true, wrapperName: 'FakeSDK', wrapperVersion: '9.9' });
83
+ expect(headers).toMatchObject({
84
+ 'User-Agent': getLDUserAgentString(platform),
85
+ 'X-LaunchDarkly-Wrapper': 'FakeSDK/9.9',
86
+ });
87
+ });
88
+ });
89
+
90
+ describe('transformHeaders', () => {
91
+ it('does not modify the headers if the option is not available', () => {
92
+ const inputHeaders = { a: '1', b: '2' };
93
+ const headers = transformHeaders(inputHeaders, {});
94
+ expect(headers).toEqual(inputHeaders);
95
+ });
96
+
97
+ it('modifies the headers if the option has a transform', () => {
98
+ const inputHeaders = { c: '3', d: '4' };
99
+ const outputHeaders = { c: '9', d: '4', e: '5' };
100
+ const headerTransform = input => {
101
+ const output = { ...input };
102
+ output['c'] = '9';
103
+ output['e'] = '5';
104
+ return output;
105
+ };
106
+ const headers = transformHeaders(inputHeaders, { requestHeaderTransform: headerTransform });
107
+ expect(headers).toEqual(outputHeaders);
108
+ });
109
+
110
+ it('cannot mutate the input header object', () => {
111
+ const inputHeaders = { f: '6' };
112
+ const expectedInputHeaders = { f: '6' };
113
+ const headerMutate = input => {
114
+ input['f'] = '7'; // eslint-disable-line no-param-reassign
115
+ return input;
116
+ };
117
+ transformHeaders(inputHeaders, { requestHeaderTransform: headerMutate });
118
+ expect(inputHeaders).toEqual(expectedInputHeaders);
119
+ });
120
+ });
121
+
122
+ describe('getLDUserAgentString', () => {
123
+ it('uses platform user-agent and package version by default', () => {
124
+ const platform = stubPlatform.defaults();
125
+ platform.version = undefined;
126
+ const ua = getLDUserAgentString(platform);
127
+ expect(ua).toEqual('stubClient/' + VERSION);
128
+ });
129
+
130
+ it('uses platform user-agent and platform version if provided', () => {
131
+ const platform = stubPlatform.defaults();
132
+ platform.version = '7.8.9';
133
+ const ua = getLDUserAgentString(platform);
134
+ expect(ua).toEqual('stubClient/7.8.9');
135
+ });
136
+ });
137
+
138
+ describe('chunkUserEventsForUrl', () => {
139
+ it('should properly chunk the list of events', () => {
140
+ const user = { key: 'foo' };
141
+ const event = { kind: 'identify', key: user.key };
142
+ const eventLength = base64URLEncode(JSON.stringify(event)).length;
143
+ const events = [event, event, event, event, event];
144
+ const chunks = chunkUserEventsForUrl(eventLength * 2, events);
145
+ expect(chunks).toEqual([[event, event], [event, event], [event]]);
146
+ });
147
+ });
148
+ });