Files
Charon/backend/internal/api/handlers/crowdsec_state_sync_test.go
T
GitHub Actions 71e44f79a7 fix: resolve CrowdSec state sync issues and remove deprecated mode toggle
- Backend: Start/Stop handlers now sync both settings and security_configs tables
- Frontend: CrowdSec toggle uses actual process status (crowdsecStatus.running)
- Frontend: Fixed LiveLogViewer WebSocket race condition by using isPausedRef
- Frontend: Removed deprecated mode toggle from CrowdSecConfig page
- Frontend: Added info banner directing users to Security Dashboard
- Frontend: Added "Start CrowdSec" button to enrollment warning panel

Fixes dual-source state conflict causing toggle to show incorrect state.
Fixes live log "disconnected" status appearing while logs stream.
Simplifies CrowdSec control to single source (Security Dashboard toggle).

Includes comprehensive test updates for new architecture.
2025-12-15 23:36:07 +00:00

283 lines
9.0 KiB
Go

package handlers
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
// setupCrowdDBWithSettings creates a test database with both SecurityConfig and Setting tables.
func setupCrowdDBWithSettings(t *testing.T) *testing.T {
t.Helper()
return t
}
// TestStartSyncsSettingsTable verifies that Start() updates the settings table.
func TestStartSyncsSettingsTable(t *testing.T) {
gin.SetMode(gin.TestMode)
db := OpenTestDB(t)
// Migrate both SecurityConfig and Setting tables
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{}))
tmpDir := t.TempDir()
fe := &fakeExec{}
h := NewCrowdsecHandler(db, fe, "/bin/false", tmpDir)
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
// Verify settings table is initially empty
var initialSetting models.Setting
err := db.Where("key = ?", "security.crowdsec.enabled").First(&initialSetting).Error
require.Error(t, err, "expected setting to not exist initially")
// Start CrowdSec
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/crowdsec/start", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
// Verify setting was created/updated to "true"
var setting models.Setting
err = db.Where("key = ?", "security.crowdsec.enabled").First(&setting).Error
require.NoError(t, err, "expected setting to be created after Start")
require.Equal(t, "true", setting.Value)
require.Equal(t, "security", setting.Category)
require.Equal(t, "bool", setting.Type)
// Also verify SecurityConfig was updated
var cfg models.SecurityConfig
err = db.First(&cfg).Error
require.NoError(t, err, "expected SecurityConfig to exist")
require.Equal(t, "local", cfg.CrowdSecMode)
require.True(t, cfg.Enabled)
}
// TestStopSyncsSettingsTable verifies that Stop() updates the settings table.
func TestStopSyncsSettingsTable(t *testing.T) {
gin.SetMode(gin.TestMode)
db := OpenTestDB(t)
// Migrate both SecurityConfig and Setting tables
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{}))
tmpDir := t.TempDir()
fe := &fakeExec{}
h := NewCrowdsecHandler(db, fe, "/bin/false", tmpDir)
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
// First start CrowdSec to create the settings
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/crowdsec/start", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
// Verify setting is "true" after start
var settingAfterStart models.Setting
err := db.Where("key = ?", "security.crowdsec.enabled").First(&settingAfterStart).Error
require.NoError(t, err)
require.Equal(t, "true", settingAfterStart.Value)
// Now stop CrowdSec
w2 := httptest.NewRecorder()
req2 := httptest.NewRequest(http.MethodPost, "/api/v1/admin/crowdsec/stop", http.NoBody)
r.ServeHTTP(w2, req2)
require.Equal(t, http.StatusOK, w2.Code)
// Verify setting was updated to "false"
var settingAfterStop models.Setting
err = db.Where("key = ?", "security.crowdsec.enabled").First(&settingAfterStop).Error
require.NoError(t, err)
require.Equal(t, "false", settingAfterStop.Value)
// Also verify SecurityConfig was updated
var cfg models.SecurityConfig
err = db.First(&cfg).Error
require.NoError(t, err)
require.Equal(t, "disabled", cfg.CrowdSecMode)
require.False(t, cfg.Enabled)
}
// TestStartAndStopStateConsistency verifies consistent state across Start/Stop cycles.
func TestStartAndStopStateConsistency(t *testing.T) {
gin.SetMode(gin.TestMode)
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{}))
tmpDir := t.TempDir()
fe := &fakeExec{}
h := NewCrowdsecHandler(db, fe, "/bin/false", tmpDir)
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
// Perform multiple start/stop cycles
for i := 0; i < 3; i++ {
// Start
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/crowdsec/start", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, "cycle %d start", i)
// Verify both tables are in sync
var setting models.Setting
err := db.Where("key = ?", "security.crowdsec.enabled").First(&setting).Error
require.NoError(t, err, "cycle %d: setting should exist after start", i)
require.Equal(t, "true", setting.Value, "cycle %d: setting should be true after start", i)
var cfg models.SecurityConfig
err = db.First(&cfg).Error
require.NoError(t, err, "cycle %d: config should exist after start", i)
require.Equal(t, "local", cfg.CrowdSecMode, "cycle %d: mode should be local after start", i)
// Stop
w2 := httptest.NewRecorder()
req2 := httptest.NewRequest(http.MethodPost, "/api/v1/admin/crowdsec/stop", http.NoBody)
r.ServeHTTP(w2, req2)
require.Equal(t, http.StatusOK, w2.Code, "cycle %d stop", i)
// Verify both tables are in sync
err = db.Where("key = ?", "security.crowdsec.enabled").First(&setting).Error
require.NoError(t, err, "cycle %d: setting should exist after stop", i)
require.Equal(t, "false", setting.Value, "cycle %d: setting should be false after stop", i)
err = db.First(&cfg).Error
require.NoError(t, err, "cycle %d: config should exist after stop", i)
require.Equal(t, "disabled", cfg.CrowdSecMode, "cycle %d: mode should be disabled after stop", i)
}
}
// TestExistingSettingIsUpdated verifies that an existing setting is updated, not duplicated.
func TestExistingSettingIsUpdated(t *testing.T) {
gin.SetMode(gin.TestMode)
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{}))
// Pre-create a setting with a different value
existingSetting := models.Setting{
Key: "security.crowdsec.enabled",
Value: "false",
Category: "security",
Type: "bool",
}
require.NoError(t, db.Create(&existingSetting).Error)
tmpDir := t.TempDir()
fe := &fakeExec{}
h := NewCrowdsecHandler(db, fe, "/bin/false", tmpDir)
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
// Start CrowdSec
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/crowdsec/start", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
// Verify the existing setting was updated (not duplicated)
var settings []models.Setting
err := db.Where("key = ?", "security.crowdsec.enabled").Find(&settings).Error
require.NoError(t, err)
require.Len(t, settings, 1, "should not create duplicate settings")
require.Equal(t, "true", settings[0].Value, "setting should be updated to true")
}
// fakeFailingExec simulates an executor that fails on Start.
type fakeFailingExec struct{}
func (f *fakeFailingExec) Start(ctx context.Context, binPath, configDir string) (int, error) {
return 0, http.ErrAbortHandler
}
func (f *fakeFailingExec) Stop(ctx context.Context, configDir string) error {
return nil
}
func (f *fakeFailingExec) Status(ctx context.Context, configDir string) (running bool, pid int, err error) {
return false, 0, nil
}
// TestStartFailureRevertsSettings verifies that a failed Start reverts the settings.
func TestStartFailureRevertsSettings(t *testing.T) {
gin.SetMode(gin.TestMode)
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{}))
tmpDir := t.TempDir()
fe := &fakeFailingExec{}
h := NewCrowdsecHandler(db, fe, "/bin/false", tmpDir)
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
// Pre-create a setting with "false" to verify it's reverted
existingSetting := models.Setting{
Key: "security.crowdsec.enabled",
Value: "false",
Category: "security",
Type: "bool",
}
require.NoError(t, db.Create(&existingSetting).Error)
// Try to start CrowdSec (this will fail)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/crowdsec/start", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusInternalServerError, w.Code)
// Verify the setting was reverted to "false"
var setting models.Setting
err := db.Where("key = ?", "security.crowdsec.enabled").First(&setting).Error
require.NoError(t, err)
require.Equal(t, "false", setting.Value, "setting should be reverted to false on failure")
}
// TestStatusResponseFormat verifies the status endpoint response format.
func TestStatusResponseFormat(t *testing.T) {
gin.SetMode(gin.TestMode)
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{}))
tmpDir := t.TempDir()
fe := &fakeExec{}
h := NewCrowdsecHandler(db, fe, "/bin/false", tmpDir)
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
// Get status
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/status", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
// Verify response contains expected fields
require.Contains(t, resp, "running")
require.Contains(t, resp, "pid")
require.Contains(t, resp, "lapi_ready")
}