Files
Charon/backend/internal/api/handlers/feature_flags_handler_coverage_test.go
GitHub Actions 197e2bf672 Add comprehensive tests for security and user handlers, enhancing coverage
- Introduced tests for the security handler, covering UpdateConfig, GetConfig, ListDecisions, CreateDecision, UpsertRuleSet, DeleteRuleSet, Enable, and Disable functionalities.
- Added tests for user handler methods including GetSetupStatus, Setup, RegenerateAPIKey, GetProfile, and UpdateProfile, ensuring robust error handling and validation.
- Implemented path traversal and injection tests in the WAF configuration to prevent security vulnerabilities.
- Updated the manager to sanitize ruleset names by stripping potentially harmful characters and patterns.
2025-12-04 17:54:17 +00:00

259 lines
6.9 KiB
Go

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 TestFeatureFlags_UpdateFlags_InvalidPayload(t *testing.T) {
db := setupFlagsDB(t)
h := NewFeatureFlagsHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.PUT("/api/v1/feature-flags", h.UpdateFlags)
// Send invalid JSON
req := httptest.NewRequest(http.MethodPut, "/api/v1/feature-flags", bytes.NewReader([]byte("invalid")))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestFeatureFlags_UpdateFlags_IgnoresInvalidKeys(t *testing.T) {
db := setupFlagsDB(t)
require.NoError(t, db.AutoMigrate(&models.Setting{}))
h := NewFeatureFlagsHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.PUT("/api/v1/feature-flags", h.UpdateFlags)
// Try to update a non-whitelisted key
payload := []byte(`{"invalid.key": true, "feature.global.enabled": true}`)
req := httptest.NewRequest(http.MethodPut, "/api/v1/feature-flags", bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Verify invalid key was NOT saved
var s models.Setting
err := db.Where("key = ?", "invalid.key").First(&s).Error
assert.Error(t, err) // Should not exist
// Valid key should be saved
err = db.Where("key = ?", "feature.global.enabled").First(&s).Error
assert.NoError(t, err)
assert.Equal(t, "true", s.Value)
}
func TestFeatureFlags_EnvFallback_ShortVariant(t *testing.T) {
// Test the short env variant (CERBERUS_ENABLED instead of FEATURE_CERBERUS_ENABLED)
t.Setenv("CERBERUS_ENABLED", "true")
db := OpenTestDB(t)
h := NewFeatureFlagsHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/api/v1/feature-flags", h.GetFlags)
req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Parse response
var flags map[string]bool
err := json.Unmarshal(w.Body.Bytes(), &flags)
require.NoError(t, err)
// Should be true via short env fallback
assert.True(t, flags["feature.cerberus.enabled"])
}
func TestFeatureFlags_EnvFallback_WithValue1(t *testing.T) {
// Test env fallback with "1" as value
t.Setenv("FEATURE_UPTIME_ENABLED", "1")
db := OpenTestDB(t)
h := NewFeatureFlagsHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/api/v1/feature-flags", h.GetFlags)
req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var flags map[string]bool
json.Unmarshal(w.Body.Bytes(), &flags)
assert.True(t, flags["feature.uptime.enabled"])
}
func TestFeatureFlags_EnvFallback_WithValue0(t *testing.T) {
// Test env fallback with "0" as value (should be false)
t.Setenv("FEATURE_DOCKER_ENABLED", "0")
db := OpenTestDB(t)
h := NewFeatureFlagsHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/api/v1/feature-flags", h.GetFlags)
req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var flags map[string]bool
json.Unmarshal(w.Body.Bytes(), &flags)
assert.False(t, flags["feature.docker.enabled"])
}
func TestFeatureFlags_DBTakesPrecedence(t *testing.T) {
// Test that DB value takes precedence over env
t.Setenv("FEATURE_NOTIFICATIONS_ENABLED", "false")
db := setupFlagsDB(t)
// Set DB value to true
db.Create(&models.Setting{Key: "feature.notifications.enabled", Value: "true", Type: "bool", Category: "feature"})
h := NewFeatureFlagsHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/api/v1/feature-flags", h.GetFlags)
req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var flags map[string]bool
json.Unmarshal(w.Body.Bytes(), &flags)
// DB value (true) should take precedence over env (false)
assert.True(t, flags["feature.notifications.enabled"])
}
func TestFeatureFlags_DBValueVariations(t *testing.T) {
db := setupFlagsDB(t)
// Test various DB value formats
testCases := []struct {
key string
dbValue string
expected bool
}{
{"feature.global.enabled", "1", true},
{"feature.cerberus.enabled", "yes", true},
{"feature.uptime.enabled", "TRUE", true},
{"feature.notifications.enabled", "false", false},
{"feature.docker.enabled", "0", false},
}
for _, tc := range testCases {
db.Create(&models.Setting{Key: tc.key, Value: tc.dbValue, Type: "bool", Category: "feature"})
}
h := NewFeatureFlagsHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/api/v1/feature-flags", h.GetFlags)
req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var flags map[string]bool
json.Unmarshal(w.Body.Bytes(), &flags)
for _, tc := range testCases {
assert.Equal(t, tc.expected, flags[tc.key], "flag %s expected %v", tc.key, tc.expected)
}
}
func TestFeatureFlags_UpdateMultipleFlags(t *testing.T) {
db := setupFlagsDB(t)
h := NewFeatureFlagsHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.PUT("/api/v1/feature-flags", h.UpdateFlags)
r.GET("/api/v1/feature-flags", h.GetFlags)
// Update multiple flags at once
payload := []byte(`{
"feature.global.enabled": true,
"feature.cerberus.enabled": false,
"feature.uptime.enabled": true
}`)
req := httptest.NewRequest(http.MethodPut, "/api/v1/feature-flags", bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Verify by getting flags
req = httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", nil)
w = httptest.NewRecorder()
r.ServeHTTP(w, req)
var flags map[string]bool
json.Unmarshal(w.Body.Bytes(), &flags)
assert.True(t, flags["feature.global.enabled"])
assert.False(t, flags["feature.cerberus.enabled"])
assert.True(t, flags["feature.uptime.enabled"])
}
func TestFeatureFlags_ShortEnvFallback_WithUnparseable(t *testing.T) {
// Test short env fallback with a value that's not directly parseable as bool
// but is "1" which should be treated as true
t.Setenv("GLOBAL_ENABLED", "1")
db := OpenTestDB(t)
h := NewFeatureFlagsHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/api/v1/feature-flags", h.GetFlags)
req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var flags map[string]bool
json.Unmarshal(w.Body.Bytes(), &flags)
assert.True(t, flags["feature.global.enabled"])
}