-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathexternal_evaluator_fallback_bug_test.go
More file actions
145 lines (121 loc) · 5.75 KB
/
external_evaluator_fallback_bug_test.go
File metadata and controls
145 lines (121 loc) · 5.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package reverseproxy
import (
"context"
"net/http"
"testing"
"github.com/GoCodeAlone/modular"
"github.com/stretchr/testify/mock"
)
// mockExternalEvaluatorReturnsErrNoDecision simulates an external evaluator (like LaunchDarkly)
// that is configured but returns ErrNoDecision due to initialization failures
type mockExternalEvaluatorReturnsErrNoDecision struct{}
func (m *mockExternalEvaluatorReturnsErrNoDecision) EvaluateFlag(ctx context.Context, flagID string, tenantID modular.TenantID, req *http.Request) (bool, error) {
// Simulate external evaluator that's configured but not working (e.g., invalid SDK key)
return false, ErrNoDecision
}
func (m *mockExternalEvaluatorReturnsErrNoDecision) EvaluateFlagWithDefault(ctx context.Context, flagID string, tenantID modular.TenantID, req *http.Request, defaultValue bool) bool {
result, err := m.EvaluateFlag(ctx, flagID, tenantID, req)
if err != nil {
return defaultValue
}
return result
}
func (m *mockExternalEvaluatorReturnsErrNoDecision) Weight() int {
return 50 // Higher priority than file evaluator (weight 1000)
}
// TestExternalEvaluatorFallbackFix reproduces and verifies the fix for the bug described in the issue.
//
// The bug: When external evaluator is provided via constructor, it bypassed the aggregator entirely,
// so when it returned ErrNoDecision, there was no fallback to file-based evaluator.
//
// The fix: Always use aggregator pattern, register external evaluator for discovery,
// ensuring proper fallback chain: External → File → Safe defaults.
func TestExternalEvaluatorFallbackFix(t *testing.T) {
// Create a mock application
moduleApp := NewMockTenantApplication()
// Configure reverseproxy with feature flags enabled and a flag set to true
config := &ReverseProxyConfig{
BackendServices: map[string]string{
"primary": "http://127.0.0.1:18080",
"alternative": "http://127.0.0.1:18081",
},
FeatureFlags: FeatureFlagsConfig{
Enabled: true,
Flags: map[string]bool{
"my-api": false, // This should route to alternative backend when external evaluator abstains
},
},
}
// Register the configuration
moduleApp.RegisterConfigSection("reverseproxy", modular.NewStdConfigProvider(config))
// Create the module
module := NewModule()
// Create a mock router and set up expected calls
mockRouter := &MockRouter{}
// Allow any HandleFunc calls
mockRouter.On("HandleFunc", mock.Anything, mock.Anything).Return()
// Create an external evaluator that returns ErrNoDecision (simulating misconfigured LaunchDarkly)
externalEvaluator := &mockExternalEvaluatorReturnsErrNoDecision{}
// Provide services via constructor - this is where the bug occurs
services := map[string]any{
"router": mockRouter,
"featureFlagEvaluator": externalEvaluator, // This bypasses the aggregator
}
constructedModule, err := module.Constructor()(moduleApp, services)
if err != nil {
t.Fatalf("Failed to construct module: %v", err)
}
module = constructedModule.(*ReverseProxyModule)
// Set up the configuration first
err = module.RegisterConfig(moduleApp)
if err != nil {
t.Fatalf("Failed to register config: %v", err)
}
// Initialize the module (this sets up the feature flag evaluator)
if err := module.Init(moduleApp); err != nil {
t.Fatalf("Failed to initialize module: %v", err)
}
// Start the module (this calls setupFeatureFlagEvaluation)
if err := module.Start(context.Background()); err != nil {
t.Fatalf("Failed to start module: %v", err)
}
// Test that the fix works correctly
req, _ := http.NewRequestWithContext(context.Background(), "GET", "/test", nil)
// Test the evaluateFeatureFlag method - should now use aggregator with fallback
result := module.evaluateFeatureFlag("my-api", req)
// Verify the fix: now uses aggregator instead of external evaluator directly
if _, isAggregator := module.featureFlagEvaluator.(*FeatureFlagAggregator); !isAggregator {
t.Errorf("Expected module to use aggregator after fix, got: %T", module.featureFlagEvaluator)
}
// The aggregator should now call external evaluator, get ErrNoDecision, then fallback to file evaluator
t.Logf("evaluateFeatureFlag result: %v", result)
t.Logf("featureFlagEvaluatorProvided: %v", module.featureFlagEvaluatorProvided)
t.Logf("featureFlagEvaluator type: %T", module.featureFlagEvaluator)
// Verify the fix: should return file evaluator result (false) instead of hard-coded default (true)
var fileEvaluator FeatureFlagEvaluator
if err := module.app.GetService("featureFlagEvaluator.file", &fileEvaluator); err == nil {
fileResult, fileErr := fileEvaluator.EvaluateFlag(context.Background(), "my-api", "", req)
t.Logf("File evaluator returns: %v, error: %v", fileResult, fileErr)
if fileErr == nil && fileResult == result {
t.Logf("SUCCESS: External evaluator fallback now correctly returns file evaluator result (%v)", fileResult)
} else {
t.Errorf("FAIL: Expected aggregator result (%v) to match file evaluator result (%v)", result, fileResult)
}
} else {
t.Logf("File evaluator not registered: %v", err)
}
// Verify that external evaluator was registered and discoverable
var registeredExternalEvaluator FeatureFlagEvaluator
if err := module.app.GetService("featureFlagEvaluator.external", ®isteredExternalEvaluator); err == nil {
t.Logf("SUCCESS: External evaluator registered and discoverable by aggregator")
// Test that external evaluator still returns ErrNoDecision when called directly
_, extErr := registeredExternalEvaluator.EvaluateFlag(context.Background(), "my-api", "", req)
if extErr == ErrNoDecision {
t.Logf("SUCCESS: External evaluator correctly returns ErrNoDecision")
} else {
t.Errorf("Expected external evaluator to return ErrNoDecision, got: %v", extErr)
}
} else {
t.Errorf("FAIL: External evaluator not registered for discovery: %v", err)
}
}