Skip to content

Commit 850b074

Browse files
Implement FallbackSanitizer and add fallback to config
1 parent 3434a91 commit 850b074

File tree

4 files changed

+183
-0
lines changed

4 files changed

+183
-0
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { FallbacksSanitizer } from '../fallbackSanitizer';
2+
import { TreatmentWithConfig } from '../../../../types/splitio';
3+
4+
describe('FallbacksSanitizer', () => {
5+
const validTreatment: TreatmentWithConfig = { treatment: 'on', config: '{"color":"blue"}' };
6+
const invalidTreatment: TreatmentWithConfig = { treatment: ' ', config: null };
7+
8+
beforeEach(() => {
9+
jest.spyOn(console, 'error').mockImplementation(() => {});
10+
});
11+
12+
afterEach(() => {
13+
(console.error as jest.Mock).mockRestore();
14+
});
15+
16+
describe('isValidFlagName', () => {
17+
it('returns true for a valid flag name', () => {
18+
// @ts-expect-private-access
19+
expect((FallbacksSanitizer as any).isValidFlagName('my_flag')).toBe(true);
20+
});
21+
22+
it('returns false for a name longer than 100 chars', () => {
23+
const longName = 'a'.repeat(101);
24+
expect((FallbacksSanitizer as any).isValidFlagName(longName)).toBe(false);
25+
});
26+
27+
it('returns false if the name contains spaces', () => {
28+
expect((FallbacksSanitizer as any).isValidFlagName('invalid flag')).toBe(false);
29+
});
30+
});
31+
32+
describe('isValidTreatment', () => {
33+
it('returns true for a valid treatment string', () => {
34+
expect((FallbacksSanitizer as any).isValidTreatment(validTreatment)).toBe(true);
35+
});
36+
37+
it('returns false for null or undefined', () => {
38+
expect((FallbacksSanitizer as any).isValidTreatment(null)).toBe(false);
39+
expect((FallbacksSanitizer as any).isValidTreatment(undefined)).toBe(false);
40+
});
41+
42+
it('returns false for a treatment longer than 100 chars', () => {
43+
const long = { treatment: 'a'.repeat(101) };
44+
expect((FallbacksSanitizer as any).isValidTreatment(long)).toBe(false);
45+
});
46+
47+
it('returns false if treatment does not match regex pattern', () => {
48+
const invalid = { treatment: 'invalid treatment!' };
49+
expect((FallbacksSanitizer as any).isValidTreatment(invalid)).toBe(false);
50+
});
51+
});
52+
53+
describe('sanitizeGlobal', () => {
54+
it('returns the treatment if valid', () => {
55+
expect(FallbacksSanitizer.sanitizeGlobal(validTreatment)).toEqual(validTreatment);
56+
expect(console.error).not.toHaveBeenCalled();
57+
});
58+
59+
it('returns undefined and logs error if invalid', () => {
60+
const result = FallbacksSanitizer.sanitizeGlobal(invalidTreatment);
61+
expect(result).toBeUndefined();
62+
expect(console.error).toHaveBeenCalledWith(
63+
expect.stringContaining('Fallback treatments - Discarded fallback')
64+
);
65+
});
66+
});
67+
68+
describe('sanitizeByFlag', () => {
69+
it('returns a sanitized map with valid entries only', () => {
70+
const input = {
71+
valid_flag: validTreatment,
72+
'invalid flag': validTreatment,
73+
bad_treatment: invalidTreatment,
74+
};
75+
76+
const result = FallbacksSanitizer.sanitizeByFlag(input);
77+
78+
expect(result).toEqual({ valid_flag: validTreatment });
79+
expect(console.error).toHaveBeenCalledTimes(2); // invalid flag + bad_treatment
80+
});
81+
82+
it('returns empty object if all invalid', () => {
83+
const input = {
84+
'invalid flag': invalidTreatment,
85+
};
86+
87+
const result = FallbacksSanitizer.sanitizeByFlag(input);
88+
expect(result).toEqual({});
89+
expect(console.error).toHaveBeenCalled();
90+
});
91+
92+
it('returns same object if all valid', () => {
93+
const input = {
94+
flag_one: validTreatment,
95+
flag_two: { treatment: 'valid_2', config: null },
96+
};
97+
98+
const result = FallbacksSanitizer.sanitizeByFlag(input);
99+
expect(result).toEqual(input);
100+
expect(console.error).not.toHaveBeenCalled();
101+
});
102+
});
103+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum FallbackDiscardReason {
2+
FlagName = 'Invalid flag name (max 100 chars, no spaces)',
3+
Treatment = 'Invalid treatment (max 100 chars and must match pattern)',
4+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { TreatmentWithConfig } from '../../../../types/splitio';
2+
import { FallbackDiscardReason } from '../constants';
3+
4+
5+
export class FallbacksSanitizer {
6+
7+
private static readonly pattern = /^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$/;
8+
9+
private static isValidFlagName(name: string): boolean {
10+
return name.length <= 100 && !name.includes(' ');
11+
}
12+
13+
private static isValidTreatment(t?: TreatmentWithConfig): boolean {
14+
if (!t || !t.treatment) {
15+
return false;
16+
}
17+
18+
const { treatment } = t;
19+
20+
if (treatment.length > 100) {
21+
return false;
22+
}
23+
24+
return FallbacksSanitizer.pattern.test(treatment);
25+
}
26+
27+
static sanitizeGlobal(treatment?: TreatmentWithConfig): TreatmentWithConfig | undefined {
28+
if (!this.isValidTreatment(treatment)) {
29+
console.error(
30+
`Fallback treatments - Discarded fallback: ${FallbackDiscardReason.Treatment}`
31+
);
32+
return undefined;
33+
}
34+
return treatment!;
35+
}
36+
37+
static sanitizeByFlag(
38+
byFlagFallbacks: Record<string, TreatmentWithConfig>
39+
): Record<string, TreatmentWithConfig> {
40+
const sanitizedByFlag: Record<string, TreatmentWithConfig> = {};
41+
42+
const entries = Object.entries(byFlagFallbacks);
43+
entries.forEach(([flag, t]) => {
44+
if (!this.isValidFlagName(flag)) {
45+
console.error(
46+
`Fallback treatments - Discarded flag '${flag}': ${FallbackDiscardReason.FlagName}`
47+
);
48+
return;
49+
}
50+
51+
if (!this.isValidTreatment(t)) {
52+
console.error(
53+
`Fallback treatments - Discarded treatment for flag '${flag}': ${FallbackDiscardReason.Treatment}`
54+
);
55+
return;
56+
}
57+
58+
sanitizedByFlag[flag] = t;
59+
});
60+
61+
return sanitizedByFlag;
62+
}
63+
}

types/splitio.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,10 @@ declare namespace SplitIO {
579579
* User consent status if using in client-side. Undefined if using in server-side (Node.js).
580580
*/
581581
readonly userConsent?: ConsentStatus;
582+
/**
583+
* Fallback treatments to be used when the SDK is not ready or the flag is not found.
584+
*/
585+
readonly fallbackTreatments?: FallbackTreatmentOptions;
582586
}
583587
/**
584588
* Log levels.
@@ -1142,6 +1146,15 @@ declare namespace SplitIO {
11421146
* User consent status.
11431147
*/
11441148
type ConsentStatus = 'GRANTED' | 'DECLINED' | 'UNKNOWN';
1149+
/**
1150+
* Fallback treatments to be used when the SDK is not ready or the flag is not found.
1151+
*/
1152+
type FallbackTreatmentOptions = {
1153+
global?: TreatmentWithConfig | Treatment,
1154+
byFlag: {
1155+
[key: string]: TreatmentWithConfig | Treatment
1156+
}
1157+
}
11451158
/**
11461159
* Logger. Its interface details are not part of the public API. It shouldn't be used directly.
11471160
*/

0 commit comments

Comments
 (0)