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,111 @@
|
|
|
1
|
+
import * as stubPlatform from './stubPlatform';
|
|
2
|
+
|
|
3
|
+
import * as messages from '../messages';
|
|
4
|
+
import Identity from '../Identity';
|
|
5
|
+
import PersistentFlagStore from '../PersistentFlagStore';
|
|
6
|
+
import PersistentStorage from '../PersistentStorage';
|
|
7
|
+
import * as utils from '../utils';
|
|
8
|
+
|
|
9
|
+
describe('PersistentFlagStore', () => {
|
|
10
|
+
const user = { key: 'user' };
|
|
11
|
+
const ident = Identity(user);
|
|
12
|
+
const env = 'ENVIRONMENT';
|
|
13
|
+
const lsKey = 'ld:' + env + ':' + utils.btoa(JSON.stringify(user));
|
|
14
|
+
|
|
15
|
+
it('stores flags', async () => {
|
|
16
|
+
const platform = stubPlatform.defaults();
|
|
17
|
+
const storage = PersistentStorage(platform.localStorage, platform.testing.logger);
|
|
18
|
+
const store = PersistentFlagStore(storage, env, '', ident, platform.testing.logger);
|
|
19
|
+
|
|
20
|
+
const flags = { flagKey: { value: 'x' } };
|
|
21
|
+
|
|
22
|
+
await store.saveFlags(flags);
|
|
23
|
+
|
|
24
|
+
const value = platform.testing.getLocalStorageImmediately(lsKey);
|
|
25
|
+
const expected = { $schema: 1, ...flags };
|
|
26
|
+
expect(JSON.parse(value)).toEqual(expected);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('retrieves and parses flags', async () => {
|
|
30
|
+
const platform = stubPlatform.defaults();
|
|
31
|
+
const storage = PersistentStorage(platform.localStorage, platform.testing.logger);
|
|
32
|
+
const store = PersistentFlagStore(storage, env, '', ident, platform.testing.logger);
|
|
33
|
+
|
|
34
|
+
const expected = { flagKey: { value: 'x' } };
|
|
35
|
+
const stored = { $schema: 1, ...expected };
|
|
36
|
+
platform.testing.setLocalStorageImmediately(lsKey, JSON.stringify(stored));
|
|
37
|
+
|
|
38
|
+
const values = await store.loadFlags();
|
|
39
|
+
expect(values).toEqual(expected);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('converts flags from old format if schema property is missing', async () => {
|
|
43
|
+
const platform = stubPlatform.defaults();
|
|
44
|
+
const storage = PersistentStorage(platform.localStorage, platform.testing.logger);
|
|
45
|
+
const store = PersistentFlagStore(storage, env, '', ident, platform.testing.logger);
|
|
46
|
+
|
|
47
|
+
const oldFlags = { flagKey: 'x' };
|
|
48
|
+
const newFlags = { flagKey: { value: 'x', version: 0 } };
|
|
49
|
+
platform.testing.setLocalStorageImmediately(lsKey, JSON.stringify(oldFlags));
|
|
50
|
+
|
|
51
|
+
const values = await store.loadFlags();
|
|
52
|
+
expect(values).toEqual(newFlags);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns null if storage is empty', async () => {
|
|
56
|
+
const platform = stubPlatform.defaults();
|
|
57
|
+
const storage = PersistentStorage(platform.localStorage, platform.testing.logger);
|
|
58
|
+
const store = PersistentFlagStore(storage, env, '', ident, platform.testing.logger);
|
|
59
|
+
|
|
60
|
+
const values = await store.loadFlags();
|
|
61
|
+
expect(values).toBe(null);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('clears storage and returns null if value is not valid JSON', async () => {
|
|
65
|
+
const platform = stubPlatform.defaults();
|
|
66
|
+
const storage = PersistentStorage(platform.localStorage, platform.testing.logger);
|
|
67
|
+
const store = PersistentFlagStore(storage, env, '', ident, platform.testing.logger);
|
|
68
|
+
|
|
69
|
+
platform.testing.setLocalStorageImmediately(lsKey, '{bad');
|
|
70
|
+
|
|
71
|
+
expect(await store.loadFlags()).toBe(null);
|
|
72
|
+
|
|
73
|
+
expect(platform.testing.getLocalStorageImmediately(lsKey)).toBe(undefined);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('uses hash, if present, instead of user properties', async () => {
|
|
77
|
+
const platform = stubPlatform.defaults();
|
|
78
|
+
const storage = PersistentStorage(platform.localStorage, platform.testing.logger);
|
|
79
|
+
const hash = '12345';
|
|
80
|
+
const keyWithHash = 'ld:' + env + ':' + hash;
|
|
81
|
+
const store = PersistentFlagStore(storage, env, hash, ident, platform.testing.logger);
|
|
82
|
+
|
|
83
|
+
const flags = { flagKey: { value: 'x' } };
|
|
84
|
+
await store.saveFlags(flags);
|
|
85
|
+
|
|
86
|
+
const value = platform.testing.getLocalStorageImmediately(keyWithHash);
|
|
87
|
+
expect(JSON.parse(value)).toEqual({ $schema: 1, ...flags });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle localStorage.get returning an error', async () => {
|
|
91
|
+
const platform = stubPlatform.defaults();
|
|
92
|
+
const storage = PersistentStorage(platform.localStorage, platform.testing.logger);
|
|
93
|
+
const store = PersistentFlagStore(storage, env, '', ident, platform.testing.logger);
|
|
94
|
+
const myError = new Error('localstorage getitem error');
|
|
95
|
+
jest.spyOn(platform.localStorage, 'get').mockImplementation(() => Promise.reject(myError));
|
|
96
|
+
|
|
97
|
+
expect(await store.loadFlags()).toBe(null);
|
|
98
|
+
expect(platform.testing.logger.output.warn).toEqual([messages.localStorageUnavailable(myError)]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should handle localStorage.set returning an error', async () => {
|
|
102
|
+
const platform = stubPlatform.defaults();
|
|
103
|
+
const storage = PersistentStorage(platform.localStorage, platform.testing.logger);
|
|
104
|
+
const store = PersistentFlagStore(storage, env, '', ident, platform.testing.logger);
|
|
105
|
+
const myError = new Error('localstorage setitem error');
|
|
106
|
+
jest.spyOn(platform.localStorage, 'set').mockImplementation(() => Promise.reject(myError));
|
|
107
|
+
|
|
108
|
+
await store.saveFlags({ foo: {} });
|
|
109
|
+
expect(platform.testing.logger.output.warn).toEqual([messages.localStorageUnavailable(myError)]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import Requestor from '../Requestor';
|
|
2
|
+
import * as errors from '../errors';
|
|
3
|
+
import * as messages from '../messages';
|
|
4
|
+
import * as utils from '../utils';
|
|
5
|
+
|
|
6
|
+
import { fakeNetworkErrorValue, networkError, respond, respondJson } from './mockHttp';
|
|
7
|
+
import * as stubPlatform from './stubPlatform';
|
|
8
|
+
|
|
9
|
+
// These tests verify that Requestor executes the expected HTTP requests to retrieve flags. Since
|
|
10
|
+
// the js-sdk-common package uses an abstraction of HTTP requests, these tests do not use HTTP but
|
|
11
|
+
// rather use a test implementation of our HTTP abstraction; the individual platform-specific SDKs
|
|
12
|
+
// are responsible for verifying that their own implementations of the same HTTP abstraction work
|
|
13
|
+
// correctly with real networking.
|
|
14
|
+
|
|
15
|
+
describe('Requestor', () => {
|
|
16
|
+
const user = { key: 'foo' };
|
|
17
|
+
const encodedUser = 'eyJrZXkiOiJmb28ifQ';
|
|
18
|
+
const env = 'FAKE_ENV';
|
|
19
|
+
const platform = stubPlatform.defaults();
|
|
20
|
+
|
|
21
|
+
async function withServer(asyncCallback) {
|
|
22
|
+
const server = platform.testing.http.newServer();
|
|
23
|
+
server.byDefault(respondJson({}));
|
|
24
|
+
const baseConfig = { baseUrl: server.url, logger: stubPlatform.logger() };
|
|
25
|
+
return await asyncCallback(baseConfig, server);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
it('resolves on success', async () => {
|
|
29
|
+
await withServer(async (baseConfig, server) => {
|
|
30
|
+
const requestor = Requestor(platform, baseConfig, env);
|
|
31
|
+
|
|
32
|
+
await requestor.fetchFlagSettings({ key: 'user1' }, 'hash1');
|
|
33
|
+
await requestor.fetchFlagSettings({ key: 'user2' }, 'hash2');
|
|
34
|
+
|
|
35
|
+
expect(server.requests.length()).toEqual(2);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('makes requests with the GET verb if useReport is disabled', async () => {
|
|
40
|
+
await withServer(async (baseConfig, server) => {
|
|
41
|
+
const requestor = Requestor(platform, { ...baseConfig, useReport: false }, env);
|
|
42
|
+
|
|
43
|
+
await requestor.fetchFlagSettings(user, 'hash1');
|
|
44
|
+
|
|
45
|
+
expect(server.requests.length()).toEqual(1);
|
|
46
|
+
const req = await server.requests.take();
|
|
47
|
+
expect(req.method).toEqual('get');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('makes requests with the REPORT verb with a payload if useReport is enabled', async () => {
|
|
52
|
+
await withServer(async (baseConfig, server) => {
|
|
53
|
+
const requestor = Requestor(platform, { ...baseConfig, useReport: true }, env);
|
|
54
|
+
|
|
55
|
+
await requestor.fetchFlagSettings(user, 'hash1');
|
|
56
|
+
|
|
57
|
+
expect(server.requests.length()).toEqual(1);
|
|
58
|
+
const req = await server.requests.take();
|
|
59
|
+
expect(req.method).toEqual('report');
|
|
60
|
+
expect(JSON.parse(req.body)).toEqual(user);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('includes environment and user in GET URL', async () => {
|
|
65
|
+
await withServer(async (baseConfig, server) => {
|
|
66
|
+
const requestor = Requestor(platform, baseConfig, env);
|
|
67
|
+
|
|
68
|
+
await requestor.fetchFlagSettings(user, null);
|
|
69
|
+
|
|
70
|
+
expect(server.requests.length()).toEqual(1);
|
|
71
|
+
const req = await server.requests.take();
|
|
72
|
+
expect(req.path).toEqual(`/sdk/evalx/${env}/users/${encodedUser}`);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('includes environment, user, and hash in GET URL', async () => {
|
|
77
|
+
await withServer(async (baseConfig, server) => {
|
|
78
|
+
const requestor = Requestor(platform, baseConfig, env);
|
|
79
|
+
|
|
80
|
+
await requestor.fetchFlagSettings(user, 'hash1');
|
|
81
|
+
|
|
82
|
+
expect(server.requests.length()).toEqual(1);
|
|
83
|
+
const req = await server.requests.take();
|
|
84
|
+
expect(req.path).toEqual(`/sdk/evalx/${env}/users/${encodedUser}?h=hash1`);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('includes environment, user, and withReasons in GET URL', async () => {
|
|
89
|
+
await withServer(async (baseConfig, server) => {
|
|
90
|
+
const requestor = Requestor(platform, { ...baseConfig, evaluationReasons: true }, env);
|
|
91
|
+
|
|
92
|
+
await requestor.fetchFlagSettings(user, null);
|
|
93
|
+
|
|
94
|
+
expect(server.requests.length()).toEqual(1);
|
|
95
|
+
const req = await server.requests.take();
|
|
96
|
+
expect(req.path).toEqual(`/sdk/evalx/${env}/users/${encodedUser}?withReasons=true`);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('includes environment, user, hash, and withReasons in GET URL', async () => {
|
|
101
|
+
await withServer(async (baseConfig, server) => {
|
|
102
|
+
const requestor = Requestor(platform, { ...baseConfig, evaluationReasons: true }, env);
|
|
103
|
+
|
|
104
|
+
await requestor.fetchFlagSettings(user, 'hash1');
|
|
105
|
+
|
|
106
|
+
expect(server.requests.length()).toEqual(1);
|
|
107
|
+
const req = await server.requests.take();
|
|
108
|
+
expect(req.path).toEqual(`/sdk/evalx/${env}/users/${encodedUser}?h=hash1&withReasons=true`);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('includes environment in REPORT URL', async () => {
|
|
113
|
+
await withServer(async (baseConfig, server) => {
|
|
114
|
+
const requestor = Requestor(platform, { ...baseConfig, useReport: true }, env);
|
|
115
|
+
|
|
116
|
+
await requestor.fetchFlagSettings(user, null);
|
|
117
|
+
|
|
118
|
+
expect(server.requests.length()).toEqual(1);
|
|
119
|
+
const req = await server.requests.take();
|
|
120
|
+
expect(req.path).toEqual(`/sdk/evalx/${env}/user`);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('includes environment and hash in REPORT URL', async () => {
|
|
125
|
+
await withServer(async (baseConfig, server) => {
|
|
126
|
+
const requestor = Requestor(platform, { ...baseConfig, useReport: true }, env);
|
|
127
|
+
|
|
128
|
+
await requestor.fetchFlagSettings(user, 'hash1');
|
|
129
|
+
|
|
130
|
+
expect(server.requests.length()).toEqual(1);
|
|
131
|
+
const req = await server.requests.take();
|
|
132
|
+
expect(req.path).toEqual(`/sdk/evalx/${env}/user?h=hash1`);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('includes environment and withReasons in REPORT URL', async () => {
|
|
137
|
+
await withServer(async (baseConfig, server) => {
|
|
138
|
+
const requestor = Requestor(platform, { ...baseConfig, useReport: true, evaluationReasons: true }, env);
|
|
139
|
+
|
|
140
|
+
await requestor.fetchFlagSettings(user, null);
|
|
141
|
+
|
|
142
|
+
expect(server.requests.length()).toEqual(1);
|
|
143
|
+
const req = await server.requests.take();
|
|
144
|
+
expect(req.path).toEqual(`/sdk/evalx/${env}/user?withReasons=true`);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('includes environment, hash, and withReasons in REPORT URL', async () => {
|
|
149
|
+
await withServer(async (baseConfig, server) => {
|
|
150
|
+
const requestor = Requestor(platform, { ...baseConfig, useReport: true, evaluationReasons: true }, env);
|
|
151
|
+
|
|
152
|
+
await requestor.fetchFlagSettings(user, 'hash1');
|
|
153
|
+
|
|
154
|
+
expect(server.requests.length()).toEqual(1);
|
|
155
|
+
const req = await server.requests.take();
|
|
156
|
+
expect(req.path).toEqual(`/sdk/evalx/${env}/user?h=hash1&withReasons=true`);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('sends custom user-agent header in GET mode when sendLDHeaders is true', async () => {
|
|
161
|
+
await withServer(async (baseConfig, server) => {
|
|
162
|
+
const config = { ...baseConfig, sendLDHeaders: true };
|
|
163
|
+
const requestor = Requestor(platform, config, env);
|
|
164
|
+
|
|
165
|
+
await requestor.fetchFlagSettings(user);
|
|
166
|
+
|
|
167
|
+
expect(server.requests.length()).toEqual(1);
|
|
168
|
+
const req = await server.requests.take();
|
|
169
|
+
expect(req.headers['user-agent']).toEqual(utils.getLDUserAgentString(platform));
|
|
170
|
+
expect(req.headers['x-launchdarkly-wrapper']).toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('sends wrapper info if specified in GET mode when sendLDHeaders is true', async () => {
|
|
175
|
+
await withServer(async (baseConfig, server) => {
|
|
176
|
+
const config = { ...baseConfig, sendLDHeaders: true, wrapperName: 'FakeSDK' };
|
|
177
|
+
const requestor = Requestor(platform, config, env);
|
|
178
|
+
|
|
179
|
+
await requestor.fetchFlagSettings(user);
|
|
180
|
+
|
|
181
|
+
expect(server.requests.length()).toEqual(1);
|
|
182
|
+
const req = await server.requests.take();
|
|
183
|
+
expect(req.headers['user-agent']).toEqual(utils.getLDUserAgentString(platform));
|
|
184
|
+
expect(req.headers['x-launchdarkly-wrapper']).toEqual('FakeSDK');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('sends custom user-agent header in REPORT mode when sendLDHeaders is true', async () => {
|
|
189
|
+
await withServer(async (baseConfig, server) => {
|
|
190
|
+
const config = { ...baseConfig, useReport: true, sendLDHeaders: true };
|
|
191
|
+
const requestor = Requestor(platform, config, env);
|
|
192
|
+
|
|
193
|
+
await requestor.fetchFlagSettings(user, 'hash1');
|
|
194
|
+
|
|
195
|
+
expect(server.requests.length()).toEqual(1);
|
|
196
|
+
const req = await server.requests.take();
|
|
197
|
+
expect(req.headers['user-agent']).toEqual(utils.getLDUserAgentString(platform));
|
|
198
|
+
expect(req.headers['x-launchdarkly-wrapper']).toBeUndefined();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('sends wrapper info if specified in REPORT mode when sendLDHeaders is true', async () => {
|
|
203
|
+
await withServer(async (baseConfig, server) => {
|
|
204
|
+
const config = { ...baseConfig, useReport: true, sendLDHeaders: true, wrapperName: 'FakeSDK' };
|
|
205
|
+
const requestor = Requestor(platform, config, env);
|
|
206
|
+
|
|
207
|
+
await requestor.fetchFlagSettings(user, 'hash1');
|
|
208
|
+
|
|
209
|
+
expect(server.requests.length()).toEqual(1);
|
|
210
|
+
const req = await server.requests.take();
|
|
211
|
+
expect(req.headers['user-agent']).toEqual(utils.getLDUserAgentString(platform));
|
|
212
|
+
expect(req.headers['x-launchdarkly-wrapper']).toEqual('FakeSDK');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('does NOT send custom user-agent header when sendLDHeaders is false', async () => {
|
|
217
|
+
await withServer(async (baseConfig, server) => {
|
|
218
|
+
const config = { ...baseConfig, sendLDHeaders: false };
|
|
219
|
+
const requestor = Requestor(platform, config, env);
|
|
220
|
+
|
|
221
|
+
await requestor.fetchFlagSettings(user);
|
|
222
|
+
|
|
223
|
+
expect(server.requests.length()).toEqual(1);
|
|
224
|
+
const req = await server.requests.take();
|
|
225
|
+
expect(req.headers['user-agent']).toBeUndefined();
|
|
226
|
+
expect(req.headers['x-launchdarkly-wrapper']).toBeUndefined();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('sends transformed headers if requestHeaderTransform function is provided', async () => {
|
|
231
|
+
await withServer(async (baseConfig, server) => {
|
|
232
|
+
const headerTransform = input => {
|
|
233
|
+
const output = { ...input };
|
|
234
|
+
output['b'] = '20';
|
|
235
|
+
return output;
|
|
236
|
+
};
|
|
237
|
+
const config = { ...baseConfig, requestHeaderTransform: headerTransform };
|
|
238
|
+
const requestor = Requestor(platform, config, env);
|
|
239
|
+
|
|
240
|
+
await requestor.fetchFlagSettings(user);
|
|
241
|
+
|
|
242
|
+
expect(server.requests.length()).toEqual(1);
|
|
243
|
+
const req = await server.requests.take();
|
|
244
|
+
expect(req.headers['b']).toEqual('20');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('returns parsed JSON response on success', async () => {
|
|
249
|
+
const data = { foo: 'bar' };
|
|
250
|
+
await withServer(async (baseConfig, server) => {
|
|
251
|
+
server.byDefault(respondJson(data));
|
|
252
|
+
const requestor = Requestor(platform, baseConfig, env);
|
|
253
|
+
|
|
254
|
+
const result = await requestor.fetchFlagSettings(user);
|
|
255
|
+
expect(result).toEqual(data);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('allows JSON content type with charset', async () => {
|
|
260
|
+
await withServer(async (baseConfig, server) => {
|
|
261
|
+
server.byDefault(respond(200, { 'content-type': 'application/json; charset=utf-8' }, '{}'));
|
|
262
|
+
const requestor = Requestor(platform, baseConfig, env);
|
|
263
|
+
|
|
264
|
+
const result = await requestor.fetchFlagSettings(user);
|
|
265
|
+
expect(result).toEqual({});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('allows extra JSON content type header', async () => {
|
|
270
|
+
await withServer(async (baseConfig, server) => {
|
|
271
|
+
// this could happen if a proxy/gateway interpolated its own content-type header; https://github.com/launchdarkly/js-client-sdk/issues/205
|
|
272
|
+
server.byDefault(respond(200, { 'content-type': 'application/json, application/json; charset=utf-8' }, '{}'));
|
|
273
|
+
const requestor = Requestor(platform, baseConfig, env);
|
|
274
|
+
|
|
275
|
+
const result = await requestor.fetchFlagSettings(user);
|
|
276
|
+
expect(result).toEqual({});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('returns error for non-JSON content type', async () => {
|
|
281
|
+
await withServer(async (baseConfig, server) => {
|
|
282
|
+
server.byDefault(respond(200, { 'content-type': 'text/plain' }, 'sorry'));
|
|
283
|
+
const requestor = Requestor(platform, baseConfig, env);
|
|
284
|
+
|
|
285
|
+
const err = new errors.LDFlagFetchError(messages.invalidContentType('text/plain'));
|
|
286
|
+
await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('returns error for unspecified content type', async () => {
|
|
291
|
+
await withServer(async (baseConfig, server) => {
|
|
292
|
+
server.byDefault(respond(200, {}, ''));
|
|
293
|
+
const requestor = Requestor(platform, baseConfig, env);
|
|
294
|
+
|
|
295
|
+
const err = new errors.LDFlagFetchError(messages.invalidContentType(''));
|
|
296
|
+
await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('signals specific error for 404 response', async () => {
|
|
301
|
+
await withServer(async (baseConfig, server) => {
|
|
302
|
+
server.byDefault(respond(404));
|
|
303
|
+
const requestor = Requestor(platform, baseConfig, env);
|
|
304
|
+
|
|
305
|
+
const err = new errors.LDInvalidEnvironmentIdError(messages.environmentNotFound());
|
|
306
|
+
await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('signals general error for non-404 error status', async () => {
|
|
311
|
+
await withServer(async (baseConfig, server) => {
|
|
312
|
+
server.byDefault(respond(500));
|
|
313
|
+
const requestor = Requestor(platform, baseConfig, env);
|
|
314
|
+
|
|
315
|
+
const err = new errors.LDFlagFetchError(messages.errorFetchingFlags('500'));
|
|
316
|
+
await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('signals general error for network error', async () => {
|
|
321
|
+
await withServer(async (baseConfig, server) => {
|
|
322
|
+
server.byDefault(networkError());
|
|
323
|
+
const requestor = Requestor(platform, baseConfig, env);
|
|
324
|
+
|
|
325
|
+
const err = new errors.LDFlagFetchError(messages.networkError(fakeNetworkErrorValue));
|
|
326
|
+
await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('coalesces multiple requests so all callers get the latest result', async () => {
|
|
331
|
+
await withServer(async (baseConfig, server) => {
|
|
332
|
+
let n = 0;
|
|
333
|
+
server.byDefault((req, res) => {
|
|
334
|
+
n++;
|
|
335
|
+
respondJson({ value: n })(req, res);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const requestor = Requestor(platform, baseConfig, env);
|
|
339
|
+
|
|
340
|
+
const r1 = requestor.fetchFlagSettings(user);
|
|
341
|
+
const r2 = requestor.fetchFlagSettings(user);
|
|
342
|
+
|
|
343
|
+
const result1 = await r1;
|
|
344
|
+
const result2 = await r2;
|
|
345
|
+
|
|
346
|
+
expect(result1).toEqual({ value: 2 });
|
|
347
|
+
expect(result2).toEqual({ value: 2 });
|
|
348
|
+
|
|
349
|
+
expect(server.requests.length()).toEqual(2);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe('When HTTP requests are not available at all', () => {
|
|
354
|
+
it('fails on fetchFlagSettings', async () => {
|
|
355
|
+
await withServer(async (baseConfig, server) => {
|
|
356
|
+
const requestor = Requestor(stubPlatform.withoutHttp(), baseConfig, env);
|
|
357
|
+
await expect(requestor.fetchFlagSettings(user, null)).rejects.toThrow(messages.httpUnavailable());
|
|
358
|
+
expect(server.requests.length()).toEqual(0);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
});
|