Skip to content
Closed
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
5 changes: 3 additions & 2 deletions src/evaluator/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { IEvaluation, IEvaluationResult, ISplitEvaluator } from './types';
import { ILogger } from '../logger/types';
import { ENGINE_DEFAULT } from '../logger/constants';
import { prerequisitesMatcherContext } from './matchers/prerequisites';
import { FallbackTreatmentsCalculator } from './fallbackTreatmentsCalculator';

function evaluationResult(result: IEvaluation | undefined, defaultTreatment: string): IEvaluationResult {
return {
Expand All @@ -19,13 +20,13 @@ function evaluationResult(result: IEvaluation | undefined, defaultTreatment: str
};
}

export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync | IStorageAsync) {
export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync | IStorageAsync, fallbackTreatmentsCalculator: FallbackTreatmentsCalculator) {
const { killed, seed, trafficAllocation, trafficAllocationSeed, status, conditions, prerequisites } = split;

const defaultTreatment = isString(split.defaultTreatment) ? split.defaultTreatment : CONTROL;

const evaluator = parser(log, conditions, storage);
const prerequisiteMatcher = prerequisitesMatcherContext(prerequisites, storage, log);
const prerequisiteMatcher = prerequisitesMatcherContext(prerequisites, storage, log, fallbackTreatmentsCalculator);

return {

Expand Down
13 changes: 13 additions & 0 deletions src/evaluator/__tests__/evaluate-feature.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { evaluateFeature } from '../index';
import { EXCEPTION, NOT_IN_SPLIT, SPLIT_ARCHIVED, SPLIT_KILLED, SPLIT_NOT_FOUND } from '../../utils/labels';
import { loggerMock } from '../../logger/__tests__/sdkLogger.mock';
import { FallbackTreatmentsCalculator } from '../fallbackTreatmentsCalculator';

const splitsMock = {
regular: { 'changeNumber': 1487277320548, 'trafficAllocationSeed': 1667452163, 'trafficAllocation': 100, 'trafficTypeName': 'user', 'name': 'always-on', 'seed': 1684183541, 'configurations': {}, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': '' }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': { 'segmentName': '' }, 'unaryNumericMatcherData': { 'dataType': '', 'value': 0 }, 'whitelistMatcherData': { 'whitelist': null }, 'betweenMatcherData': { 'dataType': '', 'start': 0, 'end': 0 } }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }] },
Expand All @@ -25,6 +26,8 @@ const mockStorage = {
}
};

const fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator();

test('EVALUATOR / should return label exception, treatment control and config null on error', async () => {
const expectedOutput = {
treatment: 'control',
Expand All @@ -37,6 +40,7 @@ test('EVALUATOR / should return label exception, treatment control and config nu
'throw_exception',
null,
mockStorage,
fallbackTreatmentsCalculator
);

// This validation is async because the only exception possible when retrieving a Split would happen with Async storages.
Expand All @@ -61,6 +65,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
'config',
null,
mockStorage,
fallbackTreatmentsCalculator
);
expect(evaluationWithConfig).toEqual(expectedOutput); // If the split is retrieved successfully we should get the right evaluation result, label and config.

Expand All @@ -70,6 +75,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
'not_existent_split',
null,
mockStorage,
fallbackTreatmentsCalculator
);
expect(evaluationNotFound).toEqual(expectedOutputControl); // If the split is not retrieved successfully because it does not exist, we should get the right evaluation result, label and config.

Expand All @@ -79,6 +85,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
'regular',
null,
mockStorage,
fallbackTreatmentsCalculator
);
expect(evaluation).toEqual({ ...expectedOutput, config: null }); // If the split is retrieved successfully we should get the right evaluation result, label and config. If Split has no config it should have config equal null.

Expand All @@ -88,6 +95,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
'killed',
null,
mockStorage,
fallbackTreatmentsCalculator
);
expect(evaluationKilled).toEqual({ ...expectedOutput, treatment: 'off', config: null, label: SPLIT_KILLED });
// If the split is retrieved but is killed, we should get the right evaluation result, label and config.
Expand All @@ -98,6 +106,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
'archived',
null,
mockStorage,
fallbackTreatmentsCalculator
);
expect(evaluationArchived).toEqual({ ...expectedOutput, treatment: 'control', label: SPLIT_ARCHIVED, config: null });
// If the split is retrieved but is archived, we should get the right evaluation result, label and config.
Expand All @@ -108,6 +117,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
'trafficAlocation1',
null,
mockStorage,
fallbackTreatmentsCalculator
);
expect(evaluationtrafficAlocation1).toEqual({ ...expectedOutput, label: NOT_IN_SPLIT, config: null, treatment: 'off' });
// If the split is retrieved but is not in split (out of Traffic Allocation), we should get the right evaluation result, label and config.
Expand All @@ -118,6 +128,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
'killedWithConfig',
null,
mockStorage,
fallbackTreatmentsCalculator
);
expect(evaluationKilledWithConfig).toEqual({ ...expectedOutput, treatment: 'off', label: SPLIT_KILLED });
// If the split is retrieved but is killed, we should get the right evaluation result, label and config.
Expand All @@ -128,6 +139,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
'archivedWithConfig',
null,
mockStorage,
fallbackTreatmentsCalculator
);
expect(evaluationArchivedWithConfig).toEqual({ ...expectedOutput, treatment: 'control', label: SPLIT_ARCHIVED, config: null });
// If the split is retrieved but is archived, we should get the right evaluation result, label and config.
Expand All @@ -138,6 +150,7 @@ test('EVALUATOR / should return right label, treatment and config if storage ret
'trafficAlocation1WithConfig',
null,
mockStorage,
fallbackTreatmentsCalculator
);
expect(evaluationtrafficAlocation1WithConfig).toEqual({ ...expectedOutput, label: NOT_IN_SPLIT, treatment: 'off' });
// If the split is retrieved but is not in split (out of Traffic Allocation), we should get the right evaluation result, label and config.
Expand Down
7 changes: 6 additions & 1 deletion src/evaluator/__tests__/evaluate-features.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { evaluateFeatures, evaluateFeaturesByFlagSets } from '../index';
import { EXCEPTION, NOT_IN_SPLIT, SPLIT_ARCHIVED, SPLIT_KILLED, SPLIT_NOT_FOUND } from '../../utils/labels';
import { loggerMock } from '../../logger/__tests__/sdkLogger.mock';
import { WARN_FLAGSET_WITHOUT_FLAGS } from '../../logger/constants';
import { FallbackTreatmentsCalculator } from '../fallbackTreatmentsCalculator';

const splitsMock = {
regular: { 'changeNumber': 1487277320548, 'trafficAllocationSeed': 1667452163, 'trafficAllocation': 100, 'trafficTypeName': 'user', 'name': 'always-on', 'seed': 1684183541, 'configurations': {}, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': '' }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': { 'segmentName': '' }, 'unaryNumericMatcherData': { 'dataType': '', 'value': 0 }, 'whitelistMatcherData': { 'whitelist': null }, 'betweenMatcherData': { 'dataType': '', 'start': 0, 'end': 0 } }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }] },
Expand Down Expand Up @@ -42,6 +43,8 @@ const mockStorage = {
}
};

const fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator({});

test('EVALUATOR - Multiple evaluations at once / should return label exception, treatment control and config null on error', async () => {
const expectedOutput = {
throw_exception: {
Expand Down Expand Up @@ -82,6 +85,7 @@ test('EVALUATOR - Multiple evaluations at once / should return right labels, tre
['config', 'not_existent_split', 'regular', 'killed', 'archived', 'trafficAlocation1', 'killedWithConfig', 'archivedWithConfig', 'trafficAlocation1WithConfig'],
null,
mockStorage,
fallbackTreatmentsCalculator
);
// assert evaluationWithConfig
expect(multipleEvaluationAtOnce['config']).toEqual(expectedOutput['config']); // If the split is retrieved successfully we should get the right evaluation result, label and config.
Expand Down Expand Up @@ -134,7 +138,8 @@ describe('EVALUATOR - Multiple evaluations at once by flag sets', () => {
flagSets,
null,
storage,
'method-name'
'method-name',
fallbackTreatmentsCalculator
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,99 @@
import { FallbackTreatmentsCalculator } from '../';
import type { FallbackTreatmentConfiguration } from '../../../../types/splitio'; // adjust path if needed
import type { FallbackTreatmentConfiguration } from '../../../../types/splitio';
import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock';
import { CONTROL } from '../../../utils/constants';

describe('FallbackTreatmentsCalculator', () => {
describe('FallbackTreatmentsCalculator' , () => {
const longName = 'a'.repeat(101);

test('logs an error if flag name is invalid - by Flag', () => {
let config: FallbackTreatmentConfiguration = {
byFlag: {
'feature A': { treatment: 'TREATMENT_A', config: '{ value: 1 }' },
},
};
new FallbackTreatmentsCalculator(loggerMock, config);
expect(loggerMock.error.mock.calls[0][0]).toBe(
'Fallback treatments - Discarded flag \'feature A\': Invalid flag name (max 100 chars, no spaces)'
);
config = {
byFlag: {
[longName]: { treatment: 'TREATMENT_A', config: '{ value: 1 }' },
},
};
new FallbackTreatmentsCalculator(loggerMock, config);
expect(loggerMock.error.mock.calls[1][0]).toBe(
`Fallback treatments - Discarded flag '${longName}': Invalid flag name (max 100 chars, no spaces)`
);

config = {
byFlag: {
'featureB': { treatment: longName, config: '{ value: 1 }' },
},
};
new FallbackTreatmentsCalculator(loggerMock, config);
expect(loggerMock.error.mock.calls[2][0]).toBe(
'Fallback treatments - Discarded treatment for flag \'featureB\': Invalid treatment (max 100 chars and must match pattern)'
);

config = {
byFlag: {
// @ts-ignore
'featureC': { config: '{ global: true }' },
},
};
new FallbackTreatmentsCalculator(loggerMock, config);
expect(loggerMock.error.mock.calls[3][0]).toBe(
'Fallback treatments - Discarded treatment for flag \'featureC\': Invalid treatment (max 100 chars and must match pattern)'
);

config = {
byFlag: {
// @ts-ignore
'featureC': { treatment: 'invalid treatment!', config: '{ global: true }' },
},
};
new FallbackTreatmentsCalculator(loggerMock, config);
expect(loggerMock.error.mock.calls[4][0]).toBe(
'Fallback treatments - Discarded treatment for flag \'featureC\': Invalid treatment (max 100 chars and must match pattern)'
);
});

test('logs an error if flag name is invalid - global', () => {
let config: FallbackTreatmentConfiguration = {
global: { treatment: longName, config: '{ value: 1 }' },
};
new FallbackTreatmentsCalculator(loggerMock, config);
expect(loggerMock.error.mock.calls[2][0]).toBe(
'Fallback treatments - Discarded treatment for flag \'featureB\': Invalid treatment (max 100 chars and must match pattern)'
);

config = {
// @ts-ignore
global: { config: '{ global: true }' },
};
new FallbackTreatmentsCalculator(loggerMock, config);
expect(loggerMock.error.mock.calls[3][0]).toBe(
'Fallback treatments - Discarded treatment for flag \'featureC\': Invalid treatment (max 100 chars and must match pattern)'
);

config = {
// @ts-ignore
global: { treatment: 'invalid treatment!', config: '{ global: true }' },
};
new FallbackTreatmentsCalculator(loggerMock, config);
expect(loggerMock.error.mock.calls[4][0]).toBe(
'Fallback treatments - Discarded treatment for flag \'featureC\': Invalid treatment (max 100 chars and must match pattern)'
);
});

test('returns specific fallback if flag exists', () => {
const config: FallbackTreatmentConfiguration = {
byFlag: {
'featureA': { treatment: 'TREATMENT_A', config: '{ value: 1 }' },
},
};
const calculator = new FallbackTreatmentsCalculator(config);
const calculator = new FallbackTreatmentsCalculator(loggerMock, config);
const result = calculator.resolve('featureA', 'label by flag');

expect(result).toEqual({
Expand All @@ -24,7 +108,7 @@ describe('FallbackTreatmentsCalculator', () => {
byFlag: {},
global: { treatment: 'GLOBAL_TREATMENT', config: '{ global: true }' },
};
const calculator = new FallbackTreatmentsCalculator(config);
const calculator = new FallbackTreatmentsCalculator(loggerMock, config);
const result = calculator.resolve('missingFlag', 'label by global');

expect(result).toEqual({
Expand All @@ -38,29 +122,29 @@ describe('FallbackTreatmentsCalculator', () => {
const config: FallbackTreatmentConfiguration = {
byFlag: {},
};
const calculator = new FallbackTreatmentsCalculator(config);
const calculator = new FallbackTreatmentsCalculator(loggerMock, config);
const result = calculator.resolve('missingFlag', 'label by noFallback');

expect(result).toEqual({
treatment: 'CONTROL',
treatment: CONTROL,
config: null,
label: 'fallback - label by noFallback',
label: 'label by noFallback',
});
});

test('returns undefined label if no label provided', () => {
const config: FallbackTreatmentConfiguration = {
byFlag: {
'featureB': { treatment: 'TREATMENT_B' },
'featureB': { treatment: 'TREATMENT_B', config: '{ value: 1 }' },
},
};
const calculator = new FallbackTreatmentsCalculator(config);
const calculator = new FallbackTreatmentsCalculator(loggerMock, config);
const result = calculator.resolve('featureB');

expect(result).toEqual({
treatment: 'TREATMENT_B',
config: undefined,
label: undefined,
config: '{ value: 1 }',
label: '',
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FallbacksSanitizer } from '../fallbackSanitizer';
import { TreatmentWithConfig } from '../../../../types/splitio';
import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock';

describe('FallbacksSanitizer', () => {
const validTreatment: TreatmentWithConfig = { treatment: 'on', config: '{"color":"blue"}' };
Expand All @@ -10,7 +11,7 @@ describe('FallbacksSanitizer', () => {
});

afterEach(() => {
(console.error as jest.Mock).mockRestore();
(loggerMock.error as jest.Mock).mockRestore();
});

describe('isValidFlagName', () => {
Expand Down Expand Up @@ -52,14 +53,14 @@ describe('FallbacksSanitizer', () => {

describe('sanitizeGlobal', () => {
test('returns the treatment if valid', () => {
expect(FallbacksSanitizer.sanitizeGlobal(validTreatment)).toEqual(validTreatment);
expect(console.error).not.toHaveBeenCalled();
expect(FallbacksSanitizer.sanitizeGlobal(loggerMock, validTreatment)).toEqual(validTreatment);
expect(loggerMock.error).not.toHaveBeenCalled();
});

test('returns undefined and logs error if invalid', () => {
const result = FallbacksSanitizer.sanitizeGlobal(invalidTreatment);
const result = FallbacksSanitizer.sanitizeGlobal(loggerMock, invalidTreatment);
expect(result).toBeUndefined();
expect(console.error).toHaveBeenCalledWith(
expect(loggerMock.error).toHaveBeenCalledWith(
expect.stringContaining('Fallback treatments - Discarded fallback')
);
});
Expand All @@ -73,20 +74,20 @@ describe('FallbacksSanitizer', () => {
bad_treatment: invalidTreatment,
};

const result = FallbacksSanitizer.sanitizeByFlag(input);
const result = FallbacksSanitizer.sanitizeByFlag(loggerMock, input);

expect(result).toEqual({ valid_flag: validTreatment });
expect(console.error).toHaveBeenCalledTimes(2); // invalid flag + bad_treatment
expect(loggerMock.error).toHaveBeenCalledTimes(2); // invalid flag + bad_treatment
});

test('returns empty object if all invalid', () => {
const input = {
'invalid flag': invalidTreatment,
};

const result = FallbacksSanitizer.sanitizeByFlag(input);
const result = FallbacksSanitizer.sanitizeByFlag(loggerMock, input);
expect(result).toEqual({});
expect(console.error).toHaveBeenCalled();
expect(loggerMock.error).toHaveBeenCalled();
});

test('returns same object if all valid', () => {
Expand All @@ -95,9 +96,9 @@ describe('FallbacksSanitizer', () => {
flag_two: { treatment: 'valid_2', config: null },
};

const result = FallbacksSanitizer.sanitizeByFlag(input);
const result = FallbacksSanitizer.sanitizeByFlag(loggerMock, input);
expect(result).toEqual(input);
expect(console.error).not.toHaveBeenCalled();
expect(loggerMock.error).not.toHaveBeenCalled();
});
});
});
Loading