Skip to content

Commit 3c1d200

Browse files
authored
fix: properly-map-environment-name (#226)
1 parent ae1fb7e commit 3c1d200

File tree

5 files changed

+363
-3
lines changed

5 files changed

+363
-3
lines changed

flagsmith-engine/environments/models.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ export class EnvironmentModel {
3838
project: ProjectModel;
3939
featureStates: FeatureStateModel[] = [];
4040
identityOverrides: IdentityModel[] = [];
41+
name: string;
4142

42-
constructor(id: number, apiKey: string, project: ProjectModel) {
43+
constructor(id: number, apiKey: string, project: ProjectModel, name: string) {
4344
this.id = id;
4445
this.apiKey = apiKey;
4546
this.project = project;
47+
this.name = name;
4648
}
4749
}

flagsmith-engine/environments/util.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export function buildEnvironmentModel(environmentJSON: any) {
1111
const environmentModel = new EnvironmentModel(
1212
environmentJSON.id,
1313
environmentJSON.api_key,
14-
project
14+
project,
15+
environmentJSON.name
1516
);
1617
environmentModel.featureStates = featureStates;
1718
if (!!environmentJSON.identity_overrides) {

flagsmith-engine/evaluation/evaluationContext/mappers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function mapEnvironmentModelToEvaluationContext(
4343
): GenericEvaluationContext {
4444
const environmentContext: EnvironmentContext = {
4545
key: environment.apiKey,
46-
name: environment.project.name
46+
name: environment.name
4747
};
4848

4949
const features: FeaturesWithMetadata<SDKFeatureMetadata> = {};

tests/engine/unit/mappers.test.ts

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
import { test, expect, describe } from 'vitest';
2+
import { getEvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/mappers.js';
3+
import { buildEnvironmentModel } from '../../../flagsmith-engine/environments/util.js';
4+
import { EnvironmentModel } from '../../../flagsmith-engine/environments/models.js';
5+
import { IdentityModel } from '../../../flagsmith-engine/identities/models.js';
6+
import { TraitModel } from '../../../flagsmith-engine/identities/traits/models.js';
7+
import { FeatureModel, FeatureStateModel } from '../../../flagsmith-engine/features/models.js';
8+
import {
9+
MultivariateFeatureOptionModel,
10+
MultivariateFeatureStateValueModel
11+
} from '../../../flagsmith-engine/features/models.js';
12+
import { CONSTANTS } from '../../../flagsmith-engine/features/constants.js';
13+
import { readFileSync } from 'fs';
14+
import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../../../flagsmith-engine/segments/constants.js';
15+
import { SegmentSource } from '../../../flagsmith-engine/evaluation/models.js';
16+
17+
const DATA_DIR = __dirname + '/../../sdk/data/';
18+
19+
describe('getEvaluationContext', () => {
20+
const environmentJSON = JSON.parse(readFileSync(DATA_DIR + 'environment.json', 'utf-8'));
21+
const testEnvironment = buildEnvironmentModel(environmentJSON);
22+
23+
test('produces evaluation context from environment document', () => {
24+
// When
25+
const context = getEvaluationContext(testEnvironment);
26+
27+
// Then - verify environment
28+
expect(context).toBeDefined();
29+
expect(context.environment?.key).toBe('B62qaMZNwfiqT76p38ggrQ');
30+
expect(context.environment?.name).toBe('Test environment');
31+
expect(context.identity).toBeUndefined();
32+
33+
// Verify segments
34+
expect(context.segments).toBeDefined();
35+
expect(context.segments).toHaveProperty('1');
36+
37+
const segment = context.segments!['1'];
38+
expect(segment.key).toBe('1');
39+
expect(segment.name).toBe('regular_segment');
40+
expect(segment.rules.length).toBe(1);
41+
expect(segment.overrides).toBeDefined();
42+
expect(Array.isArray(segment.overrides)).toBe(true);
43+
expect(segment.metadata?.source).toBe(SegmentSource.API);
44+
expect(segment.metadata?.id).toBe(1);
45+
46+
// Verify segment rules
47+
expect(segment.rules[0].type).toBe('ALL');
48+
expect(segment.rules[0].conditions).toEqual([]);
49+
expect(segment.rules[0].rules?.length).toBe(1);
50+
51+
const nestedRule = segment.rules[0].rules?.[0]!;
52+
expect(nestedRule.type).toBe('ANY');
53+
expect(nestedRule.conditions?.length).toBe(1);
54+
expect(nestedRule.rules?.length).toEqual(0);
55+
56+
const condition = nestedRule.conditions?.[0]!;
57+
expect(condition.property).toBe('age');
58+
expect(condition.operator).toBe('LESS_THAN');
59+
expect(condition.value).toBe('40');
60+
61+
// Verify identity override segment
62+
const identityOverrideSegment = Object.values(context.segments!).find(
63+
s => s.name === IDENTITY_OVERRIDE_SEGMENT_NAME
64+
);
65+
expect(identityOverrideSegment).toBeDefined();
66+
expect(identityOverrideSegment!.name).toBe(IDENTITY_OVERRIDE_SEGMENT_NAME);
67+
expect(identityOverrideSegment!.rules.length).toBe(1);
68+
expect(identityOverrideSegment!.overrides?.length).toBe(1);
69+
70+
const overrideRule = identityOverrideSegment!.rules?.[0]!;
71+
expect(overrideRule.type).toBe('ALL');
72+
expect(overrideRule.conditions?.length).toBe(1);
73+
74+
const overrideCondition = overrideRule.conditions?.[0]!;
75+
expect(overrideCondition.property).toBe('$.identity.identifier');
76+
expect(overrideCondition.operator).toBe('IN');
77+
expect(overrideCondition.value).toContain('overridden-id');
78+
79+
const override = identityOverrideSegment!.overrides?.[0]!;
80+
expect(override.name).toBe('some_feature');
81+
expect(override.enabled).toBe(false);
82+
expect(override.value).toBe('some-overridden-value');
83+
expect(override.priority).toBe(-Infinity);
84+
expect(override.metadata?.id).toBe(1);
85+
86+
// Verify features
87+
expect(context.features).toBeDefined();
88+
expect(context.features).toHaveProperty('some_feature');
89+
90+
const someFeature = context.features!['some_feature'];
91+
expect(someFeature.name).toBe('some_feature');
92+
expect(someFeature.enabled).toBe(true);
93+
expect(someFeature.value).toBe('some-value');
94+
expect(someFeature.priority).toBeUndefined();
95+
expect(someFeature.metadata?.id).toBe(1);
96+
97+
// Verify multivariate feature
98+
expect(context.features).toHaveProperty('mv_feature');
99+
const mvFeature = context.features!['mv_feature'];
100+
expect(mvFeature.name).toBe('mv_feature');
101+
expect(mvFeature.enabled).toBe(false);
102+
expect(mvFeature.value).toBe('foo');
103+
expect(mvFeature.priority).toBeUndefined();
104+
expect(mvFeature.variants?.length).toBe(1);
105+
106+
const variant = mvFeature.variants![0];
107+
expect(variant.value).toBe('bar');
108+
expect(variant.weight).toBe(100);
109+
expect(variant.priority).toBe(1);
110+
});
111+
112+
test('maps multivariate features with multiple variants correctly', () => {
113+
// Given
114+
const mvOption1 = new MultivariateFeatureOptionModel('variant_a', 100);
115+
const mvOption2 = new MultivariateFeatureOptionModel('variant_b', 200);
116+
const mvOption3 = new MultivariateFeatureOptionModel('variant_c', 150);
117+
118+
const mvValue1 = new MultivariateFeatureStateValueModel(
119+
mvOption1,
120+
30,
121+
100,
122+
'00000000-0000-0000-0000-000000000001'
123+
);
124+
125+
const mvValue2 = new MultivariateFeatureStateValueModel(
126+
mvOption2,
127+
50,
128+
200,
129+
'00000000-0000-0000-0000-000000000002'
130+
);
131+
132+
const mvValue3 = new MultivariateFeatureStateValueModel(
133+
mvOption3,
134+
20,
135+
150,
136+
'00000000-0000-0000-0000-000000000003'
137+
);
138+
139+
const feature = new FeatureModel(999, 'multi_variant_feature', CONSTANTS.MULTIVARIATE);
140+
const featureState = new FeatureStateModel(feature, true, 999);
141+
featureState.setValue('control');
142+
featureState.multivariateFeatureStateValues = [mvValue1, mvValue2, mvValue3];
143+
144+
const envWithMv = new EnvironmentModel(1, 'test_key', testEnvironment.project, 'Test Env');
145+
envWithMv.featureStates = [featureState];
146+
147+
// When
148+
const context = getEvaluationContext(envWithMv);
149+
150+
// Then
151+
const featureContext = context.features!['multi_variant_feature'];
152+
expect(featureContext.variants?.length).toBe(3);
153+
154+
expect(featureContext.variants![0].value).toBe('variant_a');
155+
expect(featureContext.variants![0].weight).toBe(30);
156+
expect(featureContext.variants![0].priority).toBe(100);
157+
158+
expect(featureContext.variants![1].value).toBe('variant_b');
159+
expect(featureContext.variants![1].weight).toBe(50);
160+
expect(featureContext.variants![1].priority).toBe(200);
161+
162+
expect(featureContext.variants![2].value).toBe('variant_c');
163+
expect(featureContext.variants![2].weight).toBe(20);
164+
expect(featureContext.variants![2].priority).toBe(150);
165+
});
166+
167+
test('handles multivariate features without IDs using UUID', () => {
168+
// Given
169+
const mvOption1 = new MultivariateFeatureOptionModel('option_x', undefined);
170+
const mvOption2 = new MultivariateFeatureOptionModel('option_y', undefined);
171+
172+
const mvValue1 = new MultivariateFeatureStateValueModel(
173+
mvOption1,
174+
60,
175+
undefined as any,
176+
'aaaaaaaa-bbbb-cccc-dddd-000000000001'
177+
);
178+
179+
const mvValue2 = new MultivariateFeatureStateValueModel(
180+
mvOption2,
181+
40,
182+
undefined as any,
183+
'aaaaaaaa-bbbb-cccc-dddd-000000000002'
184+
);
185+
186+
const feature = new FeatureModel(888, 'uuid_variant_feature', CONSTANTS.MULTIVARIATE);
187+
const featureState = new FeatureStateModel(feature, true, 888);
188+
featureState.setValue('default');
189+
featureState.multivariateFeatureStateValues = [mvValue1, mvValue2];
190+
191+
const envWithUuid = new EnvironmentModel(
192+
1,
193+
'test_key',
194+
testEnvironment.project,
195+
'Test Env'
196+
);
197+
envWithUuid.featureStates = [featureState];
198+
199+
// When
200+
const context = getEvaluationContext(envWithUuid);
201+
202+
// Then
203+
const featureContext = context.features!['uuid_variant_feature'];
204+
expect(featureContext.variants?.length).toBe(2);
205+
206+
// When using UUID-based priorities, they become bigints
207+
expect(
208+
typeof featureContext.variants![0].priority === 'number' ||
209+
typeof featureContext.variants![0].priority === 'bigint'
210+
).toBe(true);
211+
expect(
212+
typeof featureContext.variants![1].priority === 'number' ||
213+
typeof featureContext.variants![1].priority === 'bigint'
214+
).toBe(true);
215+
expect(featureContext.variants![0].priority).not.toBe(featureContext.variants![1].priority);
216+
});
217+
218+
test('handles environment with no features', () => {
219+
// Given - create a copy with no features
220+
const emptyEnvJSON = { ...environmentJSON, feature_states: [] };
221+
const emptyEnv = buildEnvironmentModel(emptyEnvJSON);
222+
223+
// When
224+
const context = getEvaluationContext(emptyEnv);
225+
226+
// Then
227+
expect(context.features).toEqual({});
228+
expect(context.environment?.key).toBe('B62qaMZNwfiqT76p38ggrQ');
229+
expect(context.environment?.name).toBe('Test environment');
230+
});
231+
232+
test('produces evaluation context with identity', () => {
233+
// Given
234+
const identity = new IdentityModel(
235+
'2024-01-01T00:00:00Z',
236+
[new TraitModel('email', '[email protected]'), new TraitModel('age', 25)],
237+
[],
238+
'B62qaMZNwfiqT76p38ggrQ',
239+
'test_user',
240+
undefined,
241+
123
242+
);
243+
244+
// When
245+
const context = getEvaluationContext(testEnvironment, identity);
246+
247+
// Then
248+
expect(context.identity).toBeDefined();
249+
expect(context.identity?.identifier).toBe('test_user');
250+
expect(context.identity?.key).toBe('123');
251+
expect(context.identity?.traits).toEqual({
252+
253+
age: 25
254+
});
255+
});
256+
257+
test('produces evaluation context with override traits', () => {
258+
// Given
259+
const identity = new IdentityModel(
260+
'2024-01-01T00:00:00Z',
261+
[new TraitModel('email', '[email protected]')],
262+
[],
263+
'B62qaMZNwfiqT76p38ggrQ',
264+
'test_user',
265+
undefined,
266+
456
267+
);
268+
269+
const overrideTraits = [
270+
new TraitModel('email', '[email protected]'),
271+
new TraitModel('premium', true)
272+
];
273+
274+
// When
275+
const context = getEvaluationContext(testEnvironment, identity, overrideTraits);
276+
277+
// Then
278+
expect(context.identity?.traits).toEqual({
279+
280+
premium: true
281+
});
282+
});
283+
284+
test('produces evaluation context without identity when isEnvironmentEvaluation is true', () => {
285+
// Given
286+
const identity = new IdentityModel(
287+
'2024-01-01T00:00:00Z',
288+
[new TraitModel('test', 'value')],
289+
[],
290+
'B62qaMZNwfiqT76p38ggrQ',
291+
'test_user',
292+
undefined,
293+
789
294+
);
295+
296+
// When
297+
const context = getEvaluationContext(testEnvironment, identity, undefined, true);
298+
299+
// Then
300+
expect(context.identity).toBeUndefined();
301+
expect(context.environment).toBeDefined();
302+
expect(context.features).toBeDefined();
303+
expect(context.segments).toBeDefined();
304+
});
305+
306+
test('handles identity without django_id', () => {
307+
// Given
308+
const identity = new IdentityModel(
309+
'2024-01-01T00:00:00Z',
310+
[new TraitModel('name', 'John')],
311+
[],
312+
'B62qaMZNwfiqT76p38ggrQ',
313+
'john_doe',
314+
undefined,
315+
undefined
316+
);
317+
318+
// When
319+
const context = getEvaluationContext(testEnvironment, identity);
320+
321+
// Then
322+
expect(context.identity?.identifier).toBe('john_doe');
323+
expect(context.identity?.key).toBeUndefined();
324+
expect(context.identity?.traits).toEqual({ name: 'John' });
325+
});
326+
327+
test('maps segment override priorities correctly', () => {
328+
// When - using fixture which has segment with priority
329+
const context = getEvaluationContext(testEnvironment);
330+
331+
// Then - verify regular_segment has a feature override
332+
const segment = context.segments!['1'];
333+
expect(segment.overrides?.length).toBeGreaterThan(0);
334+
335+
// The segment override from the fixture has no explicit priority, should be undefined
336+
const segmentOverride = segment.overrides?.[0]!;
337+
expect(segmentOverride.name).toBe('some_feature');
338+
expect(segmentOverride.priority).toBeUndefined();
339+
});
340+
341+
test('handles multiple identity overrides with same features', () => {
342+
// Given - the fixture already has identity override with 'overridden-id'
343+
// Verify it's mapped correctly
344+
const context = getEvaluationContext(testEnvironment);
345+
346+
// Then
347+
const overrideSegments = Object.values(context.segments!).filter(
348+
s => s.name === IDENTITY_OVERRIDE_SEGMENT_NAME
349+
);
350+
351+
// The fixture has one identity override
352+
expect(overrideSegments.length).toBe(1);
353+
expect(overrideSegments[0].rules?.[0].conditions?.[0].value).toContain('overridden-id');
354+
expect(overrideSegments[0].overrides?.length).toBe(1);
355+
});
356+
});

tests/sdk/data/environment.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"api_key": "B62qaMZNwfiqT76p38ggrQ",
3+
"name": "Test environment",
34
"project": {
45
"name": "Test project",
56
"organisation": {

0 commit comments

Comments
 (0)