Files
Charon/backend/internal/api/handlers/feature_flags_handler_coverage_test.go
GitHub Actions e6c4e46dd8 chore: Refactor test setup for Gin framework
- Removed redundant `gin.SetMode(gin.TestMode)` calls from individual test files.
- Introduced a centralized `TestMain` function in `testmain_test.go` to set the Gin mode for all tests.
- Ensured consistent test environment setup across various handler test files.
2026-03-25 22:00:07 +00:00

469 lines
12 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 TestFeatureFlagsHandler_GetFlags_DBPrecedence(t *testing.T) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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)
}
func TestFeatureFlagsHandler_GetFlags_EmailFlagDefaultFalse(t *testing.T) {
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)
assert.False(t, flags["feature.notifications.service.email.enabled"])
}