@depup/launchdarkly-node-server-sdk 7.0.4-depup.0

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 (107) hide show
  1. package/.babelrc +16 -0
  2. package/.circleci/config.yml +89 -0
  3. package/.eslintignore +5 -0
  4. package/.eslintrc.yaml +114 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  6. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  8. package/.github/pull_request_template.md +21 -0
  9. package/.github/workflows/stale.yml +8 -0
  10. package/.hound.yml +33 -0
  11. package/.ldrelease/config.yml +28 -0
  12. package/.prettierrc +6 -0
  13. package/CHANGELOG.md +603 -0
  14. package/CODEOWNERS +2 -0
  15. package/CONTRIBUTING.md +55 -0
  16. package/LICENSE.txt +13 -0
  17. package/README.md +36 -0
  18. package/SECURITY.md +5 -0
  19. package/attribute_reference.js +217 -0
  20. package/big_segments.js +117 -0
  21. package/caching_store_wrapper.js +240 -0
  22. package/changes.json +30 -0
  23. package/configuration.js +235 -0
  24. package/context.js +98 -0
  25. package/context_filter.js +137 -0
  26. package/contract-tests/README.md +7 -0
  27. package/contract-tests/index.js +109 -0
  28. package/contract-tests/log.js +23 -0
  29. package/contract-tests/package.json +15 -0
  30. package/contract-tests/sdkClientEntity.js +110 -0
  31. package/contract-tests/testharness-suppressions.txt +2 -0
  32. package/diagnostic_events.js +151 -0
  33. package/docs/typedoc.js +10 -0
  34. package/errors.js +26 -0
  35. package/evaluator.js +822 -0
  36. package/event_factory.js +121 -0
  37. package/event_processor.js +320 -0
  38. package/event_summarizer.js +101 -0
  39. package/feature_store.js +120 -0
  40. package/feature_store_event_wrapper.js +258 -0
  41. package/file_data_source.js +192 -0
  42. package/flags_state.js +46 -0
  43. package/index.d.ts +2426 -0
  44. package/index.js +452 -0
  45. package/integrations.js +7 -0
  46. package/interfaces.js +2 -0
  47. package/loggers.js +125 -0
  48. package/messages.js +31 -0
  49. package/operators.js +106 -0
  50. package/package.json +105 -0
  51. package/polling.js +70 -0
  52. package/requestor.js +62 -0
  53. package/scripts/better-audit.sh +76 -0
  54. package/sharedtest/big_segment_store_tests.js +86 -0
  55. package/sharedtest/feature_store_tests.js +177 -0
  56. package/sharedtest/persistent_feature_store_tests.js +183 -0
  57. package/sharedtest/store_tests.js +7 -0
  58. package/streaming.js +179 -0
  59. package/test/LDClient-big-segments-test.js +92 -0
  60. package/test/LDClient-end-to-end-test.js +218 -0
  61. package/test/LDClient-evaluation-all-flags-test.js +226 -0
  62. package/test/LDClient-evaluation-test.js +204 -0
  63. package/test/LDClient-events-test.js +502 -0
  64. package/test/LDClient-listeners-test.js +180 -0
  65. package/test/LDClient-test.js +96 -0
  66. package/test/LDClient-tls-test.js +110 -0
  67. package/test/attribute_reference-test.js +494 -0
  68. package/test/big_segments-test.js +182 -0
  69. package/test/caching_store_wrapper-test.js +434 -0
  70. package/test/configuration-test.js +249 -0
  71. package/test/context-test.js +93 -0
  72. package/test/context_filter-test.js +424 -0
  73. package/test/diagnostic_events-test.js +152 -0
  74. package/test/evaluator-big-segments-test.js +301 -0
  75. package/test/evaluator-bucketing-test.js +333 -0
  76. package/test/evaluator-clause-test.js +277 -0
  77. package/test/evaluator-flag-test.js +452 -0
  78. package/test/evaluator-pre-conditions-test.js +105 -0
  79. package/test/evaluator-rule-test.js +131 -0
  80. package/test/evaluator-segment-match-test.js +310 -0
  81. package/test/evaluator_helpers.js +106 -0
  82. package/test/event_processor-test.js +680 -0
  83. package/test/event_summarizer-test.js +146 -0
  84. package/test/feature_store-test.js +42 -0
  85. package/test/feature_store_event_wrapper-test.js +182 -0
  86. package/test/feature_store_test_base.js +60 -0
  87. package/test/file_data_source-test.js +255 -0
  88. package/test/loggers-test.js +126 -0
  89. package/test/operators-test.js +102 -0
  90. package/test/polling-test.js +158 -0
  91. package/test/requestor-test.js +60 -0
  92. package/test/store_tests_big_segments-test.js +61 -0
  93. package/test/streaming-test.js +323 -0
  94. package/test/stubs.js +107 -0
  95. package/test/test_data-test.js +341 -0
  96. package/test/update_queue-test.js +61 -0
  97. package/test-types.ts +210 -0
  98. package/test_data.js +323 -0
  99. package/tsconfig.json +14 -0
  100. package/update_queue.js +28 -0
  101. package/utils/__tests__/httpUtils-test.js +39 -0
  102. package/utils/__tests__/wrapPromiseCallback-test.js +33 -0
  103. package/utils/asyncUtils.js +32 -0
  104. package/utils/httpUtils.js +105 -0
  105. package/utils/stringifyAttrs.js +14 -0
  106. package/utils/wrapPromiseCallback.js +36 -0
  107. package/versioned_data_kind.js +34 -0
@@ -0,0 +1,183 @@
1
+ const { nullLogger } = require('../loggers');
2
+ const dataKind = require('../versioned_data_kind');
3
+
4
+ const { runFeatureStoreTests } = require('./feature_store_tests');
5
+
6
+ const { promisifySingle, withCloseable } = require('launchdarkly-js-test-helpers');
7
+
8
+ // See index.d.ts for interface documentation
9
+
10
+ const cacheTime = 30;
11
+ const logger = nullLogger();
12
+
13
+ function runPersistentFeatureStoreTests(createStore, clearExistingData, createStoreWithConcurrentUpdateHook) {
14
+ function doAllTestsWithPrefix(prefix) {
15
+ describe('without cache', () => {
16
+ runFeatureStoreTests(
17
+ () => createStore(prefix, 0, logger),
18
+ () => clearExistingData(prefix)
19
+ );
20
+
21
+ runPersistentFeatureStoreUncachedTests(prefix, createStore, clearExistingData);
22
+ });
23
+
24
+ describe('with cache', () => {
25
+ runFeatureStoreTests(
26
+ () => createStore(prefix, cacheTime, logger),
27
+ () => clearExistingData(prefix)
28
+ );
29
+
30
+ // There are no special tests here that apply only when caching is enabled.
31
+ // We are testing the cache behavior separately in caching_store_wrapper-test.
32
+ });
33
+
34
+ if (createStoreWithConcurrentUpdateHook) {
35
+ describe('concurrent modification tests', () => {
36
+ runPersistentFeatureStoreConcurrentUpdateTests(
37
+ prefix,
38
+ createStore,
39
+ clearExistingData,
40
+ createStoreWithConcurrentUpdateHook
41
+ );
42
+ });
43
+ }
44
+ }
45
+
46
+ describe('with non-empty prefix', () => {
47
+ doAllTestsWithPrefix('testprefix');
48
+ });
49
+
50
+ describe('with empty prefix', () => {
51
+ doAllTestsWithPrefix(undefined);
52
+ });
53
+
54
+ runPersistentFeatureStoreSeparatePrefixesTest(createStore, clearExistingData);
55
+ }
56
+
57
+ function runPersistentFeatureStoreUncachedTests(prefix, createStore, clearExistingData) {
58
+ const feature1 = { key: 'foo', version: 10 };
59
+
60
+ async function testInitStateDetection(initData) {
61
+ await clearExistingData(prefix);
62
+ await withCloseable(createStore(prefix, 0, logger), async store1 => {
63
+ await withCloseable(createStore(prefix, 0, logger), async store2 => {
64
+ const result1 = await promisifySingle(store1.initialized)();
65
+ expect(result1).toBe(false);
66
+
67
+ await promisifySingle(store2.init)(initData);
68
+ const result2 = await promisifySingle(store1.initialized)();
69
+ expect(result2).toBe(true);
70
+ });
71
+ });
72
+ }
73
+
74
+ it('can detect if another instance has initialized the store', async () => {
75
+ await testInitStateDetection({ features: { foo: feature1 } });
76
+ });
77
+
78
+ it('can detect if another instance has initialized the store, even with empty data', async () => {
79
+ await testInitStateDetection({ features: {} });
80
+ });
81
+ }
82
+
83
+ function runPersistentFeatureStoreSeparatePrefixesTest(createStore, clearExistingData) {
84
+ it('is independent from other instances with different prefixes', async () => {
85
+ const prefix1 = 'a';
86
+ const prefix2 = 'b';
87
+ const flag = { key: 'flag', version: 1 };
88
+ await clearExistingData(prefix1);
89
+ await clearExistingData(prefix2);
90
+ await withCloseable(createStore(prefix1, 0, logger), async storeA => {
91
+ await promisifySingle(storeA.init)({ features: { flag: flag } });
92
+ await withCloseable(createStore(prefix2, 0, logger), async storeB => {
93
+ await promisifySingle(storeB.init)({ features: {} });
94
+ // create another instance just to make sure we're not reading cached data
95
+ await withCloseable(createStore(prefix2, 0, logger), async storeB1 => {
96
+ const item1 = await promisifySingle(storeB1.get)(dataKind.features, 'flag');
97
+ expect(item1).toBe(null);
98
+ const item2 = await promisifySingle(storeA.get)(dataKind.features, 'flag');
99
+ expect(item2).toEqual(flag);
100
+ });
101
+ });
102
+ });
103
+ });
104
+ }
105
+
106
+ function runPersistentFeatureStoreConcurrentUpdateTests(
107
+ prefix,
108
+ createStore,
109
+ clearExistingData,
110
+ createStoreWithConcurrentUpdateHook
111
+ ) {
112
+ const flagKey = 'flag';
113
+ const initialVersion = 1;
114
+
115
+ function makeFlagWithVersion(v) {
116
+ return { key: flagKey, version: v };
117
+ }
118
+
119
+ async function initStore(store) {
120
+ const allData = { features: {} };
121
+ allData['features'][flagKey] = makeFlagWithVersion(initialVersion);
122
+ await promisifySingle(store.init)(allData);
123
+ }
124
+
125
+ function writeCompetingVersions(competingStore, flagVersionsToWrite) {
126
+ let i = 0;
127
+ return callback => {
128
+ if (i < flagVersionsToWrite.length) {
129
+ const newFlag = makeFlagWithVersion(flagVersionsToWrite[i]);
130
+ i++;
131
+ competingStore.upsert(dataKind.features, newFlag, callback);
132
+ } else {
133
+ callback();
134
+ }
135
+ };
136
+ }
137
+
138
+ it('handles upsert race condition against other client with lower version', async () => {
139
+ await clearExistingData(prefix);
140
+ await withCloseable(createStore(prefix, 0, logger), async competingStore => {
141
+ const myDesiredVersion = 10;
142
+ const competingStoreVersions = [2, 3, 4]; // proves that we can retry multiple times if necessary
143
+
144
+ const myStore = createStoreWithConcurrentUpdateHook(
145
+ prefix,
146
+ logger,
147
+ writeCompetingVersions(competingStore, competingStoreVersions)
148
+ );
149
+ await withCloseable(myStore, async myStore => {
150
+ await initStore(myStore);
151
+ await promisifySingle(myStore.upsert)(dataKind.features, makeFlagWithVersion(myDesiredVersion));
152
+ const result = await promisifySingle(myStore.get)(dataKind.features, flagKey);
153
+ expect(result.version).toEqual(myDesiredVersion);
154
+ });
155
+ });
156
+ });
157
+
158
+ it('handles upsert race condition against other client with higher version', async () => {
159
+ await clearExistingData(prefix);
160
+ await withCloseable(createStore(prefix, 0, logger), async competingStore => {
161
+ const myDesiredVersion = 2;
162
+ const competingStoreVersion = 3;
163
+
164
+ const myStore = createStoreWithConcurrentUpdateHook(
165
+ prefix,
166
+ logger,
167
+ writeCompetingVersions(competingStore, [competingStoreVersion])
168
+ );
169
+ await withCloseable(myStore, async myStore => {
170
+ await initStore(myStore);
171
+ await promisifySingle(myStore.upsert)(dataKind.features, makeFlagWithVersion(myDesiredVersion));
172
+ const result = await promisifySingle(myStore.get)(dataKind.features, flagKey);
173
+ expect(result.version).toEqual(competingStoreVersion);
174
+ });
175
+ });
176
+ });
177
+ }
178
+
179
+ module.exports = {
180
+ runPersistentFeatureStoreTests,
181
+ runPersistentFeatureStoreUncachedTests,
182
+ runPersistentFeatureStoreConcurrentUpdateTests,
183
+ };
@@ -0,0 +1,7 @@
1
+ const { runBigSegmentStoreTests } = require('./big_segment_store_tests');
2
+ const { runPersistentFeatureStoreTests } = require('./persistent_feature_store_tests');
3
+
4
+ module.exports = {
5
+ runBigSegmentStoreTests,
6
+ runPersistentFeatureStoreTests,
7
+ };
package/streaming.js ADDED
@@ -0,0 +1,179 @@
1
+ const errors = require('./errors');
2
+ const httpUtils = require('./utils/httpUtils');
3
+ const messages = require('./messages');
4
+ const { EventSource } = require('launchdarkly-eventsource');
5
+ const dataKind = require('./versioned_data_kind');
6
+
7
+ // The read timeout for the stream is a fixed value that is set to be slightly longer than the expected
8
+ // interval between heartbeats from the LaunchDarkly streaming server. If this amount of time elapses
9
+ // with no new data, the connection will be cycled.
10
+ const streamReadTimeoutMillis = 5 * 60 * 1000; // 5 minutes
11
+
12
+ // Note that the requestor parameter is unused now that LD no longer uses "indirect" stream
13
+ // events. The parameter is retained here for backward compatibility with any code that uses
14
+ // this constructor directly, since it is documented in index.d.ts.
15
+ function StreamProcessor(sdkKey, config, requestor, diagnosticsManager, specifiedEventSourceFactory) {
16
+ const processor = {},
17
+ featureStore = config.featureStore;
18
+ let es;
19
+ let connectionAttemptStartTime;
20
+
21
+ const headers = httpUtils.getDefaultHeaders(sdkKey, config);
22
+
23
+ const eventSourceFactory = specifiedEventSourceFactory || EventSource;
24
+
25
+ function getKeyFromPath(kind, path) {
26
+ return path.startsWith(kind.streamApiPath) ? path.substring(kind.streamApiPath.length) : null;
27
+ }
28
+
29
+ function logConnectionStarted() {
30
+ connectionAttemptStartTime = new Date().getTime();
31
+ }
32
+
33
+ function logConnectionResult(success) {
34
+ if (connectionAttemptStartTime && diagnosticsManager) {
35
+ diagnosticsManager.recordStreamInit(
36
+ connectionAttemptStartTime,
37
+ !success,
38
+ new Date().getTime() - connectionAttemptStartTime
39
+ );
40
+ }
41
+ connectionAttemptStartTime = null;
42
+ }
43
+
44
+ processor.start = fn => {
45
+ const cb = fn || function () {};
46
+
47
+ logConnectionStarted();
48
+
49
+ function handleError(err) {
50
+ // launchdarkly-eventsource expects this function to return true if it should retry, false to shut down.
51
+ if (err.status && !errors.isHttpErrorRecoverable(err.status)) {
52
+ const message = messages.httpErrorMessage(err, 'streaming request');
53
+ config.logger.error(message);
54
+ logConnectionResult(false);
55
+ cb(new errors.LDStreamingError(err.message, err.status));
56
+ return false;
57
+ }
58
+ const message = messages.httpErrorMessage(err, 'streaming request', 'will retry');
59
+ config.logger.warn(message);
60
+ logConnectionResult(false);
61
+ logConnectionStarted();
62
+ return true;
63
+ }
64
+
65
+ es = new eventSourceFactory(config.streamUri + '/all', {
66
+ agent: config.proxyAgent,
67
+ errorFilter: handleError,
68
+ headers,
69
+ initialRetryDelayMillis: 1000 * config.streamInitialReconnectDelay,
70
+ readTimeoutMillis: streamReadTimeoutMillis,
71
+ retryResetIntervalMillis: 60000,
72
+ tlsParams: config.tlsParams,
73
+ });
74
+
75
+ es.onclose = () => {
76
+ config.logger.info('Closed LaunchDarkly stream connection');
77
+ };
78
+
79
+ // This stub handler only exists because error events must have a listener; handleError() does the work.
80
+ es.onerror = () => {};
81
+
82
+ es.onopen = () => {
83
+ config.logger.info('Opened LaunchDarkly stream connection');
84
+ };
85
+
86
+ es.onretrying = e => {
87
+ config.logger.info('Will retry stream connection in ' + e.delayMillis + ' milliseconds');
88
+ };
89
+
90
+ function reportJsonError(type, data) {
91
+ config.logger.error('Stream received invalid data in "' + type + '" message');
92
+ config.logger.debug('Invalid JSON follows: ' + data);
93
+ cb(new errors.LDStreamingError('Malformed JSON data in event stream'));
94
+ }
95
+
96
+ es.addEventListener('put', e => {
97
+ config.logger.debug('Received put event');
98
+ if (e && e.data) {
99
+ logConnectionResult(true);
100
+ let all;
101
+ try {
102
+ all = JSON.parse(e.data);
103
+ } catch (err) {
104
+ reportJsonError('put', e.data);
105
+ return;
106
+ }
107
+ const initData = {};
108
+ initData[dataKind.features.namespace] = all.data.flags;
109
+ initData[dataKind.segments.namespace] = all.data.segments;
110
+ featureStore.init(initData, () => {
111
+ cb();
112
+ });
113
+ } else {
114
+ cb(new errors.LDStreamingError('Unexpected payload from event stream'));
115
+ }
116
+ });
117
+
118
+ es.addEventListener('patch', e => {
119
+ config.logger.debug('Received patch event');
120
+ if (e && e.data) {
121
+ let patch;
122
+ try {
123
+ patch = JSON.parse(e.data);
124
+ } catch (err) {
125
+ reportJsonError('patch', e.data);
126
+ return;
127
+ }
128
+ for (const kind of Object.values(dataKind)) {
129
+ const key = getKeyFromPath(kind, patch.path);
130
+ if (key !== null) {
131
+ config.logger.debug('Updating ' + key + ' in ' + kind.namespace);
132
+ featureStore.upsert(kind, patch.data);
133
+ break;
134
+ }
135
+ }
136
+ } else {
137
+ cb(new errors.LDStreamingError('Unexpected payload from event stream'));
138
+ }
139
+ });
140
+
141
+ es.addEventListener('delete', e => {
142
+ config.logger.debug('Received delete event');
143
+ if (e && e.data) {
144
+ let data;
145
+ try {
146
+ data = JSON.parse(e.data);
147
+ } catch (err) {
148
+ reportJsonError('delete', e.data);
149
+ return;
150
+ }
151
+ const version = data.version;
152
+ for (const kind of Object.values(dataKind)) {
153
+ const key = getKeyFromPath(kind, data.path);
154
+ if (key !== null) {
155
+ config.logger.debug('Deleting ' + key + ' in ' + kind.namespace);
156
+ featureStore.delete(kind, key, version);
157
+ break;
158
+ }
159
+ }
160
+ } else {
161
+ cb(new errors.LDStreamingError('Unexpected payload from event stream'));
162
+ }
163
+ });
164
+ };
165
+
166
+ processor.stop = () => {
167
+ if (es) {
168
+ es.close();
169
+ }
170
+ };
171
+
172
+ processor.close = () => {
173
+ processor.stop();
174
+ };
175
+
176
+ return processor;
177
+ }
178
+
179
+ module.exports = StreamProcessor;
@@ -0,0 +1,92 @@
1
+ const { hashForUserKey } = require('../big_segments');
2
+ const { makeBigSegmentRef } = require('../evaluator');
3
+ const { TestData } = require('../integrations');
4
+ const stubs = require('./stubs');
5
+ const { makeSegmentMatchClause } = require('./evaluator_helpers');
6
+ const { withCloseable } = require('launchdarkly-js-test-helpers');
7
+
8
+ describe('LDClient - big segments', () => {
9
+
10
+ const user = { key: 'userkey' };
11
+ const bigSegment = {
12
+ key: 'segmentkey',
13
+ version: 1,
14
+ unbounded: true,
15
+ generation: 2,
16
+ };
17
+ const flag = {
18
+ key: 'flagkey',
19
+ on: true,
20
+ variations: [ false, true ],
21
+ fallthrough: { variation: 0 },
22
+ rules: [
23
+ { variation: 1, clauses: [ makeSegmentMatchClause(bigSegment) ] },
24
+ ],
25
+ }
26
+
27
+ async function makeClient(bigSegmentsStore, config) {
28
+ const td = TestData();
29
+ td.usePreconfiguredFlag(flag);
30
+ td.usePreconfiguredSegment(bigSegment);
31
+
32
+ const bigSegmentsConfig = {
33
+ store: bigSegmentsStore && (() => bigSegmentsStore),
34
+ ...(config && config.bigSegments),
35
+ };
36
+
37
+ return stubs.createClient({ ...config, updateProcessor: td, bigSegments: bigSegmentsConfig });
38
+ }
39
+
40
+ it('user not found in big segment store', async () => {
41
+ const store = {
42
+ getMetadata: async () => { return { lastUpToDate: new Date().getTime() } },
43
+ getUserMembership: async userHash => null,
44
+ };
45
+
46
+ await withCloseable(await makeClient(store), async client => {
47
+ await client.waitForInitialization();
48
+ const result = await client.variationDetail(flag.key, user, false);
49
+ expect(result.value).toBe(false);
50
+ expect(result.reason.bigSegmentsStatus).toEqual('HEALTHY');
51
+ });
52
+ });
53
+
54
+ it('user found, segment matched', async () => {
55
+ const membership = { [makeBigSegmentRef(bigSegment)]: true };
56
+ const store = {
57
+ getMetadata: async () => { return { lastUpToDate: new Date().getTime() } },
58
+ getUserMembership: async userHash => (userHash === hashForUserKey(user.key) ? membership : null),
59
+ };
60
+
61
+ await withCloseable(await makeClient(store), async client => {
62
+ await client.waitForInitialization();
63
+ const result = await client.variationDetail(flag.key, user, false);
64
+ expect(result.value).toBe(true);
65
+ expect(result.reason.bigSegmentsStatus).toEqual('HEALTHY');
66
+ });
67
+ });
68
+
69
+ it('store error', async () => {
70
+ const store = {
71
+ getMetadata: async () => { return { lastUpToDate: new Date().getTime() } },
72
+ getUserMembership: async userHash => { throw new Error("sorry") },
73
+ };
74
+
75
+ await withCloseable(await makeClient(store), async client => {
76
+ await client.waitForInitialization();
77
+ const result = await client.variationDetail(flag.key, user, false);
78
+ expect(result.value).toBe(false);
79
+ expect(result.reason.bigSegmentsStatus).toEqual('STORE_ERROR');
80
+ });
81
+ });
82
+
83
+ it('not configured', async () => {
84
+ await withCloseable(await makeClient(null), async client => {
85
+ await client.waitForInitialization();
86
+ const result = await client.variationDetail(flag.key, user, false);
87
+ expect(result.value).toBe(false);
88
+ expect(result.reason.bigSegmentsStatus).toEqual('NOT_CONFIGURED');
89
+ });
90
+ });
91
+
92
+ });
@@ -0,0 +1,218 @@
1
+ const LDClient = require('../index.js');
2
+ import { AsyncQueue, TestHttpHandlers, TestHttpServer, withCloseable } from 'launchdarkly-js-test-helpers';
3
+ import { stubLogger } from './stubs';
4
+
5
+ async function withAllServers(asyncCallback) {
6
+ return await withCloseable(TestHttpServer.start, async pollingServer =>
7
+ withCloseable(TestHttpServer.start, async streamingServer =>
8
+ withCloseable(TestHttpServer.start, async eventsServer => {
9
+ const servers = { polling: pollingServer, streaming: streamingServer, events: eventsServer };
10
+ const baseConfig = {
11
+ baseUri: pollingServer.url,
12
+ streamUri: streamingServer.url,
13
+ eventsUri: eventsServer.url,
14
+ logger: stubLogger(),
15
+ };
16
+ return await asyncCallback(servers, baseConfig);
17
+ })
18
+ )
19
+ );
20
+ }
21
+
22
+ describe('LDClient end-to-end', () => {
23
+ const sdkKey = 'sdkKey';
24
+ const flagKey = 'flagKey';
25
+ const expectedFlagValue = 'yes';
26
+ const flag = {
27
+ key: flagKey,
28
+ version: 1,
29
+ on: false,
30
+ offVariation: 0,
31
+ variations: [ expectedFlagValue, 'no' ]
32
+ };
33
+ const allData = { flags: { flagKey: flag }, segments: {} };
34
+
35
+ const user = { key: 'userKey' };
36
+
37
+ it('starts in polling mode', async () => {
38
+ await withAllServers(async (servers, config) => {
39
+ servers.polling.forMethodAndPath('get', '/sdk/latest-all', TestHttpHandlers.respondJson(allData));
40
+ servers.events.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200));
41
+ servers.events.forMethodAndPath('post', '/diagnostic', TestHttpHandlers.respond(200));
42
+
43
+ config.stream = false;
44
+ await withCloseable(LDClient.init(sdkKey, config), async client => {
45
+ await client.waitForInitialization();
46
+ expect(client.initialized()).toBe(true);
47
+
48
+ const value = await client.variation(flag.key, user);
49
+ expect(value).toEqual(expectedFlagValue);
50
+
51
+ await client.flush();
52
+ });
53
+
54
+ expect(servers.polling.requestCount()).toEqual(1);
55
+ expect(servers.streaming.requestCount()).toEqual(0);
56
+ expect(servers.events.requestCount()).toEqual(2);
57
+ const req0 = await servers.events.nextRequest();
58
+ expect(req0.path).toEqual('/diagnostic');
59
+ const req1 = await servers.events.nextRequest();
60
+ expect(req1.path).toEqual('/bulk');
61
+ });
62
+ });
63
+
64
+ it('fails in polling mode with 401 error', async () => {
65
+ await withAllServers(async (servers, config) => {
66
+ servers.polling.forMethodAndPath('get', '/sdk/latest-all', TestHttpHandlers.respond(401));
67
+ servers.events.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200));
68
+ servers.events.forMethodAndPath('post', '/diagnostic', TestHttpHandlers.respond(200));
69
+
70
+ config.stream = false;
71
+
72
+ await withCloseable(LDClient.init(sdkKey, config), async client => {
73
+ await expect(client.waitForInitialization()).rejects.toThrow();
74
+ expect(client.initialized()).toBe(false);
75
+ });
76
+
77
+ expect(servers.polling.requestCount()).toEqual(1);
78
+ expect(servers.streaming.requestCount()).toEqual(0);
79
+ });
80
+ });
81
+
82
+ it('starts in streaming mode', async () => {
83
+ await withAllServers(async (servers, config) => {
84
+ const streamEvent = { type: 'put', data: JSON.stringify({ data: allData }) };
85
+ await withCloseable(new AsyncQueue(), async events => {
86
+ events.add(streamEvent);
87
+ servers.streaming.forMethodAndPath('get', '/all', TestHttpHandlers.sseStream(events));
88
+ servers.events.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200));
89
+ servers.events.forMethodAndPath('post', '/diagnostic', TestHttpHandlers.respond(200));
90
+
91
+ await withCloseable(LDClient.init(sdkKey, config), async client => {
92
+ await client.waitForInitialization();
93
+ expect(client.initialized()).toBe(true);
94
+
95
+ const value = await client.variation(flag.key, user);
96
+ expect(value).toEqual(expectedFlagValue);
97
+
98
+ await client.flush();
99
+ });
100
+
101
+ expect(servers.polling.requestCount()).toEqual(0);
102
+ expect(servers.streaming.requestCount()).toEqual(1);
103
+ expect(servers.events.requestCount()).toEqual(2);
104
+ const req0 = await servers.events.nextRequest();
105
+ expect(req0.path).toEqual('/diagnostic');
106
+ const req1 = await servers.events.nextRequest();
107
+ expect(req1.path).toEqual('/bulk');
108
+ });
109
+ });
110
+ });
111
+
112
+ it('fails in streaming mode with 401 error', async () => {
113
+ await withAllServers(async (servers, config) => {
114
+ servers.streaming.forMethodAndPath('get', '/all', TestHttpHandlers.respond(401));
115
+ servers.events.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200));
116
+ servers.events.forMethodAndPath('post', '/diagnostic', TestHttpHandlers.respond(200));
117
+
118
+ await withCloseable(LDClient.init(sdkKey, config), async client => {
119
+ await expect(client.waitForInitialization()).rejects.toThrow();
120
+ expect(client.initialized()).toBe(false);
121
+ });
122
+
123
+ expect(servers.polling.requestCount()).toEqual(0);
124
+ expect(servers.streaming.requestCount()).toEqual(1);
125
+ });
126
+ });
127
+
128
+ it('can use proxy in polling mode', async () => {
129
+ await withCloseable(TestHttpServer.startProxy, async fakeProxyServer => {
130
+ await withCloseable(TestHttpServer.start, async pollingServer => {
131
+ pollingServer.forMethodAndPath('get', '/sdk/latest-all', TestHttpHandlers.respondJson(allData));
132
+
133
+ const config = {
134
+ baseUri: pollingServer.url,
135
+ proxyHost: fakeProxyServer.hostname,
136
+ proxyPort: fakeProxyServer.port,
137
+ stream: false,
138
+ sendEvents: false,
139
+ logger: stubLogger(),
140
+ };
141
+
142
+ await withCloseable(LDClient.init(sdkKey, config), async client => {
143
+ await client.waitForInitialization();
144
+ expect(client.initialized()).toBe(true);
145
+
146
+ // If the proxy server did not log a request then the SDK did not actually use the proxy
147
+ expect(fakeProxyServer.requestCount()).toEqual(1);
148
+ const req = await fakeProxyServer.nextRequest();
149
+ expect(req.path).toEqual(pollingServer.url);
150
+ });
151
+ });
152
+ });
153
+ });
154
+
155
+ it('can use proxy in streaming mode', async () => {
156
+ await withCloseable(TestHttpServer.startProxy, async fakeProxyServer => {
157
+ await withCloseable(TestHttpServer.start, async streamingServer => {
158
+ const streamEvent = { type: 'put', data: JSON.stringify({ data: allData }) };
159
+ await withCloseable(new AsyncQueue(), async events => {
160
+ events.add(streamEvent);
161
+ streamingServer.forMethodAndPath('get', '/all', TestHttpHandlers.sseStream(events));
162
+
163
+ const config = {
164
+ streamUri: streamingServer.url,
165
+ proxyHost: fakeProxyServer.hostname,
166
+ proxyPort: fakeProxyServer.port,
167
+ sendEvents: false,
168
+ logger: stubLogger(),
169
+ };
170
+
171
+ await withCloseable(LDClient.init(sdkKey, config), async client => {
172
+ await client.waitForInitialization();
173
+ expect(client.initialized()).toBe(true);
174
+
175
+ // If the proxy server did not log a request then the SDK did not actually use the proxy
176
+ expect(fakeProxyServer.requestCount()).toEqual(1);
177
+ const req = await fakeProxyServer.nextRequest();
178
+ expect(req.path).toEqual(streamingServer.url);
179
+ });
180
+ });
181
+ });
182
+ });
183
+ });
184
+
185
+ it('can use proxy for events', async () => {
186
+ await withCloseable(TestHttpServer.startProxy, async fakeProxyServer => {
187
+ await withAllServers(async (servers) => {
188
+ servers.polling.forMethodAndPath('get', '/sdk/latest-all', TestHttpHandlers.respondJson(allData));
189
+ servers.events.forMethodAndPath('post', '/diagnostic', TestHttpHandlers.respond(200));
190
+
191
+ const config = {
192
+ baseUri: servers.polling.url,
193
+ eventsUri: servers.events.url,
194
+ proxyHost: fakeProxyServer.hostname,
195
+ proxyPort: fakeProxyServer.port,
196
+ stream: false,
197
+ logger: stubLogger(),
198
+ };
199
+
200
+ await withCloseable(LDClient.init(sdkKey, config), async client => {
201
+ await client.waitForInitialization();
202
+ expect(client.initialized()).toBe(true);
203
+
204
+ // If the proxy server did not log a request then the SDK did not actually use the proxy
205
+ expect(fakeProxyServer.requestCount()).toEqual(2);
206
+ const req0 = await fakeProxyServer.nextRequest();
207
+ const req1 = await fakeProxyServer.nextRequest();
208
+ if (req0.path === servers.polling.url) {
209
+ expect(req1.path).toEqual(servers.events.url);
210
+ } else {
211
+ expect(req0.path).toEqual(servers.events.url);
212
+ expect(req1.path).toEqual(servers.polling.url);
213
+ }
214
+ });
215
+ });
216
+ });
217
+ });
218
+ });