From 52ade970e94c98522f504751fde8d0a75f2eb126 Mon Sep 17 00:00:00 2001 From: Deep Santoshwar Date: Sat, 1 Aug 2026 00:58:16 +0530 Subject: [PATCH] fix(Utils): coerce non-string values before split in validateRule The semver and MODULO branches of validateRule guarded against a missing rule value but still called .split() directly on rule.value, so a truthy non-string value (e.g. a boolean) reached .split and threw. Coerce to a string first so validation degrades to false instead of crashing CreateSegment/EditSegment's isValid check. --- frontend/common/utils/__tests__/utils.test.ts | 96 +++++++++++++++++++ frontend/common/utils/utils.tsx | 4 +- 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 frontend/common/utils/__tests__/utils.test.ts diff --git a/frontend/common/utils/__tests__/utils.test.ts b/frontend/common/utils/__tests__/utils.test.ts new file mode 100644 index 000000000000..f8608c206b3f --- /dev/null +++ b/frontend/common/utils/__tests__/utils.test.ts @@ -0,0 +1,96 @@ +// These stores pull in `window` globals (via the flux dispatcher) that +// aren't available under the jest `node` test environment, so they're +// mocked out to allow `common/utils/utils` to load for real. +jest.mock('common/stores/account-store', () => ({})) +jest.mock('common/stores/project-store', () => ({})) +jest.mock('common/store', () => ({ getStore: () => ({}) })) +jest.mock('@flagsmith/flagsmith', () => ({ + getValue: (_key: string, opts: { fallback: unknown }) => opts.fallback, +})) + +import Utils from 'common/utils/utils' +import { SegmentCondition } from 'common/types/responses' + +const buildRule = (overrides: Partial): SegmentCondition => ({ + operator: 'EQUAL', + property: 'trait', + value: '', + ...overrides, +}) + +describe('validateRule', () => { + it('returns false for a falsy rule', () => { + expect(Utils.validateRule(undefined as unknown as SegmentCondition)).toBe( + false, + ) + }) + + it('returns true for a rule marked as deleted regardless of its value', () => { + const rule = buildRule({ + delete: true, + operator: 'MODULO', + value: undefined as unknown as string, + }) + expect(() => Utils.validateRule(rule)).not.toThrow() + expect(Utils.validateRule(rule)).toBe(true) + }) + + describe('does not throw and returns false when value is missing', () => { + it.each<[string, SegmentCondition['value']]>([ + ['undefined', undefined as unknown as string], + ['null', null], + ['empty string', ''], + ])('MODULO with %s value', (_label, value) => { + const rule = buildRule({ operator: 'MODULO', value }) + expect(() => Utils.validateRule(rule)).not.toThrow() + expect(Utils.validateRule(rule)).toBe(false) + }) + + it.each<[string, SegmentCondition['value']]>([ + ['undefined', undefined as unknown as string], + ['null', null], + ['empty string', ''], + ])('semver operator with %s value', (_label, value) => { + const rule = buildRule({ operator: 'GREATER_THAN:semver', value }) + expect(() => Utils.validateRule(rule)).not.toThrow() + expect(Utils.validateRule(rule)).toBe(false) + }) + }) + + it('does not throw when value is a non-string type for MODULO', () => { + const rule = buildRule({ operator: 'MODULO', value: true }) + expect(() => Utils.validateRule(rule)).not.toThrow() + }) + + it('does not throw when value is a non-string type for a semver operator', () => { + const rule = buildRule({ operator: 'GREATER_THAN:semver', value: true }) + expect(() => Utils.validateRule(rule)).not.toThrow() + }) + + it('is valid for a hideValue operator (e.g. IS_SET) even with a null value', () => { + const rule = buildRule({ operator: 'IS_SET', value: null }) + expect(() => Utils.validateRule(rule)).not.toThrow() + expect(Utils.validateRule(rule)).toBe(true) + }) + + it('validates a correct MODULO value', () => { + const rule = buildRule({ operator: 'MODULO', value: '2|1' }) + expect(Utils.validateRule(rule)).toBe(true) + }) + + it('validates a correct semver value', () => { + const rule = buildRule({ + operator: 'GREATER_THAN:semver', + value: '1.0.0:semver', + }) + expect(Utils.validateRule(rule)).toBe(true) + }) + + it('rejects an invalid semver value', () => { + const rule = buildRule({ + operator: 'GREATER_THAN:semver', + value: 'not-a-version', + }) + expect(Utils.validateRule(rule)).toBe(false) + }) +}) diff --git a/frontend/common/utils/utils.tsx b/frontend/common/utils/utils.tsx index 9adb9b7acdc4..9c9ab4e0d6cd 100644 --- a/frontend/common/utils/utils.tsx +++ b/frontend/common/utils/utils.tsx @@ -823,7 +823,7 @@ const Utils = Object.assign({}, BaseUtils, { if (!rule.value) { return false } - return !!semver.valid(`${rule.value.split(':')[0]}`) + return !!semver.valid(`${rule.value}`.split(':')[0]) } switch (rule.operator) { @@ -846,7 +846,7 @@ const Utils = Object.assign({}, BaseUtils, { if (!rule.value) { return false } - const valueSplit = rule.value.split('|') + const valueSplit = `${rule.value}`.split('|') if (valueSplit.length === 2) { const [divisor, remainder] = [ parseFloat(valueSplit[0]),