@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,146 @@
1
+ var EventSummarizer = require('../event_summarizer');
2
+
3
+ describe('EventSummarizer', function() {
4
+
5
+ var user = { key: 'key1' };
6
+
7
+ it('does nothing for identify event', function() {
8
+ var es = EventSummarizer();
9
+ var snapshot = es.getSummary();
10
+ es.summarizeEvent({ kind: 'identify', creationDate: 1000, user: user });
11
+ expect(es.getSummary()).toEqual(snapshot);
12
+ });
13
+
14
+ it('does nothing for custom event', function() {
15
+ var es = EventSummarizer();
16
+ var snapshot = es.getSummary();
17
+ es.summarizeEvent({ kind: 'custom', creationDate: 1000, key: 'eventkey', context: user });
18
+ expect(es.getSummary()).toEqual(snapshot);
19
+ });
20
+
21
+ it('sets start and end dates for feature events', function() {
22
+ var es = EventSummarizer();
23
+ var event1 = { kind: 'feature', creationDate: 2000, key: 'key', context: user };
24
+ var event2 = { kind: 'feature', creationDate: 1000, key: 'key', context: user };
25
+ var event3 = { kind: 'feature', creationDate: 1500, key: 'key', context: user };
26
+ es.summarizeEvent(event1);
27
+ es.summarizeEvent(event2);
28
+ es.summarizeEvent(event3);
29
+ var data = es.getSummary();
30
+
31
+ expect(data.startDate).toEqual(1000);
32
+ expect(data.endDate).toEqual(2000);
33
+ });
34
+
35
+ it('increments counters for feature events', function() {
36
+ var es = EventSummarizer();
37
+ var event1 = { kind: 'feature', creationDate: 1000, key: 'key1', version: 11, context: user,
38
+ variation: 1, value: 100, default: 111 };
39
+ var event2 = { kind: 'feature', creationDate: 1000, key: 'key1', version: 11, context: user,
40
+ variation: 2, value: 200, default: 111 };
41
+ var event3 = { kind: 'feature', creationDate: 1000, key: 'key2', version: 22, context: user,
42
+ variation: 1, value: 999, default: 222 };
43
+ var event4 = { kind: 'feature', creationDate: 1000, key: 'key1', version: 11, context: user,
44
+ variation: 1, value: 100, default: 111 };
45
+ var event5 = { kind: 'feature', creationDate: 1000, key: 'badkey', context: user,
46
+ value: 333, default: 333 };
47
+ var event6 = { kind: 'feature', creationDate: 1000, key: 'zero-version', version: 0, context: user,
48
+ variation: 1, value: 100, default: 444 };
49
+ es.summarizeEvent(event1);
50
+ es.summarizeEvent(event2);
51
+ es.summarizeEvent(event3);
52
+ es.summarizeEvent(event4);
53
+ es.summarizeEvent(event5);
54
+ es.summarizeEvent(event6);
55
+ var data = es.getSummary();
56
+
57
+ data.features.key1.counters.sort(function(a, b) { return a.value - b.value; });
58
+ var expectedFeatures = {
59
+ 'zero-version': {
60
+ default: 444,
61
+ counters: [
62
+ { variation: 1, value: 100, version: 0, count: 1}
63
+ ],
64
+ contextKinds: ['user']
65
+ },
66
+ key1: {
67
+ default: 111,
68
+ counters: [
69
+ { variation: 1, value: 100, version: 11, count: 2 },
70
+ { variation: 2, value: 200, version: 11, count: 1 }
71
+ ],
72
+ contextKinds: ['user']
73
+ },
74
+ key2: {
75
+ default: 222,
76
+ counters: [ { variation: 1, value: 999, version: 22, count: 1 }],
77
+ contextKinds: ['user']
78
+ },
79
+ badkey: {
80
+ default: 333,
81
+ counters: [ { value: 333, unknown: true, count: 1 }],
82
+ contextKinds: ['user']
83
+ },
84
+ };
85
+ expect(data.features).toEqual(expectedFeatures);
86
+ });
87
+
88
+ it('distinguishes between zero and null/undefined in feature variation', function() {
89
+ var es = EventSummarizer();
90
+ var event1 = { kind: 'feature', creationDate: 1000, key: 'key1', version: 11, context: user,
91
+ variation: 0, value: 100, default: 111 };
92
+ var event2 = { kind: 'feature', creationDate: 1000, key: 'key1', version: 11, context: user,
93
+ variation: null, value: 111, default: 111 };
94
+ var event3 = { kind: 'feature', creationDate: 1000, key: 'key1', version: 11, context: user,
95
+ /* variation undefined */ value: 111, default: 111 };
96
+ es.summarizeEvent(event1);
97
+ es.summarizeEvent(event2);
98
+ es.summarizeEvent(event3);
99
+ var data = es.getSummary();
100
+
101
+ data.features.key1.counters.sort(function(a, b) { return a.value - b.value; });
102
+ var expectedFeatures = {
103
+ key1: {
104
+ default: 111,
105
+ counters: [
106
+ { variation: 0, value: 100, version: 11, count: 1 },
107
+ { value: 111, version: 11, count: 2 }
108
+ ],
109
+ contextKinds: ['user']
110
+ }
111
+ };
112
+ expect(data.features).toEqual(expectedFeatures);
113
+ });
114
+
115
+ it('includes keys from all kinds', () => {
116
+ const es = EventSummarizer();
117
+ const event1 = {
118
+ kind: 'feature', creationDate: 1000, key: 'key1', version: 11, context: { key: "test" },
119
+ variation: 1, value: 100, default: 111
120
+ };
121
+ const event2 = {
122
+ kind: 'feature', creationDate: 1000, key: 'key1', version: 11, context: { kind: 'org', key: "test" },
123
+ variation: 1, value: 100, default: 111
124
+ };
125
+ const event3 = {
126
+ kind: 'feature', creationDate: 1000, key: 'key1', version: 11,
127
+ context: { kind: 'multi', bacon: { key: "crispy" }, eggs: { key: "scrambled" } },
128
+ variation: 1, value: 100, default: 111
129
+ };
130
+ es.summarizeEvent(event1);
131
+ es.summarizeEvent(event2);
132
+ es.summarizeEvent(event3);
133
+ const data = es.getSummary();
134
+
135
+ const expectedFeatures = {
136
+ key1: {
137
+ default: 111,
138
+ counters: [
139
+ { variation: 1, value: 100, version: 11, count: 3 },
140
+ ],
141
+ contextKinds: ['user', 'org', 'bacon', 'eggs']
142
+ }
143
+ };
144
+ expect(data.features).toEqual(expectedFeatures);
145
+ });
146
+ });
@@ -0,0 +1,42 @@
1
+ const InMemoryFeatureStore = require('../feature_store');
2
+ const dataKind = require('../versioned_data_kind');
3
+ const { runFeatureStoreTests } = require('../sharedtest/feature_store_tests');
4
+ const stubs = require('./stubs');
5
+ const { promisifySingle } = require('launchdarkly-js-test-helpers');
6
+
7
+ describe('InMemoryFeatureStore', () => {
8
+ runFeatureStoreTests(
9
+ () => new InMemoryFeatureStore(),
10
+ );
11
+ });
12
+
13
+ describe('custom feature store in configuration', () => {
14
+ const defaultUser = { key: 'user' };
15
+
16
+ async function makeStoreWithFlag() {
17
+ const store = new InMemoryFeatureStore();
18
+ const flag = { key: 'flagkey', on: false, offVariation: 0, variations: [ true ] };
19
+ const data = {};
20
+ data[dataKind.features.namespace] = { 'flagkey': flag };
21
+ await promisifySingle(store.init)(data);
22
+ return store;
23
+ }
24
+
25
+ it('can be specified as an instance', async () => {
26
+ const store = await makeStoreWithFlag();
27
+ const config = { featureStore: store };
28
+ const client = stubs.createClient(config);
29
+ await client.waitForInitialization();
30
+ const result = await client.variation('flagkey', defaultUser, false);
31
+ expect(result).toEqual(true);
32
+ });
33
+
34
+ it('can be specified as a factory function', async () => {
35
+ const store = await makeStoreWithFlag();
36
+ const config = { featureStore: () => store };
37
+ const client = stubs.createClient(config);
38
+ await client.waitForInitialization();
39
+ const result = await client.variation('flagkey', defaultUser, false);
40
+ expect(result).toEqual(true);
41
+ });
42
+ })
@@ -0,0 +1,182 @@
1
+ const EventEmitter = require('events').EventEmitter;
2
+ const FeatureStoreEventWrapper = require('../feature_store_event_wrapper');
3
+ const InMemoryFeatureStore = require('../feature_store');
4
+ const dataKind = require('../versioned_data_kind');
5
+ const { AsyncQueue, promisifySingle } = require('launchdarkly-js-test-helpers');
6
+
7
+ describe('FeatureStoreEventWrapper', () => {
8
+ function listenAndStoreEvents(emitter, queue, eventName) {
9
+ emitter.on(eventName, arg => {
10
+ queue.add([eventName, arg]);
11
+ });
12
+ }
13
+
14
+ it('sends events for init of empty store', async () => {
15
+ const store = InMemoryFeatureStore();
16
+ const allData = {
17
+ features: {
18
+ a: { key: 'a', version: 1 },
19
+ b: { key: 'b', version: 1 }
20
+ },
21
+ segments: {}
22
+ };
23
+ const emitter = new EventEmitter();
24
+ const queue = new AsyncQueue();
25
+ listenAndStoreEvents(emitter, queue, 'update');
26
+ listenAndStoreEvents(emitter, queue, 'update:a');
27
+ listenAndStoreEvents(emitter, queue, 'update:b');
28
+
29
+ const wrapper = FeatureStoreEventWrapper(store, emitter);
30
+
31
+ await promisifySingle(wrapper.init)(allData);
32
+
33
+ expect(await queue.take()).toEqual(['update', { key: 'a' }]);
34
+ expect(await queue.take()).toEqual(['update:a', { key: 'a' }]);
35
+ expect(await queue.take()).toEqual(['update', { key: 'b' }]);
36
+ expect(await queue.take()).toEqual(['update:b', { key: 'b' }]);
37
+ expect(queue.isEmpty()).toEqual(true);
38
+ });
39
+
40
+ it('sends events for reinit of non-empty store', async () => {
41
+ const store = InMemoryFeatureStore();
42
+ const allData0 = {
43
+ features: {
44
+ a: { key: 'a', version: 1 },
45
+ b: { key: 'b', version: 1 },
46
+ c: { key: 'c', version: 1 }
47
+ },
48
+ segments: {}
49
+ };
50
+ const allData1 = {
51
+ features: {
52
+ a: { key: 'a', version: 1 },
53
+ b: { key: 'b', version: 2 }
54
+ },
55
+ segments: {}
56
+ };
57
+ const emitter = new EventEmitter();
58
+ const queue = new AsyncQueue();
59
+ listenAndStoreEvents(emitter, queue, 'update');
60
+ listenAndStoreEvents(emitter, queue, 'update:a');
61
+ listenAndStoreEvents(emitter, queue, 'update:b');
62
+ listenAndStoreEvents(emitter, queue, 'update:c');
63
+
64
+ const wrapper = FeatureStoreEventWrapper(store, emitter);
65
+
66
+ await promisifySingle(wrapper.init)(allData0);
67
+
68
+ expect(await queue.take()).toEqual(['update', { key: 'a' }]);
69
+ expect(await queue.take()).toEqual(['update:a', { key: 'a' }]);
70
+ expect(await queue.take()).toEqual(['update', { key: 'b' }]);
71
+ expect(await queue.take()).toEqual(['update:b', { key: 'b' }]);
72
+ expect(await queue.take()).toEqual(['update', { key: 'c' }]);
73
+ expect(await queue.take()).toEqual(['update:c', { key: 'c' }]);
74
+ expect(queue.isEmpty()).toEqual(true);
75
+
76
+ await promisifySingle(wrapper.init)(allData1);
77
+ expect(await queue.take()).toEqual(['update', { key: 'b' }]); // b was updated to version 2
78
+ expect(await queue.take()).toEqual(['update:b', { key: 'b' }]);
79
+ expect(await queue.take()).toEqual(['update', { key: 'c' }]); // c was deleted
80
+ expect(await queue.take()).toEqual(['update:c', { key: 'c' }]);
81
+ expect(queue.isEmpty()).toEqual(true);
82
+ });
83
+
84
+ it('sends event for update', async () => {
85
+ const store = InMemoryFeatureStore();
86
+ const allData = {
87
+ features: {
88
+ a: { key: 'a', version: 1 }
89
+ },
90
+ segments: {}
91
+ };
92
+ const emitter = new EventEmitter();
93
+ const queue = new AsyncQueue();
94
+ listenAndStoreEvents(emitter, queue, 'update');
95
+ listenAndStoreEvents(emitter, queue, 'update:a');
96
+
97
+ const wrapper = FeatureStoreEventWrapper(store, emitter);
98
+
99
+ await promisifySingle(wrapper.init)(allData);
100
+
101
+ expect(await queue.take()).toEqual(['update', { key: 'a' }]);
102
+ expect(await queue.take()).toEqual(['update:a', { key: 'a' }]);
103
+ expect(queue.isEmpty()).toEqual(true);
104
+
105
+ await promisifySingle(wrapper.upsert)(dataKind.features, { key: 'a', version: 2 });
106
+ await promisifySingle(wrapper.upsert)(dataKind.features, { key: 'a', version: 2 }); // no event for this one
107
+ expect(await queue.take()).toEqual(['update', { key: 'a' }]);
108
+ expect(await queue.take()).toEqual(['update:a', { key: 'a' }]);
109
+ expect(queue.isEmpty()).toEqual(true);
110
+ });
111
+
112
+ it('sends event for delete', async () => {
113
+ const store = InMemoryFeatureStore();
114
+ const allData = {
115
+ features: {
116
+ a: { key: 'a', version: 1 }
117
+ },
118
+ segments: {}
119
+ };
120
+ const emitter = new EventEmitter();
121
+ const queue = new AsyncQueue();
122
+ listenAndStoreEvents(emitter, queue, 'update');
123
+ listenAndStoreEvents(emitter, queue, 'update:a');
124
+
125
+ const wrapper = FeatureStoreEventWrapper(store, emitter);
126
+
127
+ await promisifySingle(wrapper.init)(allData);
128
+
129
+ expect(await queue.take()).toEqual(['update', { key: 'a' }]);
130
+ expect(await queue.take()).toEqual(['update:a', { key: 'a' }]);
131
+ expect(queue.isEmpty()).toEqual(true);
132
+
133
+ await promisifySingle(wrapper.delete)(dataKind.features, 'a', 2);
134
+ expect(await queue.take()).toEqual(['update', { key: 'a' }]);
135
+ expect(await queue.take()).toEqual(['update:a', { key: 'a' }]);
136
+ expect(queue.isEmpty()).toEqual(true);
137
+ });
138
+
139
+ it('sends update events for transitive dependencies', async () => {
140
+ const store = InMemoryFeatureStore();
141
+ const allData = {
142
+ features: {
143
+ a: { key: 'a', version: 1 },
144
+ b: { key: 'b', version: 1, prerequisites: [ { key: 'c' }, { key: 'e' } ] },
145
+ c: { key: 'c', version: 1, prerequisites: [ { key: 'd' } ],
146
+ rules: [
147
+ { clauses: [ { op: 'segmentMatch', values: [ 's0' ] } ] }
148
+ ]
149
+ },
150
+ d: { key: 'd', version: 1, prerequisites: [ { key: 'e' } ] },
151
+ e: { key: 'e', version: 1 }
152
+ },
153
+ segments: {
154
+ s0: { key: 's0', version: 1 }
155
+ }
156
+ };
157
+ const emitter = new EventEmitter();
158
+ const queue = new AsyncQueue();
159
+ listenAndStoreEvents(emitter, queue, 'update');
160
+
161
+ const wrapper = FeatureStoreEventWrapper(store, emitter);
162
+
163
+ await promisifySingle(wrapper.init)(allData);
164
+
165
+ expect(await queue.take()).toEqual(['update', { key: 'a' }]);
166
+ expect(await queue.take()).toEqual(['update', { key: 'b' }]);
167
+ expect(await queue.take()).toEqual(['update', { key: 'c' }]);
168
+ expect(await queue.take()).toEqual(['update', { key: 'd' }]);
169
+ expect(await queue.take()).toEqual(['update', { key: 'e' }]);
170
+ expect(queue.isEmpty()).toEqual(true);
171
+
172
+ await promisifySingle(wrapper.upsert)(dataKind.features,
173
+ { key: 'd', version: 2, prerequisites: [ { key: 'e' } ] });
174
+ expect(await queue.take()).toEqual(['update', { key: 'b' }]);
175
+ expect(await queue.take()).toEqual(['update', { key: 'c' }]);
176
+ expect(await queue.take()).toEqual(['update', { key: 'd' }]);
177
+
178
+ await promisifySingle(wrapper.upsert)(dataKind.segments, { key: 's0', version: 2 });
179
+ expect(await queue.take()).toEqual(['update', { key: 'b' }]);
180
+ expect(await queue.take()).toEqual(['update', { key: 'c' }]);
181
+ });
182
+ });
@@ -0,0 +1,60 @@
1
+ var dataKind = require('../versioned_data_kind');
2
+ const { runFeatureStoreTests } = require('../sharedtest/feature_store_tests');
3
+ const {
4
+ runPersistentFeatureStoreUncachedTests,
5
+ runPersistentFeatureStoreConcurrentUpdateTests,
6
+ } = require('../sharedtest/persistent_feature_store_tests');
7
+
8
+ // This file contains obsolete entry points with somewhat different semantics for the
9
+ // standard test suites in sharedtest/store_tests. It is retained here because older versions
10
+ // of the database integration packages reference this file directly, even though it was
11
+ // never documented. It isn't referenced by the SDK's own tests, and can be removed in the
12
+ // next major version.
13
+
14
+ // Parameters:
15
+ // - makeStore(): creates an instance of the feature store
16
+ // - clearExistingData(callback): if specified, will be called before each test to clear any
17
+ // storage that the store instances may be sharing; this also implies that the feature store
18
+ // - isCached: true if the instances returned by makeStore() have caching enabled.
19
+ // - makeStoreWithPrefix(prefix): creates an uncached instance of the store with a key prefix
20
+ function baseFeatureStoreTests(makeStore, clearExistingData, isCached, makeStoreWithPrefix) {
21
+ if (clearExistingData) {
22
+ // We're testing a persistent feature store implementation.
23
+ const asyncClearExistingData = () => new Promise(resolve => clearExistingData(resolve));
24
+ runFeatureStoreTests(
25
+ makeStore,
26
+ asyncClearExistingData,
27
+ );
28
+ if (!isCached) {
29
+ runPersistentFeatureStoreUncachedTests(
30
+ (prefix, cacheTTL, logger) => makeStorePrefix ? makeStoreWithPrefix(prefix) : makeStore(),
31
+ asyncClearExistingData,
32
+ );
33
+ }
34
+ } else {
35
+ // We're testing an in-memory store or some other nonstandard implementation that doesn't
36
+ // have shared-database semantics.
37
+ runFeatureStoreTests(
38
+ makeStore,
39
+ () => new Promise(resolve => clearExistingData(resolve)),
40
+ );
41
+ }
42
+ }
43
+
44
+ // Parameters:
45
+ // - makeStore(): creates a normal feature store.
46
+ // - makeStoreWithHook(hook): creates a feature store that operates on the same underlying data as
47
+ // the first store. This store will call the hook function (passing a callback) immediately before
48
+ // it attempts to make any update.
49
+
50
+ function concurrentModificationTests(makeStore, makeStoreWithHook) {
51
+ runPersistentFeatureStoreConcurrentUpdateTests(
52
+ prefix => makeStore(),
53
+ (prefix, hook) => makeStoreWithHook(hook),
54
+ )
55
+ }
56
+
57
+ module.exports = {
58
+ baseFeatureStoreTests: baseFeatureStoreTests,
59
+ concurrentModificationTests: concurrentModificationTests
60
+ };
@@ -0,0 +1,255 @@
1
+ const fs = require('fs');
2
+ const tmp = require('tmp');
3
+ const dataKind = require('../versioned_data_kind');
4
+ const { promisify, promisifySingle, sleepAsync } = require('launchdarkly-js-test-helpers');
5
+ const { stubLogger } = require('./stubs');
6
+
7
+ const LaunchDarkly = require('../index');
8
+ const FileDataSource = require('../file_data_source');
9
+ const InMemoryFeatureStore = require('../feature_store');
10
+
11
+ const flag1Key = 'flag1';
12
+ const flag2Key = 'flag2';
13
+ const flag2Value = 'value2';
14
+ const segment1Key = 'seg1';
15
+
16
+ const flag1 = {
17
+ "key": flag1Key,
18
+ "on": true,
19
+ "fallthrough": {
20
+ "variation": 2
21
+ },
22
+ "variations": [ "fall", "off", "on" ]
23
+ };
24
+
25
+ const segment1 = {
26
+ "key": segment1Key,
27
+ "include": ["user1"]
28
+ };
29
+
30
+ const flagOnlyJson = `
31
+ {
32
+ "flags": {
33
+ "${flag1Key}": ${ JSON.stringify(flag1) }
34
+ }
35
+ }`;
36
+
37
+ const segmentOnlyJson = `
38
+ {
39
+ "segments": {
40
+ "${segment1Key}": ${ JSON.stringify(segment1) }
41
+ }
42
+ }`;
43
+
44
+ const allPropertiesJson = `
45
+ {
46
+ "flags": {
47
+ "${flag1Key}": ${ JSON.stringify(flag1) }
48
+ },
49
+ "flagValues": {
50
+ "${flag2Key}": "${flag2Value}"
51
+ },
52
+ "segments": {
53
+ "${segment1Key}": ${ JSON.stringify(segment1) }
54
+ }
55
+ }`;
56
+
57
+ const allPropertiesYaml = `
58
+ flags:
59
+ ${flag1Key}:
60
+ key: ${flag1Key}
61
+ on: true
62
+ fallthrough:
63
+ variation: 2
64
+ variations:
65
+ - fall
66
+ - off
67
+ - on
68
+ flagValues:
69
+ ${flag2Key}: "${flag2Value}"
70
+ segments:
71
+ ${segment1Key}:
72
+ key: ${segment1Key}
73
+ include:
74
+ - user1
75
+ `;
76
+
77
+ describe('FileDataSource', function() {
78
+ var store;
79
+ var dataSources = [];
80
+ var logger;
81
+
82
+ beforeEach(() => {
83
+ store = InMemoryFeatureStore();
84
+ dataSources = [];
85
+ logger = stubLogger();
86
+ });
87
+
88
+ afterEach(() => {
89
+ dataSources.forEach(s => s.close());
90
+ });
91
+
92
+ function makeTempFile(content) {
93
+ return promisify(tmp.file)()
94
+ .then(path => {
95
+ return replaceFileContent(path, content).then(() => path);
96
+ });
97
+ }
98
+
99
+ function replaceFileContent(path, content) {
100
+ return promisify(fs.writeFile)(path, content);
101
+ }
102
+
103
+ function setupDataSource(options) {
104
+ var factory = FileDataSource(Object.assign({ logger: logger }, options));
105
+ var ds = factory({ featureStore: store });
106
+ dataSources.push(ds);
107
+ return ds;
108
+ }
109
+
110
+ function sorted(a) {
111
+ var a1 = Array.from(a);
112
+ a1.sort();
113
+ return a1;
114
+ }
115
+
116
+ it('does not load flags prior to start', async () => {
117
+ var path = await makeTempFile('{"flagValues":{"key":"value"}}');
118
+ var fds = setupDataSource({ paths: [path] });
119
+
120
+ expect(fds.initialized()).toBe(false);
121
+ expect(await promisifySingle(store.initialized)()).toBe(false);
122
+ expect(await promisifySingle(store.all)(dataKind.features)).toEqual({});
123
+ expect(await promisifySingle(store.all)(dataKind.segments)).toEqual({});
124
+ });
125
+
126
+ async function testLoadAllProperties(content, extraOptions) {
127
+ var path = await makeTempFile(content);
128
+ var fds = setupDataSource({ paths: [path], ...extraOptions });
129
+ await promisifySingle(fds.start)();
130
+
131
+ expect(fds.initialized()).toBe(true);
132
+ expect(await promisifySingle(store.initialized)()).toBe(true);
133
+ var items = await promisifySingle(store.all)(dataKind.features);
134
+ expect(sorted(Object.keys(items))).toEqual([ flag1Key, flag2Key ]);
135
+ var flag = await promisifySingle(store.get)(dataKind.features, flag1Key);
136
+ expect(flag).toEqual(flag1);
137
+ items = await promisifySingle(store.all)(dataKind.segments);
138
+ expect(items).toEqual({ seg1: segment1 });
139
+ }
140
+
141
+ it('loads flags on start - from JSON', () => testLoadAllProperties(allPropertiesJson));
142
+
143
+ it('loads flags on start - from YAML', () => testLoadAllProperties(allPropertiesYaml));
144
+
145
+ it('does not load if file is missing', async () => {
146
+ var fds = setupDataSource({ paths: ['no-such-file'] });
147
+ await promisifySingle(fds.start)();
148
+
149
+ expect(fds.initialized()).toBe(false);
150
+ expect(await promisifySingle(store.initialized)()).toBe(false);
151
+ });
152
+
153
+ it('does not load if file data is malformed', async () => {
154
+ var path = await makeTempFile('{x');
155
+ var fds = setupDataSource({ paths: [path] });
156
+ await promisifySingle(fds.start)();
157
+
158
+ expect(fds.initialized()).toBe(false);
159
+ expect(await promisifySingle(store.initialized)()).toBe(false);
160
+ });
161
+
162
+ it('can load multiple files', async () => {
163
+ var path1 = await makeTempFile(flagOnlyJson);
164
+ var path2 = await makeTempFile(segmentOnlyJson);
165
+ var fds = setupDataSource({ paths: [path1, path2] });
166
+ await promisifySingle(fds.start)();
167
+
168
+ expect(fds.initialized()).toBe(true);
169
+ expect(await promisifySingle(store.initialized)()).toBe(true);
170
+
171
+ var items = await promisifySingle(store.all)(dataKind.features);
172
+ expect(Object.keys(items)).toEqual([ flag1Key ]);
173
+ items = await promisifySingle(store.all)(dataKind.segments);
174
+ expect(Object.keys(items)).toEqual([ segment1Key ]);
175
+ });
176
+
177
+ it('does not allow duplicate keys', async () => {
178
+ var path1 = await makeTempFile(flagOnlyJson);
179
+ var path2 = await makeTempFile(flagOnlyJson);
180
+ var fds = setupDataSource({ paths: [path1, path2] });
181
+ await promisifySingle(fds.start)();
182
+
183
+ expect(fds.initialized()).toBe(false);
184
+ expect(await promisifySingle(store.initialized)()).toBe(false);
185
+ });
186
+
187
+ it('does not reload modified file if auto-update is off', async () => {
188
+ var path = await makeTempFile(flagOnlyJson);
189
+ var fds = setupDataSource({ paths: [path] });
190
+ await promisifySingle(fds.start)();
191
+
192
+ var items = await promisifySingle(store.all)(dataKind.segments);
193
+ expect(Object.keys(items).length).toEqual(0);
194
+
195
+ await sleepAsync(200);
196
+ await replaceFileContent(path, segmentOnlyJson);
197
+ await sleepAsync(200);
198
+
199
+ items = await promisifySingle(store.all)(dataKind.segments);
200
+ expect(Object.keys(items).length).toEqual(0);
201
+ });
202
+
203
+ it('reloads modified file if auto-update is on', async () => {
204
+ var path = await makeTempFile(flagOnlyJson);
205
+ var fds = setupDataSource({ paths: [path], autoUpdate: true });
206
+ await promisifySingle(fds.start)();
207
+
208
+ var items = await promisifySingle(store.all)(dataKind.segments);
209
+ expect(Object.keys(items).length).toEqual(0);
210
+
211
+ await sleepAsync(200);
212
+ await replaceFileContent(path, segmentOnlyJson);
213
+ await sleepAsync(4000); // the long wait here is to see if we get any spurious reloads (ch32123)
214
+
215
+ items = await promisifySingle(store.all)(dataKind.segments);
216
+ expect(Object.keys(items).length).toEqual(1);
217
+
218
+ // We call logger.warn() once for each reload. It should only have reloaded once, but for
219
+ // unknown reasons it occasionally fires twice in Windows.
220
+ expect(logger.warn.mock.calls.length).toBeGreaterThan(0);
221
+ expect(logger.warn.mock.calls.length).toBeLessThanOrEqual(2);
222
+ });
223
+
224
+ it('evaluates simplified flag with client as expected', async () => {
225
+ var path = await makeTempFile(allPropertiesJson);
226
+ var factory = FileDataSource({ paths: [ path ]});
227
+ var config = { updateProcessor: factory, sendEvents: false, logger: logger };
228
+ var client = LaunchDarkly.init('dummy-key', config);
229
+ var user = { key: 'userkey' };
230
+
231
+ try {
232
+ await client.waitForInitialization();
233
+ var result = await client.variation(flag2Key, user, '');
234
+ expect(result).toEqual(flag2Value);
235
+ } finally {
236
+ client.close();
237
+ }
238
+ });
239
+
240
+ it('evaluates full flag with client as expected', async () => {
241
+ var path = await makeTempFile(allPropertiesJson);
242
+ var factory = FileDataSource({ paths: [ path ]});
243
+ var config = { updateProcessor: factory, sendEvents: false, logger: logger };
244
+ var client = LaunchDarkly.init('dummy-key', config);
245
+ var user = { key: 'userkey' };
246
+
247
+ try {
248
+ await client.waitForInitialization();
249
+ var result = await client.variation(flag1Key, user, '');
250
+ expect(result).toEqual('on');
251
+ } finally {
252
+ client.close();
253
+ }
254
+ });
255
+ });