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

1311 lines
40 KiB
Go

package handlers
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// setupDashboardHandler creates a CrowdsecHandler with an in-memory DB seeded with decisions.
func setupDashboardHandler(t *testing.T) (*CrowdsecHandler, *gin.Engine) {
t.Helper()
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{}))
h := &CrowdsecHandler{
DB: db,
Executor: &fakeExec{},
CmdExec: &fastCmdExec{},
BinPath: "/bin/false",
DataDir: t.TempDir(),
dashCache: newDashboardCache(),
}
seedDashboardData(t, h)
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
return h, r
}
// seedDashboardData inserts representative records for testing.
func seedDashboardData(t *testing.T, h *CrowdsecHandler) {
t.Helper()
now := time.Now().UTC()
decisions := []models.SecurityDecision{
{UUID: uuid.NewString(), Source: "crowdsec", Action: "block", IP: "10.0.0.1", Scenario: "crowdsecurity/http-probing", Country: "US", CreatedAt: now.Add(-1 * time.Hour)},
{UUID: uuid.NewString(), Source: "crowdsec", Action: "block", IP: "10.0.0.1", Scenario: "crowdsecurity/http-probing", Country: "US", CreatedAt: now.Add(-2 * time.Hour)},
{UUID: uuid.NewString(), Source: "crowdsec", Action: "challenge", IP: "10.0.0.2", Scenario: "crowdsecurity/ssh-bf", Country: "DE", CreatedAt: now.Add(-3 * time.Hour)},
{UUID: uuid.NewString(), Source: "crowdsec", Action: "block", IP: "10.0.0.3", Scenario: "crowdsecurity/http-probing", Country: "FR", CreatedAt: now.Add(-5 * time.Hour)},
{UUID: uuid.NewString(), Source: "crowdsec", Action: "block", IP: "10.0.0.4", Scenario: "crowdsecurity/http-bad-user-agent", Country: "", CreatedAt: now.Add(-10 * time.Hour)},
// Old record outside 24h but within 7d
{UUID: uuid.NewString(), Source: "crowdsec", Action: "block", IP: "10.0.0.5", Scenario: "crowdsecurity/http-probing", Country: "JP", CreatedAt: now.Add(-48 * time.Hour)},
// Non-crowdsec source
{UUID: uuid.NewString(), Source: "waf", Action: "block", IP: "10.0.0.99", Scenario: "waf-rule", Country: "CN", CreatedAt: now.Add(-1 * time.Hour)},
}
for _, d := range decisions {
require.NoError(t, h.DB.Create(&d).Error)
}
}
func TestParseTimeRange(t *testing.T) {
t.Parallel()
tests := []struct {
input string
valid bool
}{
{"1h", true},
{"6h", true},
{"24h", true},
{"7d", true},
{"30d", true},
{"", true},
{"2h", false},
{"1w", false},
{"invalid", false},
}
for _, tc := range tests {
t.Run(fmt.Sprintf("range_%s", tc.input), func(t *testing.T) {
_, err := parseTimeRange(tc.input)
if tc.valid {
assert.NoError(t, err)
} else {
assert.Error(t, err)
}
})
}
}
func TestParseTimeRange_DefaultEmpty(t *testing.T) {
t.Parallel()
result, err := parseTimeRange("")
require.NoError(t, err)
expected := time.Now().UTC().Add(-24 * time.Hour)
assert.InDelta(t, expected.UnixMilli(), result.UnixMilli(), 1000)
}
func TestDashboardSummary_OK(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=24h", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Contains(t, body, "total_decisions")
assert.Contains(t, body, "active_decisions")
assert.Contains(t, body, "unique_ips")
assert.Contains(t, body, "top_scenario")
assert.Contains(t, body, "decisions_trend")
assert.Contains(t, body, "range")
assert.Contains(t, body, "generated_at")
assert.Equal(t, "24h", body["range"])
// 5 crowdsec decisions within 24h (excludes 48h-old one)
total := body["total_decisions"].(float64)
assert.Equal(t, float64(5), total)
// 4 unique crowdsec IPs within 24h
assert.Equal(t, float64(4), body["unique_ips"].(float64))
// LAPI unreachable in test => -1
assert.Equal(t, float64(-1), body["active_decisions"].(float64))
}
func TestDashboardSummary_InvalidRange(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=99z", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestDashboardSummary_Cached(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
// First call populates cache
w1 := httptest.NewRecorder()
req1 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=24h", http.NoBody)
r.ServeHTTP(w1, req1)
assert.Equal(t, http.StatusOK, w1.Code)
// Second call should hit cache
w2 := httptest.NewRecorder()
req2 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=24h", http.NoBody)
r.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusOK, w2.Code)
}
func TestDashboardTimeline_OK(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/timeline?range=24h", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Contains(t, body, "buckets")
assert.Contains(t, body, "interval")
assert.Equal(t, "1h", body["interval"])
assert.Equal(t, "24h", body["range"])
}
func TestDashboardTimeline_CustomInterval(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/timeline?range=6h&interval=15m", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Equal(t, "15m", body["interval"])
}
func TestDashboardTimeline_InvalidInterval(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/timeline?interval=99m", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestDashboardTopIPs_OK(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/top-ips?range=24h&limit=3", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
ips := body["ips"].([]interface{})
assert.LessOrEqual(t, len(ips), 3)
// 10.0.0.1 has 2 hits, should be first
if len(ips) > 0 {
first := ips[0].(map[string]interface{})
assert.Equal(t, "10.0.0.1", first["ip"])
assert.Equal(t, float64(2), first["count"])
}
}
func TestDashboardTopIPs_LimitCap(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
// Limit > 50 should be capped
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/top-ips?limit=100", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestDashboardScenarios_OK(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/scenarios?range=24h", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Contains(t, body, "scenarios")
assert.Contains(t, body, "total")
scenarios := body["scenarios"].([]interface{})
assert.Greater(t, len(scenarios), 0)
// Verify percentages sum to ~100
var totalPct float64
for _, s := range scenarios {
sc := s.(map[string]interface{})
totalPct += sc["percentage"].(float64)
assert.Contains(t, sc, "name")
assert.Contains(t, sc, "count")
}
assert.InDelta(t, 100.0, totalPct, 1.0)
}
func TestListAlerts_OK(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Contains(t, body, "alerts")
assert.Contains(t, body, "source")
// Falls back to cscli which returns empty/error in test
assert.Equal(t, "cscli", body["source"])
}
func TestListAlerts_InvalidRange(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=invalid", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestExportDecisions_CSV(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?format=csv&range=24h", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Header().Get("Content-Type"), "text/csv")
assert.Contains(t, w.Header().Get("Content-Disposition"), "attachment")
assert.Contains(t, w.Body.String(), "uuid,ip,action,source,scenario")
}
func TestExportDecisions_JSON(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?format=json&range=24h", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Header().Get("Content-Type"), "application/json")
var decisions []models.SecurityDecision
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &decisions))
assert.Greater(t, len(decisions), 0)
}
func TestExportDecisions_InvalidFormat(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?format=xml", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestExportDecisions_InvalidSource(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?source=evil", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestSanitizeCSVField(t *testing.T) {
t.Parallel()
tests := []struct {
input string
expected string
}{
{"normal", "normal"},
{"=cmd", "'=cmd"},
{"+cmd", "'+cmd"},
{"-cmd", "'-cmd"},
{"@cmd", "'@cmd"},
{"\tcmd", "'\tcmd"},
{"\rcmd", "'\rcmd"},
{"", ""},
}
for _, tc := range tests {
assert.Equal(t, tc.expected, sanitizeCSVField(tc.input))
}
}
func TestDashboardCache_Invalidate(t *testing.T) {
t.Parallel()
cache := newDashboardCache()
cache.Set("dashboard:summary:24h", "data1", 5*time.Minute)
cache.Set("dashboard:timeline:24h", "data2", 5*time.Minute)
cache.Set("other:key", "data3", 5*time.Minute)
cache.Invalidate("dashboard")
_, ok1 := cache.Get("dashboard:summary:24h")
assert.False(t, ok1)
_, ok2 := cache.Get("dashboard:timeline:24h")
assert.False(t, ok2)
_, ok3 := cache.Get("other:key")
assert.True(t, ok3)
}
func TestDashboardCache_TTLExpiry(t *testing.T) {
t.Parallel()
cache := newDashboardCache()
cache.Set("key", "value", 1*time.Millisecond)
time.Sleep(5 * time.Millisecond)
_, ok := cache.Get("key")
assert.False(t, ok)
}
func TestDashboardCache_TTLExpiry_DeletesEntry(t *testing.T) {
t.Parallel()
cache := newDashboardCache()
cache.Set("expired", "data", 1*time.Millisecond)
time.Sleep(5 * time.Millisecond)
_, ok := cache.Get("expired")
assert.False(t, ok)
cache.mu.Lock()
_, stillPresent := cache.entries["expired"]
cache.mu.Unlock()
assert.False(t, stillPresent, "expired entry should be deleted from map")
}
func TestDashboardSummary_DecisionsTrend(t *testing.T) {
t.Parallel()
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{}))
h := &CrowdsecHandler{
DB: db,
Executor: &fakeExec{},
CmdExec: &fastCmdExec{},
BinPath: "/bin/false",
DataDir: t.TempDir(),
dashCache: newDashboardCache(),
}
now := time.Now().UTC()
// Seed 3 decisions in the current 1h period
for i := 0; i < 3; i++ {
require.NoError(t, db.Create(&models.SecurityDecision{
UUID: uuid.NewString(), Source: "crowdsec", Action: "block",
IP: "192.168.1.1", Scenario: "crowdsecurity/http-probing",
CreatedAt: now.Add(-time.Duration(i+1) * time.Minute),
}).Error)
}
// Seed 2 decisions in the previous 1h period
for i := 0; i < 2; i++ {
require.NoError(t, db.Create(&models.SecurityDecision{
UUID: uuid.NewString(), Source: "crowdsec", Action: "block",
IP: "192.168.1.2", Scenario: "crowdsecurity/http-probing",
CreatedAt: now.Add(-1*time.Hour - time.Duration(i+1)*time.Minute),
}).Error)
}
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=1h", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
// (3 - 2) / 2 * 100 = 50.0
trend := body["decisions_trend"].(float64)
assert.InDelta(t, 50.0, trend, 0.1)
}
func TestExportDecisions_SourceFilter(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?format=json&range=7d&source=waf", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var decisions []models.SecurityDecision
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &decisions))
for _, d := range decisions {
assert.Equal(t, "waf", d.Source)
}
}
// ---------------------------------------------------------------------------
// Helper functions unit tests
// ---------------------------------------------------------------------------
func TestNormalizeRange(t *testing.T) {
t.Parallel()
assert.Equal(t, "24h", normalizeRange(""))
assert.Equal(t, "1h", normalizeRange("1h"))
assert.Equal(t, "7d", normalizeRange("7d"))
assert.Equal(t, "30d", normalizeRange("30d"))
}
func TestIntervalForRange_AllBranches(t *testing.T) {
t.Parallel()
tests := []struct {
rangeStr string
expected string
}{
{"1h", "5m"},
{"6h", "15m"},
{"24h", "1h"},
{"", "1h"},
{"7d", "6h"},
{"30d", "1d"},
{"unknown", "1h"},
}
for _, tc := range tests {
assert.Equal(t, tc.expected, intervalForRange(tc.rangeStr), "intervalForRange(%q)", tc.rangeStr)
}
}
func TestIntervalToStrftime_AllBranches(t *testing.T) {
t.Parallel()
tests := []struct {
interval string
contains string
}{
{"5m", "/ 5) * 5"},
{"15m", "/ 15) * 15"},
{"1h", "%H:00:00Z"},
{"6h", "/ 6) * 6"},
{"1d", "T00:00:00Z"},
{"unknown", "%H:00:00Z"},
}
for _, tc := range tests {
result := intervalToStrftime(tc.interval)
assert.Contains(t, result, tc.contains, "intervalToStrftime(%q)", tc.interval)
}
}
func TestValidInterval_AllBranches(t *testing.T) {
t.Parallel()
for _, v := range []string{"5m", "15m", "1h", "6h", "1d"} {
assert.True(t, validInterval(v), "validInterval(%q)", v)
}
assert.False(t, validInterval("10m"))
assert.False(t, validInterval(""))
}
// ---------------------------------------------------------------------------
// DashboardSummary edge cases
// ---------------------------------------------------------------------------
func TestDashboardSummary_EmptyRange(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Equal(t, "24h", body["range"])
}
func TestDashboardSummary_7dRange(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=7d", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Equal(t, "7d", body["range"])
// 7d includes the 48h-old record
total := body["total_decisions"].(float64)
assert.Equal(t, float64(6), total)
}
func TestDashboardSummary_TrendNegative100(t *testing.T) {
t.Parallel()
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{}))
h := &CrowdsecHandler{
DB: db,
Executor: &fakeExec{},
CmdExec: &fastCmdExec{},
BinPath: "/bin/false",
DataDir: t.TempDir(),
dashCache: newDashboardCache(),
}
now := time.Now().UTC()
// Only seed decisions in the PREVIOUS 1h period (nothing in current)
for i := 0; i < 3; i++ {
require.NoError(t, db.Create(&models.SecurityDecision{
UUID: uuid.NewString(), Source: "crowdsec", Action: "block",
IP: "192.168.1.1", Scenario: "crowdsecurity/http-probing",
CreatedAt: now.Add(-1*time.Hour - time.Duration(i+1)*time.Minute),
}).Error)
}
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=1h", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Equal(t, -100.0, body["decisions_trend"])
}
// ---------------------------------------------------------------------------
// DashboardTimeline edge cases
// ---------------------------------------------------------------------------
func TestDashboardTimeline_Cached(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
// First call populates cache
w1 := httptest.NewRecorder()
req1 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/timeline?range=24h", http.NoBody)
r.ServeHTTP(w1, req1)
assert.Equal(t, http.StatusOK, w1.Code)
// Second call hits cache
w2 := httptest.NewRecorder()
req2 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/timeline?range=24h", http.NoBody)
r.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusOK, w2.Code)
}
func TestDashboardTimeline_InvalidRange(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/timeline?range=99z&interval=1h", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestDashboardTimeline_AllRangeIntervals(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
ranges := []struct {
rangeStr string
interval string
}{
{"1h", "5m"},
{"6h", "15m"},
{"7d", "6h"},
{"30d", "1d"},
}
for _, tc := range ranges {
t.Run(tc.rangeStr, func(t *testing.T) {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet,
fmt.Sprintf("/api/v1/admin/crowdsec/dashboard/timeline?range=%s", tc.rangeStr), http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Equal(t, tc.interval, body["interval"])
})
}
}
// ---------------------------------------------------------------------------
// DashboardTopIPs edge cases
// ---------------------------------------------------------------------------
func TestDashboardTopIPs_InvalidRange(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/top-ips?range=bad", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestDashboardTopIPs_BadLimit(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/top-ips?limit=abc", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestDashboardTopIPs_NegativeLimit(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/top-ips?limit=-5", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestDashboardTopIPs_Cached(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w1 := httptest.NewRecorder()
req1 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/top-ips?range=24h&limit=10", http.NoBody)
r.ServeHTTP(w1, req1)
assert.Equal(t, http.StatusOK, w1.Code)
w2 := httptest.NewRecorder()
req2 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/top-ips?range=24h&limit=10", http.NoBody)
r.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusOK, w2.Code)
}
// ---------------------------------------------------------------------------
// DashboardScenarios edge cases
// ---------------------------------------------------------------------------
func TestDashboardScenarios_InvalidRange(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/scenarios?range=bad", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestDashboardScenarios_Cached(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w1 := httptest.NewRecorder()
req1 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/scenarios?range=24h", http.NoBody)
r.ServeHTTP(w1, req1)
assert.Equal(t, http.StatusOK, w1.Code)
w2 := httptest.NewRecorder()
req2 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/scenarios?range=24h", http.NoBody)
r.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusOK, w2.Code)
}
// ---------------------------------------------------------------------------
// ListAlerts edge cases
// ---------------------------------------------------------------------------
func TestListAlerts_BadLimit(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?limit=abc", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestListAlerts_LimitCap(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?limit=999", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestListAlerts_NegativeLimit(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?limit=-1", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestListAlerts_BadOffset(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?offset=abc", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestListAlerts_NegativeOffset(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?offset=-5", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestListAlerts_Cached(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w1 := httptest.NewRecorder()
req1 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody)
r.ServeHTTP(w1, req1)
assert.Equal(t, http.StatusOK, w1.Code)
w2 := httptest.NewRecorder()
req2 := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody)
r.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusOK, w2.Code)
}
func TestListAlerts_ScenarioFilter(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?scenario=crowdsecurity/http-probing", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Contains(t, body, "source")
}
// ---------------------------------------------------------------------------
// ExportDecisions edge cases
// ---------------------------------------------------------------------------
func TestExportDecisions_InvalidRange(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?range=bad", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestExportDecisions_EmptyRange(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?format=json", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestExportDecisions_CSVWithSourceFilter(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?format=csv&source=crowdsec&range=7d", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Header().Get("Content-Type"), "text/csv")
body := w.Body.String()
assert.Contains(t, body, "uuid,ip,action,source,scenario")
// Verify all rows are crowdsec source
assert.NotContains(t, body, ",waf,")
}
func TestExportDecisions_AllSources(t *testing.T) {
t.Parallel()
_, r := setupDashboardHandler(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/decisions/export?format=json&source=all&range=7d", http.NoBody)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var decisions []models.SecurityDecision
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &decisions))
// Should include both crowdsec and waf sources
assert.GreaterOrEqual(t, len(decisions), 2)
}
// ---------------------------------------------------------------------------
// LAPI integration paths via httptest server
// ---------------------------------------------------------------------------
func TestDashboardSummary_ActiveDecisions_LAPIReachable(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[{"id":"1"},{"id":"2"},{"id":"3"}]`))
}))
t.Cleanup(server.Close)
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{}))
require.NoError(t, db.Create(&models.SecurityConfig{
UUID: "default",
Name: "default",
CrowdSecAPIURL: server.URL,
}).Error)
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir())
h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) }
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
now := time.Now().UTC()
require.NoError(t, db.Create(&models.SecurityDecision{
UUID: uuid.NewString(), Source: "crowdsec", Action: "block",
IP: "10.0.0.1", Scenario: "test", CreatedAt: now.Add(-30 * time.Minute),
}).Error)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=1h", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Equal(t, float64(3), body["active_decisions"])
}
func TestDashboardSummary_ActiveDecisions_LAPIBadStatus(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
t.Cleanup(server.Close)
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{}))
require.NoError(t, db.Create(&models.SecurityConfig{
UUID: "default", Name: "default", CrowdSecAPIURL: server.URL,
}).Error)
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir())
h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) }
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=1h", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Equal(t, float64(-1), body["active_decisions"])
}
func TestDashboardSummary_ActiveDecisions_LAPIBadJSON(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`not-json`))
}))
t.Cleanup(server.Close)
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{}))
require.NoError(t, db.Create(&models.SecurityConfig{
UUID: "default", Name: "default", CrowdSecAPIURL: server.URL,
}).Error)
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir())
h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) }
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/dashboard/summary?range=1h", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Equal(t, float64(-1), body["active_decisions"])
}
func TestListAlerts_LAPISuccess(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[{"id":"a1"},{"id":"a2"},{"id":"a3"},{"id":"a4"},{"id":"a5"}]`))
}))
t.Cleanup(server.Close)
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{}))
require.NoError(t, db.Create(&models.SecurityConfig{
UUID: "default", Name: "default", CrowdSecAPIURL: server.URL,
}).Error)
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir())
h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) }
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Equal(t, "lapi", body["source"])
assert.Equal(t, float64(5), body["total"])
}
func TestListAlerts_LAPISuccessWithOffset(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[{"id":"a1"},{"id":"a2"},{"id":"a3"},{"id":"a4"},{"id":"a5"}]`))
}))
t.Cleanup(server.Close)
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{}))
require.NoError(t, db.Create(&models.SecurityConfig{
UUID: "default", Name: "default", CrowdSecAPIURL: server.URL,
}).Error)
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir())
h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) }
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h&offset=3&limit=10", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Equal(t, "lapi", body["source"])
assert.Equal(t, float64(5), body["total"])
alerts := body["alerts"].([]interface{})
assert.Equal(t, 2, len(alerts))
}
func TestListAlerts_LAPISuccessWithLargeOffset(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[{"id":"a1"},{"id":"a2"}]`))
}))
t.Cleanup(server.Close)
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{}))
require.NoError(t, db.Create(&models.SecurityConfig{
UUID: "default", Name: "default", CrowdSecAPIURL: server.URL,
}).Error)
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir())
h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) }
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h&offset=100", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Equal(t, "lapi", body["source"])
// offset >= len(rawAlerts) returns nil, which marshals as JSON null
alerts := body["alerts"]
if alerts != nil {
assert.Equal(t, 0, len(alerts.([]interface{})))
}
}
func TestListAlerts_LAPISuccessWithLimitSlicing(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[{"id":"a1"},{"id":"a2"},{"id":"a3"},{"id":"a4"},{"id":"a5"}]`))
}))
t.Cleanup(server.Close)
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{}))
require.NoError(t, db.Create(&models.SecurityConfig{
UUID: "default", Name: "default", CrowdSecAPIURL: server.URL,
}).Error)
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir())
h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) }
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h&limit=2", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Equal(t, "lapi", body["source"])
assert.Equal(t, float64(5), body["total"])
alerts := body["alerts"].([]interface{})
assert.Equal(t, 2, len(alerts))
}
func TestListAlerts_LAPIBadJSON(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`not-json`))
}))
t.Cleanup(server.Close)
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{}))
require.NoError(t, db.Create(&models.SecurityConfig{
UUID: "default", Name: "default", CrowdSecAPIURL: server.URL,
}).Error)
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir())
h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) }
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
// Falls back to cscli
assert.Equal(t, "cscli", body["source"])
}
func TestListAlerts_LAPIBadStatus(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusForbidden)
}))
t.Cleanup(server.Close)
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{}))
require.NoError(t, db.Create(&models.SecurityConfig{
UUID: "default", Name: "default", CrowdSecAPIURL: server.URL,
}).Error)
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir())
h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) }
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Equal(t, "cscli", body["source"])
}
func TestListAlerts_LAPIWithScenarioFilter(t *testing.T) {
t.Parallel()
var capturedQuery string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedQuery = r.URL.RawQuery
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[{"id":"a1"}]`))
}))
t.Cleanup(server.Close)
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{}))
require.NoError(t, db.Create(&models.SecurityConfig{
UUID: "default", Name: "default", CrowdSecAPIURL: server.URL,
}).Error)
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", t.TempDir())
h.validateLAPIURL = func(raw string) (*url.URL, error) { return url.Parse(raw) }
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h&scenario=crowdsecurity/http-probing", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, capturedQuery, "scenario=crowdsecurity")
}
func TestFetchAlertsCscli_ErrorExec(t *testing.T) {
t.Parallel()
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{}))
h := &CrowdsecHandler{
DB: db,
Executor: &fakeExec{},
CmdExec: &mockCmdExecutor{output: nil, err: fmt.Errorf("cscli not found")},
BinPath: "/bin/false",
DataDir: t.TempDir(),
dashCache: newDashboardCache(),
}
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Equal(t, "cscli", body["source"])
assert.Equal(t, float64(0), body["total"])
}
func TestFetchAlertsCscli_ValidJSON(t *testing.T) {
t.Parallel()
db := OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityConfig{}, &models.Setting{}))
h := &CrowdsecHandler{
DB: db,
Executor: &fakeExec{},
CmdExec: &mockCmdExecutor{output: []byte(`[{"id":"1"},{"id":"2"}]`), err: nil},
BinPath: "/bin/false",
DataDir: t.TempDir(),
dashCache: newDashboardCache(),
}
r := gin.New()
g := r.Group("/api/v1")
h.RegisterRoutes(g)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/alerts?range=24h", http.NoBody)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
assert.Equal(t, "cscli", body["source"])
assert.Equal(t, float64(2), body["total"])
}