package handlers import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/Wikid82/charon/backend/internal/models" ) func TestFeatureFlagsHandler_GetFlags_DBPrecedence(t *testing.T) { gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // Set a flag in DB db.Create(&models.Setting{ Key: "feature.cerberus.enabled", Value: "false", Type: "bool", Category: "feature", }) // Set env var that should be ignored (DB takes precedence) t.Setenv("FEATURE_CERBERUS_ENABLED", "true") h := NewFeatureFlagsHandler(db) r := gin.New() r.GET("/api/v1/feature-flags", h.GetFlags) req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var flags map[string]bool err := json.Unmarshal(w.Body.Bytes(), &flags) require.NoError(t, err) // DB value (false) should take precedence over env (true) assert.False(t, flags["feature.cerberus.enabled"]) } func TestFeatureFlagsHandler_GetFlags_EnvFallback(t *testing.T) { gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // Set env var (no DB value exists) t.Setenv("FEATURE_CERBERUS_ENABLED", "false") h := NewFeatureFlagsHandler(db) r := gin.New() r.GET("/api/v1/feature-flags", h.GetFlags) req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var flags map[string]bool err := json.Unmarshal(w.Body.Bytes(), &flags) require.NoError(t, err) // Env value should be used assert.False(t, flags["feature.cerberus.enabled"]) } func TestFeatureFlagsHandler_GetFlags_EnvShortForm(t *testing.T) { gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // Set short form env var (CERBERUS_ENABLED instead of FEATURE_CERBERUS_ENABLED) t.Setenv("CERBERUS_ENABLED", "false") h := NewFeatureFlagsHandler(db) r := gin.New() r.GET("/api/v1/feature-flags", h.GetFlags) req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var flags map[string]bool err := json.Unmarshal(w.Body.Bytes(), &flags) require.NoError(t, err) // Short form env value should be used assert.False(t, flags["feature.cerberus.enabled"]) } func TestFeatureFlagsHandler_GetFlags_EnvNumeric(t *testing.T) { gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // Set numeric env var (1/0 instead of true/false) t.Setenv("FEATURE_UPTIME_ENABLED", "0") h := NewFeatureFlagsHandler(db) r := gin.New() r.GET("/api/v1/feature-flags", h.GetFlags) req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var flags map[string]bool err := json.Unmarshal(w.Body.Bytes(), &flags) require.NoError(t, err) // "0" should be parsed as false assert.False(t, flags["feature.uptime.enabled"]) } func TestFeatureFlagsHandler_GetFlags_DefaultTrue(t *testing.T) { gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // No DB value, no env var - check defaults h := NewFeatureFlagsHandler(db) r := gin.New() r.GET("/api/v1/feature-flags", h.GetFlags) req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var flags map[string]bool err := json.Unmarshal(w.Body.Bytes(), &flags) require.NoError(t, err) // Cerberus defaults to false (OFF by default per diagnostic fix) assert.False(t, flags["feature.cerberus.enabled"]) // Uptime defaults to true (no explicit default set) assert.True(t, flags["feature.uptime.enabled"]) } func TestFeatureFlagsHandler_GetFlags_AllDefaultFlagsPresent(t *testing.T) { gin.SetMode(gin.TestMode) db := setupFlagsDB(t) h := NewFeatureFlagsHandler(db) r := gin.New() r.GET("/api/v1/feature-flags", h.GetFlags) req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var flags map[string]bool err := json.Unmarshal(w.Body.Bytes(), &flags) require.NoError(t, err) // Ensure all default flags are present for _, key := range defaultFlags { _, ok := flags[key] assert.True(t, ok, "expected flag %s to be present", key) } } func TestFeatureFlagsHandler_UpdateFlags_Success(t *testing.T) { gin.SetMode(gin.TestMode) db := setupFlagsDB(t) h := NewFeatureFlagsHandler(db) r := gin.New() r.PUT("/api/v1/feature-flags", h.UpdateFlags) payload := map[string]bool{ "feature.cerberus.enabled": false, "feature.uptime.enabled": true, } b, _ := json.Marshal(payload) req := httptest.NewRequest(http.MethodPut, "/api/v1/feature-flags", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) // Verify DB persistence var s1 models.Setting err := db.Where("key = ?", "feature.cerberus.enabled").First(&s1).Error require.NoError(t, err) assert.Equal(t, "false", s1.Value) assert.Equal(t, "bool", s1.Type) assert.Equal(t, "feature", s1.Category) var s2 models.Setting err = db.Where("key = ?", "feature.uptime.enabled").First(&s2).Error require.NoError(t, err) assert.Equal(t, "true", s2.Value) } func TestFeatureFlagsHandler_UpdateFlags_Upsert(t *testing.T) { gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // Create existing setting db.Create(&models.Setting{ Key: "feature.cerberus.enabled", Value: "true", Type: "bool", Category: "feature", }) h := NewFeatureFlagsHandler(db) r := gin.New() r.PUT("/api/v1/feature-flags", h.UpdateFlags) // Update existing setting payload := map[string]bool{ "feature.cerberus.enabled": false, } b, _ := json.Marshal(payload) req := httptest.NewRequest(http.MethodPut, "/api/v1/feature-flags", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) // Verify update var s models.Setting err := db.Where("key = ?", "feature.cerberus.enabled").First(&s).Error require.NoError(t, err) assert.Equal(t, "false", s.Value) // Verify only one record exists var count int64 db.Model(&models.Setting{}).Where("key = ?", "feature.cerberus.enabled").Count(&count) assert.Equal(t, int64(1), count) } func TestFeatureFlagsHandler_UpdateFlags_InvalidJSON(t *testing.T) { gin.SetMode(gin.TestMode) db := setupFlagsDB(t) h := NewFeatureFlagsHandler(db) r := gin.New() r.PUT("/api/v1/feature-flags", h.UpdateFlags) req := httptest.NewRequest(http.MethodPut, "/api/v1/feature-flags", bytes.NewReader([]byte("invalid json"))) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestFeatureFlagsHandler_UpdateFlags_OnlyAllowedKeys(t *testing.T) { gin.SetMode(gin.TestMode) db := setupFlagsDB(t) h := NewFeatureFlagsHandler(db) r := gin.New() r.PUT("/api/v1/feature-flags", h.UpdateFlags) // Try to set a key not in defaultFlags payload := map[string]bool{ "feature.cerberus.enabled": false, "feature.invalid.key": true, // Should be ignored } b, _ := json.Marshal(payload) req := httptest.NewRequest(http.MethodPut, "/api/v1/feature-flags", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) // Verify allowed key was saved var s1 models.Setting err := db.Where("key = ?", "feature.cerberus.enabled").First(&s1).Error require.NoError(t, err) // Verify disallowed key was NOT saved var s2 models.Setting err = db.Where("key = ?", "feature.invalid.key").First(&s2).Error assert.Error(t, err) } func TestFeatureFlagsHandler_UpdateFlags_EmptyPayload(t *testing.T) { gin.SetMode(gin.TestMode) db := setupFlagsDB(t) h := NewFeatureFlagsHandler(db) r := gin.New() r.PUT("/api/v1/feature-flags", h.UpdateFlags) payload := map[string]bool{} b, _ := json.Marshal(payload) req := httptest.NewRequest(http.MethodPut, "/api/v1/feature-flags", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestFeatureFlagsHandler_GetFlags_DBValueVariants(t *testing.T) { tests := []struct { name string dbValue string expected bool }{ {"lowercase true", "true", true}, {"uppercase TRUE", "TRUE", true}, {"mixed case True", "True", true}, {"numeric 1", "1", true}, {"yes", "yes", true}, {"YES uppercase", "YES", true}, {"lowercase false", "false", false}, {"numeric 0", "0", false}, {"no", "no", false}, {"empty string", "", false}, {"random string", "random", false}, {"whitespace padded true", " true ", true}, {"whitespace padded false", " false ", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // Set flag with test value db.Create(&models.Setting{ Key: "feature.cerberus.enabled", Value: tt.dbValue, Type: "bool", Category: "feature", }) h := NewFeatureFlagsHandler(db) r := gin.New() r.GET("/api/v1/feature-flags", h.GetFlags) req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var flags map[string]bool err := json.Unmarshal(w.Body.Bytes(), &flags) require.NoError(t, err) assert.Equal(t, tt.expected, flags["feature.cerberus.enabled"], "dbValue=%q should result in %v", tt.dbValue, tt.expected) }) } } func TestFeatureFlagsHandler_GetFlags_EnvValueVariants(t *testing.T) { tests := []struct { name string envValue string expected bool }{ {"true string", "true", true}, {"TRUE uppercase", "TRUE", true}, {"1 numeric", "1", true}, {"false string", "false", false}, {"FALSE uppercase", "FALSE", false}, {"0 numeric", "0", false}, {"invalid value defaults to numeric check", "invalid", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gin.SetMode(gin.TestMode) db := setupFlagsDB(t) // Set env var (no DB value) t.Setenv("FEATURE_CERBERUS_ENABLED", tt.envValue) h := NewFeatureFlagsHandler(db) r := gin.New() r.GET("/api/v1/feature-flags", h.GetFlags) req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var flags map[string]bool err := json.Unmarshal(w.Body.Bytes(), &flags) require.NoError(t, err) assert.Equal(t, tt.expected, flags["feature.cerberus.enabled"], "envValue=%q should result in %v", tt.envValue, tt.expected) }) } } func TestFeatureFlagsHandler_UpdateFlags_BoolValues(t *testing.T) { tests := []struct { name string value bool dbExpect string }{ {"true", true, "true"}, {"false", false, "false"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gin.SetMode(gin.TestMode) db := setupFlagsDB(t) h := NewFeatureFlagsHandler(db) r := gin.New() r.PUT("/api/v1/feature-flags", h.UpdateFlags) payload := map[string]bool{ "feature.cerberus.enabled": tt.value, } b, _ := json.Marshal(payload) req := httptest.NewRequest(http.MethodPut, "/api/v1/feature-flags", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var s models.Setting err := db.Where("key = ?", "feature.cerberus.enabled").First(&s).Error require.NoError(t, err) assert.Equal(t, tt.dbExpect, s.Value) }) } } func TestFeatureFlagsHandler_NewFeatureFlagsHandler(t *testing.T) { db := setupFlagsDB(t) h := NewFeatureFlagsHandler(db) assert.NotNil(t, h) assert.NotNil(t, h.DB) assert.Equal(t, db, h.DB) }