flagsmith-nodejs 6.2.0 → 7.0.3
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/.github/workflows/publish.yml +8 -5
- package/.github/workflows/pull_request.yaml +3 -0
- package/.gitmodules +1 -1
- package/.husky/pre-commit +1 -0
- package/.nvmrc +1 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +49 -0
- package/build/cjs/flagsmith-engine/environments/models.d.ts +2 -1
- package/build/cjs/flagsmith-engine/environments/models.js +3 -1
- package/build/cjs/flagsmith-engine/environments/util.js +1 -1
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +8 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/mappers.js +156 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationContext/types.js +8 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
- package/build/cjs/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +8 -0
- package/build/cjs/flagsmith-engine/evaluation/models.d.ts +50 -0
- package/build/cjs/flagsmith-engine/evaluation/models.js +26 -0
- package/build/cjs/flagsmith-engine/features/models.js +1 -1
- package/build/cjs/flagsmith-engine/features/types.d.ts +5 -0
- package/build/cjs/flagsmith-engine/features/types.js +9 -0
- package/build/cjs/flagsmith-engine/features/util.d.ts +1 -0
- package/build/cjs/flagsmith-engine/features/util.js +5 -1
- package/build/cjs/flagsmith-engine/index.d.ts +61 -9
- package/build/cjs/flagsmith-engine/index.js +176 -56
- package/build/cjs/flagsmith-engine/segments/constants.d.ts +1 -0
- package/build/cjs/flagsmith-engine/segments/constants.js +2 -1
- package/build/cjs/flagsmith-engine/segments/evaluators.d.ts +41 -7
- package/build/cjs/flagsmith-engine/segments/evaluators.js +136 -24
- package/build/cjs/flagsmith-engine/segments/models.d.ts +9 -4
- package/build/cjs/flagsmith-engine/segments/models.js +115 -13
- package/build/cjs/flagsmith-engine/utils/hashing/index.d.ts +1 -1
- package/build/cjs/flagsmith-engine/utils/hashing/index.js +4 -4
- package/build/cjs/sdk/index.d.ts +1 -3
- package/build/cjs/sdk/index.js +22 -19
- package/build/cjs/sdk/models.d.ts +8 -1
- package/build/cjs/sdk/models.js +29 -1
- package/build/esm/flagsmith-engine/environments/models.d.ts +2 -1
- package/build/esm/flagsmith-engine/environments/models.js +3 -1
- package/build/esm/flagsmith-engine/environments/util.js +1 -1
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.d.ts +230 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js +7 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.d.ts +5 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/mappers.js +152 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.d.ts +216 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationContext/types.js +7 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.d.ts +68 -0
- package/build/esm/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.js +7 -0
- package/build/esm/flagsmith-engine/evaluation/models.d.ts +50 -0
- package/build/esm/flagsmith-engine/evaluation/models.js +9 -0
- package/build/esm/flagsmith-engine/features/models.js +2 -2
- package/build/esm/flagsmith-engine/features/types.d.ts +5 -0
- package/build/esm/flagsmith-engine/features/types.js +6 -0
- package/build/esm/flagsmith-engine/features/util.d.ts +1 -0
- package/build/esm/flagsmith-engine/features/util.js +3 -0
- package/build/esm/flagsmith-engine/index.d.ts +61 -9
- package/build/esm/flagsmith-engine/index.js +161 -43
- package/build/esm/flagsmith-engine/segments/constants.d.ts +1 -0
- package/build/esm/flagsmith-engine/segments/constants.js +1 -0
- package/build/esm/flagsmith-engine/segments/evaluators.d.ts +41 -7
- package/build/esm/flagsmith-engine/segments/evaluators.js +137 -25
- package/build/esm/flagsmith-engine/segments/models.d.ts +9 -4
- package/build/esm/flagsmith-engine/segments/models.js +115 -13
- package/build/esm/flagsmith-engine/utils/hashing/index.d.ts +1 -1
- package/build/esm/flagsmith-engine/utils/hashing/index.js +2 -2
- package/build/esm/sdk/index.d.ts +1 -3
- package/build/esm/sdk/index.js +21 -18
- package/build/esm/sdk/models.d.ts +8 -1
- package/build/esm/sdk/models.js +29 -1
- package/flagsmith-engine/environments/models.ts +3 -1
- package/flagsmith-engine/environments/util.ts +2 -1
- package/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +247 -0
- package/flagsmith-engine/evaluation/evaluationContext/mappers.ts +204 -0
- package/flagsmith-engine/evaluation/evaluationContext/types.ts +233 -0
- package/flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts +71 -0
- package/flagsmith-engine/evaluation/models.ts +96 -0
- package/flagsmith-engine/features/models.ts +3 -2
- package/flagsmith-engine/features/types.ts +5 -0
- package/flagsmith-engine/features/util.ts +4 -0
- package/flagsmith-engine/index.ts +229 -72
- package/flagsmith-engine/segments/constants.ts +1 -0
- package/flagsmith-engine/segments/evaluators.ts +178 -62
- package/flagsmith-engine/segments/models.ts +171 -23
- package/flagsmith-engine/utils/hashing/index.ts +2 -2
- package/package.json +13 -2
- package/sdk/index.ts +36 -23
- package/sdk/models.ts +44 -2
- package/tests/engine/e2e/engine.test.ts +43 -38
- package/tests/engine/unit/engine.test.ts +306 -59
- package/tests/engine/unit/mappers.test.ts +353 -0
- package/tests/engine/unit/segments/segment_evaluators.test.ts +391 -49
- package/tests/engine/unit/segments/segments_model.test.ts +85 -0
- package/tests/engine/unit/utils/utils.test.ts +7 -7
- package/tests/engine/unit/utils.ts +1 -1
- package/tests/sdk/data/environment.json +1 -0
- package/tests/sdk/flagsmith.test.ts +29 -3
- package/tests/sdk/offline-handlers.test.ts +3 -1
- package/vitest.config.esm.ts +34 -0
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
CONDITION_OPERATORS,
|
|
4
|
-
PERCENTAGE_SPLIT
|
|
5
|
-
} from '../../../../flagsmith-engine/segments/constants.js';
|
|
6
|
-
import { SegmentConditionModel } from '../../../../flagsmith-engine/segments/models.js';
|
|
1
|
+
import { ALL_RULE, CONDITION_OPERATORS } from '../../../../flagsmith-engine/segments/constants.js';
|
|
2
|
+
|
|
7
3
|
import {
|
|
8
4
|
traitsMatchSegmentCondition,
|
|
9
|
-
|
|
5
|
+
getContextValue,
|
|
6
|
+
getIdentitySegments
|
|
10
7
|
} from '../../../../flagsmith-engine/segments/evaluators.js';
|
|
11
|
-
import { TraitModel
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
8
|
+
import { TraitModel } from '../../../../flagsmith-engine/index.js';
|
|
9
|
+
import { getHashedPercentageForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js';
|
|
10
|
+
import {
|
|
11
|
+
EvaluationContext,
|
|
12
|
+
InSegmentCondition,
|
|
13
|
+
SegmentCondition,
|
|
14
|
+
SegmentCondition1
|
|
15
|
+
} from '../../../../flagsmith-engine/evaluation/models.js';
|
|
16
|
+
|
|
17
|
+
const isEsmBuild = process.env.ESM_BUILD === 'true';
|
|
15
18
|
|
|
16
19
|
// todo: work out how to implement this in a test function or before hook
|
|
17
20
|
vi.mock('../../../../flagsmith-engine/utils/hashing', () => ({
|
|
18
|
-
|
|
21
|
+
getHashedPercentageForObjIds: vi.fn(() => 1)
|
|
19
22
|
}));
|
|
20
23
|
|
|
21
24
|
let traitExistenceTestCases: [
|
|
@@ -48,49 +51,388 @@ let traitExistenceTestCases: [
|
|
|
48
51
|
test('test_traits_match_segment_condition_for_trait_existence_operators', () => {
|
|
49
52
|
for (const testCase of traitExistenceTestCases) {
|
|
50
53
|
const [operator, conditionProperty, conditionValue, traits, expectedResult] = testCase;
|
|
51
|
-
let
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
let segmentConditionModel = {
|
|
55
|
+
operator,
|
|
56
|
+
value: conditionValue,
|
|
57
|
+
property: conditionProperty
|
|
58
|
+
};
|
|
59
|
+
const traitsMap = traits.reduce((acc, trait) => {
|
|
60
|
+
acc[trait.traitKey] = trait.traitValue;
|
|
61
|
+
return acc;
|
|
62
|
+
}, {});
|
|
63
|
+
const context: EvaluationContext = {
|
|
64
|
+
environment: {
|
|
65
|
+
key: 'any',
|
|
66
|
+
name: 'any'
|
|
67
|
+
},
|
|
68
|
+
identity: {
|
|
69
|
+
traits: traitsMap,
|
|
70
|
+
key: 'any',
|
|
71
|
+
identifier: 'any'
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
expect(
|
|
75
|
+
traitsMatchSegmentCondition(segmentConditionModel as SegmentCondition, 'any', context)
|
|
76
|
+
).toBe(expectedResult);
|
|
55
77
|
}
|
|
56
78
|
});
|
|
57
79
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
describe('getIdentitySegments integration', () => {
|
|
81
|
+
test('returns only matching segments', () => {
|
|
82
|
+
const context: EvaluationContext = {
|
|
83
|
+
environment: { key: 'env', name: 'test' },
|
|
84
|
+
identity: {
|
|
85
|
+
key: 'user',
|
|
86
|
+
identifier: 'premium@example.com',
|
|
87
|
+
traits: { subscription: 'premium' }
|
|
88
|
+
},
|
|
89
|
+
segments: {
|
|
90
|
+
'1': {
|
|
91
|
+
key: '1',
|
|
92
|
+
name: 'premium_users',
|
|
93
|
+
rules: [
|
|
94
|
+
{
|
|
95
|
+
type: 'ALL',
|
|
96
|
+
conditions: [
|
|
97
|
+
{ property: 'subscription', operator: 'EQUAL', value: 'premium' }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
overrides: []
|
|
102
|
+
},
|
|
103
|
+
'2': {
|
|
104
|
+
key: '2',
|
|
105
|
+
name: 'basic_users',
|
|
106
|
+
rules: [
|
|
107
|
+
{
|
|
108
|
+
type: 'ALL',
|
|
109
|
+
conditions: [
|
|
110
|
+
{ property: 'subscription', operator: 'EQUAL', value: 'basic' }
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
overrides: []
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
features: {}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const result = getIdentitySegments(context);
|
|
121
|
+
|
|
122
|
+
expect(result).toHaveLength(1);
|
|
123
|
+
expect(result[0].name).toBe('premium_users');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('returns empty array when no segments match', () => {
|
|
127
|
+
const context: EvaluationContext = {
|
|
128
|
+
environment: { key: 'env', name: 'test' },
|
|
129
|
+
identity: {
|
|
130
|
+
key: 'user',
|
|
131
|
+
identifier: 'test@example.com',
|
|
132
|
+
traits: { subscription: 'free' }
|
|
133
|
+
},
|
|
134
|
+
segments: {
|
|
135
|
+
'1': {
|
|
136
|
+
key: '1',
|
|
137
|
+
name: 'premium_users',
|
|
138
|
+
rules: [
|
|
139
|
+
{
|
|
140
|
+
type: 'ALL',
|
|
141
|
+
conditions: [
|
|
142
|
+
{ property: 'subscription', operator: 'EQUAL', value: 'premium' }
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
overrides: []
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
features: {}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const result = getIdentitySegments(context);
|
|
153
|
+
expect(result).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('IN operator', () => {
|
|
158
|
+
const mockContext: EvaluationContext = {
|
|
159
|
+
environment: { key: 'env', name: 'test' },
|
|
160
|
+
identity: {
|
|
161
|
+
key: 'test-user',
|
|
162
|
+
identifier: 'test',
|
|
163
|
+
traits: { name: 'test' }
|
|
164
|
+
},
|
|
165
|
+
segments: {},
|
|
166
|
+
features: {}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
test.each([
|
|
170
|
+
// Array of strings
|
|
171
|
+
[
|
|
72
172
|
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
173
|
+
property: '$.identity.identifier',
|
|
174
|
+
operator: CONDITION_OPERATORS.IN,
|
|
175
|
+
value: ['test', 'john-doe']
|
|
176
|
+
},
|
|
177
|
+
true
|
|
178
|
+
],
|
|
179
|
+
[
|
|
180
|
+
{
|
|
181
|
+
property: '$.identity.identifier',
|
|
182
|
+
operator: CONDITION_OPERATORS.IN,
|
|
183
|
+
value: ['john-doe']
|
|
184
|
+
},
|
|
185
|
+
false
|
|
186
|
+
],
|
|
187
|
+
|
|
188
|
+
// JSON encoded
|
|
189
|
+
[
|
|
190
|
+
{
|
|
191
|
+
property: '$.identity.identifier',
|
|
192
|
+
operator: CONDITION_OPERATORS.IN,
|
|
193
|
+
value: '["test", "john-doe"]'
|
|
194
|
+
},
|
|
195
|
+
true
|
|
83
196
|
],
|
|
84
|
-
|
|
197
|
+
[
|
|
198
|
+
{
|
|
199
|
+
property: '$.identity.identifier',
|
|
200
|
+
operator: CONDITION_OPERATORS.IN,
|
|
201
|
+
value: '["john-doe"]'
|
|
202
|
+
},
|
|
203
|
+
false
|
|
204
|
+
],
|
|
205
|
+
|
|
206
|
+
// Legacy value string to split
|
|
207
|
+
[
|
|
208
|
+
{
|
|
209
|
+
property: '$.identity.identifier',
|
|
210
|
+
operator: CONDITION_OPERATORS.IN,
|
|
211
|
+
value: 'test,john-doe'
|
|
212
|
+
},
|
|
213
|
+
true
|
|
214
|
+
],
|
|
215
|
+
[
|
|
216
|
+
{
|
|
217
|
+
property: '$.identity.identifier',
|
|
218
|
+
operator: CONDITION_OPERATORS.IN,
|
|
219
|
+
value: 'john-doe'
|
|
220
|
+
},
|
|
221
|
+
false
|
|
222
|
+
],
|
|
223
|
+
// Fails because the value is split in middle
|
|
224
|
+
[
|
|
225
|
+
{
|
|
226
|
+
property: '$.identity.identifier',
|
|
227
|
+
operator: CONDITION_OPERATORS.IN,
|
|
228
|
+
value: 'te,st,john-doe'
|
|
229
|
+
},
|
|
230
|
+
false
|
|
231
|
+
],
|
|
232
|
+
|
|
233
|
+
// Edge cases
|
|
234
|
+
[{ property: '$.identity.identifier', operator: CONDITION_OPERATORS.IN, value: '' }, false],
|
|
235
|
+
[{ property: '$.identity.identifier', operator: CONDITION_OPERATORS.IN, value: [] }, false],
|
|
236
|
+
[
|
|
237
|
+
{ property: '$.identity.identifier', operator: CONDITION_OPERATORS.IN, value: '[]' },
|
|
238
|
+
false
|
|
239
|
+
]
|
|
240
|
+
] as Array<[SegmentCondition | InSegmentCondition, boolean]>)(
|
|
241
|
+
'evaluates IN condition %j to %s',
|
|
242
|
+
(condition: SegmentCondition | InSegmentCondition, expected: boolean) => {
|
|
243
|
+
const result = traitsMatchSegmentCondition(condition, 'segment', mockContext);
|
|
244
|
+
expect(result).toBe(expected);
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('getIdentitySegments single segment evaluation', () => {
|
|
250
|
+
const baseContext: EvaluationContext = {
|
|
251
|
+
environment: { key: 'env', name: 'test' },
|
|
252
|
+
identity: { key: 'user', identifier: 'test@example.com', traits: { age: 25 } },
|
|
253
|
+
segments: {},
|
|
254
|
+
features: {}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
test('returns empty array for segment with no rules', () => {
|
|
258
|
+
const context = {
|
|
259
|
+
...baseContext,
|
|
260
|
+
segments: {
|
|
261
|
+
'1': {
|
|
262
|
+
key: '1',
|
|
263
|
+
name: 'empty_segment',
|
|
264
|
+
rules: [],
|
|
265
|
+
overrides: []
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
expect(getIdentitySegments(context)).toEqual([]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('returns segment when all rules match', () => {
|
|
274
|
+
const context: EvaluationContext = {
|
|
275
|
+
...baseContext,
|
|
276
|
+
segments: {
|
|
277
|
+
'1': {
|
|
278
|
+
key: '1',
|
|
279
|
+
name: 'matching_segment',
|
|
280
|
+
rules: [
|
|
281
|
+
{
|
|
282
|
+
type: ALL_RULE,
|
|
283
|
+
conditions: [
|
|
284
|
+
{
|
|
285
|
+
property: '$.identity.identifier',
|
|
286
|
+
operator: 'EQUAL',
|
|
287
|
+
value: 'test@example.com'
|
|
288
|
+
}
|
|
289
|
+
],
|
|
290
|
+
rules: []
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
type: ALL_RULE,
|
|
294
|
+
conditions: [
|
|
295
|
+
{
|
|
296
|
+
property: '$.identity.identifier',
|
|
297
|
+
operator: 'CONTAINS',
|
|
298
|
+
value: 'test@example.com'
|
|
299
|
+
}
|
|
300
|
+
],
|
|
301
|
+
rules: []
|
|
302
|
+
}
|
|
303
|
+
],
|
|
304
|
+
overrides: []
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const result = getIdentitySegments(context);
|
|
310
|
+
expect(result).toHaveLength(1);
|
|
311
|
+
expect(result[0].name).toBe('matching_segment');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('returns empty array when any rule fails', () => {
|
|
315
|
+
const context: EvaluationContext = {
|
|
316
|
+
...baseContext,
|
|
317
|
+
segments: {
|
|
318
|
+
'1': {
|
|
319
|
+
key: '1',
|
|
320
|
+
name: 'failing_segment',
|
|
321
|
+
rules: [
|
|
322
|
+
{
|
|
323
|
+
type: ALL_RULE,
|
|
324
|
+
conditions: [
|
|
325
|
+
{
|
|
326
|
+
property: '$.identity.identifier',
|
|
327
|
+
operator: 'EQUAL',
|
|
328
|
+
value: 'test@example.com'
|
|
329
|
+
}
|
|
330
|
+
],
|
|
331
|
+
rules: []
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
type: ALL_RULE,
|
|
335
|
+
conditions: [{ property: 'age', operator: 'EQUAL', value: '30' }],
|
|
336
|
+
rules: []
|
|
337
|
+
}
|
|
338
|
+
],
|
|
339
|
+
overrides: []
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
expect(getIdentitySegments(context)).toEqual([]);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe('getContextValue', () => {
|
|
349
|
+
const mockContext: EvaluationContext = {
|
|
350
|
+
environment: {
|
|
351
|
+
key: 'test-env-key',
|
|
352
|
+
name: 'Test Environment'
|
|
353
|
+
},
|
|
354
|
+
identity: {
|
|
355
|
+
key: 'user-123',
|
|
356
|
+
identifier: 'user@example.com'
|
|
357
|
+
},
|
|
358
|
+
segments: {},
|
|
359
|
+
features: {}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// Success cases
|
|
363
|
+
test.each([
|
|
364
|
+
['$.identity.identifier', 'user@example.com'],
|
|
365
|
+
['$.environment.name', 'Test Environment'],
|
|
366
|
+
['$.environment.key', 'test-env-key']
|
|
367
|
+
])('returns correct value for path %s', (jsonPath, expected) => {
|
|
368
|
+
const result = getContextValue(jsonPath, mockContext);
|
|
369
|
+
expect(result).toBe(expected);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Undefined or invalid cases
|
|
373
|
+
test.each([
|
|
374
|
+
['$.identity.traits.user_type', 'unsupported nested path'],
|
|
375
|
+
['identity.identifier', 'missing $ prefix'],
|
|
376
|
+
['$.invalid.path', 'completely invalid path'],
|
|
377
|
+
['$.identity.nonexistent', 'valid structure but missing property'],
|
|
378
|
+
['', 'empty string'],
|
|
379
|
+
['$', 'just $ symbol']
|
|
380
|
+
])('returns undefined for %s (%s)', jsonPath => {
|
|
381
|
+
const result = getContextValue(jsonPath, mockContext);
|
|
382
|
+
expect(result).toBeUndefined();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Context error cases
|
|
386
|
+
test.each([
|
|
387
|
+
[undefined, '$.identity.identifier', 'undefined context'],
|
|
388
|
+
[{ segments: {}, features: {} }, '$.identity.identifier', 'missing identity'],
|
|
389
|
+
[
|
|
390
|
+
{ identity: { key: 'test', identifier: 'test' }, segments: {}, features: {} },
|
|
391
|
+
'$.environment.name',
|
|
392
|
+
'missing environment'
|
|
393
|
+
]
|
|
394
|
+
])('returns undefined when %s', (context, jsonPath, _) => {
|
|
395
|
+
const result = getContextValue(jsonPath, context as EvaluationContext);
|
|
396
|
+
expect(result).toBeUndefined();
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Skip in ESM build: vi.mock doesn't work with external modules
|
|
401
|
+
describe.skipIf(isEsmBuild)('percentage split operator', () => {
|
|
402
|
+
const mockContext: EvaluationContext = {
|
|
403
|
+
environment: { key: 'env', name: 'Test Env' },
|
|
404
|
+
identity: {
|
|
405
|
+
key: 'user-123',
|
|
406
|
+
identifier: 'test@example.com',
|
|
407
|
+
traits: {
|
|
408
|
+
age: 25,
|
|
409
|
+
subscription: 'premium',
|
|
410
|
+
active: true
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
segments: {},
|
|
414
|
+
features: {}
|
|
85
415
|
};
|
|
86
|
-
|
|
416
|
+
beforeEach(() => {
|
|
417
|
+
vi.clearAllMocks();
|
|
418
|
+
});
|
|
87
419
|
|
|
88
|
-
|
|
420
|
+
test.each([
|
|
421
|
+
[25.5, 30, true],
|
|
422
|
+
[25.5, 20, false],
|
|
423
|
+
[25.5, 25.5, true],
|
|
424
|
+
[0, 0, true],
|
|
425
|
+
[100, 99.9, false]
|
|
426
|
+
])('percentage %d with threshold %d returns %s', (hashedValue, threshold, expected) => {
|
|
427
|
+
const mockHashFn = getHashedPercentageForObjIds;
|
|
428
|
+
mockHashFn.mockReturnValue(hashedValue);
|
|
429
|
+
const condition = {
|
|
430
|
+
operator: 'PERCENTAGE_SPLIT',
|
|
431
|
+
value: threshold.toString()
|
|
432
|
+
} as SegmentCondition1 | InSegmentCondition;
|
|
433
|
+
const result = traitsMatchSegmentCondition(condition, 'seg1', mockContext);
|
|
89
434
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
segmentModel.id,
|
|
94
|
-
identityModel.djangoID
|
|
95
|
-
]);
|
|
435
|
+
expect(result).toBe(expected);
|
|
436
|
+
expect(getHashedPercentageForObjIds).toHaveBeenCalledWith(['seg1', 'user-123']);
|
|
437
|
+
});
|
|
96
438
|
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { SegmentSource } from '../../../../flagsmith-engine/evaluation/models';
|
|
2
|
+
import { EvaluationContext } from '../../../../flagsmith-engine/evaluationContext/evaluationContext.types';
|
|
3
|
+
import { CONSTANTS } from '../../../../flagsmith-engine/features/constants';
|
|
1
4
|
import {
|
|
2
5
|
ALL_RULE,
|
|
3
6
|
ANY_RULE,
|
|
@@ -8,6 +11,7 @@ import {
|
|
|
8
11
|
all,
|
|
9
12
|
any,
|
|
10
13
|
SegmentConditionModel,
|
|
14
|
+
SegmentModel,
|
|
11
15
|
SegmentRuleModel
|
|
12
16
|
} from '../../../../flagsmith-engine/segments/models';
|
|
13
17
|
|
|
@@ -135,3 +139,84 @@ test('test_segment_rule_matching_function', () => {
|
|
|
135
139
|
expect(new SegmentRuleModel(testCase[0]).matchingFunction()).toBe(testCase[1]);
|
|
136
140
|
}
|
|
137
141
|
});
|
|
142
|
+
|
|
143
|
+
test('test_fromSegmentResult_with_multiple_variants', () => {
|
|
144
|
+
const segmentResults = [{ name: 'test_segment', metadata: { id: '1' } }];
|
|
145
|
+
|
|
146
|
+
const evaluationContext: EvaluationContext = {
|
|
147
|
+
identity: {
|
|
148
|
+
key: 'not_exist',
|
|
149
|
+
identifier: 'not_exist'
|
|
150
|
+
},
|
|
151
|
+
environment: {
|
|
152
|
+
key: 'test',
|
|
153
|
+
name: 'test'
|
|
154
|
+
},
|
|
155
|
+
features: {},
|
|
156
|
+
segments: {
|
|
157
|
+
'1': {
|
|
158
|
+
key: '1',
|
|
159
|
+
name: 'test_segment',
|
|
160
|
+
metadata: {
|
|
161
|
+
source: SegmentSource.API,
|
|
162
|
+
id: '1'
|
|
163
|
+
},
|
|
164
|
+
rules: [
|
|
165
|
+
{
|
|
166
|
+
type: 'ALL',
|
|
167
|
+
conditions: [
|
|
168
|
+
{
|
|
169
|
+
property: '$.identity.identifier',
|
|
170
|
+
operator: 'EQUAL',
|
|
171
|
+
value: 'test-user'
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
],
|
|
176
|
+
overrides: [
|
|
177
|
+
{
|
|
178
|
+
key: 'override',
|
|
179
|
+
name: 'multivariate_feature',
|
|
180
|
+
enabled: true,
|
|
181
|
+
value: 'default_value',
|
|
182
|
+
priority: 1,
|
|
183
|
+
metadata: {
|
|
184
|
+
id: 1
|
|
185
|
+
},
|
|
186
|
+
variants: [
|
|
187
|
+
{ id: 1, value: 'variant_a', weight: 30 },
|
|
188
|
+
{ id: 2, value: 'variant_b', weight: 70 }
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const result = SegmentModel.fromSegmentResult(segmentResults, evaluationContext);
|
|
197
|
+
|
|
198
|
+
expect(result).toHaveLength(1);
|
|
199
|
+
|
|
200
|
+
const segment = result[0];
|
|
201
|
+
expect(segment.name).toBe('test_segment');
|
|
202
|
+
expect(segment.featureStates).toHaveLength(1);
|
|
203
|
+
|
|
204
|
+
const featureState = segment.featureStates[0];
|
|
205
|
+
expect(featureState.feature.name).toBe('multivariate_feature');
|
|
206
|
+
expect(featureState.feature.type).toBe(CONSTANTS.MULTIVARIATE);
|
|
207
|
+
expect(featureState.enabled).toBe(true);
|
|
208
|
+
expect(featureState.getValue()).toBe('default_value');
|
|
209
|
+
|
|
210
|
+
// Test multivariate variants
|
|
211
|
+
expect(featureState.multivariateFeatureStateValues).toHaveLength(2);
|
|
212
|
+
|
|
213
|
+
const variant1 = featureState.multivariateFeatureStateValues[0];
|
|
214
|
+
expect(variant1.multivariateFeatureOption.value).toBe('variant_a');
|
|
215
|
+
expect(variant1.percentageAllocation).toBe(30);
|
|
216
|
+
expect(variant1.id).toBe(1);
|
|
217
|
+
|
|
218
|
+
const variant2 = featureState.multivariateFeatureStateValues[1];
|
|
219
|
+
expect(variant2.multivariateFeatureOption.value).toBe('variant_b');
|
|
220
|
+
expect(variant2.percentageAllocation).toBe(70);
|
|
221
|
+
expect(variant2.id).toBe(2);
|
|
222
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { randomUUID as uuidv4 } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { getHashedPercentageForObjIds } from '../../../../flagsmith-engine/utils/hashing/index.js';
|
|
3
3
|
|
|
4
4
|
describe('getHashedPercentageForObjIds', () => {
|
|
5
5
|
it.each([[[12, 93]], [[uuidv4(), 99]], [[99, uuidv4()]], [[uuidv4(), uuidv4()]]])(
|
|
6
6
|
'returns x where 0 <= x < 100',
|
|
7
7
|
(objIds: (string | number)[]) => {
|
|
8
|
-
let result =
|
|
8
|
+
let result = getHashedPercentageForObjIds(objIds);
|
|
9
9
|
expect(result).toBeLessThan(100);
|
|
10
10
|
expect(result).toBeGreaterThanOrEqual(0);
|
|
11
11
|
}
|
|
@@ -14,15 +14,15 @@ describe('getHashedPercentageForObjIds', () => {
|
|
|
14
14
|
it.each([[[12, 93]], [[uuidv4(), 99]], [[99, uuidv4()]], [[uuidv4(), uuidv4()]]])(
|
|
15
15
|
'returns the same value each time',
|
|
16
16
|
(objIds: (string | number)[]) => {
|
|
17
|
-
let resultOne =
|
|
18
|
-
let resultTwo =
|
|
17
|
+
let resultOne = getHashedPercentageForObjIds(objIds);
|
|
18
|
+
let resultTwo = getHashedPercentageForObjIds(objIds);
|
|
19
19
|
expect(resultOne).toEqual(resultTwo);
|
|
20
20
|
}
|
|
21
21
|
);
|
|
22
22
|
|
|
23
23
|
it('is unique for different object ids', () => {
|
|
24
|
-
let resultOne =
|
|
25
|
-
let resultTwo =
|
|
24
|
+
let resultOne = getHashedPercentageForObjIds([14, 106]);
|
|
25
|
+
let resultTwo = getHashedPercentageForObjIds([53, 200]);
|
|
26
26
|
expect(resultOne).not.toEqual(resultTwo);
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -40,7 +40,7 @@ describe('getHashedPercentageForObjIds', () => {
|
|
|
40
40
|
);
|
|
41
41
|
|
|
42
42
|
// When
|
|
43
|
-
let values = objectIdPairs.map(objIds =>
|
|
43
|
+
let values = objectIdPairs.map(objIds => getHashedPercentageForObjIds(objIds));
|
|
44
44
|
|
|
45
45
|
// Then
|
|
46
46
|
for (let i = 0; i++; i < numTestBuckets) {
|
|
@@ -20,7 +20,7 @@ export function segmentCondition() {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function traitMatchingSegment() {
|
|
23
|
-
return new TraitModel(segmentCondition().
|
|
23
|
+
return new TraitModel(segmentCondition().property as string, segmentCondition().value);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function organisation() {
|
|
@@ -14,8 +14,12 @@ import { EnvironmentModel } from '../../flagsmith-engine/environments/models.js'
|
|
|
14
14
|
import { BaseOfflineHandler } from '../../sdk/offline_handlers.js';
|
|
15
15
|
import { Agent } from 'undici';
|
|
16
16
|
|
|
17
|
+
const isEsmBuild = process.env.ESM_BUILD === 'true';
|
|
18
|
+
|
|
17
19
|
vi.mock('../../sdk/polling_manager');
|
|
18
|
-
|
|
20
|
+
|
|
21
|
+
// Skip in ESM build: vi.mock doesn't work with external modules
|
|
22
|
+
test.skipIf(isEsmBuild)('test_flagsmith_starts_polling_manager_on_init_if_enabled', () => {
|
|
19
23
|
new Flagsmith({
|
|
20
24
|
environmentKey: 'ser.key',
|
|
21
25
|
enableLocalEvaluation: true
|
|
@@ -32,7 +36,8 @@ test('test_flagsmith_local_evaluation_key_required', () => {
|
|
|
32
36
|
}).toThrow('Using local evaluation requires a server-side environment key');
|
|
33
37
|
});
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
// Skip in ESM build: instanceof fails across module boundaries
|
|
40
|
+
test.skipIf(isEsmBuild)('test_update_environment_sets_environment', async () => {
|
|
36
41
|
const flg = flagsmith({
|
|
37
42
|
environmentKey: 'ser.key'
|
|
38
43
|
});
|
|
@@ -513,9 +518,30 @@ test('getIdentityFlags succeeds if initial fetch failed then succeeded', async (
|
|
|
513
518
|
expect(flags2.isFeatureEnabled('some_feature')).toBe(true);
|
|
514
519
|
});
|
|
515
520
|
|
|
516
|
-
|
|
521
|
+
// Skip in ESM build: require() path resolution differs
|
|
522
|
+
test.skipIf(isEsmBuild)('get_user_agent_extracts_version_from_package_json', async () => {
|
|
517
523
|
const userAgent = getUserAgent();
|
|
518
524
|
const packageJson = require('../../package.json');
|
|
519
525
|
|
|
520
526
|
expect(userAgent).toBe(`flagsmith-nodejs-sdk/${packageJson.version}`);
|
|
521
527
|
});
|
|
528
|
+
|
|
529
|
+
test('Flags.fromEvaluationResult throws error when metadata.id is missing', () => {
|
|
530
|
+
const evaluationResult = {
|
|
531
|
+
flags: {
|
|
532
|
+
test_feature: {
|
|
533
|
+
enabled: true,
|
|
534
|
+
name: 'test_feature',
|
|
535
|
+
value: 'test_value',
|
|
536
|
+
reason: 'DEFAULT',
|
|
537
|
+
metadata: {}
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
segments: []
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
expect(() => Flags.fromEvaluationResult(evaluationResult as any)).toThrow(
|
|
544
|
+
'FlagResult metadata.id is missing for feature "test_feature". ' +
|
|
545
|
+
'This indicates a bug in the SDK, please report it.'
|
|
546
|
+
);
|
|
547
|
+
});
|