Skip to content

Commit 9a43f5e

Browse files
fix missing filter evaluation (#41)
1 parent b9c1c85 commit 9a43f5e

2 files changed

Lines changed: 177 additions & 0 deletions

File tree

featuremanagement/feature_manager.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ func (fm *FeatureManager) isEnabled(featureFlag FeatureFlag, appContext any) (bo
202202
matchedFeatureFilter, exists := fm.featureFilters[clientFilter.Name]
203203
if !exists {
204204
log.Printf("Feature filter %s is not found", clientFilter.Name)
205+
if requirementType == RequirementTypeAny {
206+
// When "Any", skip missing filters and continue evaluating the rest
207+
continue
208+
}
209+
// When "All", a missing filter means the feature cannot be enabled
205210
return false, nil
206211
}
207212

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package featuremanagement
5+
6+
import (
7+
"testing"
8+
)
9+
10+
// alwaysTrueFilter is a test filter that always returns true.
11+
type alwaysTrueFilter struct{}
12+
13+
func (f *alwaysTrueFilter) Name() string { return "AlwaysTrue" }
14+
func (f *alwaysTrueFilter) Evaluate(_ FeatureFilterEvaluationContext, _ any) (bool, error) {
15+
return true, nil
16+
}
17+
18+
// alwaysFalseFilter is a test filter that always returns false.
19+
type alwaysFalseFilter struct{}
20+
21+
func (f *alwaysFalseFilter) Name() string { return "AlwaysFalse" }
22+
func (f *alwaysFalseFilter) Evaluate(_ FeatureFilterEvaluationContext, _ any) (bool, error) {
23+
return false, nil
24+
}
25+
26+
func TestMissingFilter_RequirementTypeAny(t *testing.T) {
27+
tests := []struct {
28+
name string
29+
filters []ClientFilter
30+
expectedResult bool
31+
explanation string
32+
}{
33+
{
34+
name: "Missing filter followed by matching filter should be enabled",
35+
filters: []ClientFilter{
36+
{Name: "UnregisteredFilter"},
37+
{Name: "AlwaysTrue"},
38+
},
39+
expectedResult: true,
40+
explanation: "With RequirementType Any, a missing filter should be skipped and the matching AlwaysTrue filter should enable the feature",
41+
},
42+
{
43+
name: "Matching filter followed by missing filter should be enabled",
44+
filters: []ClientFilter{
45+
{Name: "AlwaysTrue"},
46+
{Name: "UnregisteredFilter"},
47+
},
48+
expectedResult: true,
49+
explanation: "With RequirementType Any, AlwaysTrue matches first so the feature should be enabled",
50+
},
51+
{
52+
name: "Only missing filters should be disabled",
53+
filters: []ClientFilter{
54+
{Name: "UnregisteredFilter"},
55+
{Name: "AnotherUnregisteredFilter"},
56+
},
57+
expectedResult: false,
58+
explanation: "With RequirementType Any, all filters are missing so no filter can match",
59+
},
60+
{
61+
name: "Missing filter with non-matching filter should be disabled",
62+
filters: []ClientFilter{
63+
{Name: "UnregisteredFilter"},
64+
{Name: "AlwaysFalse"},
65+
},
66+
expectedResult: false,
67+
explanation: "With RequirementType Any, missing filter is skipped and AlwaysFalse does not match",
68+
},
69+
}
70+
71+
for _, tc := range tests {
72+
t.Run(tc.name, func(t *testing.T) {
73+
provider := &mockFeatureFlagProvider{
74+
featureFlags: []FeatureFlag{
75+
{
76+
ID: "TestFeature",
77+
Enabled: true,
78+
Conditions: &Conditions{
79+
RequirementType: RequirementTypeAny,
80+
ClientFilters: tc.filters,
81+
},
82+
},
83+
},
84+
}
85+
86+
fm, err := NewFeatureManager(provider, &Options{
87+
Filters: []FeatureFilter{&alwaysTrueFilter{}, &alwaysFalseFilter{}},
88+
})
89+
if err != nil {
90+
t.Fatalf("Failed to create feature manager: %v", err)
91+
}
92+
93+
result, err := fm.IsEnabled("TestFeature")
94+
if err != nil {
95+
t.Fatalf("Unexpected error: %v", err)
96+
}
97+
98+
if result != tc.expectedResult {
99+
t.Errorf("Expected %v, got %v - %s", tc.expectedResult, result, tc.explanation)
100+
}
101+
})
102+
}
103+
}
104+
105+
func TestMissingFilter_RequirementTypeAll(t *testing.T) {
106+
tests := []struct {
107+
name string
108+
filters []ClientFilter
109+
expectedResult bool
110+
explanation string
111+
}{
112+
{
113+
name: "Missing filter with matching filter should be disabled",
114+
filters: []ClientFilter{
115+
{Name: "UnregisteredFilter"},
116+
{Name: "AlwaysTrue"},
117+
},
118+
expectedResult: false,
119+
explanation: "With RequirementType All, a missing filter means not all filters can pass so the feature should be disabled",
120+
},
121+
{
122+
name: "Matching filter followed by missing filter should be disabled",
123+
filters: []ClientFilter{
124+
{Name: "AlwaysTrue"},
125+
{Name: "UnregisteredFilter"},
126+
},
127+
expectedResult: false,
128+
explanation: "With RequirementType All, a missing filter means not all filters can pass so the feature should be disabled",
129+
},
130+
{
131+
name: "Only missing filters should be disabled",
132+
filters: []ClientFilter{
133+
{Name: "UnregisteredFilter"},
134+
},
135+
expectedResult: false,
136+
explanation: "With RequirementType All, a missing filter means the feature should be disabled",
137+
},
138+
}
139+
140+
for _, tc := range tests {
141+
t.Run(tc.name, func(t *testing.T) {
142+
provider := &mockFeatureFlagProvider{
143+
featureFlags: []FeatureFlag{
144+
{
145+
ID: "TestFeature",
146+
Enabled: true,
147+
Conditions: &Conditions{
148+
RequirementType: RequirementTypeAll,
149+
ClientFilters: tc.filters,
150+
},
151+
},
152+
},
153+
}
154+
155+
fm, err := NewFeatureManager(provider, &Options{
156+
Filters: []FeatureFilter{&alwaysTrueFilter{}, &alwaysFalseFilter{}},
157+
})
158+
if err != nil {
159+
t.Fatalf("Failed to create feature manager: %v", err)
160+
}
161+
162+
result, err := fm.IsEnabled("TestFeature")
163+
if err != nil {
164+
t.Fatalf("Unexpected error: %v", err)
165+
}
166+
167+
if result != tc.expectedResult {
168+
t.Errorf("Expected %v, got %v - %s", tc.expectedResult, result, tc.explanation)
169+
}
170+
})
171+
}
172+
}

0 commit comments

Comments
 (0)