From df34c810fe5e21742915515f19dc1bd04ca35c29 Mon Sep 17 00:00:00 2001 From: linglingye001 <143174321+linglingye001@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:22:51 +0800 Subject: [PATCH 1/5] fix missing filter evaluation (#41) --- featuremanagement/feature_manager.go | 5 + featuremanagement/missing_filter_test.go | 172 +++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 featuremanagement/missing_filter_test.go diff --git a/featuremanagement/feature_manager.go b/featuremanagement/feature_manager.go index 783fe69..c943f9f 100644 --- a/featuremanagement/feature_manager.go +++ b/featuremanagement/feature_manager.go @@ -202,6 +202,11 @@ func (fm *FeatureManager) isEnabled(featureFlag FeatureFlag, appContext any) (bo matchedFeatureFilter, exists := fm.featureFilters[clientFilter.Name] if !exists { log.Printf("Feature filter %s is not found", clientFilter.Name) + if requirementType == RequirementTypeAny { + // When "Any", skip missing filters and continue evaluating the rest + continue + } + // When "All", a missing filter means the feature cannot be enabled return false, nil } diff --git a/featuremanagement/missing_filter_test.go b/featuremanagement/missing_filter_test.go new file mode 100644 index 0000000..b07f68a --- /dev/null +++ b/featuremanagement/missing_filter_test.go @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package featuremanagement + +import ( + "testing" +) + +// alwaysTrueFilter is a test filter that always returns true. +type alwaysTrueFilter struct{} + +func (f *alwaysTrueFilter) Name() string { return "AlwaysTrue" } +func (f *alwaysTrueFilter) Evaluate(_ FeatureFilterEvaluationContext, _ any) (bool, error) { + return true, nil +} + +// alwaysFalseFilter is a test filter that always returns false. +type alwaysFalseFilter struct{} + +func (f *alwaysFalseFilter) Name() string { return "AlwaysFalse" } +func (f *alwaysFalseFilter) Evaluate(_ FeatureFilterEvaluationContext, _ any) (bool, error) { + return false, nil +} + +func TestMissingFilter_RequirementTypeAny(t *testing.T) { + tests := []struct { + name string + filters []ClientFilter + expectedResult bool + explanation string + }{ + { + name: "Missing filter followed by matching filter should be enabled", + filters: []ClientFilter{ + {Name: "UnregisteredFilter"}, + {Name: "AlwaysTrue"}, + }, + expectedResult: true, + explanation: "With RequirementType Any, a missing filter should be skipped and the matching AlwaysTrue filter should enable the feature", + }, + { + name: "Matching filter followed by missing filter should be enabled", + filters: []ClientFilter{ + {Name: "AlwaysTrue"}, + {Name: "UnregisteredFilter"}, + }, + expectedResult: true, + explanation: "With RequirementType Any, AlwaysTrue matches first so the feature should be enabled", + }, + { + name: "Only missing filters should be disabled", + filters: []ClientFilter{ + {Name: "UnregisteredFilter"}, + {Name: "AnotherUnregisteredFilter"}, + }, + expectedResult: false, + explanation: "With RequirementType Any, all filters are missing so no filter can match", + }, + { + name: "Missing filter with non-matching filter should be disabled", + filters: []ClientFilter{ + {Name: "UnregisteredFilter"}, + {Name: "AlwaysFalse"}, + }, + expectedResult: false, + explanation: "With RequirementType Any, missing filter is skipped and AlwaysFalse does not match", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "TestFeature", + Enabled: true, + Conditions: &Conditions{ + RequirementType: RequirementTypeAny, + ClientFilters: tc.filters, + }, + }, + }, + } + + fm, err := NewFeatureManager(provider, &Options{ + Filters: []FeatureFilter{&alwaysTrueFilter{}, &alwaysFalseFilter{}}, + }) + if err != nil { + t.Fatalf("Failed to create feature manager: %v", err) + } + + result, err := fm.IsEnabled("TestFeature") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if result != tc.expectedResult { + t.Errorf("Expected %v, got %v - %s", tc.expectedResult, result, tc.explanation) + } + }) + } +} + +func TestMissingFilter_RequirementTypeAll(t *testing.T) { + tests := []struct { + name string + filters []ClientFilter + expectedResult bool + explanation string + }{ + { + name: "Missing filter with matching filter should be disabled", + filters: []ClientFilter{ + {Name: "UnregisteredFilter"}, + {Name: "AlwaysTrue"}, + }, + expectedResult: false, + explanation: "With RequirementType All, a missing filter means not all filters can pass so the feature should be disabled", + }, + { + name: "Matching filter followed by missing filter should be disabled", + filters: []ClientFilter{ + {Name: "AlwaysTrue"}, + {Name: "UnregisteredFilter"}, + }, + expectedResult: false, + explanation: "With RequirementType All, a missing filter means not all filters can pass so the feature should be disabled", + }, + { + name: "Only missing filters should be disabled", + filters: []ClientFilter{ + {Name: "UnregisteredFilter"}, + }, + expectedResult: false, + explanation: "With RequirementType All, a missing filter means the feature should be disabled", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "TestFeature", + Enabled: true, + Conditions: &Conditions{ + RequirementType: RequirementTypeAll, + ClientFilters: tc.filters, + }, + }, + }, + } + + fm, err := NewFeatureManager(provider, &Options{ + Filters: []FeatureFilter{&alwaysTrueFilter{}, &alwaysFalseFilter{}}, + }) + if err != nil { + t.Fatalf("Failed to create feature manager: %v", err) + } + + result, err := fm.IsEnabled("TestFeature") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if result != tc.expectedResult { + t.Errorf("Expected %v, got %v - %s", tc.expectedResult, result, tc.explanation) + } + }) + } +} From 61b675b7f951f33e2f8a19e73f4934755513ade9 Mon Sep 17 00:00:00 2001 From: linglingye001 <143174321+linglingye001@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:24:20 +0800 Subject: [PATCH 2/5] fix index out of bounds when GetFeatureNames (#43) --- featuremanagement/feature_manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/featuremanagement/feature_manager.go b/featuremanagement/feature_manager.go index c943f9f..09789a6 100644 --- a/featuremanagement/feature_manager.go +++ b/featuremanagement/feature_manager.go @@ -167,7 +167,7 @@ func (fm *FeatureManager) GetFeatureNames() []string { return nil } - res := make([]string, 0, len(flags)) + res := make([]string, len(flags)) for i, flag := range flags { res[i] = flag.ID } From 7436a33e3d6efbdba02ac1b1d821b4ac5c7174c5 Mon Sep 17 00:00:00 2001 From: linglingye001 <143174321+linglingye001@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:48:51 +0800 Subject: [PATCH 3/5] Add tests for feature manager (#44) --- featuremanagement/feature_manager_test.go | 730 ++++++++++++++++++++++ 1 file changed, 730 insertions(+) create mode 100644 featuremanagement/feature_manager_test.go diff --git a/featuremanagement/feature_manager_test.go b/featuremanagement/feature_manager_test.go new file mode 100644 index 0000000..dac47a4 --- /dev/null +++ b/featuremanagement/feature_manager_test.go @@ -0,0 +1,730 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package featuremanagement + +import ( + "encoding/json" + "fmt" + "testing" +) + +// errorFilter is a custom filter that always returns an error +type errorFilter struct{} + +func (f *errorFilter) Name() string { return "ErrorFilter" } +func (f *errorFilter) Evaluate(_ FeatureFilterEvaluationContext, _ any) (bool, error) { + return false, fmt.Errorf("filter error") +} + +// errorProvider is a mock provider that always returns errors +type errorProvider struct{} + +func (p *errorProvider) GetFeatureFlag(name string) (FeatureFlag, error) { + return FeatureFlag{}, fmt.Errorf("provider error") +} +func (p *errorProvider) GetFeatureFlags() ([]FeatureFlag, error) { + return nil, fmt.Errorf("provider error") +} + +func TestNewFeatureManager_NilProvider(t *testing.T) { + _, err := NewFeatureManager(nil, nil) + if err == nil { + t.Error("Expected error when provider is nil, got nil") + } +} + +func TestNewFeatureManager_CustomFilters(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "CustomFilterFeature", + Enabled: true, + Conditions: &Conditions{ + ClientFilters: []ClientFilter{ + {Name: "AlwaysTrue"}, + }, + }, + }, + }, + } + + fm, err := NewFeatureManager(provider, &Options{ + Filters: []FeatureFilter{&alwaysTrueFilter{}}, + }) + if err != nil { + t.Fatalf("Failed to create feature manager: %v", err) + } + + enabled, err := fm.IsEnabled("CustomFilterFeature") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !enabled { + t.Error("Expected feature to be enabled with custom AlwaysTrue filter") + } +} + +func TestNewFeatureManager_NilFilterInOptions(t *testing.T) { + provider := &mockFeatureFlagProvider{} + _, err := NewFeatureManager(provider, &Options{ + Filters: []FeatureFilter{nil}, + }) + if err != nil { + t.Fatalf("Should not error with nil filter in options: %v", err) + } +} + +func TestGetFeatureNames(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + {ID: "Feature1", Enabled: true}, + {ID: "Feature2", Enabled: false}, + {ID: "Feature3", Enabled: true}, + }, + } + + fm, err := NewFeatureManager(provider, nil) + if err != nil { + t.Fatalf("Failed to create feature manager: %v", err) + } + + names := fm.GetFeatureNames() + if len(names) != 3 { + t.Fatalf("Expected 3 feature names, got %d", len(names)) + } + + expected := []string{"Feature1", "Feature2", "Feature3"} + for i, name := range names { + if name != expected[i] { + t.Errorf("Expected name %q at index %d, got %q", expected[i], i, name) + } + } +} + +func TestGetFeatureNames_Empty(t *testing.T) { + provider := &mockFeatureFlagProvider{featureFlags: []FeatureFlag{}} + fm, _ := NewFeatureManager(provider, nil) + + names := fm.GetFeatureNames() + if len(names) != 0 { + t.Errorf("Expected 0 feature names, got %d", len(names)) + } +} + +func TestGetFeatureNames_ProviderError(t *testing.T) { + fm, _ := NewFeatureManager(&errorProvider{}, nil) + + names := fm.GetFeatureNames() + if names != nil { + t.Errorf("Expected nil when provider errors, got %v", names) + } +} + +func TestUnknownFilter(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "UnknownFilterFeature", + Enabled: true, + Conditions: &Conditions{ + ClientFilters: []ClientFilter{ + {Name: "NonExistentFilter"}, + }, + }, + }, + }, + } + + fm, _ := NewFeatureManager(provider, nil) + + enabled, err := fm.IsEnabled("UnknownFilterFeature") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if enabled { + t.Error("Expected feature with unknown filter to be disabled") + } +} + +func TestRequirementTypeAll(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "AllFiltersTrue", + Enabled: true, + Conditions: &Conditions{ + RequirementType: RequirementTypeAll, + ClientFilters: []ClientFilter{ + {Name: "AlwaysTrue"}, + {Name: "AlwaysTrue"}, + }, + }, + }, + { + ID: "AllFiltersMixed", + Enabled: true, + Conditions: &Conditions{ + RequirementType: RequirementTypeAll, + ClientFilters: []ClientFilter{ + {Name: "AlwaysTrue"}, + {Name: "AlwaysFalse"}, + }, + }, + }, + }, + } + + fm, _ := NewFeatureManager(provider, &Options{ + Filters: []FeatureFilter{&alwaysTrueFilter{}, &alwaysFalseFilter{}}, + }) + + t.Run("All filters true", func(t *testing.T) { + enabled, err := fm.IsEnabled("AllFiltersTrue") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !enabled { + t.Error("Expected feature to be enabled when all filters return true") + } + }) + + t.Run("Mixed filters with RequirementTypeAll", func(t *testing.T) { + enabled, err := fm.IsEnabled("AllFiltersMixed") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if enabled { + t.Error("Expected feature to be disabled when one filter returns false with RequirementTypeAll") + } + }) +} + +func TestRequirementTypeAny(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "AnyFilterMixed", + Enabled: true, + Conditions: &Conditions{ + RequirementType: RequirementTypeAny, + ClientFilters: []ClientFilter{ + {Name: "AlwaysFalse"}, + {Name: "AlwaysTrue"}, + }, + }, + }, + { + ID: "AnyFilterAllFalse", + Enabled: true, + Conditions: &Conditions{ + RequirementType: RequirementTypeAny, + ClientFilters: []ClientFilter{ + {Name: "AlwaysFalse"}, + {Name: "AlwaysFalse"}, + }, + }, + }, + }, + } + + fm, _ := NewFeatureManager(provider, &Options{ + Filters: []FeatureFilter{&alwaysTrueFilter{}, &alwaysFalseFilter{}}, + }) + + t.Run("Any filter true", func(t *testing.T) { + enabled, _ := fm.IsEnabled("AnyFilterMixed") + if !enabled { + t.Error("Expected feature to be enabled when one filter returns true with RequirementTypeAny") + } + }) + + t.Run("All filters false", func(t *testing.T) { + enabled, _ := fm.IsEnabled("AnyFilterAllFalse") + if enabled { + t.Error("Expected feature to be disabled when all filters return false") + } + }) +} + +func TestFilterError(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "ErrorFilterFeature", + Enabled: true, + Conditions: &Conditions{ + ClientFilters: []ClientFilter{ + {Name: "ErrorFilter"}, + }, + }, + }, + }, + } + + fm, _ := NewFeatureManager(provider, &Options{ + Filters: []FeatureFilter{&errorFilter{}}, + }) + + _, err := fm.IsEnabled("ErrorFilterFeature") + if err == nil { + t.Error("Expected error from filter evaluation") + } +} + +func TestStatusOverrideEnabled(t *testing.T) { + jsonData := `{ + "feature_flags": [ + { + "id": "StatusOverrideEnabledFeature", + "enabled": true, + "variants": [ + { + "name": "MyVariant", + "configuration_value": "override-value", + "status_override": "Enabled" + } + ], + "allocation": { + "user": [ + { + "variant": "MyVariant", + "users": ["TestUser"] + } + ] + } + } + ] + }` + + var fm struct { + FeatureFlags []FeatureFlag `json:"feature_flags"` + } + json.Unmarshal([]byte(jsonData), &fm) + + provider := &mockFeatureFlagProvider{featureFlags: fm.FeatureFlags} + manager, _ := NewFeatureManager(provider, nil) + + ctx := TargetingContext{UserID: "TestUser"} + + enabled, err := manager.IsEnabledWithAppContext("StatusOverrideEnabledFeature", ctx) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !enabled { + t.Error("Expected feature to be enabled due to StatusOverride=Enabled") + } +} + +func TestGetVariant_NonExistentFeature(t *testing.T) { + provider := &mockFeatureFlagProvider{featureFlags: []FeatureFlag{}} + manager, _ := NewFeatureManager(provider, nil) + + variant, err := manager.GetVariant("NonExistent", nil) + if err == nil { + t.Error("Expected error for non-existent feature") + } + if variant != nil { + t.Error("Expected nil variant for non-existent feature") + } +} + +func TestGetVariant_NoVariants(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + {ID: "NoVariants", Enabled: true}, + }, + } + manager, _ := NewFeatureManager(provider, nil) + + variant, err := manager.GetVariant("NoVariants", TargetingContext{UserID: "user1"}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if variant != nil { + t.Error("Expected nil variant when feature has no variants") + } +} + +func TestGetVariant_NoAllocation(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "NoAllocation", + Enabled: true, + Variants: []VariantDefinition{ + {Name: "Small", ConfigurationValue: "300px"}, + }, + }, + }, + } + manager, _ := NewFeatureManager(provider, nil) + + variant, err := manager.GetVariant("NoAllocation", TargetingContext{UserID: "user1"}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if variant != nil { + t.Error("Expected nil variant when no allocation matches") + } +} + +func TestGetVariant_UserOverridesDefault(t *testing.T) { + jsonData := `{ + "feature_flags": [ + { + "id": "UserOverride", + "enabled": true, + "variants": [ + {"name": "Medium", "configuration_value": "450px"}, + {"name": "Small", "configuration_value": "300px"} + ], + "allocation": { + "default_when_enabled": "Medium", + "user": [ + {"variant": "Small", "users": ["Jeff"]} + ] + } + } + ] + }` + + var fm struct { + FeatureFlags []FeatureFlag `json:"feature_flags"` + } + json.Unmarshal([]byte(jsonData), &fm) + + provider := &mockFeatureFlagProvider{featureFlags: fm.FeatureFlags} + manager, _ := NewFeatureManager(provider, nil) + + variant, err := manager.GetVariant("UserOverride", TargetingContext{UserID: "Jeff"}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if variant == nil { + t.Fatal("Expected variant, got nil") + } + if variant.Name != "Small" { + t.Errorf("Expected user-allocated variant 'Small', got '%s'", variant.Name) + } +} + +func TestGetVariant_PointerTargetingContext(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "PtrCtx", + Enabled: true, + Variants: []VariantDefinition{ + {Name: "V1", ConfigurationValue: "val"}, + }, + Allocation: &VariantAllocation{ + User: []UserAllocation{ + {Variant: "V1", Users: []string{"user1"}}, + }, + }, + }, + }, + } + manager, _ := NewFeatureManager(provider, nil) + + ctx := &TargetingContext{UserID: "user1"} + variant, err := manager.GetVariant("PtrCtx", ctx) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if variant == nil || variant.Name != "V1" { + t.Error("Expected variant V1 when using pointer TargetingContext") + } +} + +func TestIsEnabledWithAppContext_NonTargetingContext(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + {ID: "SimpleEnabled", Enabled: true}, + }, + } + manager, _ := NewFeatureManager(provider, nil) + + enabled, err := manager.IsEnabledWithAppContext("SimpleEnabled", "some-string-context") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !enabled { + t.Error("Expected feature to be enabled") + } +} + +// Validation tests + +func TestValidation_EmptyID(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + {ID: "", Enabled: true}, + }, + } + manager, _ := NewFeatureManager(provider, nil) + + _, err := manager.IsEnabled("") + if err == nil { + t.Error("Expected validation error for empty feature ID") + } +} + +func TestValidation_InvalidRequirementType(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "BadReqType", + Enabled: true, + Conditions: &Conditions{ + RequirementType: "Invalid", + }, + }, + }, + } + manager, _ := NewFeatureManager(provider, nil) + + _, err := manager.IsEnabled("BadReqType") + if err == nil { + t.Error("Expected validation error for invalid requirement type") + } +} + +func TestValidation_VariantMissingName(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "BadVariant", + Enabled: true, + Variants: []VariantDefinition{{Name: ""}}, + }, + }, + } + manager, _ := NewFeatureManager(provider, nil) + + _, err := manager.IsEnabled("BadVariant") + if err == nil { + t.Error("Expected validation error for variant missing name") + } +} + +func TestValidation_InvalidStatusOverride(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "BadOverride", + Enabled: true, + Variants: []VariantDefinition{{Name: "V1", StatusOverride: "Invalid"}}, + }, + }, + } + manager, _ := NewFeatureManager(provider, nil) + + _, err := manager.IsEnabled("BadOverride") + if err == nil { + t.Error("Expected validation error for invalid status override") + } +} + +func TestValidation_InvalidPercentileRange(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "BadPercentile", + Enabled: true, + Allocation: &VariantAllocation{ + Percentile: []PercentileAllocation{ + {Variant: "V1", From: -1, To: 50}, + }, + }, + }, + }, + } + manager, _ := NewFeatureManager(provider, nil) + + _, err := manager.IsEnabled("BadPercentile") + if err == nil { + t.Error("Expected validation error for invalid percentile range") + } +} + +func TestValidation_UserAllocationEmptyUsers(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "EmptyUsers", + Enabled: true, + Allocation: &VariantAllocation{ + User: []UserAllocation{ + {Variant: "V1", Users: []string{}}, + }, + }, + }, + }, + } + manager, _ := NewFeatureManager(provider, nil) + + _, err := manager.IsEnabled("EmptyUsers") + if err == nil { + t.Error("Expected validation error for empty users list") + } +} + +func TestValidation_GroupAllocationEmptyGroups(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "EmptyGroups", + Enabled: true, + Allocation: &VariantAllocation{ + Group: []GroupAllocation{ + {Variant: "V1", Groups: []string{}}, + }, + }, + }, + }, + } + manager, _ := NewFeatureManager(provider, nil) + + _, err := manager.IsEnabled("EmptyGroups") + if err == nil { + t.Error("Expected validation error for empty groups list") + } +} + +func TestValidation_FilterMissingName(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "EmptyFilterName", + Enabled: true, + Conditions: &Conditions{ + ClientFilters: []ClientFilter{ + {Name: ""}, + }, + }, + }, + }, + } + manager, _ := NewFeatureManager(provider, nil) + + _, err := manager.IsEnabled("EmptyFilterName") + if err == nil { + t.Error("Expected validation error for filter missing name") + } +} + +func TestValidation_PercentileAllocationMissingVariant(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "NoVariantPercentile", + Enabled: true, + Allocation: &VariantAllocation{ + Percentile: []PercentileAllocation{ + {Variant: "", From: 0, To: 50}, + }, + }, + }, + }, + } + manager, _ := NewFeatureManager(provider, nil) + + _, err := manager.IsEnabled("NoVariantPercentile") + if err == nil { + t.Error("Expected validation error for percentile allocation missing variant") + } +} + +func TestValidation_UserAllocationMissingVariant(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "NoVariantUser", + Enabled: true, + Allocation: &VariantAllocation{ + User: []UserAllocation{ + {Variant: "", Users: []string{"user1"}}, + }, + }, + }, + }, + } + manager, _ := NewFeatureManager(provider, nil) + + _, err := manager.IsEnabled("NoVariantUser") + if err == nil { + t.Error("Expected validation error for user allocation missing variant") + } +} + +func TestValidation_GroupAllocationMissingVariant(t *testing.T) { + provider := &mockFeatureFlagProvider{ + featureFlags: []FeatureFlag{ + { + ID: "NoVariantGroup", + Enabled: true, + Allocation: &VariantAllocation{ + Group: []GroupAllocation{ + {Variant: "", Groups: []string{"group1"}}, + }, + }, + }, + }, + } + manager, _ := NewFeatureManager(provider, nil) + + _, err := manager.IsEnabled("NoVariantGroup") + if err == nil { + t.Error("Expected validation error for group allocation missing variant") + } +} + +func TestIsEnabled_ProviderError(t *testing.T) { + manager, _ := NewFeatureManager(&errorProvider{}, nil) + + _, err := manager.IsEnabled("AnyFeature") + if err == nil { + t.Error("Expected error when provider fails") + } +} + +func TestIsEnabledWithAppContext_ProviderError(t *testing.T) { + manager, _ := NewFeatureManager(&errorProvider{}, nil) + + _, err := manager.IsEnabledWithAppContext("AnyFeature", nil) + if err == nil { + t.Error("Expected error when provider fails") + } +} + +func TestGetVariant_ProviderError(t *testing.T) { + manager, _ := NewFeatureManager(&errorProvider{}, nil) + + _, err := manager.GetVariant("AnyFeature", nil) + if err == nil { + t.Error("Expected error when provider fails") + } +} + +func TestFeatureManagementStruct(t *testing.T) { + jsonData := `{ + "feature_flags": [ + {"id": "F1", "enabled": true}, + {"id": "F2", "enabled": false} + ] + }` + + var fm FeatureManagement + err := json.Unmarshal([]byte(jsonData), &fm) + if err != nil { + t.Fatalf("Failed to unmarshal FeatureManagement: %v", err) + } + if len(fm.FeatureFlags) != 2 { + t.Errorf("Expected 2 feature flags, got %d", len(fm.FeatureFlags)) + } + if fm.FeatureFlags[0].ID != "F1" { + t.Errorf("Expected first flag ID 'F1', got '%s'", fm.FeatureFlags[0].ID) + } +} From ab296cb926b0253b69b44ae0caf9fef1e94c1ff4 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:11:51 +0800 Subject: [PATCH 4/5] Version bump v1.1.2 (#45) * Initial plan * chore: bump featuremanagement module version to v1.1.2 Agent-Logs-Url: https://github.com/microsoft/FeatureManagement-Go/sessions/573e58f9-5445-4311-b948-b7bdfb0bd9d7 Co-authored-by: linglingye001 <143174321+linglingye001@users.noreply.github.com> * chore: finalize v1.1.2 version bump updates Agent-Logs-Url: https://github.com/microsoft/FeatureManagement-Go/sessions/573e58f9-5445-4311-b948-b7bdfb0bd9d7 Co-authored-by: linglingye001 <143174321+linglingye001@users.noreply.github.com> * chore: remove unintended example go.sum file Agent-Logs-Url: https://github.com/microsoft/FeatureManagement-Go/sessions/573e58f9-5445-4311-b948-b7bdfb0bd9d7 Co-authored-by: linglingye001 <143174321+linglingye001@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: linglingye001 <143174321+linglingye001@users.noreply.github.com> --- featuremanagement/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/featuremanagement/version.go b/featuremanagement/version.go index a3291b4..06e559d 100644 --- a/featuremanagement/version.go +++ b/featuremanagement/version.go @@ -3,4 +3,4 @@ package featuremanagement -const moduleVersion = "1.1.1" +const moduleVersion = "1.1.2" From 7445c72c040a9e95f1802adaa07cf9a27266c81b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:21:26 +0800 Subject: [PATCH 5/5] [WIP] Feature flag provider: update dependency v1.1.2 (#46) * Initial plan * chore(azappconfig): bump featuremanagement dependency to v1.1.2 Agent-Logs-Url: https://github.com/microsoft/FeatureManagement-Go/sessions/6c140c8e-7989-43c7-aad8-73be66c1d0e2 Co-authored-by: linglingye001 <143174321+linglingye001@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: linglingye001 <143174321+linglingye001@users.noreply.github.com> --- featuremanagement/providers/azappconfig/go.mod | 2 +- featuremanagement/providers/azappconfig/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/featuremanagement/providers/azappconfig/go.mod b/featuremanagement/providers/azappconfig/go.mod index c77adcd..2ac91d0 100644 --- a/featuremanagement/providers/azappconfig/go.mod +++ b/featuremanagement/providers/azappconfig/go.mod @@ -4,7 +4,7 @@ go 1.24.0 require github.com/Azure/AppConfiguration-GoProvider/azureappconfiguration v1.3.0 -require github.com/microsoft/Featuremanagement-Go/featuremanagement v1.1.1 +require github.com/microsoft/Featuremanagement-Go/featuremanagement v1.1.2 require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2 // indirect diff --git a/featuremanagement/providers/azappconfig/go.sum b/featuremanagement/providers/azappconfig/go.sum index a896c60..b5fe59b 100644 --- a/featuremanagement/providers/azappconfig/go.sum +++ b/featuremanagement/providers/azappconfig/go.sum @@ -24,8 +24,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/microsoft/Featuremanagement-Go/featuremanagement v1.1.1 h1:kZDIZTTja0WzFMjeqVdCSPsZdKGJrKo3gyW11uOfBWI= -github.com/microsoft/Featuremanagement-Go/featuremanagement v1.1.1/go.mod h1:F+TJy4hpwRn+y6tOGkFQ837JGiUEi2aqIB5+qADM4z4= +github.com/microsoft/Featuremanagement-Go/featuremanagement v1.1.2 h1:/EWQK2C5GV4DDwTeIH0awPqiq8/pUp2l/zkoA8i9r3I= +github.com/microsoft/Featuremanagement-Go/featuremanagement v1.1.2/go.mod h1:F+TJy4hpwRn+y6tOGkFQ837JGiUEi2aqIB5+qADM4z4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=