71e44f79a7
- 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.
283 lines
9.0 KiB
Go
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")
|
|
}
|