-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtenant_config_override_test.go
More file actions
428 lines (373 loc) · 14.2 KB
/
tenant_config_override_test.go
File metadata and controls
428 lines (373 loc) · 14.2 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
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
package reverseproxy
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/GoCodeAlone/modular"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
// TestTenantRequestTimeoutOverride verifies that tenant-specific request_timeout overrides global configuration
func TestTenantRequestTimeoutOverride(t *testing.T) {
// Backend that sleeps 2.5 seconds but respects context cancellation
slowBackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case <-time.After(2500 * time.Millisecond):
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "delayed"})
case <-r.Context().Done():
// Context cancelled - don't write anything, just return
return
}
}))
defer slowBackend.Close()
// Create mock application
mockApp := &mockTenantApplication{}
mockApp.On("Logger").Return(&mockLogger{})
// Create router service
router := NewMockRouter()
tenantID := modular.TenantID("tenant1")
// Global config with 30s timeout
globalConfig := &ReverseProxyConfig{
BackendServices: map[string]string{
"default": slowBackend.URL,
},
Routes: map[string]string{
"/api/test": "default",
},
DefaultBackend: "default",
RequestTimeout: 30 * time.Second, // Global: 30 second timeout
TenantIDHeader: "X-Affiliate-Id",
RequireTenantID: true,
}
// Tenant config with 1s timeout - should override global 30s
tenantConfig := &ReverseProxyConfig{
BackendServices: map[string]string{
"default": slowBackend.URL,
},
RequestTimeout: 1 * time.Second, // Tenant: 1 second timeout (should override)
}
// Configure mock app
mockCP := NewStdConfigProvider(globalConfig)
tenantMockCP := NewStdConfigProvider(tenantConfig)
mockApp.On("GetConfigSection", "reverseproxy").Return(mockCP, nil)
mockApp.On("GetTenantConfig", tenantID, "reverseproxy").Return(tenantMockCP, nil)
mockApp.On("ConfigProvider").Return(mockCP)
mockApp.On("ConfigSections").Return(map[string]modular.ConfigProvider{
"reverseproxy": mockCP,
})
mockApp.On("RegisterModule", mock.Anything).Return()
mockApp.On("RegisterConfigSection", mock.Anything, mock.Anything).Return()
mockApp.On("SvcRegistry").Return(map[string]any{})
mockApp.On("RegisterService", mock.Anything, mock.Anything).Return(nil)
mockApp.On("GetService", mock.Anything, mock.Anything).Return(nil)
mockApp.On("Init").Return(nil)
mockApp.On("Start").Return(nil)
mockApp.On("Stop").Return(nil)
mockApp.On("Run").Return(nil)
mockApp.On("GetTenants").Return([]modular.TenantID{tenantID})
mockApp.On("RegisterTenant", mock.Anything, mock.Anything).Return(nil)
mockApp.On("RemoveTenant", mock.Anything).Return(nil)
mockApp.On("RegisterTenantAwareModule", mock.Anything).Return(nil)
mockApp.On("GetTenantService").Return(nil, nil)
mockApp.On("WithTenant", mock.Anything).Return(&modular.TenantContext{}, nil)
router.On("HandleFunc", "/api/test", mock.AnythingOfType("http.HandlerFunc")).Return()
router.On("HandleFunc", "/*", mock.AnythingOfType("http.HandlerFunc")).Return()
router.On("Use", mock.Anything).Return()
// Create and initialize module
module := NewModule()
module.app = mockApp
// Register tenant before initialization
module.OnTenantRegistered(tenantID)
err := module.Init(mockApp)
require.NoError(t, err)
module.router = router
err = module.Start(context.Background())
require.NoError(t, err)
// Verify the merged config has the tenant timeout
mergedCfg, exists := module.tenants[tenantID]
require.True(t, exists, "Tenant config should exist")
assert.Equal(t, 1*time.Second, mergedCfg.RequestTimeout, "Merged config should have tenant's 1s timeout")
// Get the captured handler
var capturedHandler http.HandlerFunc
for _, call := range router.Calls {
if call.Method == "HandleFunc" && call.Arguments[0].(string) == "/api/test" {
capturedHandler = call.Arguments[1].(http.HandlerFunc)
break
}
}
require.NotNil(t, capturedHandler, "Handler should have been captured")
// Make request with tenant header
start := time.Now()
req := httptest.NewRequest("GET", "/api/test", nil)
req.Header.Set("X-Affiliate-Id", string(tenantID))
rr := httptest.NewRecorder()
capturedHandler(rr, req)
duration := time.Since(start)
// CRITICAL TEST: Verify tenant timeout (1s) was used, not global timeout (30s)
// Backend sleeps 2.5s, so with 1s timeout it should fail around 1s
// If global timeout (30s) was incorrectly used, duration would be ~2.5s
assert.True(t, duration < 2*time.Second,
"Expected timeout around 1s (tenant), got %v - tenant timeout not being used!", duration)
assert.True(t, duration >= 900*time.Millisecond,
"Timeout should be at least 900ms, got %v", duration)
// Status should indicate timeout (504 Gateway Timeout or 502 Bad Gateway)
assert.True(t, rr.Code == http.StatusGatewayTimeout || rr.Code == http.StatusBadGateway,
"Expected 504 or 502, got %d", rr.Code)
}
// TestTenantCacheTTLOverride verifies that tenant-specific cache_ttl overrides global configuration
func TestTenantCacheTTLOverride(t *testing.T) {
globalConfig := &ReverseProxyConfig{
CacheEnabled: true,
CacheTTL: 120 * time.Second, // Global: 120s
}
tenantConfig := &ReverseProxyConfig{
CacheEnabled: true,
CacheTTL: 60 * time.Second, // Tenant: 60s (should override)
}
merged := mergeConfigs(globalConfig, tenantConfig)
assert.True(t, merged.CacheEnabled, "Cache should be enabled")
assert.Equal(t, 60*time.Second, merged.CacheTTL,
"Merged config should use tenant's 60s CacheTTL, not global 120s")
}
// TestTenantMetricsPathOverride verifies that tenant-specific metrics_path overrides global configuration
func TestTenantMetricsPathOverride(t *testing.T) {
globalConfig := &ReverseProxyConfig{
MetricsEnabled: true,
MetricsPath: "/metrics/global",
}
tenantConfig := &ReverseProxyConfig{
MetricsEnabled: true,
MetricsPath: "/metrics/tenant1",
}
merged := mergeConfigs(globalConfig, tenantConfig)
assert.True(t, merged.MetricsEnabled, "Metrics should be enabled")
assert.Equal(t, "/metrics/tenant1", merged.MetricsPath,
"Merged config should use tenant's metrics path, not global")
}
// TestTenantFeatureFlagsOverride documents that tenant-specific feature flags are NOT currently merged
// The mergeConfigs function doesn't perform deep merging of the FeatureFlags.Flags map,
// resulting in zero values for the entire FeatureFlags struct in the merged configuration.
// TODO: This is a known limitation that should be addressed in a separate issue
func TestTenantFeatureFlagsOverride(t *testing.T) {
globalConfig := &ReverseProxyConfig{
FeatureFlags: FeatureFlagsConfig{
Enabled: true,
Flags: map[string]bool{
"feature_a": true,
"feature_b": false,
},
},
}
tenantConfig := &ReverseProxyConfig{
FeatureFlags: FeatureFlagsConfig{
Enabled: true,
Flags: map[string]bool{
"feature_a": false, // Override to false
"feature_c": true, // New flag
},
},
}
merged := mergeConfigs(globalConfig, tenantConfig)
// KNOWN LIMITATION: mergeConfigs doesn't perform deep merging of FeatureFlags
// The merged config will have zero values for the entire FeatureFlags struct
assert.False(t, merged.FeatureFlags.Enabled,
"KNOWN LIMITATION: FeatureFlags struct is zero-valued in merged config because deep merging is not implemented")
t.Log("NOTE: FeatureFlags deep merging is not implemented. The entire FeatureFlags struct remains zero-valued in merged configs. This is a separate issue to be addressed.")
}
// TestTenantCircuitBreakerOverride verifies that tenant-specific circuit breaker config overrides global
func TestTenantCircuitBreakerOverride(t *testing.T) {
globalConfig := &ReverseProxyConfig{
CircuitBreakerConfig: CircuitBreakerConfig{
Enabled: true,
FailureThreshold: 5,
OpenTimeout: 30 * time.Second,
},
}
tenantConfig := &ReverseProxyConfig{
CircuitBreakerConfig: CircuitBreakerConfig{
Enabled: true,
FailureThreshold: 3, // Override to 3
OpenTimeout: 20 * time.Second, // Override to 20s
},
}
merged := mergeConfigs(globalConfig, tenantConfig)
assert.True(t, merged.CircuitBreakerConfig.Enabled, "Circuit breaker should be enabled")
assert.Equal(t, 3, merged.CircuitBreakerConfig.FailureThreshold,
"Merged config should use tenant's failure threshold (3), not global (5)")
assert.Equal(t, 20*time.Second, merged.CircuitBreakerConfig.OpenTimeout,
"Merged config should use tenant's open timeout (20s), not global (30s)")
}
// TestTenantHealthCheckOverride verifies that tenant-specific health check config overrides global
func TestTenantHealthCheckOverride(t *testing.T) {
globalConfig := &ReverseProxyConfig{
HealthCheck: HealthCheckConfig{
Enabled: true,
Interval: 30 * time.Second,
Timeout: 5 * time.Second,
},
}
tenantConfig := &ReverseProxyConfig{
HealthCheck: HealthCheckConfig{
Enabled: true,
Interval: 60 * time.Second, // Override to 60s
Timeout: 10 * time.Second, // Override to 10s
},
}
merged := mergeConfigs(globalConfig, tenantConfig)
assert.True(t, merged.HealthCheck.Enabled, "Health check should be enabled")
assert.Equal(t, 60*time.Second, merged.HealthCheck.Interval,
"Merged config should use tenant's interval (60s), not global (30s)")
assert.Equal(t, 10*time.Second, merged.HealthCheck.Timeout,
"Merged config should use tenant's timeout (10s), not global (5s)")
}
// TestTenantBackendServicesOverride verifies that tenant-specific backend services override global ones
func TestTenantBackendServicesOverride(t *testing.T) {
globalConfig := &ReverseProxyConfig{
BackendServices: map[string]string{
"default": "http://global-backend.example.com",
"service": "http://global-service.example.com",
},
DefaultBackend: "default",
}
tenantConfig := &ReverseProxyConfig{
BackendServices: map[string]string{
"default": "http://tenant-backend.example.com", // Override
},
DefaultBackend: "default",
}
merged := mergeConfigs(globalConfig, tenantConfig)
assert.Equal(t, "http://tenant-backend.example.com", merged.BackendServices["default"],
"Merged config should use tenant's backend URL, not global")
assert.Equal(t, "http://global-service.example.com", merged.BackendServices["service"],
"Non-overridden global backends should be preserved")
assert.Equal(t, "default", merged.DefaultBackend, "Default backend should be preserved")
}
// TestTenantRoutesOverride verifies that tenant-specific routes override global routes
func TestTenantRoutesOverride(t *testing.T) {
globalConfig := &ReverseProxyConfig{
Routes: map[string]string{
"/api/v1/*": "backend1",
"/api/admin/*": "admin",
},
}
tenantConfig := &ReverseProxyConfig{
Routes: map[string]string{
"/api/v1/*": "backend2", // Override
"/api/tenant/*": "tenant", // New route
},
}
merged := mergeConfigs(globalConfig, tenantConfig)
assert.Equal(t, "backend2", merged.Routes["/api/v1/*"],
"Merged config should use tenant's route mapping, not global")
assert.Equal(t, "admin", merged.Routes["/api/admin/*"],
"Non-overridden global routes should be preserved")
assert.Equal(t, "tenant", merged.Routes["/api/tenant/*"],
"Tenant-specific routes should be added")
}
// TestMergeConfigsRequestTimeout specifically tests the RequestTimeout merge logic
func TestMergeConfigsRequestTimeout(t *testing.T) {
tests := []struct {
name string
globalTimeout time.Duration
tenantTimeout time.Duration
expectedTimeout time.Duration
description string
}{
{
name: "tenant overrides global",
globalTimeout: 30 * time.Second,
tenantTimeout: 60 * time.Second,
expectedTimeout: 60 * time.Second,
description: "When tenant specifies timeout, it should override global",
},
{
name: "tenant not specified, use global",
globalTimeout: 30 * time.Second,
tenantTimeout: 0,
expectedTimeout: 30 * time.Second,
description: "When tenant doesn't specify timeout (0), use global",
},
{
name: "both zero",
globalTimeout: 0,
tenantTimeout: 0,
expectedTimeout: 0,
description: "When both are zero, merged should be zero",
},
{
name: "only tenant specified",
globalTimeout: 0,
tenantTimeout: 45 * time.Second,
expectedTimeout: 45 * time.Second,
description: "When only tenant specifies timeout, use tenant value",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
globalConfig := &ReverseProxyConfig{
RequestTimeout: tt.globalTimeout,
}
tenantConfig := &ReverseProxyConfig{
RequestTimeout: tt.tenantTimeout,
}
merged := mergeConfigs(globalConfig, tenantConfig)
assert.Equal(t, tt.expectedTimeout, merged.RequestTimeout, tt.description)
})
}
}
// TestMergeConfigsGlobalTimeout specifically tests the GlobalTimeout merge logic
func TestMergeConfigsGlobalTimeout(t *testing.T) {
tests := []struct {
name string
globalTimeout time.Duration
tenantTimeout time.Duration
expectedTimeout time.Duration
description string
}{
{
name: "tenant overrides global",
globalTimeout: 30 * time.Second,
tenantTimeout: 60 * time.Second,
expectedTimeout: 60 * time.Second,
description: "When tenant specifies GlobalTimeout, it should override global",
},
{
name: "tenant not specified, use global",
globalTimeout: 30 * time.Second,
tenantTimeout: 0,
expectedTimeout: 30 * time.Second,
description: "When tenant doesn't specify GlobalTimeout (0), use global",
},
{
name: "both zero",
globalTimeout: 0,
tenantTimeout: 0,
expectedTimeout: 0,
description: "When both are zero, merged should be zero",
},
{
name: "only tenant specified",
globalTimeout: 0,
tenantTimeout: 45 * time.Second,
expectedTimeout: 45 * time.Second,
description: "When only tenant specifies GlobalTimeout, use tenant value",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
globalConfig := &ReverseProxyConfig{
GlobalTimeout: tt.globalTimeout,
}
tenantConfig := &ReverseProxyConfig{
GlobalTimeout: tt.tenantTimeout,
}
merged := mergeConfigs(globalConfig, tenantConfig)
assert.Equal(t, tt.expectedTimeout, merged.GlobalTimeout, tt.description)
})
}
}