diff --git a/packages/core/src/__tests__/api.test.ts b/packages/core/src/__tests__/api.test.ts index 6905960..5591e88 100644 --- a/packages/core/src/__tests__/api.test.ts +++ b/packages/core/src/__tests__/api.test.ts @@ -34,6 +34,23 @@ describe("api", () => { }); }); + describe("evaluate and evaluateAll consistency", () => { + it("both return false for an invalid status value", () => { + const flagsConfig = [ + { + key: "new-homepage", + status: "enbaled" as FlagsConfiguration[number]["status"], + strategies: [], + }, + ]; + + const engine = createFlagEngine(flagsConfig); + const userCtx = engine.createUserContext({ __id: "yo" }); + expect(userCtx.evaluate("new-homepage")).toEqual(false); + expect(userCtx.evaluateAll()).toEqual({ "new-homepage": false }); + }); + }); + describe("evaluate", () => { it("returns false when the flag does not exist", () => { const flagsConfig: FlagsConfiguration = [ diff --git a/packages/core/src/__tests__/isEligibleForStrategy.test.ts b/packages/core/src/__tests__/isEligibleForStrategy.test.ts index a3d7f47..9c7c061 100644 --- a/packages/core/src/__tests__/isEligibleForStrategy.test.ts +++ b/packages/core/src/__tests__/isEligibleForStrategy.test.ts @@ -294,6 +294,72 @@ describe("isEligibleForStrategy", () => { } ); + it("not_contains returns true when the field is missing from user configuration", () => { + const rules: Rule[] = [ + { + operator: "not_contains", + field: "email", + value: "@competitor.com", + }, + ]; + + const userConfiguration: UserConfiguration = { + __id: "yo", + }; + + expect(isEligibleForStrategy(rules, userConfiguration)).toBe(true); + }); + + it("not_contains returns true when the field is a non-string value", () => { + const rules: Rule[] = [ + { + operator: "not_contains", + field: "email", + value: "@competitor.com", + }, + ]; + + const userConfiguration: UserConfiguration = { + __id: "yo", + email: 42, + }; + + expect(isEligibleForStrategy(rules, userConfiguration)).toBe(true); + }); + + it("not_contains returns false when the rule value is not a string", () => { + const rules: Rule[] = [ + { + operator: "not_contains", + field: "email", + value: 123, + }, + ]; + + const userConfiguration: UserConfiguration = { + __id: "yo", + email: "test@example.com", + }; + + expect(isEligibleForStrategy(rules, userConfiguration)).toBe(false); + }); + + it("not_in returns true when the field is missing from user configuration", () => { + const rules: Rule[] = [ + { + operator: "not_in", + field: "country", + value: ["US", "UK"], + }, + ]; + + const userConfiguration: UserConfiguration = { + __id: "yo", + }; + + expect(isEligibleForStrategy(rules, userConfiguration)).toBe(true); + }); + it.each([ [2, [1, 2, 3], false], [4, [1, 2, 3], true], @@ -330,4 +396,58 @@ describe("isEligibleForStrategy", () => { expect(isEligibleForStrategy(rules, userConfiguration)).toBe(expected); } ); + + describe("segment depth protection", () => { + const buildNestedSegment = (depth: number): Rule => { + let innermost: Rule = { + field: "country", + operator: "equals", + value: "FR", + }; + + for (let i = 0; i < depth; i++) { + innermost = { + inSegment: { + name: `segment-${i}`, + rules: [innermost], + }, + }; + } + + return innermost; + }; + + it("returns false when segment nesting exceeds MAX_SEGMENT_DEPTH (10)", () => { + const deepRule = buildNestedSegment(11); + + const userConfiguration: UserConfiguration = { + __id: "yo", + country: "FR", + }; + + expect(isEligibleForStrategy([deepRule], userConfiguration)).toBe(false); + }); + + it("returns true when segment nesting is exactly at MAX_SEGMENT_DEPTH (10)", () => { + const deepRule = buildNestedSegment(10); + + const userConfiguration: UserConfiguration = { + __id: "yo", + country: "FR", + }; + + expect(isEligibleForStrategy([deepRule], userConfiguration)).toBe(true); + }); + + it("returns false when segment nesting is at MAX_SEGMENT_DEPTH but rule doesn't match", () => { + const deepRule = buildNestedSegment(10); + + const userConfiguration: UserConfiguration = { + __id: "yo", + country: "US", + }; + + expect(isEligibleForStrategy([deepRule], userConfiguration)).toBe(false); + }); + }); }); diff --git a/packages/core/src/__tests__/repartition.test.ts b/packages/core/src/__tests__/repartition.test.ts index 422a9b1..4668a12 100644 --- a/packages/core/src/__tests__/repartition.test.ts +++ b/packages/core/src/__tests__/repartition.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { createFlagEngine } from ".."; describe("repartition", () => { - it("should be distributive", () => { + it("should distribute 50/50 across 1M users", () => { const engine = createFlagEngine([ { key: "summer-sale", @@ -49,7 +49,7 @@ describe("repartition", () => { expect(B).toBeLessThan(halfCountUpper); }); - it("should be distributive", () => { + it("should distribute 50/50 across 10M users with tighter tolerance", () => { const engine = createFlagEngine([ { key: "summer-sale", @@ -96,7 +96,7 @@ describe("repartition", () => { expect(B).toBeLessThan(halfCountUpper); }); - it("should be distributive", () => { + it("should distribute 10/20/30/40 across four variants", () => { const engine = createFlagEngine([ { key: "summer-sale", diff --git a/packages/core/src/getEligibleStrategy.ts b/packages/core/src/getEligibleStrategy.ts index 6421c7c..2af0a58 100644 --- a/packages/core/src/getEligibleStrategy.ts +++ b/packages/core/src/getEligibleStrategy.ts @@ -76,11 +76,9 @@ export const isEligibleForStrategy = ( case "not_contains": { const fieldValue = userConfiguration[rule.field]; - return ( - isString(fieldValue) && - isString(rule.value) && - !fieldValue.includes(rule.value) - ); + if (!isString(rule.value)) return false; + if (!isString(fieldValue)) return true; + return !fieldValue.includes(rule.value); } case "in": { @@ -94,9 +92,8 @@ export const isEligibleForStrategy = ( case "not_in": { const fieldValue = userConfiguration[rule.field]; - return ( - Array.isArray(rule.value) && rule.value.indexOf(fieldValue) === -1 - ); + if (!Array.isArray(rule.value)) return false; + return rule.value.indexOf(fieldValue) === -1; } default: diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index dbfffca..f051d0d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -16,7 +16,7 @@ const createUserContext = ( const evaluate = (flagKey: string): string | boolean => { const flagConfig = flagsConfig.find((f) => f.key === flagKey); if (!flagConfig) return false; - if (flagConfig.status === "disabled") return false; + if (flagConfig.status !== "enabled") return false; return evaluateFlag(flagConfig, _userConfiguration); };