Files
Charon/backend/internal/api/handlers/crowdsec_stop_lapi_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

442 lines
12 KiB
Go

package handlers
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/Wikid82/charon/backend/internal/services"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
// permissiveLAPIURLValidator allows any localhost URL for testing with mock servers.
func permissiveLAPIURLValidator(raw string) (*url.URL, error) {
return url.Parse(raw)
}
// mockStopExecutor is a mock for the CrowdsecExecutor interface for Stop tests
type mockStopExecutor struct {
stopCalled bool
stopErr error
}
func (m *mockStopExecutor) Start(_ context.Context, _, _ string) (int, error) {
return 0, nil
}
func (m *mockStopExecutor) Stop(_ context.Context, _ string) error {
m.stopCalled = true
return m.stopErr
}
func (m *mockStopExecutor) Status(_ context.Context, _ string) (running bool, pid int, err error) {
return false, 0, nil
}
// createTestSecurityService creates a SecurityService for testing
func createTestSecurityService(t *testing.T, db *gorm.DB) *services.SecurityService {
t.Helper()
svc := services.NewSecurityService(db)
t.Cleanup(func() { svc.Close() })
return svc
}
// TestCrowdsecHandler_Stop_Success tests the Stop handler with successful execution
func TestCrowdsecHandler_Stop_Success(t *testing.T) {
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{}))
// Create security config to be updated on stop
cfg := models.SecurityConfig{Enabled: true, CrowdSecMode: "enabled"}
require.NoError(t, db.Create(&cfg).Error)
tmpDir := t.TempDir()
mockExec := &mockStopExecutor{}
h := &CrowdsecHandler{
DB: db,
Executor: mockExec,
CmdExec: &mockCommandExecutor{},
DataDir: tmpDir,
}
r := gin.New()
r.POST("/stop", h.Stop)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/stop", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.True(t, mockExec.stopCalled)
var response map[string]any
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "stopped", response["status"])
// Verify config was updated
var updatedCfg models.SecurityConfig
require.NoError(t, db.First(&updatedCfg).Error)
assert.Equal(t, "disabled", updatedCfg.CrowdSecMode)
assert.False(t, updatedCfg.Enabled)
// Verify setting was synced
var setting models.Setting
require.NoError(t, db.Where("key = ?", "security.crowdsec.enabled").First(&setting).Error)
assert.Equal(t, "false", setting.Value)
}
// TestCrowdsecHandler_Stop_Error tests the Stop handler with an execution error
func TestCrowdsecHandler_Stop_Error(t *testing.T) {
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{}))
tmpDir := t.TempDir()
mockExec := &mockStopExecutor{stopErr: assert.AnError}
h := &CrowdsecHandler{
DB: db,
Executor: mockExec,
CmdExec: &mockCommandExecutor{},
DataDir: tmpDir,
}
r := gin.New()
r.POST("/stop", h.Stop)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/stop", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.True(t, mockExec.stopCalled)
}
// TestCrowdsecHandler_Stop_NoSecurityConfig tests Stop when there's no existing SecurityConfig
func TestCrowdsecHandler_Stop_NoSecurityConfig(t *testing.T) {
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{}))
// Don't create security config - test the path where no config exists
tmpDir := t.TempDir()
mockExec := &mockStopExecutor{}
h := &CrowdsecHandler{
DB: db,
Executor: mockExec,
CmdExec: &mockCommandExecutor{},
DataDir: tmpDir,
}
r := gin.New()
r.POST("/stop", h.Stop)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/stop", http.NoBody)
r.ServeHTTP(w, req)
// Should still return OK even without existing config
assert.Equal(t, http.StatusOK, w.Code)
assert.True(t, mockExec.stopCalled)
}
// TestGetLAPIDecisions_WithMockServer tests GetLAPIDecisions with a mock LAPI server
func TestGetLAPIDecisions_WithMockServer(t *testing.T) {
// Create a mock LAPI server
mockLAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`[{"id":1,"origin":"cscli","scope":"Ip","value":"1.2.3.4","type":"ban","duration":"4h","scenario":"manual ban"}]`))
}))
defer mockLAPI.Close()
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}))
// Create security config with mock LAPI URL
cfg := models.SecurityConfig{CrowdSecAPIURL: mockLAPI.URL}
require.NoError(t, db.Create(&cfg).Error)
secSvc := createTestSecurityService(t, db)
h := &CrowdsecHandler{
DB: db,
Security: secSvc,
CmdExec: &mockCommandExecutor{},
DataDir: t.TempDir(),
validateLAPIURL: permissiveLAPIURLValidator,
}
r := gin.New()
r.GET("/decisions/lapi", h.GetLAPIDecisions)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/decisions/lapi", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]any
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Equal(t, "lapi", response["source"])
decisions, ok := response["decisions"].([]any)
require.True(t, ok)
assert.Len(t, decisions, 1)
}
// TestGetLAPIDecisions_Unauthorized tests GetLAPIDecisions when LAPI returns 401
func TestGetLAPIDecisions_Unauthorized(t *testing.T) {
// Create a mock LAPI server that returns 401
mockLAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
defer mockLAPI.Close()
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}))
cfg := models.SecurityConfig{CrowdSecAPIURL: mockLAPI.URL}
require.NoError(t, db.Create(&cfg).Error)
secSvc := createTestSecurityService(t, db)
h := &CrowdsecHandler{
DB: db,
Security: secSvc,
CmdExec: &mockCommandExecutor{},
DataDir: t.TempDir(),
validateLAPIURL: permissiveLAPIURLValidator,
}
r := gin.New()
r.GET("/decisions/lapi", h.GetLAPIDecisions)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/decisions/lapi", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
// TestGetLAPIDecisions_NullResponse tests GetLAPIDecisions when LAPI returns null
func TestGetLAPIDecisions_NullResponse(t *testing.T) {
mockLAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`null`))
}))
defer mockLAPI.Close()
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}))
cfg := models.SecurityConfig{CrowdSecAPIURL: mockLAPI.URL}
require.NoError(t, db.Create(&cfg).Error)
secSvc := createTestSecurityService(t, db)
h := &CrowdsecHandler{
DB: db,
Security: secSvc,
CmdExec: &mockCommandExecutor{},
DataDir: t.TempDir(),
validateLAPIURL: permissiveLAPIURLValidator,
}
r := gin.New()
r.GET("/decisions/lapi", h.GetLAPIDecisions)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/decisions/lapi", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]any
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Equal(t, "lapi", response["source"])
assert.Equal(t, float64(0), response["total"])
}
// TestGetLAPIDecisions_NonJSONContentType tests the fallback when LAPI returns non-JSON
func TestGetLAPIDecisions_NonJSONContentType(t *testing.T) {
mockLAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`<html>Error</html>`))
}))
defer mockLAPI.Close()
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}))
cfg := models.SecurityConfig{CrowdSecAPIURL: mockLAPI.URL}
require.NoError(t, db.Create(&cfg).Error)
secSvc := createTestSecurityService(t, db)
h := &CrowdsecHandler{
DB: db,
Security: secSvc,
CmdExec: &mockCommandExecutor{output: []byte(`[]`)}, // Fallback mock
DataDir: t.TempDir(),
}
r := gin.New()
r.GET("/decisions/lapi", h.GetLAPIDecisions)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/decisions/lapi", http.NoBody)
r.ServeHTTP(w, req)
// Should fallback to cscli and return OK
assert.Equal(t, http.StatusOK, w.Code)
}
// TestCheckLAPIHealth_WithMockServer tests CheckLAPIHealth with a healthy LAPI
func TestCheckLAPIHealth_WithMockServer(t *testing.T) {
mockLAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/health" {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"ok"}`))
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer mockLAPI.Close()
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}))
cfg := models.SecurityConfig{CrowdSecAPIURL: mockLAPI.URL}
require.NoError(t, db.Create(&cfg).Error)
secSvc := createTestSecurityService(t, db)
h := &CrowdsecHandler{
DB: db,
Security: secSvc,
CmdExec: &mockCommandExecutor{},
DataDir: t.TempDir(),
validateLAPIURL: permissiveLAPIURLValidator,
}
r := gin.New()
r.GET("/health", h.CheckLAPIHealth)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/health", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]any
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.True(t, response["healthy"].(bool))
}
// TestCheckLAPIHealth_FallbackToDecisions tests the fallback to /v1/decisions endpoint
// when the primary /health endpoint is unreachable
func TestCheckLAPIHealth_FallbackToDecisions(t *testing.T) {
// Create a mock server that only responds to /v1/decisions, not /health
mockLAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v1/decisions" {
// Return 401 which indicates LAPI is running (just needs auth)
w.WriteHeader(http.StatusUnauthorized)
} else {
// Close connection without responding to simulate unreachable endpoint
panic(http.ErrAbortHandler)
}
}))
defer mockLAPI.Close()
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}))
cfg := models.SecurityConfig{CrowdSecAPIURL: mockLAPI.URL}
require.NoError(t, db.Create(&cfg).Error)
secSvc := createTestSecurityService(t, db)
h := &CrowdsecHandler{
DB: db,
Security: secSvc,
CmdExec: &mockCommandExecutor{},
DataDir: t.TempDir(),
validateLAPIURL: permissiveLAPIURLValidator,
}
r := gin.New()
r.GET("/health", h.CheckLAPIHealth)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/health", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]any
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
// Should be healthy via fallback
assert.True(t, response["healthy"].(bool))
if note, ok := response["note"].(string); ok {
assert.Contains(t, note, "decisions endpoint")
}
}
// TestGetLAPIKey_AllEnvVars tests that getLAPIKey checks all environment variable names
func TestGetLAPIKey_AllEnvVars(t *testing.T) {
envVars := []string{
"CROWDSEC_API_KEY",
"CROWDSEC_BOUNCER_API_KEY",
"CERBERUS_SECURITY_CROWDSEC_API_KEY",
"CHARON_SECURITY_CROWDSEC_API_KEY",
"CPM_SECURITY_CROWDSEC_API_KEY",
}
// Clean up all env vars first
originals := make(map[string]string)
for _, key := range envVars {
originals[key] = os.Getenv(key)
_ = os.Unsetenv(key)
}
defer func() {
for key, val := range originals {
if val != "" {
_ = os.Setenv(key, val)
}
}
}()
// Test each env var in order of priority
for i, envVar := range envVars {
t.Run(envVar, func(t *testing.T) {
// Clear all vars
for _, key := range envVars {
_ = os.Unsetenv(key)
}
// Set only this env var
testValue := "test-key-" + envVar
_ = os.Setenv(envVar, testValue)
key := getLAPIKey()
if i == 0 || key == testValue {
// First one should always be found, others only if earlier ones not set
assert.Equal(t, testValue, key)
}
})
}
}