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
17 changes: 17 additions & 0 deletions packages/core/src/__tests__/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
120 changes: 120 additions & 0 deletions packages/core/src/__tests__/isEligibleForStrategy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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);
});
});
});
6 changes: 3 additions & 3 deletions packages/core/src/__tests__/repartition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
13 changes: 5 additions & 8 deletions packages/core/src/getEligibleStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down