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
651 lines
15 KiB
Go
651 lines
15 KiB
Go
package backends
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestConnectionPool_BasicOperations tests basic pool operations
|
|
func TestConnectionPool_BasicOperations(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
config := &PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
MaxConnections: 5,
|
|
ConnectTimeout: 5 * time.Second,
|
|
ReadTimeout: 3 * time.Second,
|
|
WriteTimeout: 3 * time.Second,
|
|
}
|
|
|
|
pool, err := NewConnectionPool(config)
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
t.Run("GetAndPutConnection", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Get a connection
|
|
conn, err := pool.Get(ctx)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
|
|
// Verify connection works
|
|
resp, err := conn.Do("PING")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "PONG", resp)
|
|
|
|
// Return to pool
|
|
pool.Put(conn)
|
|
|
|
// Get again - should reuse same connection
|
|
conn2, err := pool.Get(ctx)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn2)
|
|
|
|
pool.Put(conn2)
|
|
})
|
|
|
|
t.Run("Stats", func(t *testing.T) {
|
|
stats := pool.Stats()
|
|
require.NotNil(t, stats)
|
|
|
|
assert.Contains(t, stats, "active_connections")
|
|
assert.Contains(t, stats, "total_connections")
|
|
assert.Contains(t, stats, "max_connections")
|
|
assert.Equal(t, 5, stats["max_connections"])
|
|
})
|
|
}
|
|
|
|
// TestConnectionPool_MaxConnections tests pool size limits
|
|
func TestConnectionPool_MaxConnections(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
maxConns := 3
|
|
config := &PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
MaxConnections: maxConns,
|
|
ConnectTimeout: 1 * time.Second,
|
|
}
|
|
|
|
pool, err := NewConnectionPool(config)
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Get max connections
|
|
conns := make([]*RedisConn, maxConns)
|
|
for i := 0; i < maxConns; i++ {
|
|
conn, err := pool.Get(ctx)
|
|
require.NoError(t, err)
|
|
conns[i] = conn
|
|
}
|
|
|
|
// Verify stats
|
|
stats := pool.Stats()
|
|
assert.Equal(t, int32(maxConns), stats["total_connections"])
|
|
assert.Equal(t, int32(maxConns), stats["active_connections"])
|
|
|
|
// Try to get one more - should block/timeout
|
|
ctx2, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
conn, err := pool.Get(ctx2)
|
|
require.Error(t, err)
|
|
require.Nil(t, conn)
|
|
|
|
// Return one connection
|
|
pool.Put(conns[0])
|
|
|
|
// Now we should be able to get a connection
|
|
conn, err = pool.Get(context.Background())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
|
|
// Cleanup
|
|
pool.Put(conn)
|
|
for i := 1; i < maxConns; i++ {
|
|
pool.Put(conns[i])
|
|
}
|
|
}
|
|
|
|
// TestConnectionPool_ConcurrentAccess tests concurrent pool usage
|
|
func TestConnectionPool_ConcurrentAccess(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
config := &PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
MaxConnections: 10,
|
|
ConnectTimeout: 5 * time.Second,
|
|
}
|
|
|
|
pool, err := NewConnectionPool(config)
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
ctx := context.Background()
|
|
numGoroutines := 50
|
|
numOperations := 20
|
|
|
|
var wg sync.WaitGroup
|
|
errors := make(chan error, numGoroutines*numOperations)
|
|
|
|
// Spawn goroutines
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
|
|
for j := 0; j < numOperations; j++ {
|
|
conn, err := pool.Get(ctx)
|
|
if err != nil {
|
|
errors <- err
|
|
continue
|
|
}
|
|
|
|
// Do some work
|
|
_, err = conn.Do("PING")
|
|
if err != nil {
|
|
errors <- err
|
|
}
|
|
|
|
// Return to pool
|
|
pool.Put(conn)
|
|
|
|
// Small delay
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errors)
|
|
|
|
// Check for errors
|
|
errorCount := 0
|
|
for err := range errors {
|
|
t.Logf("Error: %v", err)
|
|
errorCount++
|
|
}
|
|
|
|
assert.Equal(t, 0, errorCount, "Expected no errors in concurrent access")
|
|
|
|
// Verify stats
|
|
stats := pool.Stats()
|
|
t.Logf("Final stats: %+v", stats)
|
|
assert.LessOrEqual(t, stats["total_connections"].(int32), int32(10))
|
|
assert.Equal(t, int32(0), stats["active_connections"])
|
|
}
|
|
|
|
// TestConnectionPool_ContextCancellation tests context cancellation
|
|
func TestConnectionPool_ContextCancellation(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
config := &PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
MaxConnections: 1,
|
|
ConnectTimeout: 5 * time.Second,
|
|
}
|
|
|
|
pool, err := NewConnectionPool(config)
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
// Get the only connection
|
|
conn, err := pool.Get(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
// Try to get another with canceled context
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
conn2, err := pool.Get(ctx)
|
|
require.Error(t, err)
|
|
require.Nil(t, conn2)
|
|
assert.Contains(t, err.Error(), "context canceled")
|
|
|
|
// Cleanup
|
|
pool.Put(conn)
|
|
}
|
|
|
|
// TestConnectionPool_Authentication tests auth support
|
|
func TestConnectionPool_Authentication(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
// Set password on miniredis
|
|
mr.server.RequireAuth("secret-password")
|
|
|
|
t.Run("CorrectPassword", func(t *testing.T) {
|
|
config := &PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
Password: "secret-password",
|
|
MaxConnections: 2,
|
|
ConnectTimeout: 5 * time.Second,
|
|
}
|
|
|
|
pool, err := NewConnectionPool(config)
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
conn, err := pool.Get(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
resp, err := conn.Do("PING")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "PONG", resp)
|
|
|
|
pool.Put(conn)
|
|
})
|
|
|
|
t.Run("WrongPassword", func(t *testing.T) {
|
|
t.Skip("Miniredis doesn't fully simulate AUTH errors like real Redis")
|
|
|
|
config := &PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
Password: "wrong-password",
|
|
MaxConnections: 2,
|
|
ConnectTimeout: 5 * time.Second,
|
|
}
|
|
|
|
_, err := NewConnectionPool(config)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "authentication failed")
|
|
})
|
|
}
|
|
|
|
// TestConnectionPool_DatabaseSelection tests DB selection
|
|
func TestConnectionPool_DatabaseSelection(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
config := &PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
DB: 5,
|
|
MaxConnections: 2,
|
|
ConnectTimeout: 5 * time.Second,
|
|
}
|
|
|
|
pool, err := NewConnectionPool(config)
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
conn, err := pool.Get(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
// Connection should be on DB 5
|
|
resp, err := conn.Do("PING")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "PONG", resp)
|
|
|
|
pool.Put(conn)
|
|
}
|
|
|
|
// TestConnectionPool_ClosedConnection tests handling closed connections
|
|
func TestConnectionPool_ClosedConnection(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
config := &PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
MaxConnections: 2,
|
|
ConnectTimeout: 5 * time.Second,
|
|
}
|
|
|
|
pool, err := NewConnectionPool(config)
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
// Get connection
|
|
conn, err := pool.Get(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
// Close it manually
|
|
conn.Close()
|
|
|
|
// Try to use it
|
|
_, err = conn.Do("PING")
|
|
require.Error(t, err)
|
|
assert.True(t, errors.Is(err, ErrBackendClosed))
|
|
|
|
// Return to pool (should be discarded)
|
|
pool.Put(conn)
|
|
|
|
// Get new connection - should create a new one
|
|
conn2, err := pool.Get(context.Background())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn2)
|
|
|
|
resp, err := conn2.Do("PING")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "PONG", resp)
|
|
|
|
pool.Put(conn2)
|
|
}
|
|
|
|
// TestConnectionPool_Close tests pool closure
|
|
func TestConnectionPool_Close(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
config := &PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
MaxConnections: 5,
|
|
ConnectTimeout: 5 * time.Second,
|
|
}
|
|
|
|
pool, err := NewConnectionPool(config)
|
|
require.NoError(t, err)
|
|
|
|
// Get some connections
|
|
conns := make([]*RedisConn, 3)
|
|
for i := 0; i < 3; i++ {
|
|
conn, err := pool.Get(context.Background())
|
|
require.NoError(t, err)
|
|
conns[i] = conn
|
|
}
|
|
|
|
// Return them
|
|
for _, conn := range conns {
|
|
pool.Put(conn)
|
|
}
|
|
|
|
// Close pool
|
|
err = pool.Close()
|
|
require.NoError(t, err)
|
|
|
|
// Try to get connection from closed pool
|
|
_, err = pool.Get(context.Background())
|
|
require.Error(t, err)
|
|
assert.True(t, errors.Is(err, ErrBackendClosed))
|
|
|
|
// Close again should be no-op
|
|
err = pool.Close()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// TestConnectionPool_Timeouts tests various timeout scenarios
|
|
func TestConnectionPool_Timeouts(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
config := &PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
MaxConnections: 2,
|
|
ConnectTimeout: 100 * time.Millisecond,
|
|
ReadTimeout: 100 * time.Millisecond,
|
|
WriteTimeout: 100 * time.Millisecond,
|
|
}
|
|
|
|
pool, err := NewConnectionPool(config)
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
conn, err := pool.Get(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
// Normal operation should work
|
|
resp, err := conn.Do("PING")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "PONG", resp)
|
|
|
|
pool.Put(conn)
|
|
}
|
|
|
|
// TestRedisConn_DoCommand tests the Do method
|
|
func TestRedisConn_DoCommand(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
config := &PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
MaxConnections: 2,
|
|
}
|
|
|
|
pool, err := NewConnectionPool(config)
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
conn, err := pool.Get(context.Background())
|
|
require.NoError(t, err)
|
|
defer pool.Put(conn)
|
|
|
|
t.Run("SET and GET", func(t *testing.T) {
|
|
// SET
|
|
resp, err := conn.Do("SET", "testkey", "testvalue")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "OK", resp)
|
|
|
|
// GET
|
|
resp, err = conn.Do("GET", "testkey")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "testvalue", resp)
|
|
})
|
|
|
|
t.Run("DEL", func(t *testing.T) {
|
|
// SET key first
|
|
_, err := conn.Do("SET", "delkey", "delvalue")
|
|
require.NoError(t, err)
|
|
|
|
// DEL
|
|
resp, err := conn.Do("DEL", "delkey")
|
|
require.NoError(t, err)
|
|
|
|
count, err := RESPInt(resp)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1), count)
|
|
})
|
|
|
|
t.Run("EXISTS", func(t *testing.T) {
|
|
// SET key first
|
|
_, err := conn.Do("SET", "existskey", "value")
|
|
require.NoError(t, err)
|
|
|
|
// EXISTS - key exists
|
|
resp, err := conn.Do("EXISTS", "existskey")
|
|
require.NoError(t, err)
|
|
|
|
count, err := RESPInt(resp)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1), count)
|
|
|
|
// EXISTS - key doesn't exist
|
|
resp, err = conn.Do("EXISTS", "nonexistent")
|
|
require.NoError(t, err)
|
|
|
|
count, err = RESPInt(resp)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(0), count)
|
|
})
|
|
|
|
t.Run("TTL commands", func(t *testing.T) {
|
|
// SETEX
|
|
resp, err := conn.Do("SETEX", "ttlkey", "60", "ttlvalue")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "OK", resp)
|
|
|
|
// TTL
|
|
resp, err = conn.Do("TTL", "ttlkey")
|
|
require.NoError(t, err)
|
|
|
|
ttl, err := RESPInt(resp)
|
|
require.NoError(t, err)
|
|
assert.Greater(t, ttl, int64(0))
|
|
assert.LessOrEqual(t, ttl, int64(60))
|
|
})
|
|
}
|
|
|
|
// TestPoolConfig_Defaults tests default configuration values
|
|
func TestPoolConfig_Defaults(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
config := &PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
// Leave other fields at zero values
|
|
}
|
|
|
|
pool, err := NewConnectionPool(config)
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
// Should use defaults
|
|
assert.Equal(t, 10, pool.config.MaxConnections)
|
|
assert.Equal(t, 5*time.Second, pool.config.ConnectTimeout)
|
|
|
|
// Verify it works
|
|
conn, err := pool.Get(context.Background())
|
|
require.NoError(t, err)
|
|
pool.Put(conn)
|
|
}
|
|
|
|
// TestConnectionPool_NilConnection tests handling nil connections
|
|
func TestConnectionPool_NilConnection(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
config := &PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
MaxConnections: 2,
|
|
}
|
|
|
|
pool, err := NewConnectionPool(config)
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
// Putting nil should be safe
|
|
pool.Put(nil)
|
|
|
|
// Pool should still work
|
|
conn, err := pool.Get(context.Background())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conn)
|
|
pool.Put(conn)
|
|
}
|
|
|
|
// TestConnectionPool_StatsTracking tests metrics tracking
|
|
func TestConnectionPool_StatsTracking(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
config := &PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
MaxConnections: 5,
|
|
}
|
|
|
|
pool, err := NewConnectionPool(config)
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Initial stats
|
|
stats := pool.Stats()
|
|
initialGets := stats["gets"].(int64)
|
|
initialPuts := stats["puts"].(int64)
|
|
|
|
// Perform operations
|
|
numOps := 10
|
|
for i := 0; i < numOps; i++ {
|
|
conn, err := pool.Get(ctx)
|
|
require.NoError(t, err)
|
|
pool.Put(conn)
|
|
}
|
|
|
|
// Check updated stats
|
|
stats = pool.Stats()
|
|
assert.Equal(t, initialGets+int64(numOps), stats["gets"].(int64))
|
|
assert.Equal(t, initialPuts+int64(numOps), stats["puts"].(int64))
|
|
assert.Equal(t, int32(0), stats["active_connections"].(int32))
|
|
}
|
|
|
|
// TestRedisConn_TooManyArguments tests protection against allocation overflow
|
|
func TestRedisConn_TooManyArguments(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
config := &PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
MaxConnections: 1,
|
|
ConnectTimeout: 5 * time.Second,
|
|
ReadTimeout: 3 * time.Second,
|
|
WriteTimeout: 3 * time.Second,
|
|
}
|
|
|
|
pool, err := NewConnectionPool(config)
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
ctx := context.Background()
|
|
conn, err := pool.Get(ctx)
|
|
require.NoError(t, err)
|
|
defer pool.Put(conn)
|
|
|
|
t.Run("AcceptableArgumentCount", func(t *testing.T) {
|
|
// Should work with reasonable number of args
|
|
args := make([]string, 100)
|
|
for i := range args {
|
|
args[i] = "value"
|
|
}
|
|
_, err := conn.Do("MSET", args...)
|
|
// May fail due to Redis constraints, but shouldn't panic or error on overflow
|
|
// Just verify it doesn't trigger our overflow protection
|
|
if err != nil {
|
|
assert.NotContains(t, err.Error(), "too many arguments")
|
|
}
|
|
})
|
|
|
|
t.Run("RejectExcessiveArguments", func(t *testing.T) {
|
|
// Create an absurdly large number of arguments that would cause overflow
|
|
// Use 1M + 1 to exceed maxSafeArgs = (1<<20)-1 = 1048575
|
|
args := make([]string, 1<<20) // 1,048,576 args
|
|
for i := range args {
|
|
args[i] = "x"
|
|
}
|
|
|
|
_, err := conn.Do("MSET", args...)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "too many arguments")
|
|
})
|
|
|
|
t.Run("BoundaryCase", func(t *testing.T) {
|
|
// Test exactly at the boundary (maxSafeArgs)
|
|
args := make([]string, (1<<20)-1) // Exactly 1,048,575 args (max allowed)
|
|
for i := range args {
|
|
args[i] = "x"
|
|
}
|
|
|
|
_, err := conn.Do("ECHO", args...)
|
|
// Should not error due to overflow protection
|
|
if err != nil {
|
|
assert.NotContains(t, err.Error(), "too many arguments")
|
|
}
|
|
})
|
|
|
|
}
|
|
|
|
// TestRedisConn_RejectOversizedArgumentBytes is a regression test for CodeQL
|
|
// alert #10 (go/allocation-size-overflow). A single argument larger than
|
|
// maxTotalArgBytes (64 MiB) must be rejected by the per-argument overflow
|
|
// guard in Do() before any allocation is attempted.
|
|
func TestRedisConn_RejectOversizedArgumentBytes(t *testing.T) {
|
|
mr := NewMiniredisServer(t)
|
|
|
|
pool, err := NewConnectionPool(&PoolConfig{
|
|
Address: mr.GetAddr(),
|
|
MaxConnections: 1,
|
|
ConnectTimeout: 5 * time.Second,
|
|
ReadTimeout: 3 * time.Second,
|
|
WriteTimeout: 3 * time.Second,
|
|
})
|
|
require.NoError(t, err)
|
|
defer pool.Close()
|
|
|
|
conn, err := pool.Get(context.Background())
|
|
require.NoError(t, err)
|
|
defer pool.Put(conn)
|
|
|
|
largeArg := strings.Repeat("x", (64<<20)+1)
|
|
|
|
_, err = conn.Do("SET", "k", largeArg)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "arguments too large")
|
|
}
|