@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,96 @@
1
+ var LDClient = require('../index.js');
2
+ var messages = require('../messages');
3
+ var stubs = require('./stubs');
4
+
5
+ describe('LDClient', () => {
6
+
7
+ describe('ready event', () => {
8
+ it('is fired in offline mode', done => {
9
+ var client = LDClient.init('sdk_key', { offline: true });
10
+ client.on('ready', () => {
11
+ done();
12
+ });
13
+ });
14
+ });
15
+
16
+ describe('failed event', () => {
17
+ it('is fired if initialization fails', done => {
18
+ var updateProcessor = stubs.stubUpdateProcessor();
19
+ updateProcessor.error = { status: 403 };
20
+ var client = stubs.createClient({ updateProcessor: updateProcessor }, {});
21
+
22
+ client.on('failed', err => {
23
+ expect(err).toEqual(updateProcessor.error);
24
+ done();
25
+ });
26
+ });
27
+ });
28
+
29
+ describe('isOffline()', () => {
30
+ it('returns true in offline mode', done => {
31
+ var client = LDClient.init('sdk_key', {offline: true});
32
+ client.on('ready', () => {
33
+ expect(client.isOffline()).toEqual(true);
34
+ done();
35
+ });
36
+ });
37
+ });
38
+
39
+ describe('secureModeHash()', () => {
40
+ it('correctly computes hash for a known message and secret', () => {
41
+ var client = LDClient.init('secret', {offline: true});
42
+ var hash = client.secureModeHash({"key": "Message"});
43
+ expect(hash).toEqual("aa747c502a898200f9e4fa21bac68136f886a0e27aec70ba06daf2e2a5cb5597");
44
+ });
45
+
46
+ it.each([
47
+ [{key: 'Message'}, 'aa747c502a898200f9e4fa21bac68136f886a0e27aec70ba06daf2e2a5cb5597'],
48
+ [{kind: 'user', key: 'Message'}, 'aa747c502a898200f9e4fa21bac68136f886a0e27aec70ba06daf2e2a5cb5597'],
49
+ [{kind: 'org', key: 'orgtest'}, '40bc9b2e66a842e269ab98dad813e4e15203bbbfd91e8c96b92f3ae6f3f5e223'],
50
+ [{kind: 'multi', user: {key: 'user:test'}, org: {key: 'org:test'}}, '607cc91526c615823e320dabca7967ce544fbe83bcb2b7287163f2d1c7aa210f']
51
+ ])('it uses the canonical key', (context, expectedHash) => {
52
+ const client = LDClient.init('secret', {offline: true});
53
+ const hash = client.secureModeHash(context);
54
+
55
+ expect(hash).toEqual(expectedHash);
56
+ });
57
+ });
58
+
59
+ describe('waitForInitialization()', () => {
60
+ it('resolves when ready', async () => {
61
+ var client = stubs.createClient({}, {});
62
+ await client.waitForInitialization();
63
+ });
64
+
65
+ it('resolves immediately if the client is already ready', async () => {
66
+ var client = stubs.createClient({}, {});
67
+ await client.waitForInitialization();
68
+ await client.waitForInitialization();
69
+ });
70
+
71
+ it('is rejected if initialization fails', async () => {
72
+ var err = { status: 403 };
73
+ var updateProcessor = stubs.stubUpdateProcessor();
74
+ updateProcessor.error = err;
75
+ var client = stubs.createClient({ updateProcessor: updateProcessor }, {});
76
+ await expect(client.waitForInitialization()).rejects.toBe(err);
77
+ });
78
+
79
+ it('creates only one Promise', async () => {
80
+ const updateProcessor = stubs.stubUpdateProcessor();
81
+ updateProcessor.shouldInitialize = false;
82
+ const client = stubs.createClient({ updateProcessor: updateProcessor }, {});
83
+ const p1 = client.waitForInitialization();
84
+ const p2 = client.waitForInitialization();
85
+ expect(p2).toBe(p1);
86
+ })
87
+ });
88
+
89
+ describe('close()', () => {
90
+ it('does not crash when closing an offline client', done => {
91
+ var client = LDClient.init('sdk_key', {offline: true});
92
+ expect(() => client.close()).not.toThrow();
93
+ done();
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,110 @@
1
+ import * as LDClient from '../index';
2
+
3
+ import {
4
+ AsyncQueue,
5
+ TestHttpHandlers,
6
+ TestHttpServer,
7
+ failOnTimeout,
8
+ withCloseable
9
+ } from 'launchdarkly-js-test-helpers';
10
+ import * as stubs from './stubs';
11
+
12
+ describe('LDClient TLS configuration', () => {
13
+ const sdkKey = 'secret';
14
+ let logger = stubs.stubLogger();
15
+
16
+ it('can connect via HTTPS to a server with a self-signed certificate, if CA is specified', async () => {
17
+ await withCloseable(TestHttpServer.startSecure, async server => {
18
+ server.forMethodAndPath('get', '/sdk/latest-all', TestHttpHandlers.respondJson({}));
19
+
20
+ const config = {
21
+ baseUri: server.url,
22
+ sendEvents: false,
23
+ stream: false,
24
+ logger: stubs.stubLogger(),
25
+ tlsParams: { ca: server.certificate },
26
+ diagnosticOptOut: true,
27
+ };
28
+
29
+ await withCloseable(LDClient.init(sdkKey, config), async client => {
30
+ await client.waitForInitialization();
31
+ });
32
+ });
33
+ });
34
+
35
+ it('cannot connect via HTTPS to a server with a self-signed certificate, using default config', async () => {
36
+ await withCloseable(TestHttpServer.startSecure, async server => {
37
+ server.forMethodAndPath('get', '/sdk/latest-all', TestHttpHandlers.respondJson({}));
38
+
39
+ const logCapture = stubs.asyncLogCapture();
40
+ const config = {
41
+ baseUri: server.url,
42
+ sendEvents: false,
43
+ stream: false,
44
+ logger: logCapture.logger,
45
+ diagnosticOptOut: true,
46
+ };
47
+
48
+ await withCloseable(LDClient.init(sdkKey, config), async client => {
49
+ const message1 = await failOnTimeout(logCapture.warn.take(), 1000, 'timed out waiting for log message');
50
+ expect(message1).toMatch(/only disable the streaming API/); // irrelevant message due to our use of polling mode
51
+ const message2 = await failOnTimeout(logCapture.warn.take(), 1000, 'timed out waiting for log message');
52
+ expect(message2).toMatch(/self.signed/);
53
+ });
54
+ });
55
+ });
56
+
57
+ it('can use custom TLS options for streaming as well as polling', async () => {
58
+ await withCloseable(TestHttpServer.startSecure, async server => {
59
+ const eventData = { data: { flags: { flag: { version: 1 } }, segments: {} } };
60
+ await withCloseable(new AsyncQueue(), async events => {
61
+ events.add({ type: 'put', data: JSON.stringify(eventData) });
62
+ server.forMethodAndPath('get', '/stream/all', TestHttpHandlers.sseStream(events));
63
+
64
+ const config = {
65
+ baseUri: server.url,
66
+ streamUri: server.url + '/stream',
67
+ sendEvents: false,
68
+ logger: logger,
69
+ tlsParams: { ca: server.certificate },
70
+ diagnosticOptOut: true,
71
+ };
72
+
73
+ await withCloseable(LDClient.init(sdkKey, config), async client => {
74
+ await client.waitForInitialization(); // this won't return until the stream receives the "put" event
75
+ });
76
+ });
77
+ });
78
+ });
79
+
80
+ it('can use custom TLS options for posting events', async () => {
81
+ await withCloseable(TestHttpServer.startSecure, async server => {
82
+ server.forMethodAndPath('post', '/events/bulk', TestHttpHandlers.respond(200));
83
+ server.forMethodAndPath('get', '/sdk/latest-all', TestHttpHandlers.respondJson({}));
84
+
85
+ const config = {
86
+ baseUri: server.url,
87
+ eventsUri: server.url + '/events',
88
+ stream: false,
89
+ logger: stubs.stubLogger(),
90
+ tlsParams: { ca: server.certificate },
91
+ diagnosticOptOut: true,
92
+ };
93
+
94
+ await withCloseable(LDClient.init(sdkKey, config), async client => {
95
+ await client.waitForInitialization();
96
+ client.identify({ key: 'user' });
97
+ await client.flush();
98
+
99
+ const flagsRequest = await server.nextRequest();
100
+ expect(flagsRequest.path).toEqual('/sdk/latest-all');
101
+
102
+ const eventsRequest = await server.nextRequest();
103
+ expect(eventsRequest.path).toEqual('/events/bulk');
104
+ const eventData = JSON.parse(eventsRequest.body);
105
+ expect(eventData.length).toEqual(1);
106
+ expect(eventData[0].kind).toEqual('identify');
107
+ });
108
+ });
109
+ });
110
+ });
@@ -0,0 +1,494 @@
1
+ const AttributeReference = require('../attribute_reference');
2
+
3
+ describe('when getting attributes by reference', () => {
4
+ it('should handle an empty reference', () => {
5
+ expect(AttributeReference.get({
6
+ 'launchdarkly': {
7
+ 'u2c': true
8
+ },
9
+ 'ld': false,
10
+ 'foo': ['bar', 'baz'],
11
+ 'a/b': 1,
12
+ 'm~n': 2,
13
+ ' ': ' ',
14
+ 'null': null
15
+ }, '')).toBeUndefined();
16
+ });
17
+
18
+ it('should handle a reference to the root element', () => {
19
+ expect(AttributeReference.get({
20
+ 'launchdarkly': {
21
+ 'u2c': true
22
+ },
23
+ 'ld': false,
24
+ 'foo': ['bar', 'baz'],
25
+ 'a/b': 1,
26
+ 'm~n': 2,
27
+ ' ': ' ',
28
+ 'null': null
29
+ }, '/')).toBeUndefined();
30
+ });
31
+
32
+ it('should handle a reference to a top level attribute without a leading slash', () => {
33
+ expect(AttributeReference.get({ ld: false }, 'ld')).toEqual(false);
34
+ });
35
+
36
+ it('should handle a reference to an array', () => {
37
+ expect(AttributeReference.get({ foo: ['bar', 'baz'] }, '/foo')).toEqual(['bar', 'baz']);
38
+ });
39
+
40
+ it('should not allow indexing an array', () => {
41
+ expect(AttributeReference.get({ foo: ['bar', 'baz'] }, '/foo/0')).toBeUndefined();
42
+ });
43
+
44
+ it('should not allow indexing a non-array object', () => {
45
+ expect(AttributeReference.get({
46
+ 'launchdarkly': {
47
+ 'u2c': true
48
+ }
49
+ }, 'launchdarkly')).toEqual({
50
+ 'u2c': true
51
+ });
52
+ });
53
+
54
+ it('should handle indexing into a nested property', () => {
55
+ expect(AttributeReference.get({
56
+ 'launchdarkly': {
57
+ 'u2c': true
58
+ }
59
+ }, '/launchdarkly/u2c')).toEqual(true);
60
+ });
61
+
62
+ it('should not treat object literals as indexing into nested objects', () => {
63
+ expect(AttributeReference.get({
64
+ 'launchdarkly': {
65
+ 'u2c': true
66
+ }
67
+ }, 'launchdarkly/u2c')).toEqual(undefined);
68
+ });
69
+
70
+ it('should allow indexing of whitepace keys', () => {
71
+ expect(AttributeReference.get({ ' ': ' ' }, '/ ')).toEqual(' ');
72
+ });
73
+
74
+
75
+ it('should handle escaped slashes in keys', () => {
76
+ expect(AttributeReference.get({ 'a/b': 1 }, '/a~1b')).toEqual(1);
77
+ });
78
+
79
+ it('should handle escaped tilde in keys', () => {
80
+ expect(AttributeReference.get({ 'm~n': 2 }, '/m~0n')).toEqual(2);
81
+ });
82
+
83
+ it('should handle literal with unescaped /', () => {
84
+ expect(AttributeReference.get({ 'a/b': 1 }, 'a/b')).toEqual(1);
85
+ });
86
+
87
+ it('should handle literal with unescaped ~', () => {
88
+ expect(AttributeReference.get({ 'm~n': 2 }, 'm~n')).toEqual(2);
89
+ });
90
+
91
+ it('should handle accessing a null value', () => {
92
+ expect(AttributeReference.get({ 'null': null }, 'null')).toEqual(null);
93
+ });
94
+
95
+ it('should handle an attempt to access inside a null value', () => {
96
+ expect(AttributeReference.get({ 'null': null }, '/null/null')).toBeUndefined();
97
+ });
98
+
99
+ it('should handle an attempt to access something that doesn\'t exist', () => {
100
+ expect(AttributeReference.get({ whatever: true }, 'badkey')).toBeUndefined();
101
+ });
102
+
103
+ it('should be able to get a top level key starting with /', () => {
104
+ expect(AttributeReference.get({ '/why': 'because', 'why': 'not' }, '/~1why')).toEqual("because");
105
+ expect(AttributeReference.get({ '/why': 'because', 'why': 'not' }, '/why')).toEqual("not");
106
+ expect(AttributeReference.get({ '/~why': 'because', 'why': 'not' }, '/~1~0why')).toEqual("because");
107
+ });
108
+
109
+ it('should allow indexing a key that has leading spaces before a slash', () => {
110
+ expect(AttributeReference.get({ ' /': 'a' }, ' /')).toEqual('a');
111
+ expect(AttributeReference.get({ ' /': 'a' }, '/ ~1')).toEqual('a');
112
+ });
113
+
114
+ it('should not allow indexing into string', () => {
115
+ expect(AttributeReference.get({attr: 'string'}, '/attr/0')).toBeUndefined();
116
+ });
117
+ });
118
+
119
+ describe('when filtering attributes by reference', () => {
120
+
121
+ it('should be able to remove a top level value', () => {
122
+ const { cloned, excluded } = AttributeReference.cloneExcluding({
123
+ 'launchdarkly': {
124
+ 'u2c': true
125
+ },
126
+ 'ld': false,
127
+ 'foo': ['bar', 'baz'],
128
+ 'a/b': 1,
129
+ 'm~n': 2,
130
+ ' ': ' ',
131
+ 'null': null
132
+ }, ['ld']);
133
+ expect(cloned).toEqual({
134
+ 'launchdarkly': {
135
+ 'u2c': true
136
+ },
137
+ 'foo': ['bar', 'baz'],
138
+ 'a/b': 1,
139
+ 'm~n': 2,
140
+ ' ': ' ',
141
+ 'null': null
142
+ });
143
+ expect(excluded).toEqual(['/ld']);
144
+ });
145
+
146
+ it('should be able to exclude a nested value', () => {
147
+ const { cloned, excluded } = AttributeReference.cloneExcluding({
148
+ 'launchdarkly': {
149
+ 'u2c': true
150
+ },
151
+ 'ld': false,
152
+ 'foo': ['bar', 'baz'],
153
+ 'a/b': 1,
154
+ 'm~n': 2,
155
+ ' ': ' ',
156
+ 'null': null
157
+ }, ['/launchdarkly/u2c']);
158
+ expect(cloned).toEqual({
159
+ 'launchdarkly': {
160
+ },
161
+ 'ld': false,
162
+ 'foo': ['bar', 'baz'],
163
+ 'a/b': 1,
164
+ 'm~n': 2,
165
+ ' ': ' ',
166
+ 'null': null
167
+ });
168
+ expect(excluded).toEqual(['/launchdarkly/u2c']);
169
+ });
170
+
171
+ it('sould be able to exclude an object', () => {
172
+ const { cloned, excluded } = AttributeReference.cloneExcluding({
173
+ 'launchdarkly': {
174
+ 'u2c': true
175
+ },
176
+ 'ld': false,
177
+ 'foo': ['bar', 'baz'],
178
+ 'a/b': 1,
179
+ 'm~n': 2,
180
+ ' ': ' ',
181
+ 'null': null
182
+ }, ['launchdarkly']);
183
+ expect(cloned).toEqual({
184
+ 'ld': false,
185
+ 'foo': ['bar', 'baz'],
186
+ 'a/b': 1,
187
+ 'm~n': 2,
188
+ ' ': ' ',
189
+ 'null': null
190
+ });
191
+ expect(excluded).toEqual(['/launchdarkly']);
192
+ });
193
+
194
+ it('sould be able to exclude an array', () => {
195
+ const { cloned, excluded } = AttributeReference.cloneExcluding({
196
+ 'launchdarkly': {
197
+ 'u2c': true
198
+ },
199
+ 'ld': false,
200
+ 'foo': ['bar', 'baz'],
201
+ 'a/b': 1,
202
+ 'm~n': 2,
203
+ ' ': ' ',
204
+ 'null': null
205
+ }, ['foo']);
206
+ expect(cloned).toEqual({
207
+ 'launchdarkly': {
208
+ 'u2c': true
209
+ },
210
+ 'ld': false,
211
+ 'a/b': 1,
212
+ 'm~n': 2,
213
+ ' ': ' ',
214
+ 'null': null
215
+ });
216
+ expect(excluded).toEqual(['/foo']);
217
+ });
218
+
219
+ it('should not allow exclude an array index', () => {
220
+ const { cloned, excluded } = AttributeReference.cloneExcluding({
221
+ 'launchdarkly': {
222
+ 'u2c': true
223
+ },
224
+ 'ld': false,
225
+ 'foo': ['bar', 'baz'],
226
+ 'a/b': 1,
227
+ 'm~n': 2,
228
+ ' ': ' ',
229
+ 'null': null
230
+ }, ['foo/0']);
231
+ expect(cloned).toEqual({
232
+ 'launchdarkly': {
233
+ 'u2c': true
234
+ },
235
+ 'ld': false,
236
+ 'foo': ['bar', 'baz'],
237
+ 'a/b': 1,
238
+ 'm~n': 2,
239
+ ' ': ' ',
240
+ 'null': null
241
+ });
242
+ expect(excluded).toEqual([]);
243
+ });
244
+
245
+ it('should not allow exclude a property inside an index of an array.', () => {
246
+ const objWithArrayOfObjects = {
247
+ array: [{
248
+ toRemove: true,
249
+ toLeave: true,
250
+ }]
251
+ };
252
+
253
+ const { cloned, excluded } = AttributeReference.cloneExcluding(objWithArrayOfObjects, ['array/0/toRemove']);
254
+ expect(cloned).toEqual(objWithArrayOfObjects);
255
+ expect(excluded).toEqual([]);
256
+ });
257
+
258
+ it('should not allow exclude the root object', () => {
259
+ const { cloned, excluded } = AttributeReference.cloneExcluding({
260
+ 'launchdarkly': {
261
+ 'u2c': true
262
+ },
263
+ 'ld': false,
264
+ 'foo': ['bar', 'baz'],
265
+ 'a/b': 1,
266
+ 'm~n': 2,
267
+ ' ': ' ',
268
+ 'null': null
269
+ }, ['/']);
270
+ expect(cloned).toEqual({
271
+ 'launchdarkly': {
272
+ 'u2c': true
273
+ },
274
+ 'ld': false,
275
+ 'foo': ['bar', 'baz'],
276
+ 'a/b': 1,
277
+ 'm~n': 2,
278
+ ' ': ' ',
279
+ 'null': null
280
+ });
281
+ expect(excluded).toEqual([]);
282
+ });
283
+
284
+ it('should allow exclude a null value', () => {
285
+ const { cloned, excluded } = AttributeReference.cloneExcluding({
286
+ 'launchdarkly': {
287
+ 'u2c': true
288
+ },
289
+ 'ld': false,
290
+ 'foo': ['bar', 'baz'],
291
+ 'a/b': 1,
292
+ 'm~n': 2,
293
+ ' ': ' ',
294
+ 'null': null
295
+ }, ['null']);
296
+ expect(cloned).toEqual({
297
+ 'launchdarkly': {
298
+ 'u2c': true
299
+ },
300
+ 'ld': false,
301
+ 'foo': ['bar', 'baz'],
302
+ 'a/b': 1,
303
+ 'm~n': 2,
304
+ ' ': ' ',
305
+ });
306
+ expect(excluded).toEqual(['/null']);
307
+ });
308
+
309
+ it('should not allow exclude a value inside null', () => {
310
+ const { cloned, excluded } = AttributeReference.cloneExcluding({
311
+ 'launchdarkly': {
312
+ 'u2c': true
313
+ },
314
+ 'ld': false,
315
+ 'foo': ['bar', 'baz'],
316
+ 'a/b': 1,
317
+ 'm~n': 2,
318
+ ' ': ' ',
319
+ 'null': null
320
+ }, ['/null/null']);
321
+ expect(cloned).toEqual({
322
+ 'launchdarkly': {
323
+ 'u2c': true
324
+ },
325
+ 'ld': false,
326
+ 'foo': ['bar', 'baz'],
327
+ 'a/b': 1,
328
+ 'm~n': 2,
329
+ ' ': ' ',
330
+ 'null': null
331
+ });
332
+ expect(excluded).toEqual([]);
333
+ });
334
+
335
+ it('should not allow exclude a value inside explicit undefined', () => {
336
+ const { cloned, excluded } = AttributeReference.cloneExcluding({
337
+ 'launchdarkly': {
338
+ 'u2c': true
339
+ },
340
+ 'ld': false,
341
+ 'foo': ['bar', 'baz'],
342
+ 'a/b': 1,
343
+ 'm~n': 2,
344
+ ' ': ' ',
345
+ 'null': null
346
+ }, ['undefined/null']);
347
+ expect(cloned).toEqual({
348
+ 'launchdarkly': {
349
+ 'u2c': true
350
+ },
351
+ 'ld': false,
352
+ 'foo': ['bar', 'baz'],
353
+ 'a/b': 1,
354
+ 'm~n': 2,
355
+ ' ': ' ',
356
+ 'null': null
357
+ });
358
+ expect(excluded).toEqual([]);
359
+ });
360
+
361
+ it('should allow removing an explicit undefined value', () => {
362
+ const objToClone = { undefined: undefined };
363
+ const { cloned, excluded } = AttributeReference.cloneExcluding(objToClone, ['undefined']);
364
+ expect(cloned).toEqual({});
365
+ expect(excluded).toEqual(['/undefined']);
366
+ });
367
+
368
+ it('should allow removing references with escape characters', () => {
369
+ const { cloned, excluded } = AttributeReference.cloneExcluding({
370
+ 'launchdarkly': {
371
+ 'u2c': true
372
+ },
373
+ 'ld': false,
374
+ 'foo': ['bar', 'baz'],
375
+ 'a/b': 1,
376
+ 'm~n': 2,
377
+ ' ': ' ',
378
+ 'null': null
379
+ }, ['/a~1b', '/m~0n']);
380
+ expect(cloned).toEqual({
381
+ 'launchdarkly': {
382
+ 'u2c': true
383
+ },
384
+ 'ld': false,
385
+ 'foo': ['bar', 'baz'],
386
+ ' ': ' ',
387
+ 'null': null
388
+ });
389
+ expect(excluded).toEqual(['/a~1b', '/m~0n']);
390
+ });
391
+
392
+ it('should allow removing literals without escape characters', () => {
393
+ const { cloned, excluded } = AttributeReference.cloneExcluding({
394
+ 'launchdarkly': {
395
+ 'u2c': true
396
+ },
397
+ 'ld': false,
398
+ 'foo': ['bar', 'baz'],
399
+ 'a/b': 1,
400
+ 'm~n': 2,
401
+ ' ': ' ',
402
+ 'null': null
403
+ }, ['a/b', 'm~n']);
404
+ expect(cloned).toEqual({
405
+ 'launchdarkly': {
406
+ 'u2c': true
407
+ },
408
+ 'ld': false,
409
+ 'foo': ['bar', 'baz'],
410
+ ' ': ' ',
411
+ 'null': null
412
+ });
413
+ expect(excluded).toEqual(['/a~1b', '/m~0n']);
414
+ });
415
+
416
+
417
+ it('should handle cycles', () => {
418
+ const item = {};
419
+ const objWithCycle = {
420
+ item,
421
+ name: 'test',
422
+ remove: 'remove'
423
+ };
424
+ item.parent = objWithCycle;
425
+ const { cloned, excluded } = AttributeReference.cloneExcluding(objWithCycle, ['remove']);
426
+ expect(cloned).toEqual({
427
+ item: {},
428
+ name: 'test',
429
+ });
430
+ expect(excluded).toEqual(['/remove']);
431
+ });
432
+
433
+ it('should allow non-circular reference and should treat them independently for filtering', () => {
434
+ const item = { value: 'value' };
435
+ const objWithSharedPeer = {
436
+ item: item,
437
+ second: item,
438
+ third: item,
439
+ fourth: item
440
+ };
441
+ const { cloned, excluded } = AttributeReference.cloneExcluding(objWithSharedPeer, ['third', '/second/value']);
442
+ expect(cloned).toEqual({
443
+ item: { value: 'value' },
444
+ second: {},
445
+ fourth: { value: 'value' },
446
+ });
447
+ expect(excluded).toEqual(['/second/value', '/third']);
448
+ });
449
+
450
+ it('should allow for an empty reference list', () => {
451
+ const { cloned, excluded } = AttributeReference.cloneExcluding({
452
+ 'launchdarkly': {
453
+ 'u2c': true
454
+ },
455
+ 'ld': false,
456
+ 'foo': ['bar', 'baz'],
457
+ 'a/b': 1,
458
+ 'm~n': 2,
459
+ ' ': ' ',
460
+ 'null': null
461
+ }, []);
462
+ expect(cloned).toEqual({
463
+ 'launchdarkly': {
464
+ 'u2c': true
465
+ },
466
+ 'ld': false,
467
+ 'foo': ['bar', 'baz'],
468
+ 'a/b': 1,
469
+ 'm~n': 2,
470
+ ' ': ' ',
471
+ 'null': null
472
+ });
473
+ expect(excluded).toEqual([]);
474
+ });
475
+ });
476
+
477
+ describe('when given a literal', () => {
478
+ it('can convert it to a reference', () => {
479
+ expect(AttributeReference.literalToReference("/~why")).toEqual("/~1~0why");
480
+ });
481
+ });
482
+
483
+ it.each([
484
+ ['kind', true],
485
+ ['/kind', true],
486
+ ['potato', false],
487
+ ['/potato', false],
488
+ ['', false],
489
+ [undefined, false],
490
+ ['//', false],
491
+ ['/', false],
492
+ ])('can check if reference isKind', (ref, is) => {
493
+ expect(AttributeReference.isKind(ref)).toEqual(is);
494
+ });