chore: enforce local patch coverage as a blocking DoD gate
- Added ~40 backend tests covering uncovered branches in CrowdSec dashboard handlers (error paths, validation, export edge cases) - Patch coverage improved from 81.5% to 98.3%, exceeding 90% threshold - Fixed DoD ordering: coverage tests now run before the patch report (the report requires coverage artifacts as input) - Rewrote the local patch coverage DoD step in both the Management agent and testing instructions to clarify purpose, prerequisites, required action on findings, and blocking gate semantics - Eliminated ambiguous "advisory" language that allowed agents to skip acting on uncovered lines
This commit is contained in:
18
.github/agents/Management.agent.md
vendored
18
.github/agents/Management.agent.md
vendored
@@ -167,23 +167,27 @@ The task is not complete until ALL of the following pass with zero issues:
|
||||
- **Base URL**: Uses `PLAYWRIGHT_BASE_URL` or default from `playwright.config.js`
|
||||
- All E2E tests must pass before proceeding to unit tests
|
||||
|
||||
2. **Local Patch Coverage Preflight (MANDATORY - Before Unit/Coverage Tests)**:
|
||||
- Ensure the local patch report is run first via VS Code task `Test: Local Patch Report` or `bash scripts/local-patch-report.sh`.
|
||||
- Verify both artifacts exist: `test-results/local-patch-report.md` and `test-results/local-patch-report.json`.
|
||||
- Use this report to identify changed files needing coverage before running backend/frontend coverage suites.
|
||||
|
||||
3. **Coverage Tests (MANDATORY - Verify Explicitly)**:
|
||||
2. **Coverage Tests (MANDATORY - Verify Explicitly)**:
|
||||
- **Backend**: Ensure `Backend_Dev` ran VS Code task "Test: Backend with Coverage" or `scripts/go-test-coverage.sh`
|
||||
- **Frontend**: Ensure `Frontend_Dev` ran VS Code task "Test: Frontend with Coverage" or `scripts/frontend-test-coverage.sh`
|
||||
- **Why**: These are in manual stage of pre-commit for performance. Subagents MUST run them via VS Code tasks or scripts.
|
||||
- Minimum coverage: 85% for both backend and frontend.
|
||||
- All tests must pass with zero failures.
|
||||
- **Outputs**: `backend/coverage.txt` and `frontend/coverage/lcov.info` — these are required inputs for step 3.
|
||||
|
||||
3. **Local Patch Coverage Report (MANDATORY - After Coverage Tests)**:
|
||||
- **Purpose**: Identify uncovered lines in files modified by this task so missing tests are written before declaring Done. This is the bridge between "overall coverage is fine" and "the actual lines I changed are tested."
|
||||
- **Prerequisites**: `backend/coverage.txt` and `frontend/coverage/lcov.info` must exist (generated by step 2). If missing, run coverage tests first.
|
||||
- **Run**: VS Code task `Test: Local Patch Report` or `bash scripts/local-patch-report.sh`.
|
||||
- **Verify artifacts**: Both `test-results/local-patch-report.md` and `test-results/local-patch-report.json` must exist with non-empty results.
|
||||
- **Act on findings**: If patch coverage for any changed file is below **90%**, delegate to the responsible agent (`Backend_Dev` or `Frontend_Dev`) to add targeted tests covering the uncovered lines. Re-run coverage (step 2) and this report until the threshold is met.
|
||||
- **Blocking gate**: 90% overall patch coverage. Do not proceed to pre-commit or security scans until resolved or explicitly waived by the user.
|
||||
|
||||
4. **Type Safety (Frontend)**:
|
||||
- Ensure `Frontend_Dev` ran VS Code task "Lint: TypeScript Check" or `npm run type-check`
|
||||
- **Why**: This check is in manual stage of pre-commit for performance. Subagents MUST run it explicitly.
|
||||
|
||||
5. **Pre-commit Hooks**: Ensure `QA_Security` ran `pre-commit run --all-files` (fast hooks only; coverage was verified in step 3)
|
||||
5. **Pre-commit Hooks**: Ensure `QA_Security` ran `pre-commit run --all-files` (fast hooks only; coverage was verified in step 2)
|
||||
|
||||
6. **Security Scans**: Ensure `QA_Security` ran the following with zero Critical or High severity issues:
|
||||
- **Trivy Filesystem Scan**: Fast scan of source code and dependencies
|
||||
|
||||
21
.github/instructions/testing.instructions.md
vendored
21
.github/instructions/testing.instructions.md
vendored
@@ -12,9 +12,19 @@ instruction files take precedence over agent files and operator documentation.
|
||||
|
||||
**MANDATORY**: Before running unit tests, verify the application UI/UX functions correctly end-to-end.
|
||||
|
||||
## 0.5 Local Patch Coverage Preflight (Before Unit Tests)
|
||||
## 0.5 Local Patch Coverage Report (After Coverage Tests)
|
||||
|
||||
**MANDATORY**: After E2E and before backend/frontend unit coverage runs, generate a local patch report so uncovered changed lines are visible early.
|
||||
**MANDATORY**: After running backend and frontend coverage tests (which generate
|
||||
`backend/coverage.txt` and `frontend/coverage/lcov.info`), run the local patch
|
||||
report to identify uncovered lines in changed files.
|
||||
|
||||
**Purpose**: Overall coverage can be healthy while the specific lines you changed
|
||||
are untested. This step catches that gap. If uncovered lines are found in
|
||||
feature code, add targeted tests before completing the task.
|
||||
|
||||
**Prerequisites**: Coverage artifacts must exist before running the report:
|
||||
- `backend/coverage.txt` — generated by `scripts/go-test-coverage.sh`
|
||||
- `frontend/coverage/lcov.info` — generated by `scripts/frontend-test-coverage.sh`
|
||||
|
||||
Run one of the following from `/projects/Charon`:
|
||||
|
||||
@@ -26,11 +36,14 @@ Test: Local Patch Report
|
||||
bash scripts/local-patch-report.sh
|
||||
```
|
||||
|
||||
Required artifacts:
|
||||
Required output artifacts:
|
||||
- `test-results/local-patch-report.md`
|
||||
- `test-results/local-patch-report.json`
|
||||
|
||||
This preflight is advisory for thresholds during rollout, but artifact generation is required in DoD.
|
||||
**Action on results**: If patch coverage for any changed file is below 90%, add
|
||||
tests targeting the uncovered changed lines. Re-run coverage and this report to
|
||||
verify improvement. Artifact generation is required for DoD regardless of
|
||||
threshold results.
|
||||
|
||||
### PREREQUISITE: Start E2E Environment
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -484,3 +485,891 @@ func TestExportDecisions_SourceFilter(t *testing.T) {
|
||||
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()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
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()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
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)
|
||||
|
||||
original := validateCrowdsecLAPIBaseURLFunc
|
||||
validateCrowdsecLAPIBaseURLFunc = func(raw string) (*url.URL, error) {
|
||||
return url.Parse(raw)
|
||||
}
|
||||
t.Cleanup(func() { validateCrowdsecLAPIBaseURLFunc = original })
|
||||
|
||||
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())
|
||||
|
||||
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()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
original := validateCrowdsecLAPIBaseURLFunc
|
||||
validateCrowdsecLAPIBaseURLFunc = func(raw string) (*url.URL, error) {
|
||||
return url.Parse(raw)
|
||||
}
|
||||
t.Cleanup(func() { validateCrowdsecLAPIBaseURLFunc = original })
|
||||
|
||||
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())
|
||||
|
||||
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()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
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)
|
||||
|
||||
original := validateCrowdsecLAPIBaseURLFunc
|
||||
validateCrowdsecLAPIBaseURLFunc = func(raw string) (*url.URL, error) {
|
||||
return url.Parse(raw)
|
||||
}
|
||||
t.Cleanup(func() { validateCrowdsecLAPIBaseURLFunc = original })
|
||||
|
||||
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())
|
||||
|
||||
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()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
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)
|
||||
|
||||
original := validateCrowdsecLAPIBaseURLFunc
|
||||
validateCrowdsecLAPIBaseURLFunc = func(raw string) (*url.URL, error) {
|
||||
return url.Parse(raw)
|
||||
}
|
||||
t.Cleanup(func() { validateCrowdsecLAPIBaseURLFunc = original })
|
||||
|
||||
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())
|
||||
|
||||
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()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
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)
|
||||
|
||||
original := validateCrowdsecLAPIBaseURLFunc
|
||||
validateCrowdsecLAPIBaseURLFunc = func(raw string) (*url.URL, error) {
|
||||
return url.Parse(raw)
|
||||
}
|
||||
t.Cleanup(func() { validateCrowdsecLAPIBaseURLFunc = original })
|
||||
|
||||
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())
|
||||
|
||||
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()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
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)
|
||||
|
||||
original := validateCrowdsecLAPIBaseURLFunc
|
||||
validateCrowdsecLAPIBaseURLFunc = func(raw string) (*url.URL, error) {
|
||||
return url.Parse(raw)
|
||||
}
|
||||
t.Cleanup(func() { validateCrowdsecLAPIBaseURLFunc = original })
|
||||
|
||||
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())
|
||||
|
||||
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()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
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)
|
||||
|
||||
original := validateCrowdsecLAPIBaseURLFunc
|
||||
validateCrowdsecLAPIBaseURLFunc = func(raw string) (*url.URL, error) {
|
||||
return url.Parse(raw)
|
||||
}
|
||||
t.Cleanup(func() { validateCrowdsecLAPIBaseURLFunc = original })
|
||||
|
||||
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())
|
||||
|
||||
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()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
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)
|
||||
|
||||
original := validateCrowdsecLAPIBaseURLFunc
|
||||
validateCrowdsecLAPIBaseURLFunc = func(raw string) (*url.URL, error) {
|
||||
return url.Parse(raw)
|
||||
}
|
||||
t.Cleanup(func() { validateCrowdsecLAPIBaseURLFunc = original })
|
||||
|
||||
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())
|
||||
|
||||
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()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
original := validateCrowdsecLAPIBaseURLFunc
|
||||
validateCrowdsecLAPIBaseURLFunc = func(raw string) (*url.URL, error) {
|
||||
return url.Parse(raw)
|
||||
}
|
||||
t.Cleanup(func() { validateCrowdsecLAPIBaseURLFunc = original })
|
||||
|
||||
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())
|
||||
|
||||
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()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
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)
|
||||
|
||||
original := validateCrowdsecLAPIBaseURLFunc
|
||||
validateCrowdsecLAPIBaseURLFunc = func(raw string) (*url.URL, error) {
|
||||
return url.Parse(raw)
|
||||
}
|
||||
t.Cleanup(func() { validateCrowdsecLAPIBaseURLFunc = original })
|
||||
|
||||
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())
|
||||
|
||||
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()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
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()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
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"])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user