fix: add LAPI readiness check to CrowdSec status endpoint
The Status() handler was only checking if the CrowdSec process was running, not if LAPI was actually responding. This caused the CrowdSecConfig page to always show "LAPI is initializing" even when LAPI was fully operational. Changes: - Backend: Add lapi_ready field to /admin/crowdsec/status response - Frontend: Add CrowdSecStatus TypeScript interface - Frontend: Update conditional logic to check lapi_ready not running - Frontend: Separate warnings for "initializing" vs "not running" - Tests: Add unit tests for Status handler LAPI check Fixes regression from crowdsec_lapi_error_diagnostic.md fixes.
This commit is contained in:
@@ -246,7 +246,7 @@ func (h *CrowdsecHandler) Stop(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "stopped"})
|
||||
}
|
||||
|
||||
// Status returns simple running state.
|
||||
// Status returns running state including LAPI availability check.
|
||||
func (h *CrowdsecHandler) Status(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
running, pid, err := h.Executor.Status(ctx, h.DataDir)
|
||||
@@ -254,7 +254,25 @@ func (h *CrowdsecHandler) Status(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"running": running, "pid": pid})
|
||||
|
||||
// Check LAPI connectivity if process is running
|
||||
lapiReady := false
|
||||
if running {
|
||||
args := []string{"lapi", "status"}
|
||||
if _, err := os.Stat(filepath.Join(h.DataDir, "config.yaml")); err == nil {
|
||||
args = append([]string{"-c", filepath.Join(h.DataDir, "config.yaml")}, args...)
|
||||
}
|
||||
checkCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
_, checkErr := h.CmdExec.Execute(checkCtx, "cscli", args...)
|
||||
cancel()
|
||||
lapiReady = (checkErr == nil)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"running": running,
|
||||
"pid": pid,
|
||||
"lapi_ready": lapiReady,
|
||||
})
|
||||
}
|
||||
|
||||
// ImportConfig accepts a tar.gz or zip upload and extracts into DataDir (backing up existing config).
|
||||
|
||||
@@ -1348,6 +1348,115 @@ func TestCrowdsecHandler_StartReturnsImmediatelyIfProcessFailsToStart(t *testing
|
||||
require.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Status Handler lapi_ready Tests
|
||||
// ============================================
|
||||
|
||||
func TestCrowdsecHandler_StatusReturnsLAPIReadyWhenRunning(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupCrowdDB(t)
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create an executor that reports as running
|
||||
runningExec := &fakeExec{started: true}
|
||||
|
||||
// Create a command executor that succeeds (LAPI is ready)
|
||||
successCmdExec := &mockCmdExec{err: nil}
|
||||
|
||||
h := NewCrowdsecHandler(db, runningExec, "/bin/false", tmpDir)
|
||||
h.CmdExec = successCmdExec
|
||||
|
||||
r := gin.New()
|
||||
g := r.Group("/api/v1")
|
||||
h.RegisterRoutes(g)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/status", http.NoBody)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, true, response["running"])
|
||||
require.Equal(t, float64(12345), response["pid"])
|
||||
require.Equal(t, true, response["lapi_ready"], "lapi_ready should be true when cscli lapi status succeeds")
|
||||
}
|
||||
|
||||
func TestCrowdsecHandler_StatusReturnsLAPINotReadyWhenCmdFails(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupCrowdDB(t)
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create an executor that reports as running
|
||||
runningExec := &fakeExec{started: true}
|
||||
|
||||
// Create a command executor that fails (LAPI not ready)
|
||||
failCmdExec := &mockCmdExec{err: errors.New("LAPI not initialized")}
|
||||
|
||||
h := NewCrowdsecHandler(db, runningExec, "/bin/false", tmpDir)
|
||||
h.CmdExec = failCmdExec
|
||||
|
||||
r := gin.New()
|
||||
g := r.Group("/api/v1")
|
||||
h.RegisterRoutes(g)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/status", http.NoBody)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, true, response["running"])
|
||||
require.Equal(t, float64(12345), response["pid"])
|
||||
require.Equal(t, false, response["lapi_ready"], "lapi_ready should be false when cscli lapi status fails")
|
||||
}
|
||||
|
||||
func TestCrowdsecHandler_StatusReturnsLAPINotReadyWhenStopped(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupCrowdDB(t)
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create an executor that reports as stopped
|
||||
stoppedExec := &fakeExec{started: false}
|
||||
|
||||
h := NewCrowdsecHandler(db, stoppedExec, "/bin/false", tmpDir)
|
||||
|
||||
r := gin.New()
|
||||
g := r.Group("/api/v1")
|
||||
h.RegisterRoutes(g)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/status", http.NoBody)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, false, response["running"])
|
||||
require.Equal(t, float64(0), response["pid"])
|
||||
require.Equal(t, false, response["lapi_ready"], "lapi_ready should be false when process is not running")
|
||||
}
|
||||
|
||||
// mockCmdExec is a mock command executor for testing
|
||||
type mockCmdExec struct {
|
||||
err error
|
||||
output []byte
|
||||
}
|
||||
|
||||
func (m *mockCmdExec) Execute(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||
return m.output, m.err
|
||||
}
|
||||
|
||||
type failingExec struct{}
|
||||
|
||||
func (f *failingExec) Start(ctx context.Context, binPath, configDir string) (int, error) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,37 +1,131 @@
|
||||
# QA Report: CrowdSec Persistence Fix
|
||||
# QA Report: CrowdSec LAPI Status Fix
|
||||
|
||||
## Execution Summary
|
||||
**Date:** December 14, 2025
|
||||
**Agent:** QA_Security
|
||||
**Issue:** CrowdSec LAPI status field was incorrectly handled, causing UI to not display proper status
|
||||
|
||||
**Date**: 2025-12-14
|
||||
**Task**: Fixing CrowdSec "Offline" status due to lack of persistence.
|
||||
**Agent**: QA_Security (Antigravity)
|
||||
---
|
||||
|
||||
## 🧪 Verification Results
|
||||
## Changes Tested
|
||||
|
||||
### Static Analysis
|
||||
1. **Backend:** `backend/internal/api/handlers/crowdsec_handler.go` - Status() now returns `lapi_ready` field
|
||||
2. **Frontend:** `frontend/src/api/crowdsec.ts` - Added CrowdSecStatus interface
|
||||
3. **Frontend:** `frontend/src/pages/CrowdSecConfig.tsx` - Updated conditionals to use `lapi_ready`
|
||||
4. **Test mocks:** Updated to support new `lapi_ready` field
|
||||
|
||||
- **Pre-commit**: ⚠️ Skipped (Tool not installed in environment).
|
||||
- **Manual Code Review**: ✅ Passed.
|
||||
- `docker-entrypoint.sh`: Logic correctly handles directory initialization, copying of defaults, and symbolic linking.
|
||||
- `docker-compose.yml`: Documentation added clearly.
|
||||
- **Idempotency**: Checked. The script checks for file/link existence before acting, preventing data overwrite on restarts.
|
||||
---
|
||||
|
||||
### Logic Audit
|
||||
## Test Results Summary
|
||||
|
||||
- **Persistence**:
|
||||
- Config: `/etc/crowdsec` -> `/app/data/crowdsec/config`.
|
||||
- Data: `DATA` env var -> `/app/data/crowdsec/data`.
|
||||
- Hub: `/etc/crowdsec/hub` is created in persistent path.
|
||||
- **Fail-safes**:
|
||||
- Fallback to `/etc/crowdsec.dist` or `/etc/crowdsec` ensures config covers missing files.
|
||||
- `cscli` checks integrity on startup.
|
||||
| Check | Status | Details |
|
||||
|-------|--------|---------|
|
||||
| Backend Build | ✅ PASSED | `go build ./...` completed successfully |
|
||||
| Backend Tests | ✅ PASSED | All 20 packages pass |
|
||||
| Backend Lint (go vet) | ✅ PASSED | No issues found |
|
||||
| Frontend Type Check | ✅ PASSED | TypeScript compilation successful |
|
||||
| Frontend Lint | ✅ PASSED | 0 errors, 6 warnings (acceptable) |
|
||||
| Frontend Tests | ✅ PASSED | 799 passed, 2 skipped |
|
||||
| Pre-commit | ✅ PASSED | All hooks pass |
|
||||
|
||||
### ⚠️ Risks & Edges
|
||||
---
|
||||
|
||||
- **First Restart**: The first restart after applying this fix requires the user to **re-enroll** with CrowdSec Console because the Machine ID will change (it is now persistent, but the previous one was ephemeral and lost).
|
||||
- **File Permissions**: Assumes the container user (`root` usually in this context) has write access to `/app/data`. This is standard for Charon.
|
||||
## Detailed Results
|
||||
|
||||
## Recommendations
|
||||
### Backend Build
|
||||
|
||||
- **Approve**. The fix addresses the root cause directly.
|
||||
- **User Action**: User must verify by running `cscli machines list` across restarts.
|
||||
```
|
||||
✅ go build ./... - SUCCESS
|
||||
```
|
||||
|
||||
### Backend Tests
|
||||
|
||||
```
|
||||
ok github.com/Wikid82/charon/backend/cmd/api
|
||||
ok github.com/Wikid82/charon/backend/cmd/seed
|
||||
ok github.com/Wikid82/charon/backend/internal/api/handlers
|
||||
ok github.com/Wikid82/charon/backend/internal/api/middleware
|
||||
ok github.com/Wikid82/charon/backend/internal/api/routes
|
||||
ok github.com/Wikid82/charon/backend/internal/api/tests
|
||||
ok github.com/Wikid82/charon/backend/internal/caddy
|
||||
ok github.com/Wikid82/charon/backend/internal/cerberus
|
||||
ok github.com/Wikid82/charon/backend/internal/config
|
||||
ok github.com/Wikid82/charon/backend/internal/crowdsec
|
||||
ok github.com/Wikid82/charon/backend/internal/database
|
||||
ok github.com/Wikid82/charon/backend/internal/logger
|
||||
ok github.com/Wikid82/charon/backend/internal/metrics
|
||||
ok github.com/Wikid82/charon/backend/internal/models
|
||||
ok github.com/Wikid82/charon/backend/internal/server
|
||||
ok github.com/Wikid82/charon/backend/internal/services
|
||||
ok github.com/Wikid82/charon/backend/internal/util
|
||||
ok github.com/Wikid82/charon/backend/internal/version
|
||||
|
||||
Coverage: 85.2% (minimum required 85%)
|
||||
```
|
||||
|
||||
### Backend Lint
|
||||
|
||||
```
|
||||
✅ go vet ./... - No issues
|
||||
```
|
||||
|
||||
### Frontend Type Check
|
||||
|
||||
```
|
||||
✅ tsc --noEmit - SUCCESS
|
||||
```
|
||||
|
||||
### Frontend Lint
|
||||
|
||||
```
|
||||
6 warnings (0 errors):
|
||||
- 1x unused variable in e2e test
|
||||
- 2x missing useEffect dependencies (existing, unrelated)
|
||||
- 3x @typescript-eslint/no-explicit-any in test files
|
||||
|
||||
Note: All warnings are acceptable and unrelated to the LAPI fix
|
||||
```
|
||||
|
||||
### Frontend Tests
|
||||
|
||||
```
|
||||
Test Files 87 passed (87)
|
||||
Tests 799 passed | 2 skipped (801)
|
||||
Duration 63.65s
|
||||
|
||||
Key test suites verified:
|
||||
- src/api/__tests__/crowdsec.test.ts (9 tests) ✅
|
||||
- src/pages/__tests__/CrowdSecConfig.test.tsx (3 tests) ✅
|
||||
- src/pages/__tests__/Security.spec.tsx (6 tests) ✅
|
||||
- src/pages/__tests__/Security.test.tsx (18 tests) ✅
|
||||
- src/pages/__tests__/Security.dashboard.test.tsx (18 tests) ✅
|
||||
```
|
||||
|
||||
### Pre-commit Hooks
|
||||
|
||||
```
|
||||
✅ Go Vet - Passed
|
||||
✅ Check .version matches latest Git tag - Passed
|
||||
✅ Prevent large files that are not tracked by LFS - Passed
|
||||
✅ Prevent committing CodeQL DB artifacts - Passed
|
||||
✅ Prevent committing data/backups files - Passed
|
||||
✅ Frontend TypeScript Check - Passed
|
||||
✅ Frontend Lint (Fix) - Passed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
**All quality gates have passed.** The CrowdSec LAPI status fix has been comprehensively tested and is ready for merge.
|
||||
|
||||
### Summary of Changes Verified
|
||||
|
||||
1. Backend correctly returns `lapi_ready` boolean field in CrowdSec status response
|
||||
2. Frontend `CrowdSecStatus` interface properly types the response
|
||||
3. UI conditionals correctly use `lapi_ready` for status display logic
|
||||
4. All existing tests pass with updated mocks
|
||||
5. No regressions detected in related security features
|
||||
|
||||
---
|
||||
|
||||
*Report generated by QA_Security agent*
|
||||
|
||||
@@ -19,8 +19,14 @@ export async function stopCrowdsec() {
|
||||
return resp.data
|
||||
}
|
||||
|
||||
export async function statusCrowdsec() {
|
||||
const resp = await client.get('/admin/crowdsec/status')
|
||||
export interface CrowdSecStatus {
|
||||
running: boolean
|
||||
pid: number
|
||||
lapi_ready: boolean
|
||||
}
|
||||
|
||||
export async function statusCrowdsec(): Promise<CrowdSecStatus> {
|
||||
const resp = await client.get<CrowdSecStatus>('/admin/crowdsec/status')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Input } from '../components/ui/Input'
|
||||
import { Switch } from '../components/ui/Switch'
|
||||
import { getSecurityStatus } from '../api/security'
|
||||
import { getFeatureFlags } from '../api/featureFlags'
|
||||
import { exportCrowdsecConfig, importCrowdsecConfig, listCrowdsecFiles, readCrowdsecFile, writeCrowdsecFile, listCrowdsecDecisions, banIP, unbanIP, CrowdSecDecision, statusCrowdsec } from '../api/crowdsec'
|
||||
import { exportCrowdsecConfig, importCrowdsecConfig, listCrowdsecFiles, readCrowdsecFile, writeCrowdsecFile, listCrowdsecDecisions, banIP, unbanIP, CrowdSecDecision, statusCrowdsec, CrowdSecStatus } from '../api/crowdsec'
|
||||
import { listCrowdsecPresets, pullCrowdsecPreset, applyCrowdsecPreset, getCrowdsecPresetCache } from '../api/presets'
|
||||
import { createBackup } from '../api/backups'
|
||||
import { updateSetting } from '../api/settings'
|
||||
@@ -62,7 +62,7 @@ export default function CrowdSecConfig() {
|
||||
}, [consoleEnrollmentEnabled, initialCheckComplete])
|
||||
|
||||
// Add LAPI status check with polling
|
||||
const lapiStatusQuery = useQuery({
|
||||
const lapiStatusQuery = useQuery<CrowdSecStatus>({
|
||||
queryKey: ['crowdsec-lapi-status'],
|
||||
queryFn: statusCrowdsec,
|
||||
enabled: consoleEnrollmentEnabled && initialCheckComplete,
|
||||
@@ -594,8 +594,8 @@ export default function CrowdSecConfig() {
|
||||
<p className="text-sm text-red-400" data-testid="console-enroll-error">{consoleErrors.submit}</p>
|
||||
)}
|
||||
|
||||
{/* Warning when CrowdSec LAPI is not running */}
|
||||
{lapiStatusQuery.data && !lapiStatusQuery.data.running && initialCheckComplete && (
|
||||
{/* Yellow warning: Process running but LAPI initializing */}
|
||||
{lapiStatusQuery.data && lapiStatusQuery.data.running && !lapiStatusQuery.data.lapi_ready && initialCheckComplete && (
|
||||
<div className="flex items-start gap-3 p-4 bg-yellow-900/20 border border-yellow-700/50 rounded-lg" data-testid="lapi-warning">
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
@@ -616,15 +616,38 @@ export default function CrowdSecConfig() {
|
||||
>
|
||||
Check Now
|
||||
</Button>
|
||||
{!status?.crowdsec?.enabled && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => navigate('/security')}
|
||||
>
|
||||
Go to Security Dashboard
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Red warning: Process not running at all */}
|
||||
{lapiStatusQuery.data && !lapiStatusQuery.data.running && initialCheckComplete && (
|
||||
<div className="flex items-start gap-3 p-4 bg-red-900/20 border border-red-700/50 rounded-lg" data-testid="lapi-not-running-warning">
|
||||
<AlertTriangle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-red-200 font-medium mb-2">
|
||||
CrowdSec is not running
|
||||
</p>
|
||||
<p className="text-xs text-red-300 mb-3">
|
||||
The CrowdSec process is not currently running. Enable CrowdSec from the Security Dashboard to use console enrollment features.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => lapiStatusQuery.refetch()}
|
||||
disabled={lapiStatusQuery.isRefetching}
|
||||
>
|
||||
Check Now
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => navigate('/security')}
|
||||
>
|
||||
Go to Security Dashboard
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -677,12 +700,12 @@ export default function CrowdSecConfig() {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() => submitConsoleEnrollment(false)}
|
||||
disabled={isConsolePending || (lapiStatusQuery.data && !lapiStatusQuery.data.running) || !enrollmentToken.trim()}
|
||||
disabled={isConsolePending || (lapiStatusQuery.data && !lapiStatusQuery.data.lapi_ready) || !enrollmentToken.trim()}
|
||||
isLoading={enrollConsoleMutation.isPending}
|
||||
data-testid="console-enroll-btn"
|
||||
title={
|
||||
lapiStatusQuery.data && !lapiStatusQuery.data.running
|
||||
? 'CrowdSec LAPI must be running to enroll'
|
||||
lapiStatusQuery.data && !lapiStatusQuery.data.lapi_ready
|
||||
? 'CrowdSec LAPI must be ready to enroll'
|
||||
: !enrollmentToken.trim()
|
||||
? 'Enrollment token is required'
|
||||
: undefined
|
||||
@@ -693,12 +716,12 @@ export default function CrowdSecConfig() {
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => submitConsoleEnrollment(true)}
|
||||
disabled={isConsolePending || !canRotateKey || (lapiStatusQuery.data && !lapiStatusQuery.data.running)}
|
||||
disabled={isConsolePending || !canRotateKey || (lapiStatusQuery.data && !lapiStatusQuery.data.lapi_ready)}
|
||||
isLoading={enrollConsoleMutation.isPending}
|
||||
data-testid="console-rotate-btn"
|
||||
title={
|
||||
lapiStatusQuery.data && !lapiStatusQuery.data.running
|
||||
? 'CrowdSec LAPI must be running to rotate key'
|
||||
lapiStatusQuery.data && !lapiStatusQuery.data.lapi_ready
|
||||
? 'CrowdSec LAPI must be ready to rotate key'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
@@ -708,12 +731,12 @@ export default function CrowdSecConfig() {
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => submitConsoleEnrollment(true)}
|
||||
disabled={isConsolePending || (lapiStatusQuery.data && !lapiStatusQuery.data.running)}
|
||||
disabled={isConsolePending || (lapiStatusQuery.data && !lapiStatusQuery.data.lapi_ready)}
|
||||
isLoading={enrollConsoleMutation.isPending}
|
||||
data-testid="console-retry-btn"
|
||||
title={
|
||||
lapiStatusQuery.data && !lapiStatusQuery.data.running
|
||||
? 'CrowdSec LAPI must be running to retry enrollment'
|
||||
lapiStatusQuery.data && !lapiStatusQuery.data.lapi_ready
|
||||
? 'CrowdSec LAPI must be ready to retry enrollment'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('CrowdSecConfig', () => {
|
||||
})
|
||||
vi.mocked(presetsApi.getCrowdsecPresetCache).mockResolvedValue({ preview: 'cached', cache_key: 'cache-123', etag: 'etag-123' })
|
||||
vi.mocked(crowdsecApi.listCrowdsecDecisions).mockResolvedValue({ decisions: [] })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234, lapi_ready: true })
|
||||
vi.mocked(featureFlagsApi.getFeatureFlags).mockResolvedValue({
|
||||
'feature.crowdsec.console_enrollment': false,
|
||||
})
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('Security Page - QA Security Audit', () => {
|
||||
})
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue(new Blob())
|
||||
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
|
||||
@@ -133,7 +133,7 @@ describe('Security Page - QA Security Audit', () => {
|
||||
...mockSecurityStatus,
|
||||
crowdsec: { mode: 'local', api_url: 'http://localhost', enabled: false },
|
||||
})
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false })
|
||||
vi.mocked(crowdsecApi.startCrowdsec).mockRejectedValue(new Error('Failed to start'))
|
||||
|
||||
await renderSecurityPage()
|
||||
@@ -150,7 +150,7 @@ describe('Security Page - QA Security Audit', () => {
|
||||
it('handles CrowdSec stop failure gracefully', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234 })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234, lapi_ready: true })
|
||||
vi.mocked(crowdsecApi.stopCrowdsec).mockRejectedValue(new Error('Failed to stop'))
|
||||
|
||||
await renderSecurityPage()
|
||||
@@ -200,7 +200,7 @@ describe('Security Page - QA Security Audit', () => {
|
||||
...mockSecurityStatus,
|
||||
crowdsec: { mode: 'local', api_url: 'http://localhost', enabled: false },
|
||||
})
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
let callCount = 0
|
||||
vi.mocked(crowdsecApi.startCrowdsec).mockImplementation(async () => {
|
||||
@@ -308,7 +308,7 @@ describe('Security Page - QA Security Audit', () => {
|
||||
|
||||
it('CrowdSec controls surface primary actions when enabled', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false })
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('Security Dashboard - Card Status Tests', () => {
|
||||
},
|
||||
})
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue(new Blob())
|
||||
vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
@@ -128,7 +128,7 @@ describe('Security Dashboard - Card Status Tests', () => {
|
||||
describe('SD-02: CrowdSec Card Active Status', () => {
|
||||
it('should show "Active" when crowdsec.enabled=true', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234 })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234, lapi_ready: true })
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
@@ -143,7 +143,7 @@ describe('Security Dashboard - Card Status Tests', () => {
|
||||
|
||||
it('should show running PID when CrowdSec is running', async () => {
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234 })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234, lapi_ready: true })
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('Security Error Handling Tests', () => {
|
||||
},
|
||||
})
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false })
|
||||
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue(new Blob())
|
||||
vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
|
||||
@@ -128,7 +128,7 @@ describe('Security Error Handling Tests', () => {
|
||||
it('should show "Failed to start CrowdSec: [message]" on start failure', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusCrowdsecDisabled)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
vi.mocked(crowdsecApi.startCrowdsec).mockRejectedValue(new Error('Service unavailable'))
|
||||
|
||||
@@ -148,7 +148,7 @@ describe('Security Error Handling Tests', () => {
|
||||
it('should show "Failed to stop CrowdSec: [message]" on stop failure', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234 })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234, lapi_ready: true })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
vi.mocked(crowdsecApi.stopCrowdsec).mockRejectedValue(new Error('Process locked'))
|
||||
|
||||
@@ -304,7 +304,7 @@ describe('Security Error Handling Tests', () => {
|
||||
it('should revert CrowdSec state on start failure', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusCrowdsecDisabled)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
vi.mocked(crowdsecApi.startCrowdsec).mockRejectedValue(new Error('Start failed'))
|
||||
|
||||
@@ -333,7 +333,7 @@ describe('Security Error Handling Tests', () => {
|
||||
it('should revert CrowdSec state on stop failure', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234 })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234, lapi_ready: true })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
vi.mocked(crowdsecApi.stopCrowdsec).mockRejectedValue(new Error('Stop failed'))
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('Security Loading Overlay Tests', () => {
|
||||
},
|
||||
})
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false })
|
||||
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue(new Blob())
|
||||
vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {})
|
||||
@@ -116,7 +116,7 @@ describe('Security Loading Overlay Tests', () => {
|
||||
it('should show specific message for CrowdSec start operation', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusCrowdsecDisabled)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
// Never-resolving promise to keep loading state
|
||||
vi.mocked(crowdsecApi.startCrowdsec).mockImplementation(() => new Promise(() => {}))
|
||||
@@ -138,7 +138,7 @@ describe('Security Loading Overlay Tests', () => {
|
||||
it('should show specific message for CrowdSec stop operation', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatusAllEnabled)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234 })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234, lapi_ready: true })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
// Never-resolving promise to keep loading state
|
||||
vi.mocked(crowdsecApi.stopCrowdsec).mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
@@ -147,7 +147,7 @@ describe('Security page', () => {
|
||||
}
|
||||
|
||||
vi.mocked(api.getSecurityStatus).mockResolvedValue(baseStatus as SecurityStatus)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false })
|
||||
vi.mocked(crowdsecApi.startCrowdsec).mockResolvedValue({ status: 'started', pid: 123, lapi_ready: true })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue(undefined)
|
||||
|
||||
@@ -161,7 +161,7 @@ describe('Security page', () => {
|
||||
|
||||
const enabledStatus: SecurityStatus = { ...baseStatus, crowdsec: { enabled: true, mode: 'local' as const, api_url: '' } }
|
||||
vi.mocked(api.getSecurityStatus).mockResolvedValue(enabledStatus as SecurityStatus)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 123 })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 123, lapi_ready: true })
|
||||
vi.mocked(crowdsecApi.stopCrowdsec).mockResolvedValue(undefined)
|
||||
|
||||
renderWithProviders(<Security />)
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('Security', () => {
|
||||
})
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false })
|
||||
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
|
||||
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue(new Blob())
|
||||
vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
@@ -192,7 +192,7 @@ describe('Security', () => {
|
||||
...mockSecurityStatus,
|
||||
crowdsec: { mode: 'local', api_url: 'http://localhost', enabled: false },
|
||||
})
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false })
|
||||
vi.mocked(crowdsecApi.startCrowdsec).mockResolvedValue({ status: 'started', pid: 123, lapi_ready: true })
|
||||
|
||||
await renderSecurityPage()
|
||||
@@ -212,7 +212,7 @@ describe('Security', () => {
|
||||
it('should stop CrowdSec when toggling off', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234 })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234, lapi_ready: true })
|
||||
vi.mocked(crowdsecApi.stopCrowdsec).mockResolvedValue({ success: true })
|
||||
|
||||
await renderSecurityPage()
|
||||
@@ -297,7 +297,7 @@ describe('Security', () => {
|
||||
...mockSecurityStatus,
|
||||
crowdsec: { mode: 'local', api_url: 'http://localhost', enabled: false },
|
||||
})
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: false, pid: 0, lapi_ready: false })
|
||||
vi.mocked(crowdsecApi.startCrowdsec).mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
await renderSecurityPage()
|
||||
@@ -312,7 +312,7 @@ describe('Security', () => {
|
||||
it('should show overlay when stopping CrowdSec', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(mockSecurityStatus)
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234 })
|
||||
vi.mocked(crowdsecApi.statusCrowdsec).mockResolvedValue({ running: true, pid: 1234, lapi_ready: true })
|
||||
vi.mocked(crowdsecApi.stopCrowdsec).mockImplementation(() => new Promise(() => {}))
|
||||
|
||||
await renderSecurityPage()
|
||||
|
||||
Reference in New Issue
Block a user