From 963007a7472576a13d0a5d195cb0086777be2b6c Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Thu, 6 Feb 2025 17:40:45 +0800 Subject: [PATCH] add testcases & remove unused code --- .../test/featureManager.test.ts | 74 ++- .../test/sampleFeatureFlags.ts | 532 ------------------ .../test/sampleVariantFeatureFlags.ts | 221 ++++++++ src/feature-management/test/variant.test.ts | 34 +- 4 files changed, 309 insertions(+), 552 deletions(-) delete mode 100644 src/feature-management/test/sampleFeatureFlags.ts create mode 100644 src/feature-management/test/sampleVariantFeatureFlags.ts diff --git a/src/feature-management/test/featureManager.test.ts b/src/feature-management/test/featureManager.test.ts index 4c235b6..f7f6daa 100644 --- a/src/feature-management/test/featureManager.test.ts +++ b/src/feature-management/test/featureManager.test.ts @@ -52,7 +52,7 @@ describe("feature manager", () => { expect(featureFlags[1]).to.eq("Beta"); }); - it("should fail when feature flag is invalid", async () => { + it("should fail when feature flag id is invalid", async () => { const dataSource = new Map(); dataSource.set("feature_management", { feature_flags: [ @@ -111,7 +111,7 @@ describe("feature manager", () => { ]); }); - it("should evaluate features with conditions", () => { + it("should fail when feature flag is invalid", () => { const dataSource = new Map(); dataSource.set("feature_management", { feature_flags: [ @@ -178,6 +178,74 @@ describe("feature manager", () => { ]); }); - it("should evaluate features with conditions"); + it("should evaluate client filters based on requirement type", () => { + const jsonObject = { + "feature_management": { + "feature_flags": [ + { + "id": "AllFeature", + "enabled": true, + "conditions": { + "requirement_type": "All", + "client_filters": [ + { + "name": "Microsoft.TimeWindow", + "parameters": { + "Start": "Tue, 4 Feb 2025 16:00:00 GMT" + } + }, + { + "name": "Microsoft.Targeting", + "parameters": { + "Audience": { + "Users": [ "Alice" ], + "Exclusion": { + "Users": [ "Dave" ] + } + } + } + } + ] + } + }, + { + "id": "AnyFeature", + "enabled": true, + "conditions": { + // "requirement_type": "Any", + "client_filters": [ + { + "name": "Microsoft.TimeWindow", + "parameters": { + "Start": "Tue, 4 Feb 2025 16:00:00 GMT" + } + }, + { + "name": "Microsoft.Targeting", + "parameters": { + "Audience": { + "Users": [ "Alice" ], + "Exclusion": { + "Users": [ "Dave" ] + } + } + } + } + ] + } + } + ] + } + }; + + const provider = new ConfigurationObjectFeatureFlagProvider(jsonObject); + const featureManager = new FeatureManager(provider); + return Promise.all([ + expect(featureManager.isEnabled("AllFeature", {userId: "Alice"})).eventually.eq(true), + expect(featureManager.isEnabled("AllFeature", {userId: "Dave"})).eventually.eq(false), + expect(featureManager.isEnabled("AnyFeature", {userId: "Dave"})).eventually.eq(true) + ]); + }); + it("should override default filters with custom filters"); }); diff --git a/src/feature-management/test/sampleFeatureFlags.ts b/src/feature-management/test/sampleFeatureFlags.ts deleted file mode 100644 index 85f69b0..0000000 --- a/src/feature-management/test/sampleFeatureFlags.ts +++ /dev/null @@ -1,532 +0,0 @@ -export enum Features { - VariantFeatureDefaultDisabled = "VariantFeatureDefaultDisabled", - VariantFeatureDefaultEnabled = "VariantFeatureDefaultEnabled", - VariantFeaturePercentileOn = "VariantFeaturePercentileOn", - VariantFeaturePercentileOff = "VariantFeaturePercentileOff", - VariantFeatureAlwaysOff = "VariantFeatureAlwaysOff", - VariantFeatureUser = "VariantFeatureUser", - VariantFeatureGroup = "VariantFeatureGroup", - VariantFeatureNoVariants = "VariantFeatureNoVariants", - VariantFeatureNoAllocation = "VariantFeatureNoAllocation", - VariantFeatureAlwaysOffNoAllocation = "VariantFeatureAlwaysOffNoAllocation", - VariantFeatureBothConfigurations = "VariantFeatureBothConfigurations", - VariantFeatureInvalidStatusOverride = "VariantFeatureInvalidStatusOverride", - VariantFeatureInvalidFromTo = "VariantFeatureInvalidFromTo", - VariantImplementationFeature = "VariantImplementationFeature", -} - -export const featureFlagsConfigurationObject = { - "feature_management": { - "feature_flags": [ - { - "id": "OnTestFeature", - "enabled": true - }, - { - "id": "OffTestFeature", - "enabled": false - }, - { - "id": "ConditionalFeature", - "enabled": true, - "conditions": { - "client_filters": [ - { - "name": "Test", - "parameters": { - "P1": "V1" - } - } - ] - } - }, - { - "id": "ContextualFeature", - "enabled": true, - "conditions": { - "client_filters": [ - { - "name": "ContextualTest", - "parameters": { - "AllowedAccounts": [ - "abc" - ] - } - } - ] - } - }, - { - "id": "AnyFilterFeature", - "enabled": true, - "conditions": { - "requirement_type": "Any", - "client_filters": [ - { - "name": "Test", - "parameters": { - "Id": "1" - } - }, - { - "name": "Test", - "parameters": { - "Id": "2" - } - } - ] - } - }, - { - "id": "AllFilterFeature", - "enabled": true, - "conditions": { - "requirement_type": "All", - "client_filters": [ - { - "name": "Test", - "parameters": { - "Id": "1" - } - }, - { - "name": "Test", - "parameters": { - "Id": "2" - } - } - ] - } - }, - { - "id": "FeatureUsesFiltersWithDuplicatedAlias", - "enabled": true, - "conditions": { - "client_filters": [ - { - "name": "DuplicatedFilterName" - }, - { - "name": "Percentage", - "parameters": { - "Value": 100 - } - } - ] - } - }, - { - "id": "TargetingTestFeature", - "enabled": true, - "conditions": { - "client_filters": [ - { - "name": "Targeting", - "parameters": { - "Audience": { - "Users": [ - "Jeff", - "Alicia" - ], - "Groups": [ - { - "Name": "Ring0", - "RolloutPercentage": 100 - }, - { - "Name": "Ring1", - "RolloutPercentage": 50 - } - ], - "DefaultRolloutPercentage": 20 - } - } - } - ] - } - }, - { - "id": "TargetingTestFeatureWithExclusion", - "enabled": true, - "conditions": { - "client_filters": [ - { - "name": "Targeting", - "parameters": { - "Audience": { - "Users": [ - "Jeff", - "Alicia" - ], - "Groups": [ - { - "Name": "Ring0", - "RolloutPercentage": 100 - }, - { - "Name": "Ring1", - "RolloutPercentage": 50 - } - ], - "DefaultRolloutPercentage": 20, - "Exclusion": { - "Users": [ - "Jeff" - ], - "Groups": [ - "Ring0", - "Ring2" - ] - } - } - } - } - ] - } - }, - { - "id": "CustomFilterFeature", - "enabled": true, - "conditions": { - "client_filters": [ - { - "name": "CustomTargetingFilter", - "parameters": { - "Audience": { - "Users": [ - "Jeff" - ] - } - } - } - ] - } - }, - { - "id": "VariantFeaturePercentileOn", - "enabled": true, - "variants": [ - { - "name": "Big", - "status_override": "Disabled" - } - ], - "allocation": { - "percentile": [ - { - "variant": "Big", - "from": 0, - "to": 50 - } - ], - "seed": "1234" - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeaturePercentileOff", - "enabled": true, - "variants": [ - { - "name": "Big" - } - ], - "allocation": { - "percentile": [ - { - "variant": "Big", - "from": 0, - "to": 50 - } - ], - "seed": "12345" - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureAlwaysOff", - "enabled": false, - "variants": [ - { - "name": "Big" - } - ], - "allocation": { - "percentile": [ - { - "variant": "Big", - "from": 0, - "to": 100 - } - ], - "seed": "12345" - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureDefaultDisabled", - "enabled": false, - "variants": [ - { - "name": "Small", - "configuration_value": "300px" - } - ], - "allocation": { - "default_when_disabled": "Small" - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureDefaultEnabled", - "enabled": true, - "variants": [ - { - "name": "Medium", - "configuration_value": { - "Size": "450px", - "Color": "Purple" - } - }, - { - "name": "Small", - "configuration_value": "300px" - } - ], - "allocation": { - "default_when_enabled": "Medium", - "user": [ - { - "variant": "Small", - "users": [ - "Jeff" - ] - } - ] - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureUser", - "enabled": true, - "variants": [ - { - "name": "Small", - "configuration_value": "300px" - } - ], - "allocation": { - "user": [ - { - "variant": "Small", - "users": [ - "Marsha" - ] - } - ] - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureGroup", - "enabled": true, - "variants": [ - { - "name": "Small", - "configuration_value": "300px" - } - ], - "allocation": { - "group": [ - { - "variant": "Small", - "groups": [ - "Group1" - ] - } - ] - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureNoVariants", - "enabled": true, - "variants": [], - "allocation": { - "user": [ - { - "variant": "Small", - "users": [ - "Marsha" - ] - } - ] - }, - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureNoAllocation", - "enabled": true, - "variants": [ - { - "name": "Small", - "configuration_value": "300px" - } - ], - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureAlwaysOffNoAllocation", - "enabled": false, - "variants": [ - { - "name": "Small", - "configuration_value": "300px" - } - ], - "telemetry": { - "enabled": true - } - }, - { - "id": "VariantFeatureBothConfigurations", - "enabled": true, - "variants": [ - { - "name": "Small", - "configuration_value": "600px" - } - ], - "allocation": { - "default_when_enabled": "Small" - } - }, - { - "id": "VariantFeatureInvalidStatusOverride", - "enabled": true, - "variants": [ - { - "name": "Small", - "configuration_value": "300px", - "status_override": "InvalidValue" - } - ], - "allocation": { - "default_when_enabled": "Small" - } - }, - { - "id": "VariantFeatureInvalidFromTo", - "enabled": true, - "variants": [ - { - "name": "Small", - "configuration_value": "300px" - } - ], - "allocation": { - "percentile": [ - { - "variant": "Small", - "from": "Invalid", - "to": "Invalid" - } - ] - } - }, - { - "id": "VariantImplementationFeature", - "enabled": true, - "conditions": { - "client_filters": [ - { - "name": "Targeting", - "parameters": { - "Audience": { - "Users": [ - "UserOmega", - "UserSigma", - "UserBeta" - ] - } - } - } - ] - }, - "variants": [ - { - "name": "AlgorithmBeta" - }, - { - "name": "Sigma", - "configuration_value": "AlgorithmSigma" - }, - { - "name": "Omega" - } - ], - "allocation": { - "user": [ - { - "variant": "AlgorithmBeta", - "users": [ - "UserBeta" - ] - }, - { - "variant": "Omega", - "users": [ - "UserOmega" - ] - }, - { - "variant": "Sigma", - "users": [ - "UserSigma" - ] - } - ] - } - }, - { - "id": "OnTelemetryTestFeature", - "enabled": true, - "telemetry": { - "enabled": true, - "metadata": { - "Tags.Tag1": "Tag1Value", - "Tags.Tag2": "Tag2Value", - "Etag": "EtagValue", - "Label": "LabelValue" - } - } - }, - { - "id": "OffTelemetryTestFeature", - "enabled": false, - "telemetry": { - "enabled": true - } - } - ] - } -}; - diff --git a/src/feature-management/test/sampleVariantFeatureFlags.ts b/src/feature-management/test/sampleVariantFeatureFlags.ts new file mode 100644 index 0000000..21f0020 --- /dev/null +++ b/src/feature-management/test/sampleVariantFeatureFlags.ts @@ -0,0 +1,221 @@ +export enum Features { + VariantFeatureDefaultDisabled = "VariantFeatureDefaultDisabled", + VariantFeatureDefaultEnabled = "VariantFeatureDefaultEnabled", + VariantFeaturePercentileOn = "VariantFeaturePercentileOn", + VariantFeaturePercentileOff = "VariantFeaturePercentileOff", + VariantFeatureUser = "VariantFeatureUser", + VariantFeatureGroup = "VariantFeatureGroup", + VariantFeatureNoVariants = "VariantFeatureNoVariants", + VariantFeatureNoAllocation = "VariantFeatureNoAllocation", + VariantFeatureInvalidStatusOverride = "VariantFeatureInvalidStatusOverride", + VariantFeatureInvalidFromTo = "VariantFeatureInvalidFromTo", +} + +export const featureFlagsConfigurationObject = { + "feature_management": { + "feature_flags": [ + { + "id": "VariantFeaturePercentileOn", + "enabled": true, + "variants": [ + { + "name": "Big", + "status_override": "Disabled" + } + ], + "allocation": { + "percentile": [ + { + "variant": "Big", + "from": 0, + "to": 50 + } + ], + "seed": "1234" + }, + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeaturePercentileOff", + "enabled": true, + "variants": [ + { + "name": "Big" + } + ], + "allocation": { + "percentile": [ + { + "variant": "Big", + "from": 0, + "to": 50 + } + ], + "seed": "12345" + }, + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeatureDefaultDisabled", + "enabled": false, + "variants": [ + { + "name": "Small", + "configuration_value": "300px" + } + ], + "allocation": { + "default_when_disabled": "Small" + }, + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeatureDefaultEnabled", + "enabled": true, + "variants": [ + { + "name": "Medium", + "configuration_value": { + "Size": "450px", + "Color": "Purple" + } + }, + { + "name": "Small", + "configuration_value": "300px" + } + ], + "allocation": { + "default_when_enabled": "Medium", + "user": [ + { + "variant": "Small", + "users": [ + "Jeff" + ] + } + ] + }, + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeatureUser", + "enabled": true, + "variants": [ + { + "name": "Small", + "configuration_value": "300px" + } + ], + "allocation": { + "user": [ + { + "variant": "Small", + "users": [ + "Marsha" + ] + } + ] + }, + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeatureGroup", + "enabled": true, + "variants": [ + { + "name": "Small", + "configuration_value": "300px" + } + ], + "allocation": { + "group": [ + { + "variant": "Small", + "groups": [ + "Group1" + ] + } + ] + }, + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeatureNoVariants", + "enabled": true, + "variants": [], + "allocation": { + "user": [ + { + "variant": "Small", + "users": [ + "Marsha" + ] + } + ] + }, + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeatureNoAllocation", + "enabled": true, + "variants": [ + { + "name": "Small", + "configuration_value": "300px" + } + ], + "telemetry": { + "enabled": true + } + }, + { + "id": "VariantFeatureInvalidStatusOverride", + "enabled": true, + "variants": [ + { + "name": "Small", + "configuration_value": "300px", + "status_override": "InvalidValue" + } + ], + "allocation": { + "default_when_enabled": "Small" + } + }, + { + "id": "VariantFeatureInvalidFromTo", + "enabled": true, + "variants": [ + { + "name": "Small", + "configuration_value": "300px" + } + ], + "allocation": { + "percentile": [ + { + "variant": "Small", + "from": "Invalid", + "to": "Invalid" + } + ] + } + } + ] + } +}; diff --git a/src/feature-management/test/variant.test.ts b/src/feature-management/test/variant.test.ts index ddfd90f..118fc03 100644 --- a/src/feature-management/test/variant.test.ts +++ b/src/feature-management/test/variant.test.ts @@ -4,7 +4,7 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "../"; -import { Features, featureFlagsConfigurationObject } from "./sampleFeatureFlags.js"; +import { Features, featureFlagsConfigurationObject } from "./sampleVariantFeatureFlags.js"; chai.use(chaiAsPromised); const expect = chai.expect; @@ -20,35 +20,35 @@ describe("feature variant", () => { describe("valid scenarios", () => { const context = { userId: "Marsha", groups: ["Group1"] }; - it("default allocation with disabled feature", async () => { + it("should perform default allocation with disabled feature", async () => { const variant = await featureManager.getVariant(Features.VariantFeatureDefaultDisabled, context); expect(variant).not.to.be.undefined; expect(variant?.name).eq("Small"); expect(variant?.configuration).eq("300px"); }); - it("default allocation with enabled feature", async () => { + it("should perform default allocation with enabled feature", async () => { const variant = await featureManager.getVariant(Features.VariantFeatureDefaultEnabled, context); expect(variant).not.to.be.undefined; expect(variant?.name).eq("Medium"); expect(variant?.configuration).deep.eq({ Size: "450px", Color: "Purple" }); }); - it("user allocation", async () => { + it("should perform user allocation", async () => { const variant = await featureManager.getVariant(Features.VariantFeatureUser, context); expect(variant).not.to.be.undefined; expect(variant?.name).eq("Small"); expect(variant?.configuration).eq("300px"); }); - it("group allocation", async () => { + it("should perform group allocation", async () => { const variant = await featureManager.getVariant(Features.VariantFeatureGroup, context); expect(variant).not.to.be.undefined; expect(variant?.name).eq("Small"); expect(variant?.configuration).eq("300px"); }); - it("percentile allocation with seed", async () => { + it("should perform percentile allocation with seed", async () => { const variant = await featureManager.getVariant(Features.VariantFeaturePercentileOn, context); expect(variant).not.to.be.undefined; expect(variant?.name).eq("Big"); @@ -57,7 +57,7 @@ describe("feature variant", () => { expect(variant2).to.be.undefined; }); - it("overwrite enabled status", async () => { + it("should overwrite enabled status", async () => { const enabledStatus = await featureManager.isEnabled(Features.VariantFeaturePercentileOn, context); expect(enabledStatus).to.be.false; // featureFlag.enabled = true, overridden to false by variant `Big`. }); @@ -67,27 +67,27 @@ describe("feature variant", () => { describe("invalid scenarios", () => { const context = { userId: "Jeff" }; - it("return undefined when no variants are specified", async () => { + it("should return undefined when no variants are specified", async () => { const variant = await featureManager.getVariant(Features.VariantFeatureNoVariants, context); expect(variant).to.be.undefined; }); - it("return undefined when no allocation is specified", async () => { + it("should return undefined when no allocation is specified", async () => { const variant = await featureManager.getVariant(Features.VariantFeatureNoAllocation, context); expect(variant).to.be.undefined; }); - it("only support configuration value", async () => { - const variant = await featureManager.getVariant(Features.VariantFeatureBothConfigurations, context); - expect(variant).not.to.be.undefined; - expect(variant?.configuration).eq("600px"); - }); - // requires IFeatureFlagProvider to throw an exception on validation - it("throw exception for invalid StatusOverride value"); + it("should throw exception for invalid StatusOverride value", async () => { + await expect(featureManager.getVariant(Features.VariantFeatureInvalidStatusOverride, context)) + .eventually.rejectedWith("Invalid feature flag: VariantFeatureInvalidStatusOverride. Variant 'status_override' must be 'None', 'Enabled', or 'Disabled'."); + }); // requires IFeatureFlagProvider to throw an exception on validation - it("throw exception for invalid doubles From and To in the Percentile section"); + it("should throw exception for invalid doubles From and To in the Percentile section", async () => { + await expect(featureManager.getVariant(Features.VariantFeatureInvalidFromTo, context)) + .eventually.rejectedWith("Invalid feature flag: VariantFeatureInvalidFromTo. Percentile allocation 'from' must be a number between 0 and 100."); + }); });