mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
e64fc7f730
* Add redis support for distributed caching * Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * ... and another all nighter. * fixup! ... and another all nighter. * fixup! fixup! ... and another all nighter. * fixup! fixup! fixup! ... and another all nighter. * Resolve issue #85 by adding ability to set custom claims in JWT tokens * Remove redundant validation in auth middleware ( issue #89 ) * Add ability to set cookie prefix for session cookies ( #87 ) * fixup! Add ability to set cookie prefix for session cookies ( #87 ) * Add ability to set cookie max age - issue #91 * Potential fix for code scanning alert no. 10: Size computation for allocation may overflow Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fixup! Merge main into 0.8.0-redis: resolve conflicts --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
405 lines
10 KiB
Go
405 lines
10 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alicebob/miniredis/v2"
|
|
"github.com/lukaszraczylo/traefikoidc/internal/cache/backends"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestRedisIntegration_MultipleInstances tests cache sharing across multiple instances
|
|
func TestRedisIntegration_MultipleInstances(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Start miniredis server
|
|
mr, err := miniredis.Run()
|
|
require.NoError(t, err)
|
|
defer mr.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create two backend instances sharing the same Redis
|
|
config1 := backends.DefaultRedisConfig(mr.Addr())
|
|
config1.RedisPrefix = "shared:"
|
|
backend1, err := backends.NewRedisBackend(config1)
|
|
require.NoError(t, err)
|
|
defer backend1.Close()
|
|
|
|
config2 := backends.DefaultRedisConfig(mr.Addr())
|
|
config2.RedisPrefix = "shared:"
|
|
backend2, err := backends.NewRedisBackend(config2)
|
|
require.NoError(t, err)
|
|
defer backend2.Close()
|
|
|
|
t.Run("ShareTokenBlacklist", func(t *testing.T) {
|
|
// Instance 1 blacklists a JTI
|
|
jti := "test-jti-12345"
|
|
err := backend1.Set(ctx, "jti:"+jti, []byte("blacklisted"), 10*time.Minute)
|
|
require.NoError(t, err)
|
|
|
|
// Instance 2 should see the blacklisted JTI
|
|
_, _, exists, err := backend2.Get(ctx, "jti:"+jti)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists, "JTI should be visible across instances")
|
|
})
|
|
|
|
t.Run("ShareTokenCache", func(t *testing.T) {
|
|
// Instance 1 caches a token
|
|
token := "access-token-xyz"
|
|
tokenData := []byte(`{"sub":"user123","exp":1234567890}`)
|
|
err := backend1.Set(ctx, "token:"+token, tokenData, 5*time.Minute)
|
|
require.NoError(t, err)
|
|
|
|
// Instance 2 retrieves the cached token
|
|
retrieved, _, exists, err := backend2.Get(ctx, "token:"+token)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
assert.Equal(t, tokenData, retrieved)
|
|
})
|
|
|
|
t.Run("ShareMetadataCache", func(t *testing.T) {
|
|
// Instance 1 caches provider metadata
|
|
metadataKey := "metadata:provider123"
|
|
metadata := []byte(`{"issuer":"https://example.com","jwks_uri":"https://example.com/jwks"}`)
|
|
err := backend1.Set(ctx, metadataKey, metadata, 1*time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
// Instance 2 retrieves the metadata
|
|
retrieved, ttl, exists, err := backend2.Get(ctx, metadataKey)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
assert.Equal(t, metadata, retrieved)
|
|
assert.Greater(t, ttl, 50*time.Minute)
|
|
})
|
|
}
|
|
|
|
// TestRedisIntegration_JTIReplayDetection tests JTI replay detection across instances
|
|
func TestRedisIntegration_JTIReplayDetection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mr, err := miniredis.Run()
|
|
require.NoError(t, err)
|
|
defer mr.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Multiple Traefik instances
|
|
instances := make([]*backends.RedisBackend, 3)
|
|
for i := 0; i < 3; i++ {
|
|
config := backends.DefaultRedisConfig(mr.Addr())
|
|
config.RedisPrefix = "jti:"
|
|
instances[i], err = backends.NewRedisBackend(config)
|
|
require.NoError(t, err)
|
|
defer instances[i].Close()
|
|
}
|
|
|
|
t.Run("PreventReplayAcrossInstances", func(t *testing.T) {
|
|
jti := "replay-test-jti"
|
|
|
|
// First instance processes token and blacklists JTI
|
|
err := instances[0].Set(ctx, jti, []byte("used"), 24*time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
// Other instances should detect the used JTI
|
|
for i := 1; i < 3; i++ {
|
|
exists, err := instances[i].Exists(ctx, jti)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists, "Instance %d should see blacklisted JTI", i)
|
|
}
|
|
})
|
|
|
|
t.Run("ConcurrentJTIChecks", func(t *testing.T) {
|
|
jtiBase := "concurrent-jti"
|
|
var wg sync.WaitGroup
|
|
|
|
// Simulate concurrent token processing across instances
|
|
for instanceID := 0; instanceID < 3; instanceID++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < 10; j++ {
|
|
jti := fmt.Sprintf("%s-%d-%d", jtiBase, id, j)
|
|
|
|
// Check if JTI exists
|
|
exists, _ := instances[id].Exists(ctx, jti)
|
|
if !exists {
|
|
// Mark as used
|
|
instances[id].Set(ctx, jti, []byte("used"), 1*time.Hour)
|
|
}
|
|
}
|
|
}(instanceID)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify all JTIs were recorded
|
|
for instanceID := 0; instanceID < 3; instanceID++ {
|
|
for j := 0; j < 10; j++ {
|
|
jti := fmt.Sprintf("%s-%d-%d", jtiBase, instanceID, j)
|
|
exists, err := instances[0].Exists(ctx, jti)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists, "JTI %s should exist", jti)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestRedisIntegration_Failover tests failover scenarios
|
|
func TestRedisIntegration_Failover(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mr, err := miniredis.Run()
|
|
require.NoError(t, err)
|
|
defer mr.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
config := backends.DefaultRedisConfig(mr.Addr())
|
|
redisBackend, err := backends.NewRedisBackend(config)
|
|
require.NoError(t, err)
|
|
defer redisBackend.Close()
|
|
|
|
t.Run("RedisTemporaryFailure", func(t *testing.T) {
|
|
// Set some data
|
|
key := "failover-key"
|
|
value := []byte("failover-value")
|
|
err := redisBackend.Set(ctx, key, value, 1*time.Minute)
|
|
require.NoError(t, err)
|
|
|
|
// Simulate Redis error
|
|
mr.SetError("simulated connection error")
|
|
|
|
// Operations should fail gracefully
|
|
_, _, exists, err := redisBackend.Get(ctx, key)
|
|
assert.Error(t, err)
|
|
assert.False(t, exists)
|
|
|
|
// Clear error
|
|
mr.SetError("")
|
|
|
|
// Operations should work again
|
|
retrieved, _, exists, err := redisBackend.Get(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
assert.Equal(t, value, retrieved)
|
|
})
|
|
}
|
|
|
|
// TestRedisIntegration_HighLoad tests high load scenarios
|
|
func TestRedisIntegration_HighLoad(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping high load test in short mode")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
mr, err := miniredis.Run()
|
|
require.NoError(t, err)
|
|
defer mr.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
config := backends.DefaultRedisConfig(mr.Addr())
|
|
config.PoolSize = 20
|
|
redisBackend, err := backends.NewRedisBackend(config)
|
|
require.NoError(t, err)
|
|
defer redisBackend.Close()
|
|
|
|
t.Run("HighConcurrency", func(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
goroutines := 50
|
|
operations := 100
|
|
|
|
errors := make(chan error, goroutines*operations)
|
|
|
|
for i := 0; i < goroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < operations; j++ {
|
|
key := fmt.Sprintf("high-load-key-%d-%d", id, j)
|
|
value := []byte(fmt.Sprintf("high-load-value-%d-%d", id, j))
|
|
|
|
// Write
|
|
if err := redisBackend.Set(ctx, key, value, 1*time.Minute); err != nil {
|
|
errors <- err
|
|
continue
|
|
}
|
|
|
|
// Read
|
|
retrieved, _, exists, err := redisBackend.Get(ctx, key)
|
|
if err != nil {
|
|
errors <- err
|
|
continue
|
|
}
|
|
if !exists {
|
|
errors <- fmt.Errorf("key %s does not exist", key)
|
|
continue
|
|
}
|
|
if string(retrieved) != string(value) {
|
|
errors <- fmt.Errorf("value mismatch for key %s", key)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errors)
|
|
|
|
// Check for errors
|
|
errorCount := 0
|
|
for err := range errors {
|
|
t.Logf("Operation error: %v", err)
|
|
errorCount++
|
|
}
|
|
|
|
// Allow small error rate (< 1%)
|
|
totalOps := goroutines * operations
|
|
errorRate := float64(errorCount) / float64(totalOps)
|
|
assert.Less(t, errorRate, 0.01, "Error rate should be less than 1%%")
|
|
})
|
|
}
|
|
|
|
// TestRedisIntegration_TTLConsistency tests TTL consistency across operations
|
|
func TestRedisIntegration_TTLConsistency(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mr, err := miniredis.Run()
|
|
require.NoError(t, err)
|
|
defer mr.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
config := backends.DefaultRedisConfig(mr.Addr())
|
|
redisBackend, err := backends.NewRedisBackend(config)
|
|
require.NoError(t, err)
|
|
defer redisBackend.Close()
|
|
|
|
t.Run("TTLAccuracy", func(t *testing.T) {
|
|
key := "ttl-test-key"
|
|
value := []byte("ttl-test-value")
|
|
ttl := 5 * time.Second
|
|
|
|
err := redisBackend.Set(ctx, key, value, ttl)
|
|
require.NoError(t, err)
|
|
|
|
// Check TTL immediately
|
|
_, ttl1, exists, err := redisBackend.Get(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
assert.Greater(t, ttl1, 4*time.Second)
|
|
assert.LessOrEqual(t, ttl1, ttl)
|
|
|
|
// Fast forward 2 seconds
|
|
mr.FastForward(2 * time.Second)
|
|
|
|
// Check TTL again
|
|
_, ttl2, exists, err := redisBackend.Get(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
assert.Less(t, ttl2, ttl1)
|
|
assert.Greater(t, ttl2, 2*time.Second)
|
|
|
|
// Fast forward past expiration
|
|
mr.FastForward(4 * time.Second)
|
|
|
|
// Should be expired
|
|
_, _, exists, err = redisBackend.Get(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.False(t, exists)
|
|
})
|
|
}
|
|
|
|
// TestRedisIntegration_MemoryUsage tests memory efficiency
|
|
func TestRedisIntegration_MemoryUsage(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping memory usage test in short mode")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
mr, err := miniredis.Run()
|
|
require.NoError(t, err)
|
|
defer mr.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
config := backends.DefaultRedisConfig(mr.Addr())
|
|
redisBackend, err := backends.NewRedisBackend(config)
|
|
require.NoError(t, err)
|
|
defer redisBackend.Close()
|
|
|
|
t.Run("LargeDataset", func(t *testing.T) {
|
|
// Store 10,000 items
|
|
itemCount := 10000
|
|
for i := 0; i < itemCount; i++ {
|
|
key := fmt.Sprintf("memory-test-key-%d", i)
|
|
value := []byte(fmt.Sprintf("memory-test-value-%d-with-some-padding-to-make-it-larger", i))
|
|
err := redisBackend.Set(ctx, key, value, 10*time.Minute)
|
|
require.NoError(t, err)
|
|
|
|
// Log progress
|
|
if i%1000 == 0 {
|
|
t.Logf("Stored %d items", i)
|
|
}
|
|
}
|
|
|
|
// Verify all items exist
|
|
for i := 0; i < itemCount; i += 100 {
|
|
key := fmt.Sprintf("memory-test-key-%d", i)
|
|
exists, err := redisBackend.Exists(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
}
|
|
|
|
// Check stats
|
|
stats := redisBackend.GetStats()
|
|
t.Logf("Redis backend stats: %+v", stats)
|
|
})
|
|
}
|
|
|
|
// TestRedisIntegration_Cleanup tests cache cleanup functionality
|
|
func TestRedisIntegration_Cleanup(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mr, err := miniredis.Run()
|
|
require.NoError(t, err)
|
|
defer mr.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
config := backends.DefaultRedisConfig(mr.Addr())
|
|
config.RedisPrefix = "cleanup-test:"
|
|
redisBackend, err := backends.NewRedisBackend(config)
|
|
require.NoError(t, err)
|
|
defer redisBackend.Close()
|
|
|
|
t.Run("BulkCleanup", func(t *testing.T) {
|
|
// Add many items
|
|
for i := 0; i < 100; i++ {
|
|
key := fmt.Sprintf("cleanup-key-%d", i)
|
|
value := []byte(fmt.Sprintf("cleanup-value-%d", i))
|
|
err := redisBackend.Set(ctx, key, value, 1*time.Minute)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Clear all
|
|
err := redisBackend.Clear(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Verify all items are gone
|
|
for i := 0; i < 100; i++ {
|
|
key := fmt.Sprintf("cleanup-key-%d", i)
|
|
exists, err := redisBackend.Exists(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.False(t, exists)
|
|
}
|
|
})
|
|
}
|