Files
traefikoidc/internal/features/flags_test.go
T
lukaszraczylo e64fc7f730 Add redis support for distributed caching (#83)
* 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>
2025-11-30 02:18:46 +00:00

484 lines
10 KiB
Go

//go:build !yaegi
package features
import (
"os"
"sync"
"sync/atomic"
"testing"
"time"
)
func TestFeatureManager_Register(t *testing.T) {
m := &FeatureManager{
flags: make(map[string]*FeatureFlag),
}
m.Register("TEST_FEATURE", "Test feature", false)
if !m.flags["TEST_FEATURE"].enabled.Load() == false {
t.Error("Expected feature to be disabled by default")
}
m.Register("TEST_ENABLED", "Test enabled feature", true)
if m.flags["TEST_ENABLED"].enabled.Load() != true {
t.Error("Expected feature to be enabled")
}
}
func TestFeatureManager_IsEnabled(t *testing.T) {
m := &FeatureManager{
flags: make(map[string]*FeatureFlag),
}
m.Register("TEST_FEATURE", "Test feature", true)
if !m.IsEnabled("TEST_FEATURE") {
t.Error("Expected feature to be enabled")
}
if m.IsEnabled("NON_EXISTENT") {
t.Error("Expected non-existent feature to return false")
}
}
func TestFeatureManager_EnableDisable(t *testing.T) {
m := &FeatureManager{
flags: make(map[string]*FeatureFlag),
}
m.Register("TEST_FEATURE", "Test feature", false)
// Enable the feature
m.Enable("TEST_FEATURE")
if !m.IsEnabled("TEST_FEATURE") {
t.Error("Expected feature to be enabled")
}
// Disable the feature
m.Disable("TEST_FEATURE")
if m.IsEnabled("TEST_FEATURE") {
t.Error("Expected feature to be disabled")
}
// Enable/Disable non-existent feature should not panic
m.Enable("NON_EXISTENT")
m.Disable("NON_EXISTENT")
}
func TestFeatureManager_Toggle(t *testing.T) {
m := &FeatureManager{
flags: make(map[string]*FeatureFlag),
}
m.Register("TEST_FEATURE", "Test feature", false)
// Toggle from false to true
m.Toggle("TEST_FEATURE")
if !m.IsEnabled("TEST_FEATURE") {
t.Error("Expected feature to be enabled after toggle")
}
// Toggle from true to false
m.Toggle("TEST_FEATURE")
if m.IsEnabled("TEST_FEATURE") {
t.Error("Expected feature to be disabled after toggle")
}
// Toggle non-existent feature should not panic
m.Toggle("NON_EXISTENT")
}
func TestFeatureManager_OnChange(t *testing.T) {
m := &FeatureManager{
flags: make(map[string]*FeatureFlag),
}
m.Register("TEST_FEATURE", "Test feature", false)
var callbackCalled atomic.Bool
var callbackValue atomic.Bool
m.OnChange("TEST_FEATURE", func(enabled bool) {
callbackCalled.Store(true)
callbackValue.Store(enabled)
})
// Enable should trigger callback
m.Enable("TEST_FEATURE")
// Wait briefly for callback
time.Sleep(10 * time.Millisecond)
if !callbackCalled.Load() {
t.Error("Expected callback to be called")
}
if !callbackValue.Load() {
t.Error("Expected callback value to be true")
}
// Setting to same value should NOT trigger callback again
callbackCalled.Store(false)
m.Enable("TEST_FEATURE")
time.Sleep(10 * time.Millisecond)
if callbackCalled.Load() {
t.Error("Expected callback NOT to be called when value doesn't change")
}
}
func TestFeatureManager_LoadFromEnv(t *testing.T) {
m := &FeatureManager{
flags: make(map[string]*FeatureFlag),
}
m.Register("TEST_FEATURE", "Test feature", false)
m.Register("TEST_FEATURE_2", "Test feature 2", false)
// Set environment variables
os.Setenv("FEATURE_TEST_FEATURE", "true")
os.Setenv("FEATURE_TEST_FEATURE_2", "1")
defer func() {
os.Unsetenv("FEATURE_TEST_FEATURE")
os.Unsetenv("FEATURE_TEST_FEATURE_2")
}()
m.LoadFromEnv()
if !m.IsEnabled("TEST_FEATURE") {
t.Error("Expected TEST_FEATURE to be enabled from env")
}
if !m.IsEnabled("TEST_FEATURE_2") {
t.Error("Expected TEST_FEATURE_2 to be enabled from env (value=1)")
}
}
func TestFeatureManager_LoadFromEnv_FalseValues(t *testing.T) {
m := &FeatureManager{
flags: make(map[string]*FeatureFlag),
}
m.Register("TEST_FEATURE", "Test feature", true) // Default true
// Set to false
os.Setenv("FEATURE_TEST_FEATURE", "false")
defer os.Unsetenv("FEATURE_TEST_FEATURE")
m.LoadFromEnv()
if m.IsEnabled("TEST_FEATURE") {
t.Error("Expected TEST_FEATURE to be disabled from env")
}
}
func TestFeatureManager_GetAll(t *testing.T) {
m := &FeatureManager{
flags: make(map[string]*FeatureFlag),
}
m.Register("FEATURE_1", "Feature 1", true)
m.Register("FEATURE_2", "Feature 2", false)
all := m.GetAll()
if len(all) != 2 {
t.Errorf("Expected 2 features, got %d", len(all))
}
if !all["FEATURE_1"] {
t.Error("Expected FEATURE_1 to be enabled")
}
if all["FEATURE_2"] {
t.Error("Expected FEATURE_2 to be disabled")
}
}
func TestFeatureManager_Reset(t *testing.T) {
m := &FeatureManager{
flags: make(map[string]*FeatureFlag),
}
m.Register("FEATURE_1", "Feature 1", true)
m.Register("FEATURE_2", "Feature 2", true)
var callbackCalled atomic.Int32
m.OnChange("FEATURE_1", func(enabled bool) {
callbackCalled.Add(1)
})
m.Reset()
// All features should be disabled
if m.IsEnabled("FEATURE_1") {
t.Error("Expected FEATURE_1 to be disabled after reset")
}
if m.IsEnabled("FEATURE_2") {
t.Error("Expected FEATURE_2 to be disabled after reset")
}
// Callbacks should be cleared
m.Enable("FEATURE_1")
time.Sleep(10 * time.Millisecond)
if callbackCalled.Load() != 0 {
t.Error("Expected callbacks to be cleared after reset")
}
}
func TestGetManager_Singleton(t *testing.T) {
// Reset global state for clean test
managerOnce = sync.Once{}
manager = nil
m1 := GetManager()
m2 := GetManager()
if m1 != m2 {
t.Error("Expected GetManager to return same instance")
}
}
func TestGetManager_Initialize(t *testing.T) {
// Reset global state for clean test
managerOnce = sync.Once{}
manager = nil
m := GetManager()
// Should have default feature flags
all := m.GetAll()
if len(all) < 6 {
t.Errorf("Expected at least 6 default feature flags, got %d", len(all))
}
// Check specific flags exist
flags := []string{
UseUnifiedConfig,
UseNewFileStructure,
UseStandardErrors,
UseEnhancedLogging,
UseOptimizedTests,
UseRedisRESP,
}
for _, flag := range flags {
if _, exists := m.flags[flag]; !exists {
t.Errorf("Expected default flag %s to exist", flag)
}
}
}
func TestHelperFunctions(t *testing.T) {
// Reset global state
managerOnce = sync.Once{}
manager = nil
// Test IsUnifiedConfigEnabled
if IsUnifiedConfigEnabled() {
t.Error("Expected unified config to be disabled by default")
}
GetManager().Enable(UseUnifiedConfig)
if !IsUnifiedConfigEnabled() {
t.Error("Expected unified config to be enabled")
}
// Reset for next test
GetManager().Reset()
// Test IsNewFileStructureEnabled
if IsNewFileStructureEnabled() {
t.Error("Expected new file structure to be disabled by default")
}
GetManager().Enable(UseNewFileStructure)
if !IsNewFileStructureEnabled() {
t.Error("Expected new file structure to be enabled")
}
// Test IsStandardErrorsEnabled
GetManager().Reset()
GetManager().Enable(UseStandardErrors)
if !IsStandardErrorsEnabled() {
t.Error("Expected standard errors to be enabled")
}
// Test IsEnhancedLoggingEnabled
GetManager().Reset()
GetManager().Enable(UseEnhancedLogging)
if !IsEnhancedLoggingEnabled() {
t.Error("Expected enhanced logging to be enabled")
}
// Test IsOptimizedTestsEnabled
GetManager().Reset()
GetManager().Enable(UseOptimizedTests)
if !IsOptimizedTestsEnabled() {
t.Error("Expected optimized tests to be enabled")
}
// Test IsRedisRESPEnabled
GetManager().Reset()
GetManager().Enable(UseRedisRESP)
if !IsRedisRESPEnabled() {
t.Error("Expected Redis RESP to be enabled")
}
}
// Race condition tests
func TestFeatureManager_ConcurrentAccess(t *testing.T) {
m := &FeatureManager{
flags: make(map[string]*FeatureFlag),
}
m.Register("TEST_FEATURE", "Test feature", false)
var wg sync.WaitGroup
iterations := 100
// Concurrent enables
for i := 0; i < iterations; i++ {
wg.Add(1)
go func() {
defer wg.Done()
m.Enable("TEST_FEATURE")
}()
}
// Concurrent disables
for i := 0; i < iterations; i++ {
wg.Add(1)
go func() {
defer wg.Done()
m.Disable("TEST_FEATURE")
}()
}
// Concurrent reads
for i := 0; i < iterations; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = m.IsEnabled("TEST_FEATURE")
}()
}
wg.Wait()
// Should not panic - final state is not deterministic but that's ok
}
func TestFeatureManager_ConcurrentCallbacks(t *testing.T) {
m := &FeatureManager{
flags: make(map[string]*FeatureFlag),
}
m.Register("TEST_FEATURE", "Test feature", false)
var callbackCount atomic.Int32
var wg sync.WaitGroup
// Register multiple callbacks concurrently
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
m.OnChange("TEST_FEATURE", func(enabled bool) {
callbackCount.Add(1)
})
}()
}
wg.Wait()
// Toggle the feature
m.Toggle("TEST_FEATURE")
// Wait for callbacks
time.Sleep(50 * time.Millisecond)
// All 10 callbacks should have been called
if callbackCount.Load() != 10 {
t.Errorf("Expected 10 callbacks, got %d", callbackCount.Load())
}
}
func TestFeatureManager_ConcurrentGetAll(t *testing.T) {
m := &FeatureManager{
flags: make(map[string]*FeatureFlag),
}
for i := 0; i < 5; i++ {
m.Register(string(rune('A'+i)), "Feature", false)
}
var wg sync.WaitGroup
// Concurrent GetAll calls
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
all := m.GetAll()
if len(all) != 5 {
t.Errorf("Expected 5 flags, got %d", len(all))
}
}()
}
// Concurrent modifications
for i := 0; i < 100; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
flag := string(rune('A' + (idx % 5)))
if idx%2 == 0 {
m.Enable(flag)
} else {
m.Disable(flag)
}
}(i)
}
wg.Wait()
}
func TestFeatureManager_LoadFromEnv_Concurrent(t *testing.T) {
m := &FeatureManager{
flags: make(map[string]*FeatureFlag),
}
m.Register("FEATURE_1", "Feature 1", false)
m.Register("FEATURE_2", "Feature 2", false)
os.Setenv("FEATURE_FEATURE_1", "true")
os.Setenv("FEATURE_FEATURE_2", "true")
defer func() {
os.Unsetenv("FEATURE_FEATURE_1")
os.Unsetenv("FEATURE_FEATURE_2")
}()
var wg sync.WaitGroup
// Load from env concurrently
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
m.LoadFromEnv()
}()
}
wg.Wait()
// Both should be enabled
if !m.IsEnabled("FEATURE_1") || !m.IsEnabled("FEATURE_2") {
t.Error("Expected features to be enabled from env")
}
}