@splitsoftware/openfeature-js-split-provider 1.0.7 → 1.1.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.
- package/CHANGES.txt +25 -13
- package/LICENSE +1 -1
- package/README.md +74 -12
- package/es/index.js +1 -0
- package/es/lib/js-split-provider.js +152 -0
- package/lib/index.js +4 -0
- package/lib/lib/js-split-provider.js +156 -0
- package/package.json +37 -29
- package/src/__tests__/mocks/redis-commands.txt +11 -0
- package/src/__tests__/nodeSuites/client.spec.js +181 -139
- package/src/__tests__/nodeSuites/client_redis.spec.js +115 -0
- package/src/__tests__/nodeSuites/provider.spec.js +241 -109
- package/src/__tests__/testUtils/eventSourceMock.js +1 -2
- package/src/__tests__/testUtils/index.js +43 -0
- package/src/lib/js-split-provider.ts +126 -58
- package/types/index.d.ts +1 -0
- package/types/lib/js-split-provider.d.ts +26 -0
- package/src/__tests__/node.spec.js +0 -7
|
@@ -1,188 +1,230 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/* eslint-disable jest/no-conditional-expect */
|
|
2
|
+
import { OpenFeatureSplitProvider } from '../../lib/js-split-provider';
|
|
3
|
+
import { getLocalHostSplitClient, getSplitFactory } from '../testUtils';
|
|
4
|
+
|
|
5
|
+
import { OpenFeature } from '@openfeature/server-sdk';
|
|
6
|
+
|
|
7
|
+
const cases = [
|
|
8
|
+
[
|
|
9
|
+
'openfeature client tests mode: splitClient',
|
|
10
|
+
() => ({ splitClient: getLocalHostSplitClient()}),
|
|
11
|
+
|
|
12
|
+
],
|
|
13
|
+
[
|
|
14
|
+
'openfeature client tests mode: splitFactory',
|
|
15
|
+
getSplitFactory
|
|
16
|
+
],
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
describe.each(cases)('%s', (label, getOptions) => {
|
|
20
|
+
|
|
21
|
+
let client;
|
|
22
|
+
let provider;
|
|
23
|
+
let options;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
|
|
27
|
+
options = getOptions();
|
|
28
|
+
provider = new OpenFeatureSplitProvider(options);
|
|
29
|
+
OpenFeature.setProvider(provider);
|
|
30
|
+
|
|
31
|
+
client = OpenFeature.getClient('test');
|
|
32
|
+
let evaluationContext = {
|
|
33
|
+
targetingKey: 'key'
|
|
34
|
+
};
|
|
35
|
+
client.setContext(evaluationContext);
|
|
36
|
+
});
|
|
37
|
+
afterEach(async () => {
|
|
38
|
+
await OpenFeature.close();
|
|
39
|
+
});
|
|
6
40
|
|
|
7
|
-
|
|
41
|
+
test('use default test', async () => {
|
|
8
42
|
let flagName = 'random-non-existent-feature';
|
|
9
43
|
|
|
10
44
|
let result = await client.getBooleanValue(flagName, false);
|
|
11
|
-
|
|
45
|
+
expect(result).toBe(false);
|
|
12
46
|
|
|
13
47
|
let result2 = await client.getBooleanValue(flagName, true);
|
|
14
|
-
|
|
48
|
+
expect(result2).toBe(true);
|
|
15
49
|
|
|
16
50
|
let defaultString = 'blah';
|
|
17
51
|
let resultString = await client.getStringValue(flagName, defaultString);
|
|
18
|
-
|
|
52
|
+
expect(resultString).toBe(defaultString);
|
|
19
53
|
|
|
20
54
|
let defaultInt = 100;
|
|
21
55
|
let resultInt = await client.getNumberValue(flagName, defaultInt);
|
|
22
|
-
|
|
56
|
+
expect(resultInt).toBe(defaultInt);
|
|
23
57
|
|
|
24
58
|
let defaultStructure = {
|
|
25
59
|
foo: 'bar'
|
|
26
60
|
};
|
|
27
61
|
let resultStructure = await client.getObjectValue(flagName, defaultStructure);
|
|
28
|
-
|
|
29
|
-
};
|
|
62
|
+
expect(resultStructure).toEqual(defaultStructure);
|
|
63
|
+
});
|
|
30
64
|
|
|
31
|
-
|
|
65
|
+
test('missing targetingKey test', async () => {
|
|
32
66
|
let details = await client.getBooleanDetails('non-existent-feature', false, { targetingKey: undefined });
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
};
|
|
67
|
+
expect(details.value).toBe(false);
|
|
68
|
+
expect(details.errorCode).toBe('TARGETING_KEY_MISSING');
|
|
69
|
+
});
|
|
36
70
|
|
|
37
|
-
|
|
71
|
+
test('evaluate Boolean control test', async () => {
|
|
38
72
|
let details = await client.getBooleanDetails('non-existent-feature', false);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
};
|
|
73
|
+
expect(details.value).toBe(false);
|
|
74
|
+
expect(details.errorCode).toBe('FLAG_NOT_FOUND');
|
|
75
|
+
expect(details.reason).toBe('ERROR');
|
|
76
|
+
});
|
|
43
77
|
|
|
44
|
-
|
|
78
|
+
test('evaluate Boolean test', async () => {
|
|
45
79
|
let result = await client.getBooleanValue('some_other_feature', true);
|
|
46
|
-
|
|
47
|
-
};
|
|
80
|
+
expect(result).toBe(false);
|
|
81
|
+
});
|
|
48
82
|
|
|
49
|
-
|
|
50
|
-
let result = await client.
|
|
51
|
-
|
|
83
|
+
test('evaluate Boolean details test', async () => {
|
|
84
|
+
let result = await client.getBooleanDetails('my_feature', false);
|
|
85
|
+
expect(result.value).toBe(true);
|
|
86
|
+
expect(result.flagMetadata).toEqual({ config: '{"desc" : "this applies only to ON treatment"}' });
|
|
52
87
|
|
|
53
|
-
result = await client.
|
|
54
|
-
|
|
55
|
-
|
|
88
|
+
result = await client.getBooleanDetails('my_feature', true, { targetingKey: 'randomKey' });
|
|
89
|
+
expect(result.value).toBe(false);
|
|
90
|
+
expect(result.flagMetadata).toEqual({ config: '' });
|
|
91
|
+
});
|
|
56
92
|
|
|
57
|
-
|
|
93
|
+
test('evaluate String test', async () => {
|
|
58
94
|
let result = await client.getStringValue('some_other_feature', 'on');
|
|
59
|
-
|
|
60
|
-
};
|
|
95
|
+
expect(result).toBe('off');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('evaluate String details test', async () => {
|
|
99
|
+
let result = await client.getStringDetails('my_feature', 'off');
|
|
100
|
+
expect(result.value).toBe('on');
|
|
101
|
+
expect(result.flagMetadata).toEqual({ config: '{"desc" : "this applies only to ON treatment"}' });
|
|
102
|
+
|
|
103
|
+
result = await client.getStringDetails('my_feature', 'on', { targetingKey: 'randomKey' });
|
|
104
|
+
expect(result.value).toBe('off');
|
|
105
|
+
expect(result.flagMetadata).toEqual({ config: '' });
|
|
106
|
+
});
|
|
61
107
|
|
|
62
|
-
|
|
108
|
+
test('evaluate Number test', async () => {
|
|
63
109
|
let result = await client.getNumberValue('int_feature', 0);
|
|
64
|
-
|
|
65
|
-
};
|
|
110
|
+
expect(result).toBe(32);
|
|
111
|
+
});
|
|
66
112
|
|
|
67
|
-
|
|
113
|
+
test('evaluate Object test', async () => {
|
|
68
114
|
let result = await client.getObjectValue('obj_feature', {});
|
|
69
|
-
|
|
70
|
-
};
|
|
115
|
+
expect(result).toEqual({ key: 'value' });
|
|
116
|
+
});
|
|
71
117
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
};
|
|
118
|
+
test('evaluate Metadata name test', async () => {
|
|
119
|
+
expect(client.metadata.name).toBe('test');
|
|
120
|
+
});
|
|
75
121
|
|
|
76
|
-
|
|
122
|
+
test('evaluate Boolean without details test', async () => {
|
|
77
123
|
let details = await client.getBooleanDetails('some_other_feature', true);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
|
|
124
|
+
expect(details.flagKey).toBe('some_other_feature');
|
|
125
|
+
expect(details.reason).toBe('TARGETING_MATCH');
|
|
126
|
+
expect(details.value).toBe(false);
|
|
127
|
+
expect(details.variant).toBe('off');
|
|
128
|
+
expect(details.errorCode).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('evaluate Number details test', async () => {
|
|
86
132
|
let details = await client.getNumberDetails('int_feature', 0);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
|
|
133
|
+
expect(details.flagKey).toBe('int_feature');
|
|
134
|
+
expect(details.reason).toBe('TARGETING_MATCH');
|
|
135
|
+
expect(details.value).toBe(32);
|
|
136
|
+
expect(details.variant).toBe('32');
|
|
137
|
+
expect(details.errorCode).toBeUndefined();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('evaluate String without details test', async () => {
|
|
95
141
|
let details = await client.getStringDetails('some_other_feature', 'blah');
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
|
|
142
|
+
expect(details.flagKey).toBe('some_other_feature');
|
|
143
|
+
expect(details.reason).toBe('TARGETING_MATCH');
|
|
144
|
+
expect(details.value).toBe('off');
|
|
145
|
+
expect(details.variant).toBe('off');
|
|
146
|
+
expect(details.errorCode).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('evaluate Object details test', async () => {
|
|
104
150
|
let details = await client.getObjectDetails('obj_feature', {});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
|
|
151
|
+
expect(details.flagKey).toBe('obj_feature');
|
|
152
|
+
expect(details.reason).toBe('TARGETING_MATCH');
|
|
153
|
+
expect(details.value).toEqual({ key: 'value' });
|
|
154
|
+
expect(details.variant).toBe('{"key": "value"}');
|
|
155
|
+
expect(details.errorCode).toBeUndefined();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('evaluate Boolean fail test', async () => {
|
|
113
159
|
let value = await client.getBooleanValue('obj_feature', false);
|
|
114
|
-
|
|
160
|
+
expect(value).toBe(false);
|
|
115
161
|
|
|
116
162
|
let details = await client.getBooleanDetails('obj_feature', false);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
};
|
|
163
|
+
expect(details.value).toBe(false);
|
|
164
|
+
expect(details.errorCode).toBe('PARSE_ERROR');
|
|
165
|
+
expect(details.reason).toBe('ERROR');
|
|
166
|
+
expect(details.variant).toBeUndefined();
|
|
167
|
+
});
|
|
122
168
|
|
|
123
|
-
|
|
169
|
+
test('evaluate Number fail test', async () => {
|
|
124
170
|
let value = await client.getNumberValue('obj_feature', 10);
|
|
125
|
-
|
|
171
|
+
expect(value).toBe(10);
|
|
126
172
|
|
|
127
173
|
let details = await client.getNumberDetails('obj_feature', 10);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
};
|
|
174
|
+
expect(details.value).toBe(10);
|
|
175
|
+
expect(details.errorCode).toBe('PARSE_ERROR');
|
|
176
|
+
expect(details.reason).toBe('ERROR');
|
|
177
|
+
expect(details.variant).toBeUndefined();
|
|
178
|
+
});
|
|
133
179
|
|
|
134
|
-
|
|
180
|
+
test('evaluate Object fail test', async () => {
|
|
135
181
|
let defaultObject = { foo: 'bar' };
|
|
136
182
|
let value = await client.getObjectValue('int_feature', defaultObject);
|
|
137
|
-
|
|
183
|
+
expect(value).toEqual(defaultObject);
|
|
138
184
|
|
|
139
185
|
let details = await client.getObjectDetails('int_feature', defaultObject);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
splitClient.destroy(); // Shut down open handles
|
|
186
|
-
|
|
187
|
-
assert.end();
|
|
188
|
-
}
|
|
186
|
+
expect(details.value).toEqual(defaultObject);
|
|
187
|
+
expect(details.errorCode).toBe('PARSE_ERROR');
|
|
188
|
+
expect(details.reason).toBe('ERROR');
|
|
189
|
+
expect(details.variant).toBeUndefined();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('track: throws when missing eventName', async () => {
|
|
193
|
+
try {
|
|
194
|
+
await client.track('', { targetingKey: 'u1', trafficType: 'user' }, {});
|
|
195
|
+
} catch (e) {
|
|
196
|
+
expect(e.message).toBe('Missing eventName, required to track');
|
|
197
|
+
expect(e.code).toBe('PARSE_ERROR');
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('track: throws when missing targetingKey', async () => {
|
|
202
|
+
try {
|
|
203
|
+
await client.track('my-event', { trafficType: 'user' }, {});
|
|
204
|
+
} catch (e) {
|
|
205
|
+
expect(e.message).toBe('Missing targetingKey, required to track');
|
|
206
|
+
expect(e.code).toBe('PARSE_ERROR');
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('track: throws when missing trafficType', async () => {
|
|
211
|
+
try {
|
|
212
|
+
await client.track('my-event', { targetingKey: 'u1' }, {});
|
|
213
|
+
} catch (e) {
|
|
214
|
+
expect(e.message).toBe('Missing trafficType variable, required to track');
|
|
215
|
+
expect(e.code).toBe('INVALID_CONTEXT');
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('track: without value', async () => {
|
|
220
|
+
const trackSpy = jest.spyOn(options.splitClient ? options.splitClient : options.client(), 'track');
|
|
221
|
+
await client.track('my-event', { targetingKey: 'u1', trafficType: 'user' }, { properties: { prop1: 'value1' } });
|
|
222
|
+
expect(trackSpy).toHaveBeenCalledWith('u1', 'user', 'my-event', undefined, { prop1: 'value1' });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('track: with value', async () => {
|
|
226
|
+
const trackSpy = jest.spyOn(options.splitClient ? options.splitClient : options.client(), 'track');
|
|
227
|
+
await client.track('my-event', { targetingKey: 'u1', trafficType: 'user' }, { value: 9.99, properties: { prop1: 'value1' } });
|
|
228
|
+
expect(trackSpy).toHaveBeenCalledWith('u1', 'user', 'my-event', 9.99, { prop1: 'value1' });
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import RedisServer from 'redis-server';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import { OpenFeature } from '@openfeature/server-sdk';
|
|
4
|
+
|
|
5
|
+
import { getRedisSplitClient } from '../testUtils';
|
|
6
|
+
import { OpenFeatureSplitProvider } from '../../lib/js-split-provider';
|
|
7
|
+
|
|
8
|
+
const redisPort = '6385';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialize redis server and run a cli bash command to load redis with data to do the proper tests
|
|
12
|
+
*/
|
|
13
|
+
const startRedis = () => {
|
|
14
|
+
// Simply pass the port that you want a Redis server to listen on.
|
|
15
|
+
const server = new RedisServer(redisPort);
|
|
16
|
+
|
|
17
|
+
const promise = new Promise((resolve, reject) => {
|
|
18
|
+
server
|
|
19
|
+
.open()
|
|
20
|
+
.then(() => {
|
|
21
|
+
exec(`cat ./src/__tests__/mocks/redis-commands.txt | redis-cli -p ${redisPort}`, err => {
|
|
22
|
+
if (err) {
|
|
23
|
+
reject(server);
|
|
24
|
+
// Node.js couldn't execute the command
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
resolve(server);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return promise;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let redisServer
|
|
36
|
+
let splitClient
|
|
37
|
+
|
|
38
|
+
beforeAll(async () => {
|
|
39
|
+
redisServer = await startRedis();
|
|
40
|
+
}, 30000);
|
|
41
|
+
|
|
42
|
+
afterAll(async () => {
|
|
43
|
+
await redisServer.close();
|
|
44
|
+
await splitClient.destroy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('Regular usage - DEBUG strategy', () => {
|
|
48
|
+
splitClient = getRedisSplitClient(redisPort);
|
|
49
|
+
const provider = new OpenFeatureSplitProvider({ splitClient });
|
|
50
|
+
|
|
51
|
+
OpenFeature.setProviderAndWait(provider);
|
|
52
|
+
const client = OpenFeature.getClient();
|
|
53
|
+
|
|
54
|
+
test('Evaluate always on flag', async () => {
|
|
55
|
+
await client.getBooleanValue('always-on', false, {targetingKey: 'emma-ss'}).then(result => {
|
|
56
|
+
expect(result).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('Evaluate user in segment', async () => {
|
|
61
|
+
await client.getBooleanValue('UT_IN_SEGMENT', false, {targetingKey: 'UT_Segment_member', properties: { /* empty properties are ignored */ }}).then(result => {
|
|
62
|
+
expect(result).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await client.getBooleanValue('UT_IN_SEGMENT', false, {targetingKey: 'UT_Segment_member', properties: { some: 'value1' } }).then(result => {
|
|
66
|
+
expect(result).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await client.getBooleanValue('UT_IN_SEGMENT', false, {targetingKey: 'other' }).then(result => {
|
|
70
|
+
expect(result).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await client.getBooleanValue('UT_NOT_IN_SEGMENT', true, {targetingKey: 'UT_Segment_member' }).then(result => {
|
|
74
|
+
expect(result).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await client.getBooleanValue('UT_NOT_IN_SEGMENT', true, {targetingKey: 'other' }).then(result => {
|
|
78
|
+
expect(result).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await client.getBooleanValue('UT_NOT_IN_SEGMENT', true, {targetingKey: 'other' }).then(result => {
|
|
82
|
+
expect(result).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('Evaluate with attributes set matcher', async () => {
|
|
87
|
+
await client.getBooleanValue('UT_SET_MATCHER', false, {targetingKey: 'UT_Segment_member', permissions: ['admin'] }).then(result => {
|
|
88
|
+
expect(result).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await client.getBooleanValue('UT_SET_MATCHER', false, {targetingKey: 'UT_Segment_member', permissions: ['not_matching'] }).then(result => {
|
|
92
|
+
expect(result).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await client.getBooleanValue('UT_NOT_SET_MATCHER', true, {targetingKey: 'UT_Segment_member', permissions: ['create'] }).then(result => {
|
|
96
|
+
expect(result).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await client.getBooleanValue('UT_NOT_SET_MATCHER', true, {targetingKey: 'UT_Segment_member', permissions: ['not_matching'] }).then(result => {
|
|
100
|
+
expect(result).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('Evaluate with dynamic config', async () => {
|
|
105
|
+
await client.getBooleanDetails('UT_NOT_SET_MATCHER', true, {targetingKey: 'UT_Segment_member', permissions: ['not_matching'] }).then(result => {
|
|
106
|
+
expect(result.value).toBe(true);
|
|
107
|
+
expect(result.flagMetadata).toEqual({'config': ''});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await client.getStringDetails('always-o.n-with-config', 'control', {targetingKey: 'other'}).then(result => {
|
|
111
|
+
expect(result.value).toBe('o.n');
|
|
112
|
+
expect(result.flagMetadata).toEqual({config: '{"color":"brown"}'});
|
|
113
|
+
});
|
|
114
|
+
})
|
|
115
|
+
});
|