Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion flagsmith-engine/environments/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ export class EnvironmentModel {
project: ProjectModel;
featureStates: FeatureStateModel[] = [];
identityOverrides: IdentityModel[] = [];
name: string;

constructor(id: number, apiKey: string, project: ProjectModel) {
constructor(id: number, apiKey: string, project: ProjectModel, name: string) {
this.id = id;
this.apiKey = apiKey;
this.project = project;
this.name = name;
}
}
3 changes: 2 additions & 1 deletion flagsmith-engine/environments/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export function buildEnvironmentModel(environmentJSON: any) {
const environmentModel = new EnvironmentModel(
environmentJSON.id,
environmentJSON.api_key,
project
project,
environmentJSON.name
);
environmentModel.featureStates = featureStates;
if (!!environmentJSON.identity_overrides) {
Expand Down
2 changes: 1 addition & 1 deletion flagsmith-engine/evaluation/evaluationContext/mappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function mapEnvironmentModelToEvaluationContext(
): GenericEvaluationContext {
const environmentContext: EnvironmentContext = {
key: environment.apiKey,
name: environment.project.name
name: environment.name
};

const features: FeaturesWithMetadata<SDKFeatureMetadata> = {};
Expand Down
356 changes: 356 additions & 0 deletions tests/engine/unit/mappers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
import { test, expect, describe } from 'vitest';
import { getEvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/mappers.js';
import { buildEnvironmentModel } from '../../../flagsmith-engine/environments/util.js';
import { EnvironmentModel } from '../../../flagsmith-engine/environments/models.js';
import { IdentityModel } from '../../../flagsmith-engine/identities/models.js';
import { TraitModel } from '../../../flagsmith-engine/identities/traits/models.js';
import { FeatureModel, FeatureStateModel } from '../../../flagsmith-engine/features/models.js';
import {
MultivariateFeatureOptionModel,
MultivariateFeatureStateValueModel
} from '../../../flagsmith-engine/features/models.js';
import { CONSTANTS } from '../../../flagsmith-engine/features/constants.js';
import { readFileSync } from 'fs';
import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../../../flagsmith-engine/segments/constants.js';
import { SegmentSource } from '../../../flagsmith-engine/evaluation/models.js';

const DATA_DIR = __dirname + '/../../sdk/data/';

describe('getEvaluationContext', () => {
const environmentJSON = JSON.parse(readFileSync(DATA_DIR + 'environment.json', 'utf-8'));
const testEnvironment = buildEnvironmentModel(environmentJSON);

test('produces evaluation context from environment document', () => {
// When
const context = getEvaluationContext(testEnvironment);

// Then - verify environment
expect(context).toBeDefined();
expect(context.environment?.key).toBe('B62qaMZNwfiqT76p38ggrQ');
expect(context.environment?.name).toBe('Test environment');
expect(context.identity).toBeUndefined();

// Verify segments
expect(context.segments).toBeDefined();
expect(context.segments).toHaveProperty('1');

const segment = context.segments!['1'];
expect(segment.key).toBe('1');
expect(segment.name).toBe('regular_segment');
expect(segment.rules.length).toBe(1);
expect(segment.overrides).toBeDefined();
expect(Array.isArray(segment.overrides)).toBe(true);
expect(segment.metadata?.source).toBe(SegmentSource.API);
expect(segment.metadata?.id).toBe(1);

// Verify segment rules
expect(segment.rules[0].type).toBe('ALL');
expect(segment.rules[0].conditions).toEqual([]);
expect(segment.rules[0].rules?.length).toBe(1);

const nestedRule = segment.rules[0].rules?.[0]!;
expect(nestedRule.type).toBe('ANY');
expect(nestedRule.conditions?.length).toBe(1);
expect(nestedRule.rules?.length).toEqual(0);

const condition = nestedRule.conditions?.[0]!;
expect(condition.property).toBe('age');
expect(condition.operator).toBe('LESS_THAN');
expect(condition.value).toBe('40');

// Verify identity override segment
const identityOverrideSegment = Object.values(context.segments!).find(
s => s.name === IDENTITY_OVERRIDE_SEGMENT_NAME
);
expect(identityOverrideSegment).toBeDefined();
expect(identityOverrideSegment!.name).toBe(IDENTITY_OVERRIDE_SEGMENT_NAME);
expect(identityOverrideSegment!.rules.length).toBe(1);
expect(identityOverrideSegment!.overrides?.length).toBe(1);

const overrideRule = identityOverrideSegment!.rules?.[0]!;
expect(overrideRule.type).toBe('ALL');
expect(overrideRule.conditions?.length).toBe(1);

const overrideCondition = overrideRule.conditions?.[0]!;
expect(overrideCondition.property).toBe('$.identity.identifier');
expect(overrideCondition.operator).toBe('IN');
expect(overrideCondition.value).toContain('overridden-id');

const override = identityOverrideSegment!.overrides?.[0]!;
expect(override.name).toBe('some_feature');
expect(override.enabled).toBe(false);
expect(override.value).toBe('some-overridden-value');
expect(override.priority).toBe(-Infinity);
expect(override.metadata?.id).toBe(1);

// Verify features
expect(context.features).toBeDefined();
expect(context.features).toHaveProperty('some_feature');

const someFeature = context.features!['some_feature'];
expect(someFeature.name).toBe('some_feature');
expect(someFeature.enabled).toBe(true);
expect(someFeature.value).toBe('some-value');
expect(someFeature.priority).toBeUndefined();
expect(someFeature.metadata?.id).toBe(1);

// Verify multivariate feature
expect(context.features).toHaveProperty('mv_feature');
const mvFeature = context.features!['mv_feature'];
expect(mvFeature.name).toBe('mv_feature');
expect(mvFeature.enabled).toBe(false);
expect(mvFeature.value).toBe('foo');
expect(mvFeature.priority).toBeUndefined();
expect(mvFeature.variants?.length).toBe(1);

const variant = mvFeature.variants![0];
expect(variant.value).toBe('bar');
expect(variant.weight).toBe(100);
expect(variant.priority).toBe(1);
});

test('maps multivariate features with multiple variants correctly', () => {
// Given
const mvOption1 = new MultivariateFeatureOptionModel('variant_a', 100);
const mvOption2 = new MultivariateFeatureOptionModel('variant_b', 200);
const mvOption3 = new MultivariateFeatureOptionModel('variant_c', 150);

const mvValue1 = new MultivariateFeatureStateValueModel(
mvOption1,
30,
100,
'00000000-0000-0000-0000-000000000001'
);

const mvValue2 = new MultivariateFeatureStateValueModel(
mvOption2,
50,
200,
'00000000-0000-0000-0000-000000000002'
);

const mvValue3 = new MultivariateFeatureStateValueModel(
mvOption3,
20,
150,
'00000000-0000-0000-0000-000000000003'
);

const feature = new FeatureModel(999, 'multi_variant_feature', CONSTANTS.MULTIVARIATE);
const featureState = new FeatureStateModel(feature, true, 999);
featureState.setValue('control');
featureState.multivariateFeatureStateValues = [mvValue1, mvValue2, mvValue3];

const envWithMv = new EnvironmentModel(1, 'test_key', testEnvironment.project, 'Test Env');
envWithMv.featureStates = [featureState];

// When
const context = getEvaluationContext(envWithMv);

// Then
const featureContext = context.features!['multi_variant_feature'];
expect(featureContext.variants?.length).toBe(3);

expect(featureContext.variants![0].value).toBe('variant_a');
expect(featureContext.variants![0].weight).toBe(30);
expect(featureContext.variants![0].priority).toBe(100);

expect(featureContext.variants![1].value).toBe('variant_b');
expect(featureContext.variants![1].weight).toBe(50);
expect(featureContext.variants![1].priority).toBe(200);

expect(featureContext.variants![2].value).toBe('variant_c');
expect(featureContext.variants![2].weight).toBe(20);
expect(featureContext.variants![2].priority).toBe(150);
});

test('handles multivariate features without IDs using UUID', () => {
// Given
const mvOption1 = new MultivariateFeatureOptionModel('option_x', undefined);
const mvOption2 = new MultivariateFeatureOptionModel('option_y', undefined);

const mvValue1 = new MultivariateFeatureStateValueModel(
mvOption1,
60,
undefined as any,
'aaaaaaaa-bbbb-cccc-dddd-000000000001'
);

const mvValue2 = new MultivariateFeatureStateValueModel(
mvOption2,
40,
undefined as any,
'aaaaaaaa-bbbb-cccc-dddd-000000000002'
);

const feature = new FeatureModel(888, 'uuid_variant_feature', CONSTANTS.MULTIVARIATE);
const featureState = new FeatureStateModel(feature, true, 888);
featureState.setValue('default');
featureState.multivariateFeatureStateValues = [mvValue1, mvValue2];

const envWithUuid = new EnvironmentModel(
1,
'test_key',
testEnvironment.project,
'Test Env'
);
envWithUuid.featureStates = [featureState];

// When
const context = getEvaluationContext(envWithUuid);

// Then
const featureContext = context.features!['uuid_variant_feature'];
expect(featureContext.variants?.length).toBe(2);

// When using UUID-based priorities, they become bigints
expect(
typeof featureContext.variants![0].priority === 'number' ||
typeof featureContext.variants![0].priority === 'bigint'
).toBe(true);
expect(
typeof featureContext.variants![1].priority === 'number' ||
typeof featureContext.variants![1].priority === 'bigint'
).toBe(true);
expect(featureContext.variants![0].priority).not.toBe(featureContext.variants![1].priority);
});

test('handles environment with no features', () => {
// Given - create a copy with no features
const emptyEnvJSON = { ...environmentJSON, feature_states: [] };
const emptyEnv = buildEnvironmentModel(emptyEnvJSON);

// When
const context = getEvaluationContext(emptyEnv);

// Then
expect(context.features).toEqual({});
expect(context.environment?.key).toBe('B62qaMZNwfiqT76p38ggrQ');
expect(context.environment?.name).toBe('Test environment');
});

test('produces evaluation context with identity', () => {
// Given
const identity = new IdentityModel(
'2024-01-01T00:00:00Z',
[new TraitModel('email', 'test@example.com'), new TraitModel('age', 25)],
[],
'B62qaMZNwfiqT76p38ggrQ',
'test_user',
undefined,
123
);

// When
const context = getEvaluationContext(testEnvironment, identity);

// Then
expect(context.identity).toBeDefined();
expect(context.identity?.identifier).toBe('test_user');
expect(context.identity?.key).toBe('123');
expect(context.identity?.traits).toEqual({
email: 'test@example.com',
age: 25
});
});

test('produces evaluation context with override traits', () => {
// Given
const identity = new IdentityModel(
'2024-01-01T00:00:00Z',
[new TraitModel('email', 'original@example.com')],
[],
'B62qaMZNwfiqT76p38ggrQ',
'test_user',
undefined,
456
);

const overrideTraits = [
new TraitModel('email', 'override@example.com'),
new TraitModel('premium', true)
];

// When
const context = getEvaluationContext(testEnvironment, identity, overrideTraits);

// Then
expect(context.identity?.traits).toEqual({
email: 'override@example.com',
premium: true
});
});

test('produces evaluation context without identity when isEnvironmentEvaluation is true', () => {
// Given
const identity = new IdentityModel(
'2024-01-01T00:00:00Z',
[new TraitModel('test', 'value')],
[],
'B62qaMZNwfiqT76p38ggrQ',
'test_user',
undefined,
789
);

// When
const context = getEvaluationContext(testEnvironment, identity, undefined, true);

// Then
expect(context.identity).toBeUndefined();
expect(context.environment).toBeDefined();
expect(context.features).toBeDefined();
expect(context.segments).toBeDefined();
});

test('handles identity without django_id', () => {
// Given
const identity = new IdentityModel(
'2024-01-01T00:00:00Z',
[new TraitModel('name', 'John')],
[],
'B62qaMZNwfiqT76p38ggrQ',
'john_doe',
undefined,
undefined
);

// When
const context = getEvaluationContext(testEnvironment, identity);

// Then
expect(context.identity?.identifier).toBe('john_doe');
expect(context.identity?.key).toBeUndefined();
expect(context.identity?.traits).toEqual({ name: 'John' });
});

test('maps segment override priorities correctly', () => {
// When - using fixture which has segment with priority
const context = getEvaluationContext(testEnvironment);

// Then - verify regular_segment has a feature override
const segment = context.segments!['1'];
expect(segment.overrides?.length).toBeGreaterThan(0);

// The segment override from the fixture has no explicit priority, should be undefined
const segmentOverride = segment.overrides?.[0]!;
expect(segmentOverride.name).toBe('some_feature');
expect(segmentOverride.priority).toBeUndefined();
});

test('handles multiple identity overrides with same features', () => {
// Given - the fixture already has identity override with 'overridden-id'
// Verify it's mapped correctly
const context = getEvaluationContext(testEnvironment);

// Then
const overrideSegments = Object.values(context.segments!).filter(
s => s.name === IDENTITY_OVERRIDE_SEGMENT_NAME
);

// The fixture has one identity override
expect(overrideSegments.length).toBe(1);
expect(overrideSegments[0].rules?.[0].conditions?.[0].value).toContain('overridden-id');
expect(overrideSegments[0].overrides?.length).toBe(1);
});
});
1 change: 1 addition & 0 deletions tests/sdk/data/environment.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"api_key": "B62qaMZNwfiqT76p38ggrQ",
"name": "Test environment",
"project": {
"name": "Test project",
"organisation": {
Expand Down