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
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();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { FallbackTreatment } from '../../../../types/splitio';
import { FallbackTreatment, Treatment, TreatmentWithConfig } from '../../../../types/splitio';
import { ILogger } from '../../../logger/types';
import { isObject, isString } from '../../../utils/lang';
import { FallbackDiscardReason } from '../constants';


Expand All @@ -10,51 +12,43 @@ export class FallbacksSanitizer {
return name.length <= 100 && !name.includes(' ');
}

private static isValidTreatment(t?: FallbackTreatment): boolean {
if (!t) {
return false;
}

if (typeof t === 'string') {
if (t.length > 100) {
return false;
}
return FallbacksSanitizer.pattern.test(t);
}
private static isValidTreatment(t?: Treatment | FallbackTreatment): boolean {
const treatment = isObject(t) ? (t as TreatmentWithConfig).treatment : t;

const { treatment } = t;
if (!treatment || treatment.length > 100) {
if (!isString(treatment) || treatment.length > 100) {
return false;
}
return FallbacksSanitizer.pattern.test(treatment);
}

static sanitizeGlobal(treatment?: FallbackTreatment): FallbackTreatment | undefined {
static sanitizeGlobal(logger: ILogger, treatment?: string | FallbackTreatment): string | FallbackTreatment | undefined {
if (!this.isValidTreatment(treatment)) {
console.error(
logger.error(
`Fallback treatments - Discarded fallback: ${FallbackDiscardReason.Treatment}`
);
return undefined;
}
return treatment!;
return treatment;
}

static sanitizeByFlag(
byFlagFallbacks: Record<string, FallbackTreatment>
): Record<string, FallbackTreatment> {
const sanitizedByFlag: Record<string, FallbackTreatment> = {};

const entries = Object.entries(byFlagFallbacks);
entries.forEach(([flag, t]) => {
logger: ILogger,
byFlagFallbacks: Record<string, string | FallbackTreatment>
): Record<string, string | FallbackTreatment> {
const sanitizedByFlag: Record<string, string | FallbackTreatment> = {};

const entries = Object.keys(byFlagFallbacks);
entries.forEach((flag) => {
const t = byFlagFallbacks[flag];
if (!this.isValidFlagName(flag)) {
console.error(
logger.error(
`Fallback treatments - Discarded flag '${flag}': ${FallbackDiscardReason.FlagName}`
);
return;
}

if (!this.isValidTreatment(t)) {
console.error(
logger.error(
`Fallback treatments - Discarded treatment for flag '${flag}': ${FallbackDiscardReason.Treatment}`
);
return;
Expand Down
36 changes: 24 additions & 12 deletions src/evaluator/fallbackTreatmentsCalculator/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import { FallbackTreatmentConfiguration, FallbackTreatment, IFallbackTreatmentsCalculator} from '../../../types/splitio';
import { FallbackTreatmentConfiguration, FallbackTreatment } from '../../../types/splitio';
import { FallbacksSanitizer } from './fallbackSanitizer';
import { CONTROL } from '../../utils/constants';
import { isString } from '../../utils/lang';
import { ILogger } from '../../logger/types';

export type IFallbackTreatmentsCalculator = {
resolve(flagName: string, label?: string): FallbackTreatment & { label?: string };
}

export class FallbackTreatmentsCalculator implements IFallbackTreatmentsCalculator {
private readonly labelPrefix = 'fallback - ';
private readonly control = 'CONTROL';
private readonly fallbacks: FallbackTreatmentConfiguration;

constructor(fallbacks: FallbackTreatmentConfiguration) {
this.fallbacks = fallbacks;
constructor(logger: ILogger, fallbacks?: FallbackTreatmentConfiguration) {
const sanitizedGlobal = fallbacks?.global ? FallbacksSanitizer.sanitizeGlobal(logger, fallbacks.global) : undefined;
const sanitizedByFlag = fallbacks?.byFlag ? FallbacksSanitizer.sanitizeByFlag(logger, fallbacks.byFlag) : {};
this.fallbacks = {
global: sanitizedGlobal,
byFlag: sanitizedByFlag
};
}

resolve(flagName: string, label?: string | undefined): FallbackTreatment {
const treatment = this.fallbacks.byFlag[flagName];
resolve(flagName: string, label?: string): FallbackTreatment & { label?: string } {
const treatment = this.fallbacks.byFlag?.[flagName];
if (treatment) {
return this.copyWithLabel(treatment, label);
}
Expand All @@ -20,14 +32,14 @@ export class FallbackTreatmentsCalculator implements IFallbackTreatmentsCalculat
}

return {
treatment: this.control,
treatment: CONTROL,
config: null,
label: this.resolveLabel(label),
label,
};
}

private copyWithLabel(fallback: FallbackTreatment, label: string | undefined): FallbackTreatment {
if (typeof fallback === 'string') {
private copyWithLabel(fallback: string | FallbackTreatment, label?: string): FallbackTreatment & { label: string } {
if (isString(fallback)) {
return {
treatment: fallback,
config: null,
Expand All @@ -42,8 +54,8 @@ export class FallbackTreatmentsCalculator implements IFallbackTreatmentsCalculat
};
}

private resolveLabel(label?: string | undefined): string | undefined {
return label ? `${this.labelPrefix}${label}` : undefined;
private resolveLabel(label?: string): string {
return label ? `${this.labelPrefix}${label}` : '';
}

}
5 changes: 4 additions & 1 deletion src/sdkClient/__tests__/clientInputValidation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { clientInputValidationDecorator } from '../clientInputValidation';
// Mocks
import { DebugLogger } from '../../logger/browser/DebugLogger';
import { createClientMock } from './testUtils';
import { FallbackTreatmentsCalculator, IFallbackTreatmentsCalculator } from '../../evaluator/fallbackTreatmentsCalculator';

const settings: any = {
log: DebugLogger(),
Expand All @@ -13,13 +14,15 @@ const settings: any = {
const EVALUATION_RESULT = 'on';
const client: any = createClientMock(EVALUATION_RESULT);

const fallbackTreatmentsCalculator: IFallbackTreatmentsCalculator = new FallbackTreatmentsCalculator(settings);

const readinessManager: any = {
isReady: () => true,
isDestroyed: () => false
};

describe('clientInputValidationDecorator', () => {
const clientWithValidation = clientInputValidationDecorator(settings, client, readinessManager);
const clientWithValidation = clientInputValidationDecorator(settings, client, readinessManager, fallbackTreatmentsCalculator);
const logSpy = jest.spyOn(console, 'log');

beforeEach(() => {
Expand Down
Loading