diff --git a/src/feature-management/src/featureManager.ts b/src/feature-management/src/featureManager.ts index 02d64fb..c72eaa4 100644 --- a/src/feature-management/src/featureManager.ts +++ b/src/feature-management/src/featureManager.ts @@ -103,7 +103,12 @@ export class FeatureManager implements IFeatureManager { const contextWithFeatureName = { featureName: featureFlag.id, parameters: clientFilter.parameters }; if (matchedFeatureFilter === undefined) { console.warn(`Feature filter ${clientFilter.name} is not found.`); - return false; + if (requirementType === "All") { + // A missing filter means we cannot satisfy all conditions. + return false; + } + // For "Any", skip the missing filter and continue evaluating. + continue; } if (await matchedFeatureFilter.evaluate(contextWithFeatureName, appContext) === shortCircuitEvaluationResult) { return shortCircuitEvaluationResult; diff --git a/src/feature-management/test/featureManager.test.ts b/src/feature-management/test/featureManager.test.ts index 035eb2b..b78f135 100644 --- a/src/feature-management/test/featureManager.test.ts +++ b/src/feature-management/test/featureManager.test.ts @@ -248,4 +248,85 @@ describe("feature manager", () => { }); it("should override default filters with custom filters"); + + it("should skip missing filter and continue evaluating when requirement type is Any", () => { + const jsonObject = { + "feature_management": { + "feature_flags": [ + { + "id": "MissingFilterAny", + "enabled": true, + "conditions": { + "requirement_type": "Any", + "client_filters": [ + { + "name": "UnregisteredFilter", + "parameters": {} + }, + { + "name": "Microsoft.Targeting", + "parameters": { + "Audience": { + "Users": [ "Alice" ] + } + } + } + ] + } + }, + { + "id": "MissingFilterAll", + "enabled": true, + "conditions": { + "requirement_type": "All", + "client_filters": [ + { + "name": "UnregisteredFilter", + "parameters": {} + }, + { + "name": "Microsoft.Targeting", + "parameters": { + "Audience": { + "Users": [ "Alice" ] + } + } + } + ] + } + }, + { + "id": "AllMissingFiltersAny", + "enabled": true, + "conditions": { + "requirement_type": "Any", + "client_filters": [ + { + "name": "UnregisteredFilter1", + "parameters": {} + }, + { + "name": "UnregisteredFilter2", + "parameters": {} + } + ] + } + } + ] + } + }; + + const provider = new ConfigurationObjectFeatureFlagProvider(jsonObject); + const featureManager = new FeatureManager(provider); + return Promise.all([ + // Any: missing filter is skipped, targeting filter matches Alice => enabled + expect(featureManager.isEnabled("MissingFilterAny", {userId: "Alice"})).eventually.eq(true), + // Any: missing filter is skipped, targeting filter does not match Bob => disabled + expect(featureManager.isEnabled("MissingFilterAny", {userId: "Bob"})).eventually.eq(false), + // All: missing filter means cannot satisfy all conditions => disabled + expect(featureManager.isEnabled("MissingFilterAll", {userId: "Alice"})).eventually.eq(false), + // Any: all filters are missing => disabled (no filter can match) + expect(featureManager.isEnabled("AllMissingFiltersAny", {userId: "Alice"})).eventually.eq(false) + ]); + }); });