-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathintegration_test.go
More file actions
402 lines (337 loc) · 11.8 KB
/
integration_test.go
File metadata and controls
402 lines (337 loc) · 11.8 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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
package reverseproxy
import (
"context"
"errors"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"reflect"
"testing"
"github.com/GoCodeAlone/modular"
)
// Integration tests for the complete feature flag aggregator system
// ExternalEvaluator simulates a third-party feature flag evaluator module
type ExternalEvaluator struct {
name string
weight int
}
func (e *ExternalEvaluator) Name() string {
return e.name
}
func (e *ExternalEvaluator) Init(app modular.Application) error {
return nil
}
func (e *ExternalEvaluator) Dependencies() []string {
return nil
}
func (e *ExternalEvaluator) ProvidesServices() []modular.ServiceProvider {
return []modular.ServiceProvider{
{
Name: "featureFlagEvaluator.external",
Instance: &ExternalEvaluatorService{weight: e.weight},
},
}
}
func (e *ExternalEvaluator) RequiresServices() []modular.ServiceDependency {
return nil
}
// ExternalEvaluatorService implements FeatureFlagEvaluator and WeightedEvaluator
type ExternalEvaluatorService struct {
weight int
}
func (e *ExternalEvaluatorService) EvaluateFlag(ctx context.Context, flagID string, tenantID modular.TenantID, req *http.Request) (bool, error) {
switch flagID {
case "external-only-flag":
return true, nil
case "priority-test-flag":
return true, nil // External should win over file evaluator due to lower weight
default:
return false, ErrNoDecision // Let other evaluators handle
}
}
func (e *ExternalEvaluatorService) EvaluateFlagWithDefault(ctx context.Context, flagID string, tenantID modular.TenantID, req *http.Request, defaultValue bool) bool {
result, err := e.EvaluateFlag(ctx, flagID, tenantID, req)
if err != nil {
return defaultValue
}
return result
}
func (e *ExternalEvaluatorService) Weight() int {
return e.weight
}
// TestCompleteFeatureFlagSystem tests the entire aggregator system end-to-end
func TestCompleteFeatureFlagSystem(t *testing.T) {
// Create logger
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
// Create application
app := NewMockTenantApplication()
// Register tenant service
tenantService := modular.NewStandardTenantService(logger)
err := app.RegisterService("tenantService", tenantService)
if err != nil {
t.Fatalf("Failed to register tenant service: %v", err)
}
// Create reverseproxy configuration with file-based flags
rpConfig := &ReverseProxyConfig{
FeatureFlags: FeatureFlagsConfig{
Enabled: true,
Flags: map[string]bool{
"file-only-flag": true,
"priority-test-flag": false, // External should override this
"fallback-flag": true,
},
},
BackendServices: map[string]string{
"test": "http://localhost:8080",
},
}
app.RegisterConfigSection("reverseproxy", modular.NewStdConfigProvider(rpConfig))
// Create and register external evaluator module (simulates third-party module)
externalModule := &ExternalEvaluator{
name: "external-evaluator",
weight: 50, // Higher priority than file evaluator (weight 1000)
}
// Create mock router for reverseproxy
router := &MockRouter{}
err = app.RegisterService("router", router)
if err != nil {
t.Fatalf("Failed to register router: %v", err)
}
// Register reverseproxy module
rpModule := NewModule()
// Register modules
app.RegisterModule(externalModule)
app.RegisterModule(rpModule)
// Initialize application
err = app.Init()
if err != nil {
t.Fatalf("Failed to initialize application: %v", err)
}
// Get the reverseproxy module instance to test its evaluator
var modules []modular.Module
for _, m := range []modular.Module{externalModule, rpModule} {
modules = append(modules, m)
}
// Find the initialized reverseproxy module
var initializedRP *ReverseProxyModule
for _, m := range modules {
if rp, ok := m.(*ReverseProxyModule); ok {
initializedRP = rp
break
}
}
if initializedRP == nil {
t.Fatal("Could not find initialized ReverseProxyModule")
}
// Test the aggregator behavior
req := httptest.NewRequest("GET", "/test", nil)
t.Run("External evaluator takes precedence", func(t *testing.T) {
// External-only flag should work
result := initializedRP.evaluateFeatureFlag("external-only-flag", req)
if !result {
t.Error("Expected external-only-flag to be true from external evaluator")
}
})
t.Run("Priority ordering works", func(t *testing.T) {
// Priority test flag: external (true) should override file (false)
result := initializedRP.evaluateFeatureFlag("priority-test-flag", req)
if !result {
t.Error("Expected external evaluator to override file evaluator for priority-test-flag")
}
})
t.Run("Fallback to file evaluator", func(t *testing.T) {
// Fallback flag should work through file evaluator
result := initializedRP.evaluateFeatureFlag("fallback-flag", req)
if !result {
t.Error("Expected fallback-flag to work through file evaluator")
}
})
t.Run("Unknown flags return default", func(t *testing.T) {
// Unknown flags should return default (true for reverseproxy)
result := initializedRP.evaluateFeatureFlag("unknown-flag", req)
if !result {
t.Error("Expected unknown flag to return default value (true)")
}
})
}
// TestBackwardsCompatibility tests that existing evaluator usage still works
func TestBackwardsCompatibility(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
// Create application
app := NewMockTenantApplication()
// Register tenant service
tenantService := modular.NewStandardTenantService(logger)
err := app.RegisterService("tenantService", tenantService)
if err != nil {
t.Fatalf("Failed to register tenant service: %v", err)
}
// Create configuration
rpConfig := &ReverseProxyConfig{
FeatureFlags: FeatureFlagsConfig{
Enabled: true,
Flags: map[string]bool{
"test-flag": true,
},
},
BackendServices: map[string]string{
"test": "http://localhost:8080",
},
}
app.RegisterConfigSection("reverseproxy", modular.NewStdConfigProvider(rpConfig))
// Test that file-based evaluator can be created directly (backwards compatibility)
fileEvaluator, err := NewFileBasedFeatureFlagEvaluator(context.Background(), app, logger)
if err != nil {
t.Fatalf("Failed to create file-based evaluator: %v", err)
}
// Test that it can evaluate flags
req := httptest.NewRequest("GET", "/test", nil)
result, err := fileEvaluator.EvaluateFlag(context.Background(), "test-flag", "", req)
if err != nil {
t.Fatalf("Failed to evaluate flag: %v", err)
}
if !result {
t.Error("Expected test-flag to be true")
}
// Test default value behavior
defaultResult := fileEvaluator.EvaluateFlagWithDefault(context.Background(), "unknown-flag", "", req, true)
if !defaultResult {
t.Error("Expected unknown flag to return default value true")
}
// Test that aggregator can be created and works with file evaluator
err = app.RegisterService("featureFlagEvaluator.file", fileEvaluator)
if err != nil {
t.Fatalf("Failed to register file evaluator: %v", err)
}
aggregator := NewFeatureFlagAggregator(app, logger)
// Test aggregator with just the file evaluator
result, err = aggregator.EvaluateFlag(context.Background(), "test-flag", "", req)
if err != nil {
t.Fatalf("Aggregator failed to evaluate flag: %v", err)
}
if !result {
t.Error("Expected aggregator to return true for test-flag via file evaluator")
}
}
// TestServiceExposure tests that the aggregator properly exposes services
func TestServiceExposure(t *testing.T) {
// Test that a basic module provides the expected service structure
rpModule := NewModule()
// Before configuration, should provide minimal services
initialServices := rpModule.ProvidesServices()
if len(initialServices) != 0 {
t.Errorf("Expected no services before configuration, got %d", len(initialServices))
}
// Test that services are provided after configuration
rpModule.config = &ReverseProxyConfig{
FeatureFlags: FeatureFlagsConfig{Enabled: true},
}
// Create a dummy aggregator for testing
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
app := NewMockTenantApplication()
rpModule.featureFlagEvaluator = NewFeatureFlagAggregator(app, logger)
// Now should provide services
services := rpModule.ProvidesServices()
// Should provide both reverseproxy.provider and featureFlagEvaluator services
var hasProvider, hasEvaluator bool
for _, svc := range services {
switch svc.Name {
case "reverseproxy.provider":
hasProvider = true
case "featureFlagEvaluator":
hasEvaluator = true
}
}
if !hasProvider {
t.Error("Expected reverseproxy.provider service to be provided")
}
if !hasEvaluator {
t.Error("Expected featureFlagEvaluator service to be provided")
}
}
// TestNoCyclePrevention tests that the cycle prevention mechanisms work
func TestNoCyclePrevention(t *testing.T) {
// This test would create a scenario where an external evaluator
// depends on reverseproxy's featureFlagEvaluator service, creating a potential cycle.
// The system should handle this by using proper service naming.
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
app := NewMockTenantApplication()
// Create a potentially problematic external module that tries to depend on reverseproxy
problematicModule := &ProblematicExternalModule{name: "problematic"}
rpModule := NewModule()
// Register tenant service
tenantService := modular.NewStandardTenantService(logger)
err := app.RegisterService("tenantService", tenantService)
if err != nil {
t.Fatalf("Failed to register tenant service: %v", err)
}
// Create configuration
rpConfig := &ReverseProxyConfig{
FeatureFlags: FeatureFlagsConfig{
Enabled: true,
Flags: map[string]bool{"test": true},
},
BackendServices: map[string]string{
"test": "http://localhost:8080",
},
}
app.RegisterConfigSection("reverseproxy", modular.NewStdConfigProvider(rpConfig))
// Create mock router
router := &MockRouter{}
err = app.RegisterService("router", router)
if err != nil {
t.Fatalf("Failed to register router: %v", err)
}
app.RegisterModule(rpModule)
app.RegisterModule(problematicModule)
// This should initialize without cycle errors due to proper service naming
err = app.Init()
if err != nil {
// If there's an error, it shouldn't be a cycle error since we use proper naming
if errors.Is(err, modular.ErrCircularDependency) {
t.Errorf("Unexpected cycle error with proper service naming: %v", err)
}
}
}
// ProblematicExternalModule tries to create a cycle by depending on featureFlagEvaluator
type ProblematicExternalModule struct {
name string
}
func (m *ProblematicExternalModule) Name() string {
return m.name
}
func (m *ProblematicExternalModule) Init(app modular.Application) error {
return nil
}
func (m *ProblematicExternalModule) Dependencies() []string {
return nil
}
func (m *ProblematicExternalModule) ProvidesServices() []modular.ServiceProvider {
return []modular.ServiceProvider{
{
Name: "featureFlagEvaluator.problematic",
Instance: &SimpleEvaluator{},
},
}
}
func (m *ProblematicExternalModule) RequiresServices() []modular.ServiceDependency {
// This module tries to consume the featureFlagEvaluator service
// In the old system, this could create a cycle
// In the new system, it should be safe due to aggregator pattern
return []modular.ServiceDependency{
{
Name: "featureFlagEvaluator",
Required: false,
MatchByInterface: true,
SatisfiesInterface: reflect.TypeOf((*FeatureFlagEvaluator)(nil)).Elem(),
},
}
}
// SimpleEvaluator is a basic evaluator implementation
type SimpleEvaluator struct{}
func (s *SimpleEvaluator) EvaluateFlag(ctx context.Context, flagID string, tenantID modular.TenantID, req *http.Request) (bool, error) {
return false, ErrNoDecision
}
func (s *SimpleEvaluator) EvaluateFlagWithDefault(ctx context.Context, flagID string, tenantID modular.TenantID, req *http.Request, defaultValue bool) bool {
return defaultValue
}