mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-06 22:49:43 +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>
422 lines
11 KiB
Go
422 lines
11 KiB
Go
package backends
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestCacheBackendContract defines a set of tests that all CacheBackend implementations must pass
|
|
// This ensures that Memory, Redis, and Hybrid backends all behave consistently
|
|
func TestCacheBackendContract(t *testing.T) {
|
|
// Test suite will be run against each backend type
|
|
t.Run("MemoryBackend", func(t *testing.T) {
|
|
backend := setupMemoryBackend(t)
|
|
runContractTests(t, backend)
|
|
})
|
|
|
|
t.Run("RedisBackend", func(t *testing.T) {
|
|
backend := setupRedisBackend(t)
|
|
runContractTests(t, backend)
|
|
})
|
|
|
|
t.Run("HybridBackend", func(t *testing.T) {
|
|
backend := setupHybridBackend(t)
|
|
runContractTests(t, backend)
|
|
})
|
|
}
|
|
|
|
// runContractTests executes all contract tests against a backend
|
|
func runContractTests(t *testing.T, backend CacheBackend) {
|
|
t.Helper()
|
|
|
|
ctx := context.Background()
|
|
|
|
t.Run("BasicSetGet", func(t *testing.T) {
|
|
testBasicSetGet(t, ctx, backend)
|
|
})
|
|
|
|
t.Run("GetNonExistent", func(t *testing.T) {
|
|
testGetNonExistent(t, ctx, backend)
|
|
})
|
|
|
|
t.Run("UpdateExisting", func(t *testing.T) {
|
|
testUpdateExisting(t, ctx, backend)
|
|
})
|
|
|
|
t.Run("Delete", func(t *testing.T) {
|
|
testDelete(t, ctx, backend)
|
|
})
|
|
|
|
t.Run("DeleteNonExistent", func(t *testing.T) {
|
|
testDeleteNonExistent(t, ctx, backend)
|
|
})
|
|
|
|
t.Run("Exists", func(t *testing.T) {
|
|
testExists(t, ctx, backend)
|
|
})
|
|
|
|
t.Run("TTLExpiration", func(t *testing.T) {
|
|
testTTLExpiration(t, ctx, backend)
|
|
})
|
|
|
|
t.Run("Clear", func(t *testing.T) {
|
|
testClear(t, ctx, backend)
|
|
})
|
|
|
|
t.Run("Ping", func(t *testing.T) {
|
|
testPing(t, ctx, backend)
|
|
})
|
|
|
|
t.Run("Stats", func(t *testing.T) {
|
|
testStats(t, ctx, backend)
|
|
})
|
|
|
|
t.Run("ConcurrentAccess", func(t *testing.T) {
|
|
testConcurrentAccess(t, ctx, backend)
|
|
})
|
|
|
|
t.Run("LargeValues", func(t *testing.T) {
|
|
testLargeValues(t, ctx, backend)
|
|
})
|
|
|
|
t.Run("EmptyValues", func(t *testing.T) {
|
|
testEmptyValues(t, ctx, backend)
|
|
})
|
|
|
|
t.Run("SpecialCharactersInKeys", func(t *testing.T) {
|
|
testSpecialCharactersInKeys(t, ctx, backend)
|
|
})
|
|
}
|
|
|
|
// testBasicSetGet verifies basic set and get operations
|
|
func testBasicSetGet(t *testing.T, ctx context.Context, backend CacheBackend) {
|
|
t.Helper()
|
|
|
|
key := "test-key-1"
|
|
value := []byte("test-value-1")
|
|
ttl := 1 * time.Minute
|
|
|
|
// Set value
|
|
err := backend.Set(ctx, key, value, ttl)
|
|
require.NoError(t, err, "Set should not return error")
|
|
|
|
// Get value
|
|
retrieved, remainingTTL, exists, err := backend.Get(ctx, key)
|
|
require.NoError(t, err, "Get should not return error")
|
|
assert.True(t, exists, "Key should exist")
|
|
assert.Equal(t, value, retrieved, "Retrieved value should match")
|
|
assert.Greater(t, remainingTTL, 50*time.Second, "TTL should be close to original")
|
|
assert.LessOrEqual(t, remainingTTL, ttl, "TTL should not exceed original")
|
|
}
|
|
|
|
// testGetNonExistent verifies behavior when getting non-existent keys
|
|
func testGetNonExistent(t *testing.T, ctx context.Context, backend CacheBackend) {
|
|
t.Helper()
|
|
|
|
key := "non-existent-key"
|
|
|
|
retrieved, ttl, exists, err := backend.Get(ctx, key)
|
|
require.NoError(t, err, "Get should not return error for non-existent key")
|
|
assert.False(t, exists, "Key should not exist")
|
|
assert.Nil(t, retrieved, "Value should be nil")
|
|
assert.Equal(t, time.Duration(0), ttl, "TTL should be zero")
|
|
}
|
|
|
|
// testUpdateExisting verifies updating an existing key
|
|
func testUpdateExisting(t *testing.T, ctx context.Context, backend CacheBackend) {
|
|
t.Helper()
|
|
|
|
key := "update-key"
|
|
value1 := []byte("original-value")
|
|
value2 := []byte("updated-value")
|
|
ttl := 1 * time.Minute
|
|
|
|
// Set initial value
|
|
err := backend.Set(ctx, key, value1, ttl)
|
|
require.NoError(t, err)
|
|
|
|
// Update value
|
|
err = backend.Set(ctx, key, value2, ttl)
|
|
require.NoError(t, err)
|
|
|
|
// Verify updated value
|
|
retrieved, _, exists, err := backend.Get(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
assert.Equal(t, value2, retrieved, "Value should be updated")
|
|
}
|
|
|
|
// testDelete verifies delete operation
|
|
func testDelete(t *testing.T, ctx context.Context, backend CacheBackend) {
|
|
t.Helper()
|
|
|
|
key := "delete-key"
|
|
value := []byte("delete-value")
|
|
|
|
// Set value
|
|
err := backend.Set(ctx, key, value, 1*time.Minute)
|
|
require.NoError(t, err)
|
|
|
|
// Verify exists
|
|
exists, err := backend.Exists(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
|
|
// Delete
|
|
deleted, err := backend.Delete(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.True(t, deleted, "Delete should return true for existing key")
|
|
|
|
// Verify deleted
|
|
exists, err = backend.Exists(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.False(t, exists, "Key should not exist after delete")
|
|
}
|
|
|
|
// testDeleteNonExistent verifies deleting non-existent keys
|
|
func testDeleteNonExistent(t *testing.T, ctx context.Context, backend CacheBackend) {
|
|
t.Helper()
|
|
|
|
key := "non-existent-delete-key"
|
|
|
|
deleted, err := backend.Delete(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.False(t, deleted, "Delete should return false for non-existent key")
|
|
}
|
|
|
|
// testExists verifies the Exists operation
|
|
func testExists(t *testing.T, ctx context.Context, backend CacheBackend) {
|
|
t.Helper()
|
|
|
|
key := "exists-key"
|
|
value := []byte("exists-value")
|
|
|
|
// Check non-existent key
|
|
exists, err := backend.Exists(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.False(t, exists, "Key should not exist initially")
|
|
|
|
// Set value
|
|
err = backend.Set(ctx, key, value, 1*time.Minute)
|
|
require.NoError(t, err)
|
|
|
|
// Check existing key
|
|
exists, err = backend.Exists(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists, "Key should exist after Set")
|
|
}
|
|
|
|
// testTTLExpiration verifies TTL expiration behavior
|
|
func testTTLExpiration(t *testing.T, ctx context.Context, backend CacheBackend) {
|
|
t.Helper()
|
|
|
|
key := "ttl-key"
|
|
value := []byte("ttl-value")
|
|
shortTTL := 100 * time.Millisecond
|
|
|
|
// Set with short TTL
|
|
err := backend.Set(ctx, key, value, shortTTL)
|
|
require.NoError(t, err)
|
|
|
|
// Verify exists immediately
|
|
exists, err := backend.Exists(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists, "Key should exist immediately after Set")
|
|
|
|
// Wait for expiration
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Verify expired
|
|
exists, err = backend.Exists(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.False(t, exists, "Key should not exist after TTL expiration")
|
|
}
|
|
|
|
// testClear verifies Clear operation
|
|
func testClear(t *testing.T, ctx context.Context, backend CacheBackend) {
|
|
t.Helper()
|
|
|
|
// Set multiple keys
|
|
for i := 0; i < 5; i++ {
|
|
key := fmt.Sprintf("clear-key-%d", i)
|
|
value := []byte(fmt.Sprintf("clear-value-%d", i))
|
|
err := backend.Set(ctx, key, value, 1*time.Minute)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Give async writes time to complete before clearing
|
|
// This prevents race conditions with async write workers
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Clear all
|
|
err := backend.Clear(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Verify all keys are gone
|
|
for i := 0; i < 5; i++ {
|
|
key := fmt.Sprintf("clear-key-%d", i)
|
|
exists, err := backend.Exists(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.False(t, exists, "Key should not exist after Clear")
|
|
}
|
|
}
|
|
|
|
// testPing verifies Ping operation
|
|
func testPing(t *testing.T, ctx context.Context, backend CacheBackend) {
|
|
t.Helper()
|
|
|
|
err := backend.Ping(ctx)
|
|
assert.NoError(t, err, "Ping should succeed on healthy backend")
|
|
}
|
|
|
|
// testStats verifies GetStats operation
|
|
func testStats(t *testing.T, ctx context.Context, backend CacheBackend) {
|
|
t.Helper()
|
|
|
|
stats := backend.GetStats()
|
|
assert.NotNil(t, stats, "Stats should not be nil")
|
|
|
|
// Stats should contain basic metrics
|
|
_, hasHits := stats["hits"]
|
|
_, hasMisses := stats["misses"]
|
|
assert.True(t, hasHits || hasMisses, "Stats should contain hits or misses")
|
|
}
|
|
|
|
// testConcurrentAccess verifies thread safety
|
|
func testConcurrentAccess(t *testing.T, ctx context.Context, backend CacheBackend) {
|
|
t.Helper()
|
|
|
|
var wg sync.WaitGroup
|
|
goroutines := 10
|
|
iterations := 20
|
|
|
|
// Concurrent writes
|
|
for i := 0; i < goroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < iterations; j++ {
|
|
key := fmt.Sprintf("concurrent-key-%d-%d", id, j)
|
|
value := []byte(fmt.Sprintf("concurrent-value-%d-%d", id, j))
|
|
err := backend.Set(ctx, key, value, 1*time.Minute)
|
|
assert.NoError(t, err)
|
|
|
|
// Read back
|
|
retrieved, _, exists, err := backend.Get(ctx, key)
|
|
assert.NoError(t, err)
|
|
if exists {
|
|
assert.Equal(t, value, retrieved)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
// testLargeValues verifies handling of large values
|
|
func testLargeValues(t *testing.T, ctx context.Context, backend CacheBackend) {
|
|
t.Helper()
|
|
|
|
key := "large-value-key"
|
|
value := GenerateLargeValue(1024 * 1024) // 1MB
|
|
|
|
err := backend.Set(ctx, key, value, 1*time.Minute)
|
|
require.NoError(t, err, "Should handle large values")
|
|
|
|
retrieved, _, exists, err := backend.Get(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
assert.Equal(t, len(value), len(retrieved), "Large value should be retrieved intact")
|
|
}
|
|
|
|
// testEmptyValues verifies handling of empty values
|
|
func testEmptyValues(t *testing.T, ctx context.Context, backend CacheBackend) {
|
|
t.Helper()
|
|
|
|
key := "empty-value-key"
|
|
value := []byte{}
|
|
|
|
err := backend.Set(ctx, key, value, 1*time.Minute)
|
|
require.NoError(t, err, "Should handle empty values")
|
|
|
|
retrieved, _, exists, err := backend.Get(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists, "Empty value should exist")
|
|
assert.Equal(t, 0, len(retrieved), "Retrieved value should be empty")
|
|
}
|
|
|
|
// testSpecialCharactersInKeys verifies handling of special characters in keys
|
|
func testSpecialCharactersInKeys(t *testing.T, ctx context.Context, backend CacheBackend) {
|
|
t.Helper()
|
|
|
|
specialKeys := []string{
|
|
"key:with:colons",
|
|
"key/with/slashes",
|
|
"key-with-dashes",
|
|
"key_with_underscores",
|
|
"key.with.dots",
|
|
"key|with|pipes",
|
|
}
|
|
|
|
for _, key := range specialKeys {
|
|
value := []byte(fmt.Sprintf("value-for-%s", key))
|
|
|
|
err := backend.Set(ctx, key, value, 1*time.Minute)
|
|
require.NoError(t, err, "Should handle special character in key: %s", key)
|
|
|
|
retrieved, _, exists, err := backend.Get(ctx, key)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists, "Key with special characters should exist: %s", key)
|
|
assert.Equal(t, value, retrieved)
|
|
}
|
|
}
|
|
|
|
// Helper functions to setup different backend types
|
|
// These will be implemented in respective test files
|
|
|
|
func setupMemoryBackend(t *testing.T) CacheBackend {
|
|
t.Helper()
|
|
// This will be implemented in memory_test.go
|
|
// For now, return nil to allow compilation
|
|
t.Skip("MemoryBackend implementation pending")
|
|
return nil
|
|
}
|
|
|
|
func setupRedisBackend(t *testing.T) CacheBackend {
|
|
t.Helper()
|
|
// This will be implemented in redis_test.go
|
|
// For now, return nil to allow compilation
|
|
t.Skip("RedisBackend implementation pending")
|
|
return nil
|
|
}
|
|
|
|
func setupHybridBackend(t *testing.T) CacheBackend {
|
|
t.Helper()
|
|
|
|
primary := newMockBackend()
|
|
secondary := newMockBackend()
|
|
|
|
config := &HybridConfig{
|
|
Primary: primary,
|
|
Secondary: secondary,
|
|
AsyncBufferSize: 100,
|
|
Logger: NewTestLogger(t),
|
|
}
|
|
|
|
hybrid, err := NewHybridBackend(config)
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
hybrid.Close()
|
|
})
|
|
|
|
return hybrid
|
|
}
|