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:
GitHub Actions
2025-12-14 17:59:43 +00:00
parent 0bba5ad05f
commit 1919530662
13 changed files with 800 additions and 978 deletions

View File

@@ -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).

View File

@@ -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

View File

@@ -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*

View File

@@ -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
}

View File

@@ -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
}
>

View File

@@ -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,
})

View File

@@ -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()

View File

@@ -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()

View File

@@ -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'))

View File

@@ -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(() => {}))

View File

@@ -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 />)

View File

@@ -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()