mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
2d1b04c637
* Multiple fixes - refresh coordinator dedup + memory pressure wire - middleware sse consolidation + timer leak + claim cache - universal cache sync backfill + isDebug gate - lazy background task race - memory monitor stw cached + refresh() api * fix(auth): suppress OIDC redirects on non-navigation requests - [x] Add isNonNavigationRequest using Sec-Fetch-Mode and Accept headers - [x] Add comprehensive TestIsNonNavigationRequest - [x] Update ServeHTTP to 401 non-navigation and AJAX requests Fixes #129 * feat(config): add custom CA and insecure skip verify for OIDC TLS - [x] Add CACertPath, CACertPEM, InsecureSkipVerify to Config - [x] Implement loadCACertPool for CA bundle loading - [x] Update HTTPClientConfig with RootCAs and InsecureSkipVerify - [x] Apply CA pool and skip verify to pooled HTTP clients - [x] Enhance configKey to distinguish TLS configs - [x] Add comprehensive ca_cert_test.go Fixes #125 * feat(oidc): add custom CA certificate support for private OIDC providers - [x] Add caCertPath, caCertPEM, insecureSkipVerify config options - [x] Update traefik.yml with new OIDC client config fields - [x] Add configuration schema descriptions for new options - [x] Update README table and add Custom CA Certificates section * Fix the documentation. * test(redis): add oversized argument rejection test - [x] Add TestRedisConn_RejectOversizedArgumentBytes - [x] Import strings package * Dependencies cleanup
546 lines
13 KiB
Go
546 lines
13 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// TestMemoryMonitorComprehensive tests memory monitor edge cases
|
|
func TestMemoryMonitorComprehensive(t *testing.T) {
|
|
t.Run("TriggerGC calls runtime GC", func(t *testing.T) {
|
|
thresholds := DefaultMemoryAlertThresholds()
|
|
monitor := NewMemoryMonitor(newNoOpLogger(), thresholds)
|
|
|
|
// Should not panic
|
|
assert.NotPanics(t, func() {
|
|
monitor.TriggerGC()
|
|
})
|
|
})
|
|
|
|
t.Run("GetMemoryPressure returns pressure level", func(t *testing.T) {
|
|
thresholds := DefaultMemoryAlertThresholds()
|
|
monitor := NewMemoryMonitor(newNoOpLogger(), thresholds)
|
|
|
|
// Initially should return None (no stats yet)
|
|
pressure := monitor.GetMemoryPressure()
|
|
assert.Equal(t, MemoryPressureNone, pressure)
|
|
|
|
// Explicitly sample to populate lastStats; GetCurrentStats is now a
|
|
// cached read and no longer forces a runtime.ReadMemStats.
|
|
monitor.Refresh()
|
|
|
|
// Now should return a valid pressure level
|
|
pressure = monitor.GetMemoryPressure()
|
|
assert.NotNil(t, pressure)
|
|
})
|
|
|
|
t.Run("StartMonitoring can be called", func(t *testing.T) {
|
|
ResetGlobalMemoryMonitor()
|
|
ResetGlobalTaskRegistry()
|
|
defer ResetGlobalMemoryMonitor()
|
|
defer ResetGlobalTaskRegistry()
|
|
|
|
thresholds := DefaultMemoryAlertThresholds()
|
|
monitor := NewMemoryMonitor(newNoOpLogger(), thresholds)
|
|
|
|
// Start monitoring should not panic. Interval is clamped to the
|
|
// minimum (30s); we rely on Refresh() when we need a synchronous
|
|
// sample instead of waiting for a tick.
|
|
assert.NotPanics(t, func() {
|
|
ctx := context.Background()
|
|
monitor.StartMonitoring(ctx, 0)
|
|
monitor.Refresh()
|
|
})
|
|
|
|
// Clean up
|
|
monitor.StopMonitoring()
|
|
})
|
|
|
|
t.Run("StopMonitoring can be called safely", func(t *testing.T) {
|
|
ResetGlobalMemoryMonitor()
|
|
defer ResetGlobalMemoryMonitor()
|
|
|
|
thresholds := DefaultMemoryAlertThresholds()
|
|
monitor := NewMemoryMonitor(newNoOpLogger(), thresholds)
|
|
|
|
// StopMonitoring should not panic even if not started
|
|
assert.NotPanics(t, func() {
|
|
monitor.StopMonitoring()
|
|
})
|
|
|
|
// Can be called multiple times safely
|
|
assert.NotPanics(t, func() {
|
|
monitor.StopMonitoring()
|
|
monitor.StopMonitoring()
|
|
})
|
|
})
|
|
|
|
t.Run("ResetGlobalMemoryMonitor resets singleton", func(t *testing.T) {
|
|
ResetGlobalMemoryMonitor()
|
|
defer ResetGlobalMemoryMonitor()
|
|
|
|
// Get initial instance
|
|
GetGlobalMemoryMonitor()
|
|
|
|
// Reset
|
|
ResetGlobalMemoryMonitor()
|
|
|
|
// Should be able to get a new instance
|
|
monitor := GetGlobalMemoryMonitor()
|
|
assert.NotNil(t, monitor)
|
|
|
|
// Clean up
|
|
monitor.StopMonitoring()
|
|
ResetGlobalMemoryMonitor()
|
|
})
|
|
|
|
t.Run("String method returns pressure name", func(t *testing.T) {
|
|
pressures := []struct {
|
|
name string
|
|
level MemoryPressureLevel
|
|
}{
|
|
{level: MemoryPressureNone, name: "None"},
|
|
{level: MemoryPressureLow, name: "Low"},
|
|
{level: MemoryPressureModerate, name: "Moderate"},
|
|
{level: MemoryPressureHigh, name: "High"},
|
|
{level: MemoryPressureCritical, name: "Critical"},
|
|
{level: MemoryPressureLevel(999), name: "Unknown"},
|
|
}
|
|
|
|
for _, p := range pressures {
|
|
assert.Equal(t, p.name, p.level.String(), "pressure level %d should return %s", p.level, p.name)
|
|
}
|
|
})
|
|
|
|
t.Run("GetCurrentStats collects statistics", func(t *testing.T) {
|
|
thresholds := DefaultMemoryAlertThresholds()
|
|
monitor := NewMemoryMonitor(newNoOpLogger(), thresholds)
|
|
|
|
// Refresh forces a synchronous sample; GetCurrentStats is a cached
|
|
// read, so we sample first to guarantee fresh data.
|
|
monitor.Refresh()
|
|
stats := monitor.GetCurrentStats()
|
|
assert.NotNil(t, stats)
|
|
assert.Greater(t, stats.HeapAllocBytes, uint64(0))
|
|
assert.Greater(t, stats.NumGoroutines, 0)
|
|
assert.NotZero(t, stats.Timestamp)
|
|
})
|
|
}
|
|
|
|
// TestBackgroundTaskRegistry tests background task registry edge cases
|
|
func TestBackgroundTaskRegistry(t *testing.T) {
|
|
t.Run("GetGlobalTaskRegistry returns singleton", func(t *testing.T) {
|
|
registry1 := GetGlobalTaskRegistry()
|
|
registry2 := GetGlobalTaskRegistry()
|
|
|
|
assert.Equal(t, registry1, registry2, "should return same instance")
|
|
})
|
|
|
|
t.Run("RegisterTask adds task to registry", func(t *testing.T) {
|
|
ResetGlobalTaskRegistry()
|
|
registry := GetGlobalTaskRegistry()
|
|
defer ResetGlobalTaskRegistry()
|
|
|
|
taskName := "test-register-task"
|
|
task := NewBackgroundTask(
|
|
taskName,
|
|
100*time.Millisecond,
|
|
func() {},
|
|
newNoOpLogger(),
|
|
)
|
|
|
|
err := registry.RegisterTask(taskName, task)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify task was registered
|
|
_, exists := registry.GetTask(taskName)
|
|
assert.True(t, exists, "task should be registered")
|
|
|
|
// Clean up
|
|
task.Stop()
|
|
})
|
|
|
|
t.Run("CreateSingletonTask is idempotent", func(t *testing.T) {
|
|
ResetGlobalTaskRegistry()
|
|
registry := GetGlobalTaskRegistry()
|
|
defer ResetGlobalTaskRegistry()
|
|
|
|
taskName := "test-singleton-idempotent"
|
|
callCount := 0
|
|
var mu sync.Mutex
|
|
|
|
taskFunc := func() {
|
|
mu.Lock()
|
|
callCount++
|
|
mu.Unlock()
|
|
}
|
|
|
|
// First creation should succeed
|
|
task1, err1 := registry.CreateSingletonTask(
|
|
taskName,
|
|
100*time.Millisecond,
|
|
taskFunc,
|
|
newNoOpLogger(),
|
|
nil,
|
|
)
|
|
|
|
assert.NoError(t, err1)
|
|
assert.NotNil(t, task1)
|
|
|
|
// Second creation should also succeed (idempotent)
|
|
// Returns same task without error
|
|
task2, err2 := registry.CreateSingletonTask(
|
|
taskName,
|
|
100*time.Millisecond,
|
|
taskFunc,
|
|
newNoOpLogger(),
|
|
nil,
|
|
)
|
|
|
|
assert.NoError(t, err2, "CreateSingletonTask should be idempotent")
|
|
assert.NotNil(t, task2)
|
|
|
|
// Clean up
|
|
if task1 != nil {
|
|
task1.Stop()
|
|
}
|
|
})
|
|
|
|
t.Run("GetTaskCount returns active task count", func(t *testing.T) {
|
|
ResetGlobalTaskRegistry()
|
|
registry := GetGlobalTaskRegistry()
|
|
defer ResetGlobalTaskRegistry()
|
|
|
|
// Initially should be 0 or small number
|
|
initialCount := registry.GetTaskCount()
|
|
|
|
// Create a task
|
|
task := NewBackgroundTask(
|
|
"count-test-task",
|
|
100*time.Millisecond,
|
|
func() {},
|
|
newNoOpLogger(),
|
|
)
|
|
|
|
err := registry.RegisterTask("count-test-task", task)
|
|
assert.NoError(t, err)
|
|
|
|
// Count should increase
|
|
newCount := registry.GetTaskCount()
|
|
assert.Equal(t, initialCount+1, newCount)
|
|
|
|
// Clean up
|
|
task.Stop()
|
|
})
|
|
|
|
t.Run("StopAllTasks stops all tasks", func(t *testing.T) {
|
|
ResetGlobalTaskRegistry()
|
|
registry := GetGlobalTaskRegistry()
|
|
defer ResetGlobalTaskRegistry()
|
|
|
|
// Create multiple tasks
|
|
for i := 0; i < 3; i++ {
|
|
taskName := "multi-task-" + string(rune(i+'0'))
|
|
task := NewBackgroundTask(
|
|
taskName,
|
|
100*time.Millisecond,
|
|
func() {},
|
|
newNoOpLogger(),
|
|
)
|
|
registry.RegisterTask(taskName, task)
|
|
}
|
|
|
|
// Verify tasks were created
|
|
assert.GreaterOrEqual(t, registry.GetTaskCount(), 3)
|
|
|
|
// Stop all tasks
|
|
registry.StopAllTasks()
|
|
|
|
// Verify all tasks are removed
|
|
taskCount := registry.GetTaskCount()
|
|
assert.Equal(t, 0, taskCount, "all tasks should be stopped")
|
|
})
|
|
|
|
t.Run("ResetGlobalTaskRegistry clears registry", func(t *testing.T) {
|
|
ResetGlobalTaskRegistry()
|
|
registry := GetGlobalTaskRegistry()
|
|
|
|
// Create a task
|
|
task := NewBackgroundTask(
|
|
"reset-test-task",
|
|
100*time.Millisecond,
|
|
func() {},
|
|
newNoOpLogger(),
|
|
)
|
|
registry.RegisterTask("reset-test-task", task)
|
|
|
|
// Reset
|
|
ResetGlobalTaskRegistry()
|
|
|
|
// Get new registry
|
|
newRegistry := GetGlobalTaskRegistry()
|
|
assert.Equal(t, 0, newRegistry.GetTaskCount(), "new registry should be empty")
|
|
})
|
|
}
|
|
|
|
// TestBackgroundTaskLifecycle tests background task lifecycle
|
|
func TestBackgroundTaskLifecycle(t *testing.T) {
|
|
t.Run("Start begins task execution", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping background task test in short mode")
|
|
}
|
|
|
|
ResetGlobalTaskRegistry()
|
|
defer ResetGlobalTaskRegistry()
|
|
|
|
executed := false
|
|
var mu sync.Mutex
|
|
|
|
task := NewBackgroundTask(
|
|
"lifecycle-test",
|
|
50*time.Millisecond,
|
|
func() {
|
|
mu.Lock()
|
|
executed = true
|
|
mu.Unlock()
|
|
},
|
|
newNoOpLogger(),
|
|
)
|
|
|
|
// Start task
|
|
task.Start()
|
|
|
|
// Wait for execution
|
|
time.Sleep(GetTestDuration(100 * time.Millisecond))
|
|
|
|
// Stop task
|
|
task.Stop()
|
|
|
|
// Verify it executed
|
|
mu.Lock()
|
|
wasExecuted := executed
|
|
mu.Unlock()
|
|
|
|
assert.True(t, wasExecuted, "task should have executed")
|
|
})
|
|
|
|
t.Run("Stop halts task execution", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping background task test in short mode")
|
|
}
|
|
|
|
ResetGlobalTaskRegistry()
|
|
defer ResetGlobalTaskRegistry()
|
|
|
|
execCount := 0
|
|
var mu sync.Mutex
|
|
|
|
task := NewBackgroundTask(
|
|
"stop-test",
|
|
30*time.Millisecond,
|
|
func() {
|
|
mu.Lock()
|
|
execCount++
|
|
mu.Unlock()
|
|
},
|
|
newNoOpLogger(),
|
|
)
|
|
|
|
// Start task
|
|
task.Start()
|
|
|
|
// Let it run a few times
|
|
time.Sleep(GetTestDuration(100 * time.Millisecond))
|
|
|
|
// Stop task
|
|
task.Stop()
|
|
|
|
// Record count
|
|
mu.Lock()
|
|
countAfterStop := execCount
|
|
mu.Unlock()
|
|
|
|
// Wait more
|
|
time.Sleep(GetTestDuration(100 * time.Millisecond))
|
|
|
|
// Count should not increase
|
|
mu.Lock()
|
|
finalCount := execCount
|
|
mu.Unlock()
|
|
|
|
assert.Equal(t, countAfterStop, finalCount, "task should not execute after stop")
|
|
})
|
|
|
|
t.Run("Multiple Start calls are safe", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping background task test in short mode")
|
|
}
|
|
|
|
ResetGlobalTaskRegistry()
|
|
defer ResetGlobalTaskRegistry()
|
|
|
|
execCount := 0
|
|
var mu sync.Mutex
|
|
|
|
task := NewBackgroundTask(
|
|
"multi-start-test",
|
|
100*time.Millisecond,
|
|
func() {
|
|
mu.Lock()
|
|
execCount++
|
|
mu.Unlock()
|
|
},
|
|
newNoOpLogger(),
|
|
)
|
|
|
|
// Multiple starts should be safe
|
|
task.Start()
|
|
task.Start()
|
|
task.Start()
|
|
|
|
// Wait a bit
|
|
time.Sleep(GetTestDuration(50 * time.Millisecond))
|
|
|
|
// Stop task
|
|
task.Stop()
|
|
|
|
// Should have executed, but only one goroutine
|
|
mu.Lock()
|
|
count := execCount
|
|
mu.Unlock()
|
|
|
|
assert.GreaterOrEqual(t, count, 0, "task should have executed at least once")
|
|
})
|
|
|
|
t.Run("Multiple Stop calls are safe", func(t *testing.T) {
|
|
ResetGlobalTaskRegistry()
|
|
defer ResetGlobalTaskRegistry()
|
|
|
|
task := NewBackgroundTask(
|
|
"multi-stop-test",
|
|
100*time.Millisecond,
|
|
func() {},
|
|
newNoOpLogger(),
|
|
)
|
|
|
|
// Start and stop
|
|
task.Start()
|
|
time.Sleep(GetTestDuration(20 * time.Millisecond))
|
|
|
|
// Multiple stops should be safe
|
|
assert.NotPanics(t, func() {
|
|
task.Stop()
|
|
task.Stop()
|
|
task.Stop()
|
|
})
|
|
})
|
|
}
|
|
|
|
// TestMemoryMonitorIntegration tests memory monitor integration
|
|
func TestMemoryMonitorIntegration(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping memory monitor integration test in short mode")
|
|
}
|
|
|
|
t.Run("monitoring updates stats", func(t *testing.T) {
|
|
ResetGlobalMemoryMonitor()
|
|
ResetGlobalTaskRegistry()
|
|
defer ResetGlobalMemoryMonitor()
|
|
defer ResetGlobalTaskRegistry()
|
|
|
|
thresholds := DefaultMemoryAlertThresholds()
|
|
monitor := NewMemoryMonitor(newNoOpLogger(), thresholds)
|
|
defer monitor.StopMonitoring()
|
|
|
|
// Start monitoring. The interval is clamped to the minimum (30s) so
|
|
// the ticker won't fire during the test; drive the sample manually via
|
|
// Refresh() instead.
|
|
ctx := context.Background()
|
|
monitor.StartMonitoring(ctx, 0)
|
|
monitor.Refresh()
|
|
|
|
// Get pressure (should be a valid pressure level)
|
|
pressure := monitor.GetMemoryPressure()
|
|
assert.Contains(t, []MemoryPressureLevel{
|
|
MemoryPressureNone,
|
|
MemoryPressureLow,
|
|
MemoryPressureModerate,
|
|
MemoryPressureHigh,
|
|
MemoryPressureCritical,
|
|
}, pressure, "pressure should be a valid level")
|
|
|
|
// Stop monitoring
|
|
monitor.StopMonitoring()
|
|
})
|
|
|
|
t.Run("global memory monitor singleton", func(t *testing.T) {
|
|
ResetGlobalMemoryMonitor()
|
|
defer ResetGlobalMemoryMonitor()
|
|
|
|
monitor1 := GetGlobalMemoryMonitor()
|
|
monitor2 := GetGlobalMemoryMonitor()
|
|
|
|
assert.Equal(t, monitor1, monitor2, "should return same instance")
|
|
})
|
|
}
|
|
|
|
// TestMemoryStatsCollection tests memory statistics collection
|
|
func TestMemoryStatsCollection(t *testing.T) {
|
|
t.Run("GetCurrentStats returns valid data", func(t *testing.T) {
|
|
thresholds := DefaultMemoryAlertThresholds()
|
|
monitor := NewMemoryMonitor(newNoOpLogger(), thresholds)
|
|
|
|
monitor.Refresh()
|
|
stats := monitor.GetCurrentStats()
|
|
|
|
assert.NotNil(t, stats)
|
|
assert.Greater(t, stats.HeapAllocBytes, uint64(0))
|
|
assert.Greater(t, stats.HeapSysBytes, uint64(0))
|
|
assert.Greater(t, stats.NumGoroutines, 0)
|
|
assert.False(t, stats.Timestamp.IsZero())
|
|
})
|
|
|
|
t.Run("Stats include memory pressure", func(t *testing.T) {
|
|
thresholds := DefaultMemoryAlertThresholds()
|
|
monitor := NewMemoryMonitor(newNoOpLogger(), thresholds)
|
|
|
|
monitor.Refresh()
|
|
stats := monitor.GetCurrentStats()
|
|
|
|
// Should calculate and include pressure level
|
|
assert.NotNil(t, stats.MemoryPressure)
|
|
assert.Contains(t, []MemoryPressureLevel{
|
|
MemoryPressureNone,
|
|
MemoryPressureLow,
|
|
MemoryPressureModerate,
|
|
MemoryPressureHigh,
|
|
MemoryPressureCritical,
|
|
}, stats.MemoryPressure)
|
|
})
|
|
|
|
t.Run("TriggerGC reduces memory", func(t *testing.T) {
|
|
thresholds := DefaultMemoryAlertThresholds()
|
|
monitor := NewMemoryMonitor(newNoOpLogger(), thresholds)
|
|
|
|
// Allocate some memory
|
|
_ = make([]byte, 1024*1024) // 1MB
|
|
|
|
// Get stats before GC (explicit Refresh so we have a fresh pre-GC
|
|
// snapshot to compare against, not the constructor baseline).
|
|
beforeStats := monitor.Refresh()
|
|
|
|
// Trigger GC (internally Refresh()es before and after)
|
|
monitor.TriggerGC()
|
|
|
|
// Get stats after GC from cache (TriggerGC already refreshed it)
|
|
afterStats := monitor.GetCurrentStats()
|
|
|
|
// After GC should have different stats
|
|
assert.NotEqual(t, beforeStats.LastGCTime, afterStats.LastGCTime)
|
|
})
|
|
}
|