flagsmith-nodejs
Version:
Flagsmith lets you manage features flags and remote config across web, mobile and server side applications. Deliver true Continuous Integration. Get builds out faster. Control who has access to new features.
354 lines (295 loc) • 13.9 kB
text/typescript
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'
);
// When
const context = getEvaluationContext(testEnvironment, identity);
// Then
expect(context.identity).toBeDefined();
expect(context.identity?.identifier).toBe('test_user');
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);
});
});