diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd0909e..a0ca387a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -124,6 +124,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **CrowdSec Upgrade**: Upgraded CrowdSec from 1.7.4 to 1.7.5 (maintenance release, no breaking changes) + - Key improvements: PAPI allowlist check, CAPI token reuse improvements - **Caddy Upgrade**: Upgraded Caddy from v2.10.2 to v2.11.0-beta.2 - **Dependency Cleanup**: Removed manual quic-go v0.57.1 patch (now included upstream at v0.58.0) - **Dependency Cleanup**: Removed manual smallstep/certificates v0.29.0 patch (now included upstream) diff --git a/Dockerfile b/Dockerfile index b3a45d9f..4824e8c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -210,7 +210,7 @@ ARG TARGETOS ARG TARGETARCH # CrowdSec version - Renovate can update this # renovate: datasource=github-releases depName=crowdsecurity/crowdsec -ARG CROWDSEC_VERSION=1.7.4 +ARG CROWDSEC_VERSION=1.7.5 RUN apt-get update && apt-get install -y --no-install-recommends \ git clang lld \ @@ -264,7 +264,7 @@ WORKDIR /tmp/crowdsec ARG TARGETARCH # CrowdSec version - Renovate can update this # renovate: datasource=github-releases depName=crowdsecurity/crowdsec -ARG CROWDSEC_VERSION=1.7.4 +ARG CROWDSEC_VERSION=1.7.5 # Note: Debian slim does NOT include tar by default - must be explicitly installed RUN apt-get update && apt-get install -y --no-install-recommends \ diff --git a/backend/go.sum b/backend/go.sum index 26d96f0d..dd78bcda 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -159,8 +159,6 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -169,7 +167,6 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -211,7 +208,6 @@ golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= diff --git a/docs/plans/crowdsec_source_build.md b/docs/plans/crowdsec_source_build.md index b78ebff8..de0b16c9 100644 --- a/docs/plans/crowdsec_source_build.md +++ b/docs/plans/crowdsec_source_build.md @@ -34,7 +34,7 @@ ARG TARGETOS ARG TARGETARCH # CrowdSec version - Renovate can update this # renovate: datasource=github-releases depName=crowdsecurity/crowdsec -ARG CROWDSEC_VERSION=1.7.4 +ARG CROWDSEC_VERSION=1.7.5 RUN apk add --no-cache git clang lld RUN xx-apk add --no-cache gcc musl-dev @@ -122,7 +122,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ ### Research Findings -**From CrowdSec GitHub (crowdsecurity/crowdsec v1.7.4):** +**From CrowdSec GitHub (crowdsecurity/crowdsec v1.7.5):** - **Language:** Go 81.3% - **License:** MIT @@ -199,7 +199,7 @@ ARG TARGETOS ARG TARGETARCH # CrowdSec version - Renovate can update this # renovate: datasource=github-releases depName=crowdsecurity/crowdsec -ARG CROWDSEC_VERSION=1.7.4 +ARG CROWDSEC_VERSION=1.7.5 # hadolint ignore=DL3018 RUN apk add --no-cache git clang lld @@ -444,7 +444,7 @@ docker rm crowdsec-test **Expected Results:** -- ✅ `cscli version` shows CrowdSec v1.7.4 +- ✅ `cscli version` shows CrowdSec v1.7.5 - ✅ `cscli hub list` displays installed scenarios/parsers - ✅ `cscli metrics` shows metrics (or "No data" if no logs processed yet) - ✅ No critical errors in logs @@ -597,8 +597,8 @@ rm ./cscli_test ./crowdsec_test **CrowdSec Version Pinning:** -- Current: `v1.7.4` (December 2025 release) -- expr-lang in v1.7.4: Likely `v1.17.2` (vulnerable) +- Current: `v1.7.5` (January 2026 release) +- expr-lang in v1.7.5: Uses patched `v1.17.7` - Post-patch: `v1.17.7` (forced upgrade via `go get`) **Potential Issues:** @@ -859,7 +859,7 @@ docker exec cscli parsers list ```bash # Clone CrowdSec -git clone --depth 1 --branch v1.7.4 https://github.com/crowdsecurity/crowdsec.git +git clone --depth 1 --branch v1.7.5 https://github.com/crowdsecurity/crowdsec.git cd crowdsec # Patch expr-lang @@ -868,11 +868,11 @@ go mod tidy # Build binaries CGO_ENABLED=1 go build -o crowdsec \ - -ldflags "-s -w -X github.com/crowdsecurity/crowdsec/pkg/cwversion.Version=v1.7.4" \ + -ldflags "-s -w -X github.com/crowdsecurity/crowdsec/pkg/cwversion.Version=v1.7.5" \ ./cmd/crowdsec CGO_ENABLED=1 go build -o cscli \ - -ldflags "-s -w -X github.com/crowdsecurity/crowdsec/pkg/cwversion.Version=v1.7.4" \ + -ldflags "-s -w -X github.com/crowdsecurity/crowdsec/pkg/cwversion.Version=v1.7.5" \ ./cmd/crowdsec-cli # Verify expr-lang version @@ -892,7 +892,7 @@ strings /usr/local/bin/cscli | grep -i "expr-lang" # Check version cscli version # Output: -# version: v1.7.4 +# version: v1.7.5 # ... ``` diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md index f11d2eb0..3c31cc3f 100644 --- a/docs/plans/current_spec.md +++ b/docs/plans/current_spec.md @@ -1,414 +1,518 @@ -# Phase 2: TestDataManager Authentication Fix Implementation Plan +# CrowdSec 1.7.5 Upgrade Verification Plan -**Goal**: Fix TestDataManager to use authenticated API context, enabling 8 skipped tests in user management. - -**Target**: Enable all user management E2E tests that are currently skipped due to TestDataManager using unauthenticated API calls. - -**Estimated Effort**: 2-3 hours +**Document Type**: Verification Plan +**Version**: 1.7.4 → 1.7.5 +**Created**: 2026-01-22 +**Status**: Ready for Implementation --- ## Executive Summary -The `testData` fixture in Playwright tests creates a `TestDataManager` instance using an **unauthenticated API context**. This causes all API calls made by `TestDataManager` (like `createUser()`, `deleteUser()`) to fail with "Admin access required" errors. This plan details how to fix the fixture to use authenticated API requests by leveraging the stored authentication state from `playwright/.auth/user.json`. +This document outlines the verification plan for upgrading CrowdSec from version 1.7.4 to 1.7.5 in the Charon project. Based on analysis of the CrowdSec 1.7.5 release notes and the current integration implementation, this upgrade appears to be a **low-risk maintenance release** focused on internal refactoring, improved error handling, and dependency updates. --- -## Table of Contents +## 1. CrowdSec 1.7.5 Release Analysis -1. [Root Cause Analysis](#1-root-cause-analysis) -2. [Proposed Solution](#2-proposed-solution) -3. [Implementation Details](#3-implementation-details) -4. [Tests to Re-Enable](#4-tests-to-re-enable) -5. [Verification Steps](#5-verification-steps) -6. [Dependencies & Prerequisites](#6-dependencies--prerequisites) -7. [Risks & Mitigations](#7-risks--mitigations) -8. [Implementation Checklist](#8-implementation-checklist) +### 1.1 Key Changes Summary + +| Category | Count | Risk Level | +|----------|-------|------------| +| Internal Refactoring | ~25 | Low | +| Bug Fixes | 8 | Low | +| Dependency Updates | ~12 | Low | +| New Features | 2 | Low | + +### 1.2 Notable Changes Relevant to Charon Integration + +#### New Features/Improvements + +1. **`ParseKVLax` for Flexible Key-Value Parsing** ([#4007](https://github.com/crowdsecurity/crowdsec/pull/4007)) + - Adds more flexible parsing capabilities + - Impact: None - internal parser enhancement + +2. **AppSec Transaction ID Header Support** ([#4124](https://github.com/crowdsecurity/crowdsec/pull/4124)) + - Enables request tracing via transaction ID header + - Impact: Optional feature, no required changes + +3. **Docker Datasource Schema** ([#4206](https://github.com/crowdsecurity/crowdsec/pull/4206)) + - Improved Docker acquisition configuration + - Impact: May benefit container monitoring setups + +#### Bug Fixes + +1. **PAPI Allowlist Check** ([#4196](https://github.com/crowdsecurity/crowdsec/pull/4196)) + - Checks if decision is allowlisted before adding + - Impact: Improved decision handling + +2. **CAPI Token Reuse** ([#4201](https://github.com/crowdsecurity/crowdsec/pull/4201)) + - Always reuses stored token for CAPI + - Impact: Better authentication stability + +3. **LAPI-Only Container Hub Fix** ([#4169](https://github.com/crowdsecurity/crowdsec/pull/4169)) + - Don't prepare hub in LAPI-only containers + - Impact: Better for containerized deployments + +#### Internal Changes (No External Impact) + +- Removed `github.com/pkg/errors` dependency - uses `fmt.Errorf` instead +- Replaced syscall with unix/windows packages +- Various linting improvements (golangci-lint 2.8) +- Refactored acquisition and leakybucket packages +- Removed global variables in favor of dependency injection +- Build improvements for Docker (larger runners) +- Updated expr to 1.17.7 (already patched in Charon Dockerfile) +- Updated modernc.org/sqlite + +### 1.3 Breaking Changes Assessment + +**No Breaking Changes Identified** + +The 1.7.5 release contains no API-breaking changes. All modifications are: +- Internal refactoring +- Bug fixes +- Dependency updates +- CI/CD improvements --- -## Supervisor Review: APPROVED ✅ +## 2. Current Charon CrowdSec Integration Analysis -**Reviewed by**: Supervisor Agent (Senior Advisor) -**Date**: 2026-01-22 -**Verdict**: Plan is ready for implementation +### 2.1 Integration Points -### Review Summary +| Component | Location | Description | +|-----------|----------|-------------| +| **Core Package** | [backend/internal/crowdsec/](backend/internal/crowdsec/) | CrowdSec integration library | +| **API Handler** | [backend/internal/api/handlers/crowdsec_handler.go](backend/internal/api/handlers/crowdsec_handler.go) | REST API endpoints | +| **Startup Service** | [backend/internal/services/crowdsec_startup.go](backend/internal/services/) | Initialization logic | +| **Dockerfile** | [Dockerfile](../../Dockerfile) (lines 199-290) | Source build configuration | -| Criterion | Status | Notes | -|-----------|--------|-------| -| Completeness | ✅ Pass | All changes documented | -| Technical Accuracy | ✅ Pass | Correct Playwright pattern | -| Risk Assessment | ✅ Pass | Adequate with fallbacks | -| Dependencies | ✅ Pass | All verified | -| Verification | ✅ Pass | Comprehensive | -| Edge Cases | 🔸 Minor | Added defensive file existence check | +### 2.2 Key Files in crowdsec Package -### Incorporated Recommendations +| File | Purpose | Functions to Verify | +|------|---------|---------------------| +| `registration.go` | Bouncer registration, LAPI health | `EnsureBouncerRegistered`, `CheckLAPIHealth`, `GetLAPIVersion` | +| `hub_sync.go` | Hub index fetching, preset pull/apply | `FetchIndex`, `Pull`, `Apply`, `extractTarGz` | +| `hub_cache.go` | Preset caching with TTL | `Store`, `Load`, `Evict` | +| `console_enroll.go` | Console enrollment | `Enroll`, `Status`, `checkLAPIAvailable` | +| `presets.go` | Curated preset definitions | `ListCuratedPresets`, `FindPreset` | -1. **Defensive `existsSync()` check**: Added to implementation to verify storage state file exists before use -2. **Import verification**: Clarified import strategy for `playwrightRequest` -3. **Dependent fixture verification**: Added to checklist to verify `adminUser`/`regularUser`/`guestUser` fixtures +### 2.3 Handler Functions (crowdsec_handler.go) + +| Handler | Line | API Endpoint | +|---------|------|--------------| +| `Start` | 188 | POST /api/crowdsec/start | +| `Stop` | 290 | POST /api/crowdsec/stop | +| `Status` | 317 | GET /api/crowdsec/status | +| `ImportConfig` | 346 | POST /api/crowdsec/import | +| `ExportConfig` | 417 | GET /api/crowdsec/export | +| `ListFiles` | 486 | GET /api/crowdsec/files | +| `ReadFile` | 513 | GET /api/crowdsec/files/:path | +| `WriteFile` | 540 | PUT /api/crowdsec/files/:path | +| `ListPresets` | 580 | GET /api/crowdsec/presets | +| `PullPreset` | 662 | POST /api/crowdsec/presets/:slug/pull | +| `ApplyPreset` | 748 | POST /api/crowdsec/presets/:slug/apply | +| `ConsoleEnroll` | 876 | POST /api/crowdsec/console/enroll | +| `ConsoleStatus` | 932 | GET /api/crowdsec/console/status | +| `DeleteConsoleEnrollment` | 954 | DELETE /api/crowdsec/console/enrollment | +| `GetCachedPreset` | 975 | GET /api/crowdsec/presets/:slug | +| `GetLAPIDecisions` | 1077 | GET /api/crowdsec/lapi/decisions | +| `CheckLAPIHealth` | 1231 | GET /api/crowdsec/lapi/health | + +### 2.4 Docker Configuration + +**Dockerfile CrowdSec Section** (lines 199-290): +- Current version: `CROWDSEC_VERSION=1.7.4` +- Build method: Source compilation with Go 1.25.6 +- Dependency patches applied: + - `github.com/expr-lang/expr@v1.17.7` + - `golang.org/x/crypto@v0.46.0` +- Fix for expr-lang v1.17.7 compatibility (sed replacement) + +**Docker Compose Files**: +- `.docker/compose/docker-compose.yml` - Production config with crowdsec_data volume +- `.docker/compose/docker-compose.local.yml` - Local development +- `.docker/compose/docker-compose.playwright.yml` - E2E testing (crowdsec disabled) --- -## 1. Root Cause Analysis +## 3. Verification Checklist -### Current Problem +### 3.1 Pre-Upgrade Verification -The `testData` fixture in `auth-fixtures.ts` creates a `TestDataManager` instance using the Playwright `request` fixture: +- [ ] **Backup current state** + - Export current CrowdSec configuration + - Document current bouncer registrations + - Note current LAPI version from `/api/crowdsec/lapi/health` -```typescript -// tests/fixtures/auth-fixtures.ts (lines 69-75) -testData: async ({ request }, use, testInfo) => { - const manager = new TestDataManager(request, testInfo.title); - await use(manager); - await manager.cleanup(); -}, +- [ ] **Review dependency patches** + - Verify if `expr-lang@v1.17.7` patch is still needed (1.7.5 updates to 1.17.7) + - Check if `golang.org/x/crypto@v0.46.0` is still required + +### 3.2 Dockerfile Update Checklist + +- [ ] Update `CROWDSEC_VERSION=1.7.5` on line 213 +- [ ] Update `CROWDSEC_VERSION=1.7.5` on line 267 (fallback stage) +- [ ] Verify expr-lang patch compatibility (line 228-235) +- [ ] Test multi-arch build (amd64, arm64) + +### 3.3 Build Verification + +```bash +# Build CrowdSec builder stage +docker build --target crowdsec-builder -t charon-crowdsec-test:1.7.5 . + +# Verify binaries +docker run --rm charon-crowdsec-test:1.7.5 /crowdsec-out/cscli version +docker run --rm charon-crowdsec-test:1.7.5 /crowdsec-out/crowdsec -version + +# Full image build +docker build -t charon:1.7.5-test . ``` -The issue: **Playwright's `request` fixture creates an unauthenticated API context**. It does NOT use the `storageState` from `playwright.config.js` that the browser context uses. +### 3.4 Unit Test Verification -When `TestDataManager.createUser()` is called: -1. It posts to `/api/v1/users` using the unauthenticated `request` context -2. The backend requires admin authentication for user creation -3. The request fails with 401/403 "Admin access required" +Run all CrowdSec-related tests: -### Evidence +```bash +# Core package tests +cd backend && go test -v -race ./internal/crowdsec/... -From [user-management.spec.ts](../../tests/settings/user-management.spec.ts#L535-L538): -```typescript -// SKIP: testData.createUser() uses unauthenticated API calls -// The TestDataManager's request context doesn't inherit auth from the browser session -// This causes user creation and cleanup to fail with "Admin access required" -// TODO: Fix by making TestDataManager use authenticated API requests +# Handler tests +go test -v -race ./internal/api/handlers/... -run "Crowdsec" + +# Startup tests +go test -v -race ./internal/services/... -run "Crowdsec" +``` + +**Test Files to Execute**: +| Test File | Purpose | +|-----------|---------| +| `hub_sync_test.go` | Hub fetching and preset application | +| `hub_cache_test.go` | Cache TTL and eviction | +| `registration_test.go` | Bouncer registration | +| `console_enroll_test.go` | Console enrollment | +| `presets_test.go` | Curated preset definitions | +| `crowdsec_handler_test.go` | Handler integration | +| `crowdsec_lapi_test.go` | LAPI communication | +| `crowdsec_decisions_test.go` | Decision handling | +| `crowdsec_startup_test.go` | Service startup | + +### 3.5 Integration Test Verification + +```bash +# Run integration tests +cd backend && go test -v -tags=integration ./integration/... -run "Crowdsec" + +# Run via task +make integration-crowdsec +``` + +### 3.6 E2E Test Verification + +**Test Files**: +| Test File | Status | Purpose | +|-----------|--------|---------|| +| `tests/security/crowdsec-config.spec.ts` | Active | CrowdSec configuration UI tests | +| `tests/security/crowdsec-decisions.spec.ts` | Skipped | LAPI decisions tests (requires running CrowdSec) | + +> **Note**: `crowdsec-decisions.spec.ts` is currently skipped as it requires a running CrowdSec instance with LAPI enabled. These tests run in CI with full infrastructure. + +```bash +# Run Playwright E2E tests (via skill runner - recommended) +.github/skills/scripts/skill-runner.sh test-e2e-playwright + +# Or run specific test files +npx playwright test --project=chromium tests/security/crowdsec-config.spec.ts +``` + +### 3.7 Functional Verification Matrix + +| Feature | Test Method | Expected Outcome | +|---------|-------------|------------------| +| **LAPI Health** | GET `/api/crowdsec/lapi/health` | Returns version "1.7.5" | +| **Start/Stop** | POST `/api/crowdsec/start`, `/stop` | Process starts/stops cleanly | +| **Status Check** | GET `/api/crowdsec/status` | Returns running state and PID | +| **Hub Index Fetch** | GET `/api/crowdsec/presets` | Returns preset list | +| **Preset Pull** | POST `/api/crowdsec/presets/base-http-scenarios/pull` | Downloads and caches preset | +| **Preset Apply** | POST `/api/crowdsec/presets/base-http-scenarios/apply` | Applies preset configuration | +| **Console Enroll** | POST `/api/crowdsec/console/enroll` | Sends enrollment request | +| **LAPI Decisions** | GET `/api/crowdsec/lapi/decisions` | Returns decision list | +| **Bouncer Registration** | Automatic on start | API key retrieved/generated | + +### 3.8 Dependency Patch Verification + +CrowdSec 1.7.5 includes `expr-lang/expr@v1.17.7` natively. Test whether the Dockerfile patch can be removed. + +**Verification Steps**: + +1. [ ] **Test WITHOUT expr-lang patch**: + ```bash + # Temporarily comment out expr-lang patch in Dockerfile (lines 225-229) + # Build and run tests + docker build --target crowdsec-builder -t charon-crowdsec-no-patch:test . + docker run --rm charon-crowdsec-no-patch:test /crowdsec-out/cscli version + ``` + +2. [ ] **If build succeeds without patch**: + - Remove `go get github.com/expr-lang/expr@v1.17.7` line + - Remove the `sed` fix for `program.Source().String()` if not needed + - Keep `golang.org/x/crypto@v0.46.0` patch for security + +3. [ ] **If build fails without patch**: + - Retain the patch with updated comment noting it's still required + - Document the specific error for future reference + +4. [ ] **Validation**: + - Run full test suite after patch removal + - Verify no regression in CrowdSec functionality + +--- + +## 4. Test Scenarios + +### 4.1 Upgrade Smoke Test + +``` +WHEN the Docker image is built with CROWDSEC_VERSION=1.7.5 +THEN the cscli version command reports v1.7.5 +AND the crowdsec binary starts successfully +AND the LAPI health endpoint responds +``` + +### 4.2 Hub Sync Compatibility + +``` +WHEN hub index is fetched after upgrade +THEN the index format is parsed correctly +AND preset pull operations complete successfully +AND preset apply operations complete without errors +``` + +### 4.3 Console Enrollment Stability + +``` +WHEN console enrollment is attempted after upgrade +THEN LAPI availability check succeeds +AND CAPI registration works if needed +AND enrollment request is sent successfully +``` + +### 4.4 Decision API Compatibility + +``` +WHEN LAPI decisions are queried after upgrade +THEN the response format is unchanged +AND decisions are correctly parsed +AND filtering by scope/type works +``` + +### 4.5 Bouncer Registration + +``` +WHEN a new bouncer is registered after upgrade +THEN cscli bouncers add command succeeds +AND the bouncer appears in cscli bouncers list +AND the API key is correctly returned ``` --- -## 2. Proposed Solution +## 5. Rollback Plan -### Approach: Create Authenticated APIRequestContext from Storage State +### 5.1 Quick Rollback -Modify the `testData` fixture to create an authenticated `APIRequestContext` using the stored authentication state from `playwright/.auth/user.json`. +If issues are encountered after upgrade: -### Key Changes +1. **Revert Dockerfile**: + ```bash + git checkout HEAD~1 -- Dockerfile + ``` -| File | Change Type | Description | -|------|-------------|-------------| -| [tests/fixtures/auth-fixtures.ts](../../tests/fixtures/auth-fixtures.ts) | Modify | Update `testData` fixture to use authenticated API context | -| [tests/auth.setup.ts](../../tests/auth.setup.ts) | Reference only | Storage state path already exported | +2. **Rebuild with previous version**: + ```bash + docker build --build-arg CROWDSEC_VERSION=1.7.4 -t charon:rollback . + ``` + +3. **Redeploy**: + ```bash + docker-compose -f .docker/compose/docker-compose.yml down + docker-compose -f .docker/compose/docker-compose.yml up -d + ``` + +### 5.2 Data Preservation + +The `crowdsec_data` volume contains: +- Configuration files +- Acquired scenarios and parsers +- Decision database +- Bouncer registrations + +This volume persists across container recreations, ensuring data is preserved during rollback. --- -## 3. Implementation Details +## 6. Files Requiring Updates -### 3.1 File: `tests/fixtures/auth-fixtures.ts` +### 6.1 Must Update -#### Current Implementation (Lines 69-75) +| File | Line(s) | Change | +|------|---------|--------| +| `Dockerfile` | 213 | `CROWDSEC_VERSION=1.7.4` → `1.7.5` | +| `Dockerfile` | 267 | `CROWDSEC_VERSION=1.7.4` → `1.7.5` | -```typescript -/** - * TestDataManager fixture with automatic cleanup - * Creates a unique namespace per test and cleans up all resources after - */ -testData: async ({ request }, use, testInfo) => { - const manager = new TestDataManager(request, testInfo.title); - await use(manager); - await manager.cleanup(); -}, +### 6.2 May Require Review + +| File | Reason | +|------|--------| +| `Dockerfile` (lines 228-235) | Verify expr-lang patch still needed | +| `docs/plans/crowdsec_source_build.md` | Update version reference | +| `docs/implementation/QUICK_FIX_SUPPLY_CHAIN.md` | Update version reference | + +### 6.3 No Changes Required (Verified Compatible) + +| File | Reason | +|------|--------| +| `backend/internal/crowdsec/*.go` | No API changes in 1.7.5 | +| `backend/internal/api/handlers/crowdsec_handler.go` | No API changes | +| `.docker/compose/*.yml` | Volume/env unchanged | + +--- + +## 7. Dependency Analysis + +### 7.1 Current Dockerfile Patches + +```dockerfile +# Dockerfile lines 225-229 +RUN go get github.com/expr-lang/expr@v1.17.7 && \ + go get golang.org/x/crypto@v0.46.0 && \ + go mod tidy ``` -#### Proposed Implementation +**1.7.5 Status**: +- CrowdSec 1.7.5 already includes `expr@v1.17.7` ([#4150](https://github.com/crowdsecurity/crowdsec/pull/4150)) +- The `expr-lang` patch **may be removable** - verify during testing +- The `golang.org/x/crypto` patch should remain for security -```typescript -// Import playwrightRequest directly from @playwright/test (not from coverage wrapper) -// @bgotink/playwright-coverage doesn't re-export request.newContext() -import { request as playwrightRequest } from '@playwright/test'; -import { existsSync } from 'fs'; -import { STORAGE_STATE } from '../auth.setup'; +### 7.2 Compatibility Fix -// ... existing code ... - -/** - * TestDataManager fixture with automatic cleanup - * - * FIXED: Now creates an authenticated API context using stored auth state. - * This ensures API calls (like createUser, deleteUser) inherit the admin - * session established by auth.setup.ts. - * - * Previous Issue: The base `request` fixture was unauthenticated, causing - * "Admin access required" errors on protected endpoints. - */ -testData: async ({ baseURL }, use, testInfo) => { - // Defensive check: Verify auth state file exists (created by auth.setup.ts) - if (!existsSync(STORAGE_STATE)) { - throw new Error( - `Auth state file not found at ${STORAGE_STATE}. ` + - 'Ensure auth.setup has run first. Check that dependencies: ["setup"] is configured.' - ); - } - - // Create an authenticated API request context using stored auth state - // This inherits the admin session from auth.setup.ts - const authenticatedContext = await playwrightRequest.newContext({ - baseURL, - storageState: STORAGE_STATE, - extraHTTPHeaders: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - }); - - const manager = new TestDataManager(authenticatedContext, testInfo.title); - - try { - await use(manager); - } finally { - // Ensure cleanup runs even if test fails - await manager.cleanup(); - // Dispose the API context to release resources - await authenticatedContext.dispose(); - } -}, +```dockerfile +# Dockerfile lines 232-235 +RUN sed -i 's/string(program\.Source())/program.Source().String()/g' pkg/exprhelpers/debugger.go ``` -### 3.2 Full Updated Import Section - -Add these imports at the top of `auth-fixtures.ts`: - -```typescript -import { test as base, expect } from '@bgotink/playwright-coverage'; -import { request as playwrightRequest } from '@playwright/test'; -import { existsSync } from 'fs'; -import { TestDataManager } from '../utils/TestDataManager'; -import { STORAGE_STATE } from '../auth.setup'; -``` - -**Note**: `playwrightRequest` is imported from `@playwright/test` directly because `@bgotink/playwright-coverage` does not re-export the `request` module needed for `request.newContext()`. - -### 3.3 Verify STORAGE_STATE Export in auth.setup.ts - -The current `auth.setup.ts` already exports `STORAGE_STATE`: - -```typescript -// tests/auth.setup.ts (lines 20-22) -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -export const STORAGE_STATE = join(__dirname, '../playwright/.auth/user.json'); -``` - -**No changes needed to this file.** +**Verification Needed**: Check if this fix is still required in 1.7.5 --- -## 4. Tests to Re-Enable +## 8. Implementation Steps -After implementing this fix, the following 8 tests in [tests/settings/user-management.spec.ts](../../tests/settings/user-management.spec.ts) should be re-enabled by removing `test.skip()`: +### Phase 1: Preparation -| # | Test Name | File:Line | Current Skip Reason | -|---|-----------|-----------|---------------------| -| 1 | should open permissions modal | [user-management.spec.ts:496](../../tests/settings/user-management.spec.ts#L496) | Permissions button + testData auth | -| 2 | should update permission mode | [user-management.spec.ts:534](../../tests/settings/user-management.spec.ts#L534) | `testData.createUser()` uses unauthenticated API calls | -| 3 | should add permitted hosts | [user-management.spec.ts:609](../../tests/settings/user-management.spec.ts#L609) | Depends on settings button + testData auth | -| 4 | should remove permitted hosts | [user-management.spec.ts:665](../../tests/settings/user-management.spec.ts#L665) | Same as above | -| 5 | should save permission changes | [user-management.spec.ts:722](../../tests/settings/user-management.spec.ts#L722) | Same as above | -| 6 | should enable/disable user | [user-management.spec.ts:773](../../tests/settings/user-management.spec.ts#L773) | TestDataManager auth + test data pollution | -| 7 | should delete user with confirmation | [user-management.spec.ts:839](../../tests/settings/user-management.spec.ts#L839) | Delete button + testData auth | -| 8 | should change user role | [user-management.spec.ts:899](../../tests/settings/user-management.spec.ts#L899) | Role badge selector + testData auth | +1. [ ] Create feature branch: `feature/crowdsec-1.7.5-upgrade` +2. [ ] Run current test suite to establish baseline +3. [ ] Document current LAPI version and status -### Note on Mixed Skip Reasons +### Phase 2: Update -Some tests have **dual skip reasons** (e.g., "UI not implemented" + "testData auth issue"). After the auth fix, these tests should be evaluated: +4. [ ] Update Dockerfile with version 1.7.5 +5. [ ] Test build locally (amd64) +6. [ ] Test build for arm64 (if available) +7. [ ] Verify binaries report correct version -- **If UI is now implemented**: Remove skip entirely -- **If UI is still missing**: Keep skip with updated reason (remove auth-related reason) +### Phase 3: Verification + +8. [ ] Run E2E tests first: `.github/skills/scripts/skill-runner.sh test-e2e-playwright` +9. [ ] Run integration tests: `make integration-crowdsec` +10. [ ] Run unit tests: `make test-backend` +11. [ ] Run coverage verification: `make test-backend-coverage` +12. [ ] Manual API verification using functional matrix + +### Phase 4: Documentation + +12. [ ] Update version references in documentation +13. [ ] Update CHANGELOG.md +14. [ ] Create PR with test results + +### Phase 5: Deployment + +15. [ ] Merge to main +16. [ ] Monitor CI/CD pipeline +17. [ ] Verify production deployment +18. [ ] Monitor logs for any CrowdSec errors --- -## 5. Verification Steps +## 9. Acceptance Criteria -### 5.1 Pre-Implementation Verification +The upgrade is considered successful when: -1. **Confirm auth state file exists**: - ```bash - ls -la playwright/.auth/user.json - ``` - -2. **Verify auth.setup.ts runs before tests**: - ```bash - npx playwright test --project=setup --reporter=list - ``` - -### 5.2 Implementation Verification - -1. **Run a single testData-dependent test**: - ```bash - npx playwright test tests/settings/user-management.spec.ts \ - --grep "should update permission mode" \ - --project=chromium \ - --reporter=list - ``` - -2. **Verify API calls are authenticated** by adding debug logging: - ```typescript - // Temporary debug in TestDataManager.createUser() - console.log('Creating user with context:', await this.request.storageState()); - ``` - -3. **Run all user-management tests**: - ```bash - npx playwright test tests/settings/user-management.spec.ts \ - --project=chromium \ - --reporter=list - ``` - -### 5.3 Post-Implementation Verification - -1. **Verify skip count reduction**: - ```bash - grep -c "test.skip" tests/settings/user-management.spec.ts - # Before: ~22 skips - # After: ~14 skips (8 removed for auth fix) - ``` - -2. **Run full E2E suite to check for regressions**: - ```bash - npx playwright test --project=chromium - ``` - -3. **Verify cleanup works** (no orphaned test users): - - Check users list in UI after running tests - - All `test-*` prefixed users should be cleaned up +- [ ] All unit tests pass +- [ ] All integration tests pass +- [ ] All E2E tests pass +- [ ] LAPI reports version 1.7.5 +- [ ] Start/Stop operations work correctly +- [ ] Hub preset operations complete successfully +- [ ] Console enrollment works (if applicable) +- [ ] No new errors in application logs +- [ ] Docker image builds successfully for amd64 and arm64 +- [ ] Coverage report generated +- [ ] Coverage threshold ≥85% maintained +- [ ] Patch coverage 100% for Dockerfile modifications +- [ ] No new CodeQL alerts introduced --- -## 6. Dependencies & Prerequisites - -### Dependencies - -| Dependency | Status | Notes | -|------------|--------|-------| -| `auth.setup.ts` runs first | ✅ Configured | Via `dependencies: ['setup']` in playwright.config.js | -| `STORAGE_STATE` exported | ✅ Already exported | From auth.setup.ts | -| Storage state file created | ✅ Auto-created | By auth.setup.ts on first run | - -### Prerequisites - -1. **E2E environment running**: Docker containers must be up -2. **Auth setup successful**: `playwright/.auth/user.json` must exist and be valid -3. **Admin user exists**: The setup user must have admin role - ---- - -## 7. Risks & Mitigations +## 10. Risk Assessment | Risk | Likelihood | Impact | Mitigation | |------|------------|--------|------------| -| Storage state file missing/stale | Low | Test failures | Defensive `existsSync()` check added; re-run setup if needed | -| Auth cookie expired mid-test | Low | API 401 errors | Tests are short; setup runs before each run | -| Circular dependency with auth fixtures | Low | Import errors | testData only imports STORAGE_STATE, not auth fixtures | -| Context disposal race condition | Low | Resource leak | Use try/finally pattern (already in proposal) | -| Parallel test isolation | Medium | Test pollution | All tests share admin session; document that `testData` is not parallelism-safe if multiple workers create conflicting resources | +| Build failure | Low | Medium | Fallback stage in Dockerfile | +| API incompatibility | Very Low | High | Comprehensive test coverage | +| Performance regression | Low | Medium | Monitor via observability | +| expr-lang patch conflict | Low | Low | Test without patch first | +| Skipped E2E tests miss regression | Medium | Medium | Integration tests cover LAPI; CI runs full suite | -### Fallback Plan +**Overall Risk Level**: **LOW** -If the storage state approach doesn't work, alternative options: - -1. **API Token Approach**: Generate a long-lived API token during setup, pass to TestDataManager -2. **Direct Login in Fixture**: Have testData fixture call login API directly before each test -3. **Shared Admin Session**: Use a dedicated admin user just for TestDataManager operations +The 1.7.5 release is a maintenance update with no breaking changes. The comprehensive test coverage in Charon provides high confidence in upgrade success. --- -## 8. Implementation Checklist +## Appendix A: Quick Reference Commands -### Core Implementation -- [ ] Update `tests/fixtures/auth-fixtures.ts` with authenticated context -- [ ] Add `existsSync` defensive check for storage state file -- [ ] Import `playwrightRequest` from `@playwright/test` (not coverage wrapper) -- [ ] Verify `STORAGE_STATE` import works - -### Verification (Supervisor Recommendations) -- [ ] Run single test to verify fix -- [ ] Verify `adminUser`/`regularUser`/`guestUser` dependent fixtures still work -- [ ] Confirm API requests include authentication cookies - -### Test Re-enablement -- [ ] Remove `test.skip()` from 8 identified tests -- [ ] Update skip comments in tests that remain skipped (remove auth-related reasons) - -### Regression & Documentation -- [ ] Run full user-management test suite -- [ ] Verify no regressions in other test files -- [ ] Update `docs/plans/skipped-tests-remediation.md` with Phase 2 completion status -- [ ] Document any remaining issues - ---- - -## 9. Phase 2 Completion Status - -### Implementation Completed ✅ - -| Item | Status | Notes | -|------|--------|-------| -| Update `auth-fixtures.ts` | ✅ Complete | Authenticated context implemented | -| Add `existsSync` check | ✅ Complete | Defensive file check added | -| Import verification | ✅ Complete | `playwrightRequest` from `@playwright/test` | -| Test re-enablement | 🔸 Partial | 2 tests re-enabled then re-skipped (see blocker) | - -### Blocker Discovered: Cookie Domain Mismatch - -**Root Cause**: Environment configuration inconsistency prevents authenticated context from working: - -1. `playwright.config.js` defaults `baseURL` to `http://localhost:8080` -2. Auth setup creates session cookies for `localhost` domain -3. Tests run against Tailscale IP `http://100.98.12.109:8080` -4. **Cookies aren't sent cross-domain** → API calls remain unauthenticated - -**Evidence**: -- Tests pass when run against `localhost:8080` -- Tests fail when run against Tailscale IP due to missing auth cookies - -**Fix Required** (separate task): ```bash -# Option 1: Set environment variable consistently -export PLAYWRIGHT_BASE_URL="http://localhost:8080" +# Build and test +make docker-build +make test-backend +make test-crowdsec -# Option 2: Update global-setup.ts default to match playwright.config.js -# tests/global-setup.ts line 8: --const baseUrl = process.env.PLAYWRIGHT_BASE_URL || 'http://100.98.12.109:8080'; -+const baseUrl = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8080'; +# Run specific test files +cd backend && go test -v ./internal/crowdsec/... -run TestHub +cd backend && go test -v ./internal/crowdsec/... -run TestConsole +cd backend && go test -v ./internal/crowdsec/... -run TestRegistration + +# Integration tests +.github/skills/scripts/skill-runner.sh integration-crowdsec + +# E2E tests +npx playwright test --project=chromium + +# Check CrowdSec version in container +docker exec charon cscli version +docker exec charon curl -s http://127.0.0.1:8085/health + +# LAPI health check +curl http://localhost:8080/api/crowdsec/lapi/health ``` -### Tests Re-Skipped with Updated Comments - -The 2 tests were re-skipped with environment documentation: -- `tests/settings/user-management.spec.ts` - "should update permission mode" -- `tests/settings/user-management.spec.ts` - "should enable/disable user" - --- -## 10. Future Improvements +## Appendix B: Related Documentation -After Phase 2 completion, consider: - -1. **Environment configuration fix**: Align `global-setup.ts` and `playwright.config.js` default URLs to prevent cookie domain mismatch. - -2. **Per-test authentication contexts**: Currently all tests share the same admin session. For true isolation, each test could create its own authenticated context. - -3. **Role-based TestDataManager**: Allow TestDataManager to operate as different roles (admin, user, guest) to test permission boundaries. - -4. **Parallel-safe user creation**: The current fix uses a single shared auth session. For highly parallel execution, consider per-worker authentication. - ---- - -## Change Log - -| Date | Author | Change | -|------|--------|--------| -| 2026-01-22 | Planning Agent | Initial Phase 2 implementation plan | -| 2026-01-22 | Supervisor Agent | Approved with recommendations: defensive checks, import verification, fixture testing | -| 2026-01-22 | Frontend_Dev | Implemented authenticated context in auth-fixtures.ts | -| 2026-01-22 | QA_Security | Discovered cookie domain mismatch blocker | -| 2026-01-22 | Management | Re-skipped tests, documented blocker for future resolution | +- [CrowdSec 1.7.5 Release Notes](https://github.com/crowdsecurity/crowdsec/releases/tag/v1.7.5) +- [CrowdSec Source Build Plan](crowdsec_source_build.md) +- [Supply Chain Remediation](../implementation/SUPPLY_CHAIN_REMEDIATION_PLAN.md) +- [Charon Security Documentation](../../SECURITY.md) diff --git a/docs/plans/current_spec.md.backup_20251224_203906 b/docs/plans/current_spec.md.backup_20251224_203906 deleted file mode 100644 index 97cd338f..00000000 --- a/docs/plans/current_spec.md.backup_20251224_203906 +++ /dev/null @@ -1,836 +0,0 @@ -# Notification Templates & Uptime Monitoring Fix - Implementation Specification - -**Date**: 2025-12-24 -**Status**: Ready for Implementation -**Priority**: High -**Supersedes**: Previous SSRF mitigation plan (moved to archive) - ---- - -## Executive Summary - -This specification addresses two distinct issues: - -1. **Task 1**: JSON notification templates are currently restricted to `webhook` type only, but should be available for all notification services that support JSON payloads (Discord, Slack, Gotify, etc.) -2. **Task 2**: Uptime monitoring is incorrectly reporting proxy hosts as "down" intermittently due to timing and race condition issues in the TCP health check system - ---- - -## Task 1: Universal JSON Template Support - -### Problem Statement - -Currently, JSON payload templates (minimal, detailed, custom) are only available when `type == "webhook"`. Other notification services like Discord, Slack, and Gotify also support JSON payloads but are forced to use basic Shoutrrr formatting, limiting customization and functionality. - -### Root Cause Analysis - -#### Backend Code Location -**File**: `/projects/Charon/backend/internal/services/notification_service.go` - -**Line 126-151**: The `SendExternal` function branches on `p.Type == "webhook"`: -```go -if p.Type == "webhook" { - if err := s.sendCustomWebhook(ctx, p, data); err != nil { - logger.Log().WithError(err).Error("Failed to send webhook") - } -} else { - // All other types use basic shoutrrr with simple title/message - url := normalizeURL(p.Type, p.URL) - msg := fmt.Sprintf("%s\n\n%s", title, message) - if err := shoutrrr.Send(url, msg); err != nil { - logger.Log().WithError(err).Error("Failed to send notification") - } -} -``` - -#### Frontend Code Location -**File**: `/projects/Charon/frontend/src/pages/Notifications.tsx` - -**Line 112**: Template UI is conditionally rendered only for webhook type: -```tsx -{type === 'webhook' && ( -
- - {/* Template selection buttons and textarea */} -
-)} -``` - -#### Model Definition -**File**: `/projects/Charon/backend/internal/models/notification_provider.go` - -**Lines 1-28**: The `NotificationProvider` model has: -- `Type` field: Accepts `discord`, `slack`, `gotify`, `telegram`, `generic`, `webhook` -- `Template` field: Has values `minimal`, `detailed`, `custom` (default: `minimal`) -- `Config` field: Stores the JSON template string - -The model itself doesn't restrict templates by type—only the logic does. - -### Services That Support JSON - -Based on Shoutrrr documentation and common webhook practices: - -| Service | Supports JSON | Notes | -|---------|---------------|-------| -| **Discord** | ✅ Yes | Native webhook API accepts JSON with embeds | -| **Slack** | ✅ Yes | Block Kit JSON format | -| **Gotify** | ✅ Yes | JSON API for messages with extras | -| **Telegram** | ⚠️ Partial | Uses URL params but can include JSON in message body | -| **Generic** | ✅ Yes | Generic HTTP POST, can be JSON | -| **Webhook** | ✅ Yes | Already supported | - -### Proposed Solution - -#### Phase 1: Backend Refactoring - -**Objective**: Allow all JSON-capable services to use template rendering. - -**Changes to `/backend/internal/services/notification_service.go`**: - -1. **Create a helper function** to determine if a service type supports JSON: -```go -// supportsJSONTemplates returns true if the provider type can use JSON templates -func supportsJSONTemplates(providerType string) bool { - switch strings.ToLower(providerType) { - case "webhook", "discord", "slack", "gotify", "generic": - return true - case "telegram": - return false // Telegram uses URL parameters - default: - return false - } -} -``` - -2. **Modify `SendExternal` function** (lines 126-151): -```go -for _, provider := range providers { - if !shouldSend { - continue - } - - go func(p models.NotificationProvider) { - // Use JSON templates for all supported services - if supportsJSONTemplates(p.Type) && p.Template != "" { - if err := s.sendJSONPayload(ctx, p, data); err != nil { - logger.Log().WithError(err).Error("Failed to send JSON notification") - } - } else { - // Fallback to basic shoutrrr for unsupported services - url := normalizeURL(p.Type, p.URL) - msg := fmt.Sprintf("%s\n\n%s", title, message) - if err := shoutrrr.Send(url, msg); err != nil { - logger.Log().WithError(err).Error("Failed to send notification") - } - } - }(provider) -} -``` - -3. **Rename `sendCustomWebhook` to `sendJSONPayload`** (lines 154-251): - - Function name: `sendCustomWebhook` → `sendJSONPayload` - - Keep all existing logic (template rendering, SSRF protection, etc.) - - Update all references in tests - -4. **Update service-specific URL handling**: - - For `discord`, `slack`, `gotify`: Still use `normalizeURL()` to format the webhook URL correctly - - For `generic` and `webhook`: Use URL as-is after SSRF validation - -#### Phase 2: Frontend Enhancement - -**Changes to `/frontend/src/pages/Notifications.tsx`**: - -1. **Line 112**: Change conditional from `type === 'webhook'` to include all JSON-capable types: -```tsx -{supportsJSONTemplates(type) && ( -
- - {/* Existing template buttons and textarea */} -
-)} -``` - -2. **Add helper function** at the top of the component: -```tsx -const supportsJSONTemplates = (type: string): boolean => { - return ['webhook', 'discord', 'slack', 'gotify', 'generic'].includes(type); -}; -``` - -3. **Update translations** to be more generic: - - Current: "Custom Webhook (JSON)" - - New: "Custom Webhook / JSON Payload" - -**Changes to `/frontend/src/api/notifications.ts`**: - -- No changes needed; the API already supports `template` and `config` fields for all provider types - -#### Phase 3: Documentation & Migration - -1. **Update `/docs/security.md`** (line 536+): - - Document Discord JSON template format - - Add examples for Slack Block Kit - - Add Gotify JSON examples - -2. **Update `/docs/features.md`**: - - Note that JSON templates are available for all compatible services - - Provide comparison table of template availability by service - -3. **Database Migration**: - - No schema changes needed - - Existing `template` and `config` fields work for all types - -### Testing Strategy - -#### Unit Tests - -**New test file**: `/backend/internal/services/notification_service_template_test.go` - -```go -func TestSupportsJSONTemplates(t *testing.T) { - tests := []struct { - providerType string - expected bool - }{ - {"webhook", true}, - {"discord", true}, - {"slack", true}, - {"gotify", true}, - {"generic", true}, - {"telegram", false}, - {"unknown", false}, - } - // Test implementation -} - -func TestSendJSONPayload_Discord(t *testing.T) { - // Test Discord webhook with JSON template -} - -func TestSendJSONPayload_Slack(t *testing.T) { - // Test Slack webhook with JSON template -} - -func TestSendJSONPayload_Gotify(t *testing.T) { - // Test Gotify API with JSON template -} -``` - -**Update existing tests**: -- Rename all `sendCustomWebhook` references to `sendJSONPayload` -- Add test cases for non-webhook JSON services - -#### Integration Tests - -1. Create test Discord webhook and verify JSON payload -2. Test template preview for Discord, Slack, Gotify -3. Verify backward compatibility (existing webhook configs still work) - -#### Frontend Tests - -**File**: `/frontend/src/pages/__tests__/Notifications.spec.tsx` - -```tsx -it('shows template selector for Discord', () => { - // Render form with type=discord - // Assert template UI is visible -}) - -it('hides template selector for Telegram', () => { - // Render form with type=telegram - // Assert template UI is hidden -}) -``` - ---- - -## Task 2: Uptime Monitoring False "Down" Status Fix - -### Problem Statement - -Proxy hosts are incorrectly reported as "down" in uptime monitoring after refreshing the page, even though they're fully accessible. The status shows "up" initially, then changes to "down" after a short time. - -### Root Cause Analysis - -**Previous Fix Applied**: Port mismatch issue was fixed in `/docs/implementation/uptime_monitoring_port_fix_COMPLETE.md`. The system now correctly uses `ProxyHost.ForwardPort` instead of extracting port from URLs. - -**Remaining Issue**: The problem persists due to **timing and race conditions** in the check cycle. - -#### Cause 1: Race Condition in CheckAll() - -**File**: `/backend/internal/services/uptime_service.go` - -**Lines 305-344**: `CheckAll()` performs host-level checks then monitor-level checks: - -```go -func (s *UptimeService) CheckAll() { - // First, check all UptimeHosts - s.checkAllHosts() // ← Calls checkHost() in loop, no wait - - var monitors []models.UptimeMonitor - s.DB.Where("enabled = ?", true).Find(&monitors) - - // Group monitors by host - for hostID, monitors := range hostMonitors { - if hostID != "" { - var uptimeHost models.UptimeHost - if err := s.DB.First(&uptimeHost, "id = ?", hostID).Error; err == nil { - if uptimeHost.Status == "down" { - s.markHostMonitorsDown(monitors, &uptimeHost) - continue // ← Skip individual checks if host is down - } - } - } - // Check individual monitors - for _, monitor := range monitors { - go s.checkMonitor(monitor) - } - } -} -``` - -**Problem**: `checkAllHosts()` runs synchronously through all hosts (line 351-353): -```go -for i := range hosts { - s.checkHost(&hosts[i]) // ← Takes 5s+ per host with multiple ports -} -``` - -If a host has 3 monitors and each TCP dial takes 5 seconds (timeout), total time is 15+ seconds. During this time: -1. The UI refreshes and calls the API -2. API reads database before `checkHost()` completes -3. Stale "down" status is returned -4. UI shows "down" even though check is still in progress - -#### Cause 2: No Status Transition Debouncing - -**Lines 422-441**: `checkHost()` immediately marks host as down after a single TCP failure: - -```go -success := false -for _, monitor := range monitors { - conn, err := net.DialTimeout("tcp", addr, 5*time.Second) - if err == nil { - success = true - break - } -} - -// Immediately flip to down if any failure -if success { - newStatus = "up" -} else { - newStatus = "down" // ← No grace period or retry -} -``` - -A single transient failure (network hiccup, container busy, etc.) immediately marks the host as down. - -#### Cause 3: Short Timeout Window - -**Line 399**: TCP timeout is only 5 seconds: -```go -conn, err := net.DialTimeout("tcp", addr, 5*time.Second) -``` - -For containers or slow networks, 5 seconds might not be enough, especially if: -- Container is warming up -- System is under load -- Multiple concurrent checks happening - -### Proposed Solution - -#### Fix 1: Synchronize Host Checks with WaitGroup - -**File**: `/backend/internal/services/uptime_service.go` - -**Update `checkAllHosts()` function** (lines 346-353): - -```go -func (s *UptimeService) checkAllHosts() { - var hosts []models.UptimeHost - if err := s.DB.Find(&hosts).Error; err != nil { - logger.Log().WithError(err).Error("Failed to fetch uptime hosts") - return - } - - var wg sync.WaitGroup - for i := range hosts { - wg.Add(1) - go func(host *models.UptimeHost) { - defer wg.Done() - s.checkHost(host) - }(&hosts[i]) - } - wg.Wait() // ← Wait for all host checks to complete - - logger.Log().WithField("host_count", len(hosts)).Info("All host checks completed") -} -``` - -**Impact**: -- All host checks run concurrently (faster overall) -- `CheckAll()` waits for completion before querying database -- Eliminates race condition between check and read - -#### Fix 2: Add Failure Count Debouncing - -**Add new field to `UptimeHost` model**: - -**File**: `/backend/internal/models/uptime_host.go` - -```go -type UptimeHost struct { - // ... existing fields ... - FailureCount int `json:"failure_count" gorm:"default:0"` // Consecutive failures -} -``` - -**Update `checkHost()` status logic** (lines 422-441): - -```go -const failureThreshold = 2 // Require 2 consecutive failures before marking down - -if success { - host.FailureCount = 0 - newStatus = "up" -} else { - host.FailureCount++ - if host.FailureCount >= failureThreshold { - newStatus = "down" - } else { - newStatus = host.Status // ← Keep current status on first failure - logger.Log().WithFields(map[string]any{ - "host_name": host.Name, - "failure_count": host.FailureCount, - "threshold": failureThreshold, - }).Warn("Host check failed, waiting for threshold") - } -} -``` - -**Rationale**: Prevents single transient failures from triggering false alarms. - -#### Fix 3: Increase Timeout and Add Retry - -**Update `checkHost()` function** (lines 359-408): - -```go -const tcpTimeout = 10 * time.Second // ← Increased from 5s -const maxRetries = 2 - -success := false -var msg string - -for retry := 0; retry < maxRetries && !success; retry++ { - if retry > 0 { - logger.Log().WithField("retry", retry).Info("Retrying TCP check") - time.Sleep(2 * time.Second) // Brief delay between retries - } - - for _, monitor := range monitors { - var port string - if monitor.ProxyHost != nil { - port = fmt.Sprintf("%d", monitor.ProxyHost.ForwardPort) - } else { - port = extractPort(monitor.URL) - } - - if port == "" { - continue - } - - addr := net.JoinHostPort(host.Host, port) - conn, err := net.DialTimeout("tcp", addr, tcpTimeout) - if err == nil { - conn.Close() - success = true - msg = fmt.Sprintf("TCP connection to %s successful (retry %d)", addr, retry) - break - } - msg = fmt.Sprintf("TCP check failed: %v", err) - } -} -``` - -**Impact**: -- More resilient to transient failures -- Increased timeout handles slow networks -- Logs show retry attempts for debugging - -#### Fix 4: Add Detailed Logging - -**Add debug logging throughout** to help diagnose future issues: - -```go -logger.Log().WithFields(map[string]any{ - "host_name": host.Name, - "host_ip": host.Host, - "port": port, - "tcp_timeout": tcpTimeout, - "retry_attempt": retry, - "success": success, - "failure_count": host.FailureCount, - "old_status": oldStatus, - "new_status": newStatus, - "elapsed_ms": time.Since(start).Milliseconds(), -}).Debug("Host TCP check completed") -``` - -### Testing Strategy for Task 2 - -#### Unit Tests - -**File**: `/backend/internal/services/uptime_service_test.go` - -Add new test cases: - -```go -func TestCheckHost_RetryLogic(t *testing.T) { - // Create a server that fails first attempt, succeeds on retry - // Verify retry logic works correctly -} - -func TestCheckHost_Debouncing(t *testing.T) { - // Verify single failure doesn't mark host as down - // Verify 2 consecutive failures do mark as down -} - -func TestCheckAllHosts_Synchronization(t *testing.T) { - // Create multiple hosts with varying check times - // Verify all checks complete before function returns - // Use channels to track completion order -} - -func TestCheckHost_ConcurrentChecks(t *testing.T) { - // Run multiple CheckAll() calls concurrently - // Verify no race conditions or deadlocks -} -``` - -#### Integration Tests - -**File**: `/backend/integration/uptime_integration_test.go` - -```go -func TestUptimeMonitoring_SlowNetwork(t *testing.T) { - // Simulate slow TCP handshake (8 seconds) - // Verify host is still marked as up with new timeout -} - -func TestUptimeMonitoring_TransientFailure(t *testing.T) { - // Fail first check, succeed second - // Verify host remains up due to debouncing -} - -func TestUptimeMonitoring_PageRefresh(t *testing.T) { - // Simulate rapid API calls during check cycle - // Verify status remains consistent -} -``` - -#### Manual Testing Checklist - -- [ ] Create proxy host with non-standard port (e.g., Wizarr on 5690) -- [ ] Enable uptime monitoring for that host -- [ ] Verify initial status shows "up" -- [ ] Refresh page 10 times over 5 minutes -- [ ] Confirm status remains "up" consistently -- [ ] Check database for heartbeat records -- [ ] Review logs for any timeout or retry messages -- [ ] Test with container restart during check -- [ ] Test with multiple hosts checked simultaneously -- [ ] Verify notifications are not triggered by transient failures - ---- - -## Implementation Phases - -### Phase 1: Task 1 Backend (Day 1) -- [ ] Add `supportsJSONTemplates()` helper function -- [ ] Rename `sendCustomWebhook` → `sendJSONPayload` -- [ ] Update `SendExternal()` to use JSON for all compatible services -- [ ] Write unit tests for new logic -- [ ] Update existing tests with renamed function - -### Phase 2: Task 1 Frontend (Day 1-2) -- [ ] Update template UI conditional in `Notifications.tsx` -- [ ] Add `supportsJSONTemplates()` helper function -- [ ] Update translations for generic JSON support -- [ ] Write frontend tests for template visibility - -### Phase 3: Task 2 Database Migration (Day 2) -- [ ] Add `FailureCount` field to `UptimeHost` model -- [ ] Create migration file -- [ ] Test migration on dev database -- [ ] Update model documentation - -### Phase 4: Task 2 Backend Fixes (Day 2-3) -- [ ] Add WaitGroup synchronization to `checkAllHosts()` -- [ ] Implement failure count debouncing in `checkHost()` -- [ ] Add retry logic with increased timeout -- [ ] Add detailed debug logging -- [ ] Write unit tests for new behavior -- [ ] Write integration tests - -### Phase 5: Documentation (Day 3) -- [ ] Update `/docs/security.md` with JSON examples for Discord, Slack, Gotify -- [ ] Update `/docs/features.md` with template availability table -- [ ] Document uptime monitoring improvements -- [ ] Add troubleshooting guide for false positives/negatives -- [ ] Update API documentation - -### Phase 6: Testing & Validation (Day 4) -- [ ] Run full backend test suite (`go test ./...`) -- [ ] Run frontend test suite (`npm test`) -- [ ] Perform manual testing for both tasks -- [ ] Test with real Discord/Slack/Gotify webhooks -- [ ] Test uptime monitoring with various scenarios -- [ ] Load testing for concurrent checks -- [ ] Code review and security audit - ---- - -## Configuration File Updates - -### `.gitignore` - -**Status**: ✅ No changes needed - -Current ignore patterns are adequate: -- `*.cover` files already ignored -- `test-results/` already ignored -- No new artifacts from these changes - -### `codecov.yml` - -**Status**: ✅ No changes needed - -Current coverage targets are appropriate: -- Backend target: 85% -- Frontend target: 70% - -New code will maintain these thresholds. - -### `.dockerignore` - -**Status**: ✅ No changes needed - -Current patterns already exclude: -- Test files (`**/*_test.go`) -- Coverage reports (`*.cover`) -- Documentation (`docs/`) - -### `Dockerfile` - -**Status**: ✅ No changes needed - -No dependencies or build steps require modification: -- No new packages needed -- No changes to multi-stage build -- No new runtime requirements - ---- - -## Risk Assessment - -### Task 1 Risks - -| Risk | Severity | Mitigation | -|------|----------|------------| -| Breaking existing webhook configs | High | Comprehensive testing, backward compatibility checks | -| Discord/Slack JSON format incompatibility | Medium | Test with real webhook endpoints, validate JSON schema | -| Template rendering errors cause notification failures | Medium | Robust error handling, fallback to basic shoutrrr format | -| SSRF vulnerabilities in new paths | High | Reuse existing security validation, audit all code paths | - -### Task 2 Risks - -| Risk | Severity | Mitigation | -|------|----------|------------| -| Increased check duration impacts performance | Medium | Monitor check times, set hard limits, run concurrently | -| Database lock contention from FailureCount updates | Low | Use lightweight updates, batch where possible | -| False positives after retry logic | Low | Tune retry count and delay based on real-world testing | -| Database migration fails on large datasets | Medium | Test on copy of production data, rollback plan ready | - ---- - -## Success Criteria - -### Task 1 -- ✅ Discord notifications can use custom JSON templates with embeds -- ✅ Slack notifications can use Block Kit JSON templates -- ✅ Gotify notifications can use custom JSON payloads -- ✅ Template preview works for all supported services -- ✅ Existing webhook configurations continue to work unchanged -- ✅ No increase in failed notification rate -- ✅ JSON validation errors are logged clearly - -### Task 2 -- ✅ Proxy hosts with non-standard ports show correct "up" status consistently -- ✅ False "down" alerts reduced by 95% or more -- ✅ Average check duration remains under 20 seconds even with retries -- ✅ Status remains stable during page refreshes -- ✅ No increase in missed down events (false negatives) -- ✅ Detailed logs available for troubleshooting -- ✅ No database corruption or lock contention - ---- - -## Rollback Plan - -### Task 1 -1. Revert `SendExternal()` to check `p.Type == "webhook"` only -2. Revert frontend conditional to `type === 'webhook'` -3. Revert function rename (`sendJSONPayload` → `sendCustomWebhook`) -4. Deploy hotfix immediately -5. Estimated rollback time: 15 minutes - -### Task 2 -1. Revert database migration (remove `FailureCount` field) -2. Revert `checkAllHosts()` to non-synchronized version -3. Remove retry logic from `checkHost()` -4. Restore original TCP timeout (5s) -5. Deploy hotfix immediately -6. Estimated rollback time: 20 minutes - -**Rollback Testing**: Test rollback procedure on staging environment before production deployment. - ---- - -## Monitoring & Alerts - -### Metrics to Track - -**Task 1**: -- Notification success rate by service type (target: >99%) -- JSON parse errors per hour (target: <5) -- Template rendering failures (target: <1%) -- Average notification send time by service - -**Task 2**: -- Uptime check duration (p50, p95, p99) (target: p95 < 15s) -- Host status transitions per hour (up → down, down → up) -- False alarm rate (user-reported vs system-detected) -- Retry count per check cycle -- FailureCount distribution across hosts - -### Log Queries - -```bash -# Task 1: Check JSON notification errors -docker logs charon 2>&1 | grep "Failed to send JSON notification" | tail -n 20 - -# Task 1: Check template rendering failures -docker logs charon 2>&1 | grep "failed to parse webhook template" | tail -n 20 - -# Task 2: Check uptime false negatives -docker logs charon 2>&1 | grep "Host status changed" | tail -n 50 - -# Task 2: Check retry patterns -docker logs charon 2>&1 | grep "Retrying TCP check" | tail -n 20 - -# Task 2: Check debouncing effectiveness -docker logs charon 2>&1 | grep "waiting for threshold" | tail -n 20 -``` - -### Grafana Dashboard Queries (if applicable) - -```promql -# Notification success rate by type -rate(notification_sent_total{status="success"}[5m]) / rate(notification_sent_total[5m]) - -# Uptime check duration -histogram_quantile(0.95, rate(uptime_check_duration_seconds_bucket[5m])) - -# Host status changes -rate(uptime_host_status_changes_total[5m]) -``` - ---- - -## Appendix: File Change Summary - -### Backend Files -| File | Lines Changed | Type | Task | -|------|---------------|------|------| -| `backend/internal/services/notification_service.go` | ~80 | Modify | 1 | -| `backend/internal/services/uptime_service.go` | ~150 | Modify | 2 | -| `backend/internal/models/uptime_host.go` | +2 | Add Field | 2 | -| `backend/internal/services/notification_service_template_test.go` | +250 | New File | 1 | -| `backend/internal/services/uptime_service_test.go` | +200 | Extend | 2 | -| `backend/integration/uptime_integration_test.go` | +150 | New File | 2 | -| `backend/internal/database/migrations/` | +20 | New Migration | 2 | - -### Frontend Files -| File | Lines Changed | Type | Task | -|------|---------------|------|------| -| `frontend/src/pages/Notifications.tsx` | ~30 | Modify | 1 | -| `frontend/src/pages/__tests__/Notifications.spec.tsx` | +80 | Extend | 1 | -| `frontend/src/locales/en/translation.json` | ~5 | Modify | 1 | - -### Documentation Files -| File | Lines Changed | Type | Task | -|------|---------------|------|------| -| `docs/security.md` | +150 | Extend | 1 | -| `docs/features.md` | +80 | Extend | 1, 2 | -| `docs/plans/current_spec.md` | ~2000 | Replace | 1, 2 | -| `docs/troubleshooting/uptime_monitoring.md` | +200 | New File | 2 | - -**Total Estimated Changes**: ~3,377 lines across 14 files - ---- - -## Database Migration - -### Migration File - -**File**: `backend/internal/database/migrations/YYYYMMDDHHMMSS_add_uptime_host_failure_count.go` - -```go -package migrations - -import ( - "gorm.io/gorm" -) - -func init() { - Migrations = append(Migrations, Migration{ - ID: "YYYYMMDDHHMMSS", - Description: "Add failure_count to uptime_hosts table", - Migrate: func(db *gorm.DB) error { - return db.Exec("ALTER TABLE uptime_hosts ADD COLUMN failure_count INTEGER DEFAULT 0").Error - }, - Rollback: func(db *gorm.DB) error { - return db.Exec("ALTER TABLE uptime_hosts DROP COLUMN failure_count").Error - }, - }) -} -``` - -### Compatibility Notes - -- SQLite supports `ALTER TABLE ADD COLUMN` -- Default value will be applied to existing rows -- No data loss on rollback (column drop is safe for new field) -- Migration is idempotent (check for column existence before adding) - ---- - -## Next Steps - -1. ✅ **Plan Review Complete**: This document is comprehensive and ready -2. ⏳ **Architecture Review**: Team lead approval for structural changes -3. ⏳ **Begin Phase 1**: Start with Task 1 backend refactoring -4. ⏳ **Parallel Development**: Task 2 can proceed independently after migration -5. ⏳ **Code Review**: Submit PRs after each phase completes -6. ⏳ **Staging Deployment**: Test both tasks in staging environment -7. ⏳ **Production Deployment**: Gradual rollout with monitoring - ---- - -**Specification Author**: GitHub Copilot -**Review Status**: ✅ Complete - Awaiting Implementation -**Estimated Implementation Time**: 4 days -**Estimated Lines of Code**: ~3,377 lines diff --git a/docs/plans/current_spec.md.backup_playwright_skill b/docs/plans/current_spec.md.backup_playwright_skill deleted file mode 100644 index a8c24514..00000000 --- a/docs/plans/current_spec.md.backup_playwright_skill +++ /dev/null @@ -1,851 +0,0 @@ - -# Custom DNS Provider Plugin Support — Remaining Work Plan - -**Date**: 2026-01-14 - -This document is a phased completion plan for the remaining work on “Custom DNS Provider Plugin Support” on branch `feature/beta-release` (see PR #461 context in `CHANGELOG.md`). - -## What’s Already Implemented (Verified) - -- **Provider plugin registry**: `dnsprovider.Global()` registry and `dnsprovider.ProviderPlugin` interface in [backend/pkg/dnsprovider](backend/pkg/dnsprovider). -- **Built-in providers moved behind the registry**: 10 built-ins live in [backend/pkg/dnsprovider/builtin](backend/pkg/dnsprovider/builtin) and are registered via the blank import in [backend/cmd/api/main.go](backend/cmd/api/main.go). -- **External plugin loader**: `PluginLoaderService` in [backend/internal/services/plugin_loader.go](backend/internal/services/plugin_loader.go) (loads `.so`, validates metadata/interface version, optional SHA-256 allowlist, secure dir perms). -- **Plugin management backend** (Phase 5): admin endpoints in `backend/internal/api/handlers/plugin_handler.go` mounted under `/api/admin/plugins` via [backend/internal/api/routes/routes.go](backend/internal/api/routes/routes.go). -- **Example external plugin**: PowerDNS reference implementation in [plugins/powerdns](plugins/powerdns). -- **Registry-driven provider CRUD and Caddy config**: - - Provider validation/testing uses registry providers via [backend/internal/services/dns_provider_service.go](backend/internal/services/dns_provider_service.go) - - Caddy config generation is registry-driven (per Phase 5 docs) -- **Manual provider type**: `manual` provider plugin in [backend/pkg/dnsprovider/custom/manual_provider.go](backend/pkg/dnsprovider/custom/manual_provider.go). -- **Manual DNS challenge flow (UI + API)**: - - API handler: [backend/internal/api/handlers/manual_challenge_handler.go](backend/internal/api/handlers/manual_challenge_handler.go) - - Routes wired in [backend/internal/api/routes/routes.go](backend/internal/api/routes/routes.go) - - Frontend API/types: [frontend/src/api/manualChallenge.ts](frontend/src/api/manualChallenge.ts) - - Frontend UI: [frontend/src/components/dns-providers/ManualDNSChallenge.tsx](frontend/src/components/dns-providers/ManualDNSChallenge.tsx) -- **Playwright coverage exists** for manual provider flows: [tests/manual-dns-provider.spec.ts](tests/manual-dns-provider.spec.ts) - -## What’s Missing (Verified) - -- **Types endpoint is not registry-driven yet**: `GET /api/v1/dns-providers/types` is currently hardcoded in [backend/internal/api/handlers/dns_provider_handler.go](backend/internal/api/handlers/dns_provider_handler.go) and will not surface: - - the `manual` provider’s field specs - - any externally loaded plugin types (e.g., PowerDNS) - - any future custom providers registered in `dnsprovider.Global()` -- **Plugin signature allowlist is not wired**: `PluginLoaderService` supports an optional SHA-256 allowlist map, but [backend/cmd/api/main.go](backend/cmd/api/main.go) passes `nil`. -- **Sandboxing limitation is structural**: Go plugins run in-process (no OS sandbox). The only practical controls are deny-by-default plugin loading + allowlisting + secure deployment guidance. -- **No first-party webhook/script/rfc2136 provider types** exist as built-in `dnsprovider.ProviderPlugin` implementations (this is optional and should be treated as a separate feature, because external plugins already cover the extensibility goal). - ---- - -## Scope - -- Make DNS provider type discovery and UI configuration **registry-driven** so built-in + manual + externally loaded plugins show up correctly. -- Close the key security gap for external plugins by wiring an **operator-controlled allowlist** for plugin SHA-256 signatures. -- Keep the scope aligned to repo conventions: no Python, minimal new files, and follow the repository structure rules for any new docs. - -## Non-Goals - -- No Python scripts or example servers. -- No unrelated refactors of existing built-in providers. -- No “script execution provider” inside Charon (in-process shell execution is a separate high-risk feature and is explicitly out of scope here). -- No broad redesign of certificate issuance beyond what’s required for correct provider type discovery and safe plugin loading. - -## Dependencies - -- Backend provider registry: [backend/pkg/dnsprovider/plugin.go](backend/pkg/dnsprovider/plugin.go) -- Provider loader: [backend/internal/services/plugin_loader.go](backend/internal/services/plugin_loader.go) -- DNS provider UI/API type fetch: [frontend/src/api/dnsProviders.ts](frontend/src/api/dnsProviders.ts) -- Manual challenge API (used as a reference pattern for “non-Caddy” flows): [backend/internal/api/handlers/manual_challenge_handler.go](backend/internal/api/handlers/manual_challenge_handler.go) -- Container build pipeline: [Dockerfile](Dockerfile) (Caddy built via `xcaddy`) - -## Risks - -- **Type discovery mismatch**: UI uses `/api/v1/dns-providers/types`; if backend remains hardcoded, registry/manual/external plugin types won’t be configurable. -- **Supply-chain risk (plugins)**: `.so` loading is inherently sensitive; SHA-256 allowlist must be operator-controlled and deny-by-default in hardened deployments. -- **No sandbox**: Go plugins execute in-process with full memory access. Treat plugins as trusted code; document this clearly and avoid implying sandboxing. -- **SSRF / outbound calls**: plugins may implement `TestCredentials()` with outbound HTTP. Core cannot reliably enforce SSRF policy inside plugin code; mitigate via operational controls (restricted egress, allowlisted outbound via infra) and guidance for plugin authors to reuse Charon URL validators. -- **Patch coverage gate**: any production changes must maintain 100% patch coverage for modified lines. - ---- - -## Definition of Done (DoD) Verification Gates (Per Phase) - -Repository testing protocol requires Playwright E2E **before** unit tests. - -- **E2E (first)**: `npx playwright test --project=chromium` -- **Backend tests**: VS Code task `shell: Test: Backend with Coverage` -- **Frontend tests**: VS Code task `shell: Test: Frontend with Coverage` -- **TypeScript**: VS Code task `shell: Lint: TypeScript Check` -- **Pre-commit**: VS Code task `shell: Lint: Pre-commit (All Files)` -- **Security scans**: - - VS Code tasks `shell: Security: CodeQL Go Scan (CI-Aligned) [~60s]` and `shell: Security: CodeQL JS Scan (CI-Aligned) [~90s]` - - VS Code task `shell: Security: Trivy Scan` - - VS Code task `shell: Security: Go Vulnerability Check` - -**Patch coverage requirement**: 100% for modified lines. - ---- - -## Phase 1 — Registry-Driven Type Discovery (Unblocks UI + plugins) - -### Deliverables - -- Backend `GET /api/v1/dns-providers/types` returns **registry-driven** types, names, fields, and docs URLs. -- The types list includes: built-in providers, `manual`, and any external plugins loaded from `CHARON_PLUGINS_DIR`. -- Unit tests cover the new type discovery logic with 100% patch coverage on modified lines. - -### Tasks & Owners - -- **Backend_Dev** - - Replace hardcoded type list behavior in [backend/internal/api/handlers/dns_provider_handler.go](backend/internal/api/handlers/dns_provider_handler.go) with registry output. - - Use the service as the abstraction boundary: - - `h.service.GetSupportedProviderTypes()` for the type list - - `h.service.GetProviderCredentialFields(type)` for field specs - - `dnsprovider.Global().Get(type).Metadata()` for display name + docs URL - - Ensure the handler returns a stable, sorted list for predictable UI rendering. - - Add/adjust tests for the types endpoint. -- **Frontend_Dev** - - Confirm `getDNSProviderTypes()` is used as the single source of truth where appropriate. - - Keep the fallback schemas in `frontend/src/data/dnsProviderSchemas.ts` as a defensive measure, but prefer server-provided fields. -- **QA_Security** - - Validate that a newly registered provider type becomes visible in the UI without a frontend deploy. -- **Docs_Writer** - - Update operator docs explaining how types are surfaced and how plugins affect the UI. - -### Acceptance Criteria - -- Creating a `manual` provider is possible end-to-end using the types endpoint output. -- `/api/v1/dns-providers/types` includes `manual` and any externally loaded provider types (when present). -- 100% patch coverage for modified lines. - -### Verification Gates - -- If UI changed: run Playwright E2E first. -- Run backend + frontend coverage tasks, TypeScript check, pre-commit, and security scans. - ---- - -## Phase 2 — Provider Implementations: `rfc2136`, `webhook`, `script` - -This phase is **optional** and should only proceed if we explicitly want “first-party” provider types inside Charon (instead of shipping these as external `.so` plugins). External plugins already satisfy the extensibility goal. - -### Deliverables - -- New provider plugins implemented (as `dnsprovider.ProviderPlugin`): - - `rfc2136` - - `webhook` - - `script` -- Each provider defines: - - `Metadata()` (name/description/docs) - - `CredentialFields()` (field definitions for UI) - - Validation (required fields, value constraints) - - `BuildCaddyConfig()` (or explicit alternate flow) with deterministic JSON output - -### Tasks & Owners - -- **Backend_Dev** - - Add provider plugin files under [backend/pkg/dnsprovider/custom](backend/pkg/dnsprovider/custom) (pattern matches `manual_provider.go`). - - Define clear field schemas for each type (avoid guessing provider-specific parameters not supported by the underlying runtime; keep minimal + extensible). - - Implement validation errors that are actionable (which field, what’s wrong). - - Add unit tests for each provider plugin: - - metadata - - fields - - validation - - config generation -- **Frontend_Dev** - - Ensure provider forms render correctly from server-provided field definitions. - - Ensure any provider-specific help text uses the docs URL from the server type info. -- **Docs_Writer** - - Add/update docs pages for each provider type describing required fields and operational expectations. - -### Docker/Caddy Decision Checkpoint (Only if needed) - -Before changing Docker/Caddy: - -- Confirm whether the running Caddy build includes the required DNS modules for the new types. -- If a module is required and not present, update [Dockerfile](Dockerfile) `xcaddy build` arguments to include it. - -### Acceptance Criteria - -- `rfc2136`, `webhook`, and `script` show up in `/dns-providers/types` with complete field definitions. -- Creating and saving a provider of each type succeeds with validation. -- 100% patch coverage for modified lines. - -### Verification Gates - -- If UI changed: run Playwright E2E first. -- Run backend + frontend coverage tasks, TypeScript check, pre-commit, and security scans. - ---- - -## Phase 3 — Plugin Security Hardening & Operator Controls - -**Status**: ✅ **Implementation Complete, QA-Approved** (2026-01-14) -- Backend implementation complete -- QA security review passed -- Operator documentation published: [docs/features/plugin-security.md](docs/features/plugin-security.md) -- Remaining: Unit test coverage for `plugin_loader_test.go` - -### Current Implementation Analysis - -**PluginLoaderService Location**: [backend/internal/services/plugin_loader.go](backend/internal/services/plugin_loader.go) - -**Constructor Signature**: -```go -func NewPluginLoaderService(db *gorm.DB, pluginDir string, allowedSignatures map[string]string) *PluginLoaderService -``` - -**Service Struct**: -```go -type PluginLoaderService struct { - pluginDir string - allowedSigs map[string]string // plugin name (without .so) -> expected signature - loadedPlugins map[string]string // plugin type -> file path - db *gorm.DB - mu sync.RWMutex -} -``` - -**Existing Security Checks**: -1. `verifyDirectoryPermissions(dir)` — rejects world-writable directories (mode `0002`) -2. Signature verification in `LoadPlugin()` when `len(s.allowedSigs) > 0`: - - Checks if plugin name exists in allowlist → returns `dnsprovider.ErrPluginNotInAllowlist` if not - - Computes SHA-256 via `computeSignature()` → returns `dnsprovider.ErrSignatureMismatch` if different -3. Interface version check via `meta.InterfaceVersion` - -**Current main.go Usage** (line ~163): -```go -pluginLoader := services.NewPluginLoaderService(db, pluginDir, nil) // <-- nil bypasses allowlist -``` - -**Allowlist Behavior**: -- When `allowedSignatures` is `nil` or empty: all plugins are loaded (permissive mode) -- When `allowedSignatures` has entries: only listed plugins with matching signatures are allowed - -**Error Types** (from [backend/pkg/dnsprovider/errors.go](backend/pkg/dnsprovider/errors.go)): -- `dnsprovider.ErrPluginNotInAllowlist` — plugin name not found in allowlist map -- `dnsprovider.ErrSignatureMismatch` — SHA-256 hash doesn't match expected value - -### Design Decision: Option A (Env Var JSON Map) - -**Environment Variable**: `CHARON_PLUGIN_SIGNATURES` - -**Format**: JSON object mapping plugin filename (with `.so`) to SHA-256 signature -```json -{"powerdns.so": "sha256:abc123...", "myplugin.so": "sha256:def456..."} -``` - -**Behavior**: -| Env Var State | Behavior | -|---------------|----------| -| Unset/empty (`""`) | Permissive mode (backward compatible) — all plugins loaded | -| Set to `{}` | Strict mode with empty allowlist — no external plugins loaded | -| Set with entries | Strict mode — only listed plugins with matching signatures | - -**Rationale for Option A**: -- Single env var keeps configuration surface minimal -- JSON is parseable in Go with `encoding/json` -- Follows existing pattern (`CHARON_PLUGINS_DIR`, `CHARON_CROWDSEC_*`) -- Operators can generate signatures with: `sha256sum plugin.so | awk '{print "sha256:" $1}'` - -### Deliverables - -1. **Parse and wire allowlist in main.go** -2. **Helper function to parse signature env var** -3. **Unit tests for PluginLoaderService** (currently missing!) -4. **Operator documentation** - -### Implementation Tasks - -#### Task 3.1: Add Signature Parsing Helper - -**File**: [backend/cmd/api/main.go](backend/cmd/api/main.go) (or new file `backend/internal/config/plugin_config.go`) - -```go -// parsePluginSignatures parses the CHARON_PLUGIN_SIGNATURES env var. -// Returns nil if unset/empty (permissive mode). -// Returns empty map if set to "{}" (strict mode, no plugins). -// Returns populated map if valid JSON with entries. -func parsePluginSignatures() (map[string]string, error) { - raw := os.Getenv("CHARON_PLUGIN_SIGNATURES") - if raw == "" { - return nil, nil // Permissive mode - } - - var sigs map[string]string - if err := json.Unmarshal([]byte(raw), &sigs); err != nil { - return nil, fmt.Errorf("invalid CHARON_PLUGIN_SIGNATURES JSON: %w", err) - } - return sigs, nil -} -``` - -#### Task 3.2: Wire Parsing into main.go - -**File**: [backend/cmd/api/main.go](backend/cmd/api/main.go) - -**Change** (around line 163): -```go -// Before: -pluginLoader := services.NewPluginLoaderService(db, pluginDir, nil) - -// After: -pluginSignatures, err := parsePluginSignatures() -if err != nil { - log.Fatalf("parse plugin signatures: %v", err) -} -if pluginSignatures != nil { - logger.Log().Infof("Plugin signature allowlist enabled with %d entries", len(pluginSignatures)) -} else { - logger.Log().Info("Plugin signature allowlist not configured (permissive mode)") -} -pluginLoader := services.NewPluginLoaderService(db, pluginDir, pluginSignatures) -``` - -#### Task 3.3: Create PluginLoaderService Unit Tests - -**File**: [backend/internal/services/plugin_loader_test.go](backend/internal/services/plugin_loader_test.go) (NEW) - -**Test Scenarios**: - -| Test Name | Setup | Expected Result | -|-----------|-------|-----------------| -| `TestNewPluginLoaderService_NilAllowlist` | `allowedSignatures: nil` | Service created, `allowedSigs` is nil | -| `TestNewPluginLoaderService_EmptyAllowlist` | `allowedSignatures: map[string]string{}` | Service created, `allowedSigs` is empty map | -| `TestNewPluginLoaderService_PopulatedAllowlist` | `allowedSignatures: {"test.so": "sha256:abc"}` | Service created with entries | -| `TestLoadPlugin_AllowlistEmpty_SkipsVerification` | Empty allowlist, mock plugin | Plugin loads without signature check | -| `TestLoadPlugin_AllowlistSet_PluginNotListed` | Allowlist without plugin | Returns `ErrPluginNotInAllowlist` | -| `TestLoadPlugin_AllowlistSet_SignatureMismatch` | Allowlist with wrong hash | Returns `ErrSignatureMismatch` | -| `TestLoadPlugin_AllowlistSet_SignatureMatch` | Allowlist with correct hash | Plugin loads successfully | -| `TestVerifyDirectoryPermissions_Secure` | Dir mode `0755` | Returns nil | -| `TestVerifyDirectoryPermissions_WorldWritable` | Dir mode `0777` | Returns error | -| `TestComputeSignature_ValidFile` | Real file | Returns `sha256:...` string | -| `TestLoadAllPlugins_DirectoryNotExist` | Non-existent dir | Returns nil (graceful skip) | -| `TestLoadAllPlugins_DirectoryInsecure` | World-writable dir | Returns error | - -**Note**: Testing actual `.so` loading requires CGO and platform-specific binaries. Focus unit tests on: -- Constructor behavior -- `verifyDirectoryPermissions()` (create temp dirs) -- `computeSignature()` (create temp files) -- Allowlist logic flow (mock the actual `plugin.Open` call) - -#### Task 3.4: Create parsePluginSignatures Unit Tests - -**File**: [backend/cmd/api/main_test.go](backend/cmd/api/main_test.go) or integrate into plugin_loader_test.go - -| Test Name | Env Value | Expected Result | -|-----------|-----------|-----------------| -| `TestParsePluginSignatures_Unset` | (not set) | `nil, nil` | -| `TestParsePluginSignatures_Empty` | `""` | `nil, nil` | -| `TestParsePluginSignatures_EmptyObject` | `"{}"` | `map[string]string{}, nil` | -| `TestParsePluginSignatures_Valid` | `{"a.so":"sha256:x"}` | `map with entry, nil` | -| `TestParsePluginSignatures_InvalidJSON` | `"not json"` | `nil, error` | -| `TestParsePluginSignatures_MultipleEntries` | `{"a.so":"sha256:x","b.so":"sha256:y"}` | `map with 2 entries, nil` | - -### Tasks & Owners - -- **Backend_Dev** - - [x] Create `parsePluginSignatures()` helper function ✅ *Completed 2026-01-14* - - [x] Update [backend/cmd/api/main.go](backend/cmd/api/main.go) to wire parsed signatures ✅ *Completed 2026-01-14* - - [ ] Create [backend/internal/services/plugin_loader_test.go](backend/internal/services/plugin_loader_test.go) with comprehensive test coverage - - [x] Add logging for allowlist mode (enabled vs permissive) ✅ *Completed 2026-01-14* -- **DevOps** - - [x] Ensure the plugin directory is mounted read-only in production (`/app/plugins:ro`) ✅ *Completed 2026-01-14* - - [x] Validate container permissions align with `verifyDirectoryPermissions()` (mode `0755` or stricter) ✅ *Completed 2026-01-14* - - [x] Document how to generate plugin signatures: `sha256sum plugin.so | awk '{print "sha256:" $1}'` ✅ *See below* -- **QA_Security** - - [x] Threat model review focused on `.so` loading risks ✅ *QA-approved 2026-01-14* - - [x] Verify error messages don't leak sensitive path information ✅ *QA-approved 2026-01-14* - - [x] Test edge cases: symlinks, race conditions, permission changes ✅ *QA-approved 2026-01-14* -- **Docs_Writer** - - [x] Create/update plugin operator docs explaining: ✅ *Completed 2026-01-14* - - `CHARON_PLUGIN_SIGNATURES` format and behavior - - How to compute signatures - - Recommended deployment pattern (read-only mounts, strict allowlist) - - Security implications of permissive mode - - [x] Created [docs/features/plugin-security.md](docs/features/plugin-security.md) ✅ *Completed 2026-01-14* - -### Acceptance Criteria - -- [x] Plugins load successfully when signature matches allowlist ✅ *QA-approved* -- [x] Plugins are rejected with `ErrPluginNotInAllowlist` when not in allowlist ✅ *QA-approved* -- [x] Plugins are rejected with `ErrSignatureMismatch` when hash differs ✅ *QA-approved* -- [x] World-writable plugin directory is detected and prevents all plugin loading ✅ *QA-approved* -- [x] Empty/unset `CHARON_PLUGIN_SIGNATURES` maintains backward compatibility (permissive) ✅ *QA-approved* -- [x] Invalid JSON in `CHARON_PLUGIN_SIGNATURES` causes startup failure with clear error ✅ *QA-approved* -- [ ] 100% patch coverage for modified lines in `main.go` -- [ ] New `plugin_loader_test.go` achieves high coverage of testable code paths -- [x] Operator documentation created: [docs/features/plugin-security.md](docs/features/plugin-security.md) ✅ *Completed 2026-01-14* - -### Verification Gates - -- Run backend coverage task: `shell: Test: Backend with Coverage` -- Run security scans: - - `shell: Security: CodeQL Go Scan (CI-Aligned) [~60s]` - - `shell: Security: Go Vulnerability Check` -- Run pre-commit: `shell: Lint: Pre-commit (All Files)` - -### Risks & Mitigations - -| Risk | Mitigation | -|------|------------| -| Invalid JSON crashes startup | Explicit error handling with descriptive message | -| Plugin name mismatch (with/without `.so`) | Document exact format; code expects filename as key | -| Signature format confusion | Enforce `sha256:` prefix; reject malformed signatures | -| Race condition: plugin modified after signature check | Document atomic deployment pattern (copy then rename) | -| Operators forget to update signatures after plugin update | Log warning when signature verification is enabled | - ---- - -## Phase 4 — E2E Coverage + Regression Safety - -**Status**: ✅ **Implementation Complete** (2026-01-15) -- 55 tests created across 3 test files -- All tests passing (52 pass, 3 conditional skip) -- Test files: `dns-provider-types.spec.ts`, `dns-provider-crud.spec.ts`, `manual-dns-provider.spec.ts` -- Fixtures created: `tests/fixtures/dns-providers.ts` - -### Current Test Coverage Analysis - -**Existing Test Files**: -| File | Purpose | Coverage Status | -|------|---------|-----------------| -| `tests/example.spec.js` | Playwright example (external site) | Not relevant to Charon | -| `tests/manual-dns-provider.spec.ts` | Manual DNS provider E2E tests | Good foundation, many tests skipped | - -**Existing `manual-dns-provider.spec.ts` Coverage**: -- ✅ Provider Selection Flow (navigation tests) -- ✅ Manual Challenge UI Display (conditional tests) -- ✅ Copy to Clipboard functionality -- ✅ Verify Button Interactions -- ✅ Accessibility Checks (keyboard navigation, ARIA) -- ✅ Component Tests (mocked API responses) -- ✅ Error Handling tests - -**Gaps Identified**: -1. **Types Endpoint Not Tested**: No tests verify `/api/v1/dns-providers/types` returns all provider types (built-in + custom + plugins) -2. **Provider Creation Flows**: No E2E tests for creating providers of each type -3. **Provider List Rendering**: No tests verify the provider cards grid renders correctly -4. **Edit/Delete Provider Flows**: No coverage for provider management operations -5. **Form Field Validation**: No tests for required field validation errors -6. **Dynamic Field Rendering**: No tests verify fields render from server-provided definitions -7. **Plugin Provider Types**: No tests for external plugin types (e.g., `powerdns`) - -### Deliverables - -1. **New Test File**: `tests/dns-provider-types.spec.ts` — Types endpoint and selector rendering -2. **New Test File**: `tests/dns-provider-crud.spec.ts` — Provider creation, edit, delete flows -3. **Updated Test File**: `tests/manual-dns-provider.spec.ts` — Enable skipped tests, add missing coverage -4. **Operator Smoke Test Documentation**: `docs/testing/e2e-smoke-tests.md` - -### Test File Organization - -``` -tests/ -├── example.spec.js # (Keep as Playwright reference) -├── manual-dns-provider.spec.ts # (Existing - Manual DNS challenge flow) -├── dns-provider-types.spec.ts # (NEW - Provider types endpoint & selector) -├── dns-provider-crud.spec.ts # (NEW - CRUD operations & validation) -└── dns-provider-a11y.spec.ts # (NEW - Focused accessibility tests) -``` - -### Test Scenarios (Prioritized) - -#### Priority 1: Core Functionality (Must Pass Before Merge) - -**File: `dns-provider-types.spec.ts`** - -| Test Name | Description | API Verified | -|-----------|-------------|--------------| -| `GET /dns-providers/types returns all built-in providers` | Verify cloudflare, route53, digitalocean, etc. in response | `GET /api/v1/dns-providers/types` | -| `GET /dns-providers/types includes custom providers` | Verify manual, webhook, rfc2136, script in response | `GET /api/v1/dns-providers/types` | -| `Provider selector dropdown shows all types` | Verify dropdown options match API response | UI + API | -| `Provider selector groups by category` | Built-in vs custom categorization | UI | -| `Provider type selection updates form fields` | Changing type loads correct credential fields | UI | - -**File: `dns-provider-crud.spec.ts`** - -| Test Name | Description | API Verified | -|-----------|-------------|--------------| -| `Create Cloudflare provider with valid credentials` | Complete create flow for built-in type | `POST /api/v1/dns-providers` | -| `Create Manual provider successfully` | Complete create flow for custom type | `POST /api/v1/dns-providers` | -| `Form shows validation errors for missing required fields` | Submit without required fields shows errors | UI validation | -| `Test Connection button shows success/failure` | Pre-save credential validation | `POST /api/v1/dns-providers/test` | -| `Edit provider updates name and settings` | Modify existing provider | `PUT /api/v1/dns-providers/:id` | -| `Delete provider with confirmation` | Delete flow with modal | `DELETE /api/v1/dns-providers/:id` | -| `Provider list renders all providers as cards` | Grid layout verification | `GET /api/v1/dns-providers` | - -#### Priority 2: Regression Safety (Manual DNS Challenge) - -**File: `manual-dns-provider.spec.ts`** (Enable and Update) - -| Test Name | Status | Action Required | -|-----------|--------|-----------------| -| `should navigate to DNS Providers page` | ✅ Active | Keep | -| `should show Add Provider button on DNS Providers page` | ⏭️ Skipped | **Enable** - requires backend | -| `should display Manual option in provider selection` | ⏭️ Skipped | **Enable** - requires backend | -| `should display challenge panel with required elements` | ✅ Conditional | Add mock data fixture | -| `Copy to clipboard functionality` | ✅ Conditional | Add fixture | -| `Verify button interactions` | ✅ Conditional | Add fixture | -| `Accessibility checks` | ✅ Partial | Expand coverage | - -**New Tests for Manual Flow**: -| Test Name | Description | -|-----------|-------------| -| `Create manual provider and verify in list` | Full create → list → verify flow | -| `Manual provider shows "Pending Challenge" state` | Verify UI state when challenge is active | -| `Manual challenge countdown timer decrements` | Time remaining updates correctly | -| `Manual challenge verification completes flow` | Success path when DNS propagates | - -#### Priority 3: Accessibility Compliance - -**File: `dns-provider-a11y.spec.ts`** - -| Test Name | WCAG Criteria | -|-----------|---------------| -| `Provider form has properly associated labels` | 1.3.1 Info and Relationships | -| `Error messages are announced to screen readers` | 4.1.3 Status Messages | -| `Keyboard navigation through form fields` | 2.1.1 Keyboard | -| `Focus visible on all interactive elements` | 2.4.7 Focus Visible | -| `Password fields are not autocompleted` | Security best practice | -| `Dialog trap focus correctly` | 2.4.3 Focus Order | -| `Form submission button has loading state` | 4.1.2 Name, Role, Value | - -#### Priority 4: Plugin Provider Types (Optional - When Plugins Present) - -**File: `dns-provider-crud.spec.ts`** (Conditional Tests) - -| Test Name | Condition | -|-----------|-----------| -| `External plugin types appear in selector` | `CHARON_PLUGINS_DIR` has `.so` files | -| `Create provider for plugin type (e.g., powerdns)` | Plugin type available in API | -| `Plugin provider test connection works` | Plugin credentials valid | - -### Implementation Guidance - -#### Test Data Strategy - -```typescript -// tests/fixtures/dns-providers.ts -export const mockProviderTypes = { - built_in: ['cloudflare', 'route53', 'digitalocean', 'googleclouddns'], - custom: ['manual', 'webhook', 'rfc2136', 'script'], -} - -export const mockCloudflareProvider = { - name: 'Test Cloudflare', - provider_type: 'cloudflare', - credentials: { - api_token: 'test-token-12345', - }, -} - -export const mockManualProvider = { - name: 'Test Manual', - provider_type: 'manual', - credentials: {}, -} -``` - -#### API Mocking Pattern (From Existing Tests) - -```typescript -// Mock provider types endpoint -await page.route('**/api/v1/dns-providers/types', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ - types: [ - { type: 'cloudflare', name: 'Cloudflare', fields: [...] }, - { type: 'manual', name: 'Manual DNS', fields: [] }, - ], - }), - }); -}); -``` - -#### Test Structure Pattern (Following Existing Conventions) - -```typescript -import { test, expect } from '@playwright/test'; - -const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3003'; - -test.describe('DNS Provider Types', () => { - test.beforeEach(async ({ page }) => { - await page.goto(BASE_URL); - }); - - test('should display all provider types in selector', async ({ page }) => { - await test.step('Navigate to DNS Providers', async () => { - await page.goto(`${BASE_URL}/dns-providers`); - }); - - await test.step('Open Add Provider dialog', async () => { - await page.getByRole('button', { name: /add provider/i }).click(); - }); - - await test.step('Verify provider type options', async () => { - const providerSelect = page.getByRole('combobox', { name: /provider type/i }); - await providerSelect.click(); - - // Verify built-in providers - await expect(page.getByRole('option', { name: /cloudflare/i })).toBeVisible(); - await expect(page.getByRole('option', { name: /route53/i })).toBeVisible(); - - // Verify custom providers - await expect(page.getByRole('option', { name: /manual/i })).toBeVisible(); - }); - }); -}); -``` - -### Tasks & Owners - -- **QA_Security** - - [ ] Create `tests/dns-provider-types.spec.ts` with Priority 1 type tests - - [ ] Create `tests/dns-provider-crud.spec.ts` with Priority 1 CRUD tests - - [ ] Enable skipped tests in `tests/manual-dns-provider.spec.ts` - - [ ] Create `tests/dns-provider-a11y.spec.ts` with Priority 3 accessibility tests - - [ ] Create `tests/fixtures/dns-providers.ts` with mock data - - [ ] Document smoke test procedures in `docs/testing/e2e-smoke-tests.md` -- **Frontend_Dev** - - [ ] Fix any UI issues uncovered by E2E (focus order, error announcements, labels) - - [ ] Ensure form field IDs are stable for test selectors - - [ ] Add `data-testid` attributes where role-based selectors are insufficient -- **Backend_Dev** - - [ ] Fix any API contract mismatches discovered by E2E - - [ ] Ensure `/api/v1/dns-providers/types` returns complete field definitions - - [ ] Verify error response format matches frontend expectations - -### Potential Issues to Watch - -Based on code analysis, these may cause test failures (fix code first, per user directive): - -| Potential Issue | Component | Symptom | -|-----------------|-----------|---------| -| Types endpoint hardcoded | `dns_provider_handler.go` | Manual/plugin types missing from selector | -| Missing field definitions | API response | Form renders without credential fields | -| Dialog not trapping focus | `DNSProviderForm.tsx` | Tab escapes dialog | -| Select not keyboard accessible | `ui/Select.tsx` | Cannot navigate with arrow keys | -| Toast not announced | `toast.ts` | Screen readers miss success/error messages | - -### Acceptance Criteria - -- [ ] All Priority 1 tests pass reliably in Chromium -- [ ] All Priority 2 (manual provider regression) tests pass -- [ ] No skipped tests in `manual-dns-provider.spec.ts` (except documented exclusions) -- [ ] Priority 3 accessibility tests pass (or issues documented for fix) -- [ ] Smoke test documentation complete and validated by QA - -### Verification Gates - -1. **Run Playwright E2E first**: `npx playwright test --project=chromium` -2. **If tests fail**: Analyze whether failure is test bug or application bug - - Application bug → Fix code first, then re-run tests - - Test bug → Fix test, document reasoning -3. **After E2E passes**: Run full verification suite - - Backend coverage: `shell: Test: Backend with Coverage` - - Frontend coverage: `shell: Test: Frontend with Coverage` - - TypeScript check: `shell: Lint: TypeScript Check` - - Pre-commit: `shell: Lint: Pre-commit (All Files)` - - Security scans: CodeQL + Trivy + Go Vulnerability Check - ---- - -## Phase 5 — Test Coverage Gaps (Required Before Merge) - -**Status**: ✅ **Complete** (2026-01-15) - -### Context - -DoD verification passed overall (85%+ coverage), but specific gaps were identified during Issue #21 / PR #461 completeness review. - -### Deliverables - -1. **Unit tests for `plugin_loader.go`** — ✅ Comprehensive tests already exist -2. **Cover missing line in `encryption_handler.go`** — ✅ Documented as defensive error handling, added tests -3. **Enable skipped E2E tests** — ✅ Validated full integration - -### Tasks & Owners - -#### Task 5.1: Create `plugin_loader_test.go` - -**File**: [backend/internal/services/plugin_loader_test.go](backend/internal/services/plugin_loader_test.go) - -**Status**: ✅ **Complete** — Tests already exist with comprehensive coverage - -- **Backend_Dev** - - [x] `TestNewPluginLoaderService_NilAllowlist` — ✅ Exists as `TestNewPluginLoaderServicePermissiveMode` - - [x] `TestNewPluginLoaderService_EmptyAllowlist` — ✅ Exists as `TestNewPluginLoaderServiceStrictModeEmpty` - - [x] `TestNewPluginLoaderService_PopulatedAllowlist` — ✅ Exists as `TestNewPluginLoaderServiceStrictModePopulated` - - [x] `TestVerifyDirectoryPermissions_Secure` — ✅ Exists as `TestVerifyDirectoryPermissions` (mode 0755) - - [x] `TestVerifyDirectoryPermissions_WorldWritable` — ✅ Exists as `TestVerifyDirectoryPermissions` (mode 0777) - - [x] `TestComputeSignature_ValidFile` — ✅ Exists as `TestComputeSignature` - - [x] `TestLoadAllPlugins_DirectoryNotExist` — ✅ Exists as `TestLoadAllPluginsNonExistentDirectory` - - [x] `TestLoadAllPlugins_DirectoryInsecure` — ✅ Exists as `TestLoadAllPluginsWorldWritableDirectory` - -**Additional tests found in existing file:** -- `TestComputeSignatureNonExistentFile` -- `TestComputeSignatureConsistency` -- `TestComputeSignatureLargeFile` -- `TestComputeSignatureSpecialCharactersInPath` -- `TestLoadPluginNotInAllowlist` -- `TestLoadPluginSignatureMismatch` -- `TestLoadPluginSignatureMatch` -- `TestLoadPluginPermissiveMode` -- `TestLoadAllPluginsEmptyDirectory` -- `TestLoadAllPluginsEmptyPluginDir` -- `TestLoadAllPluginsSkipsDirectories` -- `TestLoadAllPluginsSkipsNonSoFiles` -- `TestListLoadedPluginsEmpty` -- `TestIsPluginLoadedFalse` -- `TestUnloadNonExistentPlugin` -- `TestCleanupEmpty` -- `TestParsePluginSignaturesLogic` -- `TestSignatureWorkflowEndToEnd` -- `TestGenerateUUIDUniqueness` -- `TestGenerateUUIDFormat` -- `TestConcurrentPluginMapAccess` - -**Note**: Actual `.so` loading requires CGO and is platform-specific. Tests focus on testable paths: -- Constructor behavior -- `verifyDirectoryPermissions()` with temp directories -- `computeSignature()` with temp files -- Allowlist validation logic - -#### Task 5.2: Cover `encryption_handler.go` Missing Line - -**Status**: ✅ **Complete** — Added documentation tests, identified defensive error handling - -- **Backend_Dev** - - [x] Identify uncovered line (likely error path in decrypt/encrypt flow) - - **Finding**: Lines 162-179 (`Validate` error path) require `ValidateKeyConfiguration()` to fail - - **Root Cause**: This only fails if `rs.currentKey == nil` (impossible after successful service creation) - - **Conclusion**: This is defensive error handling; cannot be triggered without mocking - - [x] Add targeted test case to reach 100% patch coverage - - Added `TestEncryptionHandler_Rotate_AuditChannelFull` — Tests audit channel saturation scenario - - Added `TestEncryptionHandler_Validate_ValidationFailurePath` — Documents the untestable path - -**Tests Added** (in `encryption_handler_test.go`): -- `TestEncryptionHandler_Rotate_AuditChannelFull` — Covers audit logging edge case -- `TestEncryptionHandler_Validate_ValidationFailurePath` — Documents limitation - -**Coverage Analysis**: -- `Validate` function at 60% — The uncovered 40% is defensive error handling -- `Rotate` function at 92.9% — Audit start log failure (line 63) is also defensive -- These paths exist for robustness but cannot be triggered in production without internal state corruption - -#### Task 5.3: Enable Skipped E2E Tests - -**Status**: ✅ **Previously Complete** (Phase 4) - -- **QA_Security** - - [x] Review skipped tests in `tests/manual-dns-provider.spec.ts` - - [x] Enable tests that have backend support - - [x] Document any tests that remain skipped with rationale - -### Acceptance Criteria - -- [x] `plugin_loader_test.go` exists with comprehensive coverage ✅ -- [x] 100% patch coverage for modified lines in PR #461 ✅ (Defensive paths documented) -- [x] All E2E tests enabled (or documented exclusions) ✅ -- [x] All verification gates pass ✅ - -### Verification Gates (Completed) - -- [x] Backend coverage: All plugin loader and encryption handler tests pass -- [x] E2E tests: Previously completed in Phase 4 -- [x] Pre-commit: No new lint errors introduced - ---- - -## Phase 6 — User Documentation (Recommended) - -**Status**: ✅ **Complete** (2026-01-15) - -### Context - -Core functionality is complete. User-facing documentation has been updated to reflect the new DNS Challenge feature. - -### Completed -- ✅ Rewrote `docs/features.md` as marketing overview (249 lines, down from 1,952 — 87% reduction) -- ✅ Added DNS Challenge feature to features.md with provider list and key benefits -- ✅ Organized features into 8 logical categories with "Learn More" links -- ✅ Created comprehensive `docs/features/dns-challenge.md` (DNS Challenge documentation) -- ✅ Created 18 feature stub pages for documentation consistency -- ✅ Updated README.md to include DNS Challenge in Top Features - -### Deliverables - -1. ~~**Rewrite `docs/features.md`**~~ — ✅ Complete (marketing overview style per new guidelines) -2. ~~**DNS Challenge Feature Docs**~~ — ✅ Complete (`docs/features/dns-challenge.md`) -3. ~~**Feature Stub Pages**~~ — ✅ Complete (18 stubs created) -4. ~~**Update README**~~ — ✅ Complete (DNS Challenge added to Top Features) - -### Tasks & Owners - -#### Task 6.1: Create DNS Troubleshooting Guide - -**File**: [docs/features/dns-troubleshooting.md](docs/features/dns-troubleshooting.md) (NEW) - -- **Docs_Writer** - - [ ] Common issues section: - - DNS propagation delays (TTL) - - Incorrect API credentials - - Missing permissions (e.g., Zone:Edit for Cloudflare) - - Firewall blocking outbound DNS API calls - - [ ] Verification steps: - - How to check if TXT record exists: `dig TXT _acme-challenge.example.com` - - How to verify credentials work before certificate request - - [ ] Provider-specific gotchas: - - Cloudflare: Zone ID vs API Token scopes - - Route53: IAM policy requirements - - DigitalOcean: API token permissions - -#### Task 6.2: Create Provider Quick-Setup Guides - -**Files**: -- [docs/providers/cloudflare.md](docs/providers/cloudflare.md) (NEW) -- [docs/providers/route53.md](docs/providers/route53.md) (NEW) -- [docs/providers/digitalocean.md](docs/providers/digitalocean.md) (NEW) - -- **Docs_Writer** - - [ ] Step-by-step credential creation (with screenshots/links) - - [ ] Required permissions/scopes - - [ ] Example Charon configuration - - [ ] Testing the provider connection - -#### Task 6.3: Update README Feature List - -**File**: [README.md](README.md) - -- **Docs_Writer** - - [ ] Add DNS Challenge / Wildcard Certificates to feature list - - [ ] Link to detailed documentation - -### Acceptance Criteria - -- [ ] DNS troubleshooting guide covers top 5 common issues -- [ ] At least 3 provider quick-setup guides exist -- [ ] README mentions wildcard certificate support -- [ ] Documentation follows markdown lint rules - -### Verification Gates - -- Run markdown lint: `npm run lint:md` -- Manual review of documentation accuracy - ---- - -## Open Questions (Need Explicit Decisions) - -- ~~For plugin signature allowlisting: what is the desired configuration shape?~~ - - **DECIDED: Option A (minimal)**: env var `CHARON_PLUGIN_SIGNATURES` with JSON map `pluginFilename.so` → `sha256:...` parsed by [backend/cmd/api/main.go](backend/cmd/api/main.go). See Phase 3 for full specification. - - ~~**Option B (operator-friendly)**: load from a mounted file path (adds new config surface)~~ — Not chosen; JSON env var is sufficient and simpler. -- For “first-party” providers (`webhook`, `script`, `rfc2136`): are these still required given external plugins already exist? - ---- - -## Notes on Accessibility - -UI work in this plan is built with accessibility in mind, but likely still requires manual review and testing (e.g., with Accessibility Insights) as changes land. diff --git a/frontend/test_output.txt b/frontend/test_output.txt new file mode 100644 index 00000000..ebcd72b3 --- /dev/null +++ b/frontend/test_output.txt @@ -0,0 +1,2277 @@ + +> charon-frontend@0.3.0 test +> vitest run --coverage + + + RUN  v4.0.17 /projects/Charon/frontend + Coverage enabled with v8 + + ✓ src/components/__tests__/ManualDNSChallenge.test.tsx (38 tests) 2080ms + ✓ renders copy buttons with aria labels  339ms +stderr | src/pages/__tests__/ProxyHosts-coverage.test.tsx > ProxyHosts - Coverage enhancements > bulk update ACL reject triggers error toast +Each child in a list should have a unique "key" prop. + +Check the render method of `Primitive.p`. It was passed a child from ProxyHosts. See https://react.dev/link/warning-keys for more information. + + ✓ src/pages/__tests__/CrowdSecConfig.coverage.test.tsx (19 tests) 4915ms + ✓ renders disabled mode message and bans control disabled  621ms + ✓ guards local apply prerequisites and succeeds when content exists  353ms + ✓ shows decisions table, handles loading/error/empty states, and unban errors  350ms + ✓ bans and unbans IPs with overlay messaging  786ms + ✓ shows overlay messaging for preset pull, apply, import, write, and mode updates  460ms +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > renders the component with initial state +security log viewer connected + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > displays incoming log messages +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > displays incoming log messages +application log viewer connected + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > filters logs by text +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > filters logs by text +application log viewer connected + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > filters logs by level +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > filters logs by level +application log viewer connected + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > pauses and resumes log streaming +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > pauses and resumes log streaming +application log viewer connected + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > pauses and resumes log streaming +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > clears all logs +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > clears all logs +application log viewer connected + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > limits the number of stored logs +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > limits the number of stored logs +application log viewer connected + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > displays log data when available +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > displays log data when available +application log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > shows correct connection status +security log viewer connected + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > shows correct connection status +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > shows correct connection status +security log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > shows correct connection status +security log viewer connected + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > shows correct connection status +security log viewer error: Event { isTrusted: [Getter] } +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > shows no-match message when filters exclude all logs +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > shows no-match message when filters exclude all logs +application log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > marks connection as disconnected when WebSocket closes +security log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > marks connection as disconnected when WebSocket closes +security log viewer disconnected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Security Mode > displays security log entries with source badges +security log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Security Mode > displays security log entries with source badges +security log viewer connected + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Security Mode > displays security log entries with source badges +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Security Mode > displays blocked requests with special styling +security log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Security Mode > filters by source in security mode +security log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Security Mode > filters by source in security mode +security log viewer connected + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Security Mode > filters by source in security mode +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Security Mode > toggles blocked only filter +security log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Security Mode > toggles blocked only filter +security log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Security Mode > toggles blocked only filter +security log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Security Mode > displays duration for security logs +security log viewer connected + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Security Mode > displays duration for security logs +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Security Mode > displays status code with appropriate color for security logs +security log viewer connected + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Security Mode > displays status code with appropriate color for security logs +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Mode Toggle > switches from application to security mode +application log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Mode Toggle > switches from application to security mode +security log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Mode Toggle > switches from security to application mode +security log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Mode Toggle > switches from security to application mode +application log viewer connected + +stderr | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Mode Toggle > clears logs when switching modes +An update to LiveLogViewer inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Mode Toggle > clears logs when switching modes +application log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Mode Toggle > clears logs when switching modes +security log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Mode Toggle > resets filters when switching modes +application log viewer connected + +stdout | src/components/__tests__/LiveLogViewer.test.tsx > LiveLogViewer > Mode Toggle > resets filters when switching modes +security log viewer connected + + ✓ src/components/__tests__/LiveLogViewer.test.tsx (26 tests) 1939ms +Not implemented: navigation to another Document + ✓ src/components/__tests__/ProxyHostForm.test.tsx (22 tests) 8400ms + ✓ handles scheme selection  419ms + ✓ prompts to save new base domain  665ms + ✓ respects "Dont ask me again" for new domains  1020ms + ✓ tests connection successfully  424ms + ✓ handles connection test failure  392ms + ✓ handles base domain selection  366ms + ✓ shows plex config helper with external URL when preset is selected  534ms + ✓ shows jellyfin config helper with internal IP  573ms + ✓ shows nextcloud config helper with php snippet  569ms + ✓ shows vaultwarden helper text  371ms + ✓ includes application field in form submission  1068ms + ✓ copies external URL to clipboard for plex  368ms + ✓ src/pages/__tests__/ProxyHosts-coverage.test.tsx (37 tests) 12188ms + ✓ creates a proxy host via Add Host form submit  2078ms + ✓ bulk update ACL reject triggers error toast  577ms + ✓ closes bulk ACL modal when clicking backdrop  368ms + ✓ unchecks ACL via onChange (delete path)  385ms + ✓ remove action triggers handleBulkApplyACL and shows removed toast  570ms + ✓ toggle action remove -> apply then back  502ms + ✓ remove action shows partial failure toast on API error result  554ms + ✓ remove action reject triggers error toast  505ms + ✓ close bulk delete modal by clicking backdrop  405ms + ✓ handles delete confirmation for a single host  340ms + ✓ applies bulk settings sequentially with progress and updates hosts  516ms + ✓ opens add form and cancels  323ms + ✓ opens edit form and submits update  329ms + ✓ alerts on delete when API fails  315ms + ✓ applies multiple ACLs sequentially with progress  699ms + ✓ select all / clear header selects and clears ACLs  497ms + ✓ shows toast error when updateHost rejects during bulk apply  562ms + ✓ src/pages/__tests__/CrowdSecConfig.spec.tsx (15 tests) 3437ms + ✓ exports config when clicking Export  352ms + ✓ validates required console enrollment fields and acknowledgement  311ms + ✓ submits console enrollment payload with snake_case fields  786ms + ✓ retries degraded enrollment and rotates key when enrolled  830ms +stderr | src/pages/__tests__/ProxyHosts-cert-cleanup.test.tsx > ProxyHosts - Certificate Cleanup Prompts > prompts to delete certificate when deleting proxy host with unique custom cert +Each child in a list should have a unique "key" prop. + +Check the render method of `Primitive.p`. It was passed a child from ProxyHosts. See https://react.dev/link/warning-keys for more information. + +stderr | src/pages/__tests__/ProxyHosts-extra.test.tsx > ProxyHosts page extra tests > delete with associated monitors prompts and deletes with deleteUptime true +Each child in a list should have a unique "key" prop. + +Check the render method of `Primitive.p`. It was passed a child from ProxyHosts. See https://react.dev/link/warning-keys for more information. + + ✓ src/pages/__tests__/ProxyHosts-extra.test.tsx (15 tests) 4023ms + ✓ sort toggles by header click  535ms + ✓ delete with associated monitors prompts and deletes with deleteUptime true  542ms + ✓ bulk ACL remove shows the confirmation card and Apply label updates when selecting ACLs  437ms + ✓ bulk ACL remove action calls bulkUpdateACL with null and shows removed toast  410ms + ✓ bulk delete modal lists hosts to be deleted  401ms + ✓ bulk apply modal returns early when no keys selected (no-op)  367ms + ✓ bulk delete creates backup and shows toast success  412ms + ✓ src/pages/__tests__/ProxyHosts-cert-cleanup.test.tsx (9 tests) 5555ms + ✓ prompts to delete certificate when deleting proxy host with unique custom cert  1336ms + ✓ does NOT prompt for certificate deletion when cert is shared by multiple hosts  580ms + ✓ does NOT prompt for production Let's Encrypt certificates  491ms + ✓ prompts for staging certificates  589ms + ✓ handles certificate deletion failure gracefully  489ms + ✓ bulk delete prompts for orphaned certificates  620ms + ✓ bulk delete does NOT prompt when certificate is still used by other hosts  498ms + ✓ allows cancelling certificate cleanup dialog  377ms + ✓ default state is unchecked for certificate deletion (conservative)  574ms +stderr | src/pages/__tests__/SystemSettings.test.tsx > SystemSettings > Application URL Card > shows invalid URL error message when validation fails +An update to SystemSettings inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stderr | src/pages/__tests__/SystemSettings.test.tsx > SystemSettings > Application URL Card > shows invalid URL error message when validation fails +An update to Tooltip inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to Tooltip inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to Tooltip inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to Select inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItemText inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItem inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItemText inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItem inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItemText inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItem inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItemText inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItem inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to Select inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItemText inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItem inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItemText inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItem inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItemText inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItem inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to Tooltip inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to Tooltip inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to Tooltip inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to Select inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItemText inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItem inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItemText inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItem inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItemText inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItem inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItemText inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItem inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to Select inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItemText inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItem inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItemText inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItem inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItemText inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act +An update to SelectItem inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + + ✓ src/pages/__tests__/SystemSettings.test.tsx (28 tests) 7377ms + ✓ renders the SSL provider select trigger  385ms + ✓ saves SSL provider setting when save button is clicked  469ms + ✓ shows green border and checkmark when URL is valid  767ms + ✓ shows red border and X icon when URL is invalid  851ms + ✓ shows invalid URL error message when validation fails  770ms + ✓ renders test button and verifies functionality  314ms + ✓ handles validation API error gracefully  779ms + ✓ src/hooks/__tests__/useConsoleEnrollment.test.tsx (25 tests) 1475ms + ✓ src/pages/__tests__/WafConfig.spec.tsx (24 tests) 3504ms + ✓ submits new ruleset and closes form on success  640ms + ✓ allows form submission with URL instead of content  466ms + ✓ toggles between blocking and detection mode  346ms +stderr | src/pages/__tests__/UsersPage.test.tsx > UsersPage > URL Preview in InviteModal > debounces URL preview for 500ms +An update to InviteModal inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + +stderr | src/pages/__tests__/ProxyHosts-bulk-acl.test.tsx > ProxyHosts - Bulk ACL Modal > opens bulk ACL modal when Manage ACL is clicked +Each child in a list should have a unique "key" prop. + +Check the render method of `Primitive.p`. It was passed a child from ProxyHosts. See https://react.dev/link/warning-keys for more information. + + ✓ src/pages/__tests__/ProxyHosts-bulk-delete.test.tsx (11 tests) 4878ms + ✓ renders bulk delete button when hosts are selected  762ms + ✓ shows confirmation modal when delete button is clicked  597ms + ✓ creates backup before deleting hosts  585ms + ✓ deletes multiple selected hosts after backup  474ms + ✓ reports partial success when some deletions fail  412ms + ✓ handles backup creation failure  339ms + ✓ closes modal after successful deletion  357ms + ✓ clears selection after successful deletion  361ms + ✓ disables confirm button while creating backup  476ms + ✓ can cancel deletion from modal  361ms +stderr | src/pages/__tests__/UsersPage.test.tsx > UsersPage > URL Preview in InviteModal > handles preview API error gracefully +An update to InviteModal inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act + + ✓ src/pages/__tests__/UsersPage.test.tsx (17 tests) 7097ms + ✓ opens invite modal when clicking invite button  399ms + ✓ invites a new user  461ms + ✓ updates user permissions from the modal  338ms + ✓ shows manual invite link flow when email is not sent and allows copy  437ms + ✓ shows URL preview when valid email is entered  760ms + ✓ debounces URL preview for 500ms  829ms + ✓ replaces sample token with ellipsis in preview  745ms + ✓ shows warning when not configured  821ms + ✓ does not show preview when email is invalid  807ms + ✓ handles preview API error gracefully  877ms + ✓ src/hooks/__tests__/useDNSProviders.test.tsx (26 tests) 1851ms + ✓ src/api/__tests__/consoleEnrollment.test.ts (25 tests) 31ms +stderr | src/pages/__tests__/ProxyHosts.bulkApplyHeaders.test.tsx > ProxyHosts - Bulk Apply Security Headers > shows security header profile option in bulk apply modal +Each child in a list should have a unique "key" prop. + +Check the render method of `Primitive.p`. It was passed a child from ProxyHosts. See https://react.dev/link/warning-keys for more information. + + ✓ src/utils/__tests__/crowdsecExport.test.ts (48 tests) 127ms + ✓ src/pages/__tests__/ProxyHosts-bulk-acl.test.tsx (13 tests) 8107ms + ✓ renders Manage ACL button when hosts are selected  549ms + ✓ opens bulk ACL modal when Manage ACL is clicked  372ms + ✓ shows Apply ACL and Remove ACL toggle buttons  501ms + ✓ shows only enabled access lists in the selection  300ms + ✓ has Apply button disabled when no ACL is selected  320ms + ✓ enables Apply button when ACL is selected  621ms + ✓ can select multiple ACLs  644ms + ✓ applies ACL to selected hosts successfully  1206ms + ✓ shows Remove ACL confirmation when Remove is selected  932ms + ✓ closes modal on Cancel  708ms + ✓ clears selection and closes modal after successful apply  809ms + ✓ shows error toast on API failure  846ms +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Input Validation > React escapes XSS in rendered text - validation check +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Input Validation > handles empty admin whitelist gracefully +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Input Validation > handles empty admin whitelist gracefully +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Input Validation > handles empty admin whitelist gracefully +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Error Handling > displays error toast when toggle mutation fails +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Error Handling > displays error toast when toggle mutation fails +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Error Handling > displays error toast when toggle mutation fails +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + + ✓ src/components/__tests__/DNSProviderSelector.test.tsx (29 tests) 584ms +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Error Handling > handles CrowdSec start failure gracefully +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Error Handling > handles CrowdSec start failure gracefully +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Error Handling > handles CrowdSec start failure gracefully +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Error Handling > handles CrowdSec stop failure gracefully +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Error Handling > handles CrowdSec stop failure gracefully +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Error Handling > handles CrowdSec stop failure gracefully +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Error Handling > handles CrowdSec status check failure gracefully +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Error Handling > handles CrowdSec status check failure gracefully +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Error Handling > handles CrowdSec status check failure gracefully +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Concurrent Operations > disables controls during pending mutations +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Concurrent Operations > disables controls during pending mutations +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Concurrent Operations > disables controls during pending mutations +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Concurrent Operations > prevents double toggle when starting CrowdSec +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Concurrent Operations > prevents double toggle when starting CrowdSec +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Concurrent Operations > prevents double toggle when starting CrowdSec +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > UI Consistency > maintains card order when services are toggled +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > UI Consistency > maintains card order when services are toggled +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > UI Consistency > maintains card order when services are toggled +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > UI Consistency > shows correct layer indicator badges +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > UI Consistency > shows correct layer indicator badges +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > UI Consistency > shows correct layer indicator badges +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > UI Consistency > shows all four security cards even when all disabled +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > UI Consistency > shows all four security cards even when all disabled +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + + ✓ src/pages/__tests__/ProxyHosts.bulkApplyHeaders.test.tsx (9 tests) 4835ms + ✓ shows security header profile option in bulk apply modal  713ms + ✓ enables profile selection when checkbox is checked  554ms + ✓ lists all available profiles in dropdown grouped correctly  436ms + ✓ applies security header profile to selected hosts using bulk endpoint  674ms + ✓ removes security header profile when "None" selected  524ms + ✓ disables Apply button when no options selected  331ms + ✓ handles partial failure with appropriate toast  614ms + ✓ resets state on modal close  566ms + ✓ shows profile description when profile is selected  419ms +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > UI Consistency > shows all four security cards even when all disabled +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Accessibility > all toggles have proper test IDs for automation +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Accessibility > all toggles have proper test IDs for automation +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Accessibility > all toggles have proper test IDs for automation +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Accessibility > CrowdSec controls surface primary actions when enabled +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Accessibility > CrowdSec controls surface primary actions when enabled +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Accessibility > CrowdSec controls surface primary actions when enabled +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Contract Verification (Spec Compliance) > pipeline order matches spec: CrowdSec → ACL → WAF → Rate Limiting +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Contract Verification (Spec Compliance) > pipeline order matches spec: CrowdSec → ACL → WAF → Rate Limiting +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Contract Verification (Spec Compliance) > pipeline order matches spec: CrowdSec → ACL → WAF → Rate Limiting +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Contract Verification (Spec Compliance) > layer indicators match spec descriptions +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Contract Verification (Spec Compliance) > layer indicators match spec descriptions +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Contract Verification (Spec Compliance) > layer indicators match spec descriptions +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Contract Verification (Spec Compliance) > threat summaries match spec when services enabled +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Contract Verification (Spec Compliance) > threat summaries match spec when services enabled +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Contract Verification (Spec Compliance) > threat summaries match spec when services enabled +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Edge Cases > handles rapid toggle clicks without crashing +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Edge Cases > handles rapid toggle clicks without crashing +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Edge Cases > handles rapid toggle clicks without crashing +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Edge Cases > handles undefined crowdsec status gracefully +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Edge Cases > handles undefined crowdsec status gracefully +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.audit.test.tsx > Security Page - QA Security Audit > Edge Cases > handles undefined crowdsec status gracefully +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.audit.test.tsx +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.audit.test.tsx +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + + ✓ src/pages/__tests__/Security.audit.test.tsx (18 tests) 3204ms + ✓ displays error toast when toggle mutation fails  325ms + ✓ maintains card order when services are toggled  346ms + ✓ handles rapid toggle clicks without crashing  406ms + ✓ src/api/__tests__/presets.test.ts (26 tests) 16ms + ✓ src/components/__tests__/CredentialManager.test.tsx (20 tests) 3236ms + ✓ renders modal with provider name in title  354ms + ✓ shows add credential button  337ms +stdout | src/pages/__tests__/Security.test.tsx > Security > Rendering > should render Cerberus Dashboard when status loads +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.test.tsx > Security > Rendering > should show banner when Cerberus is disabled +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.test.tsx > Security > Rendering > should show banner when Cerberus is disabled +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.test.tsx > Security > Service Toggles > should toggle CrowdSec on +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.test.tsx > Security > Service Toggles > should toggle WAF on +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.test.tsx > Security > Service Toggles > should toggle WAF on +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.test.tsx > Security > Service Toggles > should toggle WAF on +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.test.tsx > Security > Service Toggles > should toggle ACL on +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.test.tsx > Security > Service Toggles > should toggle ACL on +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.test.tsx > Security > Service Toggles > should toggle ACL on +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.test.tsx > Security > Service Toggles > should toggle Rate Limiting on +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.test.tsx > Security > Service Toggles > should toggle Rate Limiting on +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.test.tsx > Security > Service Toggles > should toggle Rate Limiting on +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.test.tsx > Security > Admin Whitelist > should load admin whitelist from config +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.test.tsx > Security > Admin Whitelist > should load admin whitelist from config +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.test.tsx > Security > Admin Whitelist > should load admin whitelist from config +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.test.tsx > Security > Admin Whitelist > should update admin whitelist on save +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.test.tsx > Security > Admin Whitelist > should update admin whitelist on save +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.test.tsx > Security > Admin Whitelist > should update admin whitelist on save +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.test.tsx > Security > CrowdSec Controls > should start CrowdSec when toggling on +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.test.tsx > Security > CrowdSec Controls > should start CrowdSec when toggling on +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.test.tsx > Security > CrowdSec Controls > should start CrowdSec when toggling on +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.test.tsx > Security > CrowdSec Controls > should stop CrowdSec when toggling off +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.test.tsx > Security > CrowdSec Controls > should stop CrowdSec when toggling off +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.test.tsx > Security > CrowdSec Controls > should stop CrowdSec when toggling off +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.test.tsx > Security > Card Order (Pipeline Sequence) > should render cards in correct pipeline order: CrowdSec → ACL → WAF → Rate Limiting +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.test.tsx > Security > Card Order (Pipeline Sequence) > should render cards in correct pipeline order: CrowdSec → ACL → WAF → Rate Limiting +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.test.tsx > Security > Card Order (Pipeline Sequence) > should render cards in correct pipeline order: CrowdSec → ACL → WAF → Rate Limiting +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.test.tsx > Security > Card Order (Pipeline Sequence) > should display layer indicators on each card +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.test.tsx > Security > Card Order (Pipeline Sequence) > should display layer indicators on each card +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.test.tsx > Security > Card Order (Pipeline Sequence) > should display layer indicators on each card +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.test.tsx > Security > Card Order (Pipeline Sequence) > should display threat protection summaries +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.test.tsx > Security > Card Order (Pipeline Sequence) > should display threat protection summaries +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.test.tsx > Security > Card Order (Pipeline Sequence) > should display threat protection summaries +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.test.tsx > Security > Loading Overlay > should show overlay when service is toggling +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.test.tsx > Security > Loading Overlay > should show overlay when service is toggling +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.test.tsx > Security > Loading Overlay > should show overlay when service is toggling +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-02: Toggle Mutation Failure Shows Toast > should call toast.error() when toggle mutation fails +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.test.tsx > Security > Loading Overlay > should show overlay when starting CrowdSec +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.test.tsx > Security > Loading Overlay > should show overlay when starting CrowdSec +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.test.tsx > Security > Loading Overlay > should show overlay when starting CrowdSec +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.test.tsx > Security > Loading Overlay > should show overlay when stopping CrowdSec +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.test.tsx > Security > Loading Overlay > should show overlay when stopping CrowdSec +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.test.tsx > Security > Loading Overlay > should show overlay when stopping CrowdSec +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-03: CrowdSec Start Failure Shows Specific Toast > should show "Failed to start CrowdSec: [message]" on start failure +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-03: CrowdSec Start Failure Shows Specific Toast > should show "Failed to start CrowdSec: [message]" on start failure +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.test.tsx +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.test.tsx +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-03: CrowdSec Start Failure Shows Specific Toast > should show "Failed to start CrowdSec: [message]" on start failure +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + + ✓ src/pages/__tests__/Security.test.tsx (18 tests) 2989ms + ✓ should toggle CrowdSec on  417ms + ✓ should update admin whitelist on save  392ms +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-04: CrowdSec Stop Failure Shows Specific Toast > should show "Failed to stop CrowdSec: [message]" on stop failure +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-04: CrowdSec Stop Failure Shows Specific Toast > should show "Failed to stop CrowdSec: [message]" on stop failure +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-04: CrowdSec Stop Failure Shows Specific Toast > should show "Failed to stop CrowdSec: [message]" on stop failure +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-05: WAF Toggle Failure Shows Error > should show error toast when WAF toggle fails +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-05: WAF Toggle Failure Shows Error > should show error toast when WAF toggle fails +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-05: WAF Toggle Failure Shows Error > should show error toast when WAF toggle fails +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-06: Rate Limiting Update Failure Shows Toast > should show error toast when rate limiting toggle fails +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-06: Rate Limiting Update Failure Shows Toast > should show error toast when rate limiting toggle fails +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-06: Rate Limiting Update Failure Shows Toast > should show error toast when rate limiting toggle fails +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-07: Network Error Shows Generic Message > should handle network errors gracefully +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-07: Network Error Shows Generic Message > should handle network errors gracefully +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-07: Network Error Shows Generic Message > should handle network errors gracefully +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-07: Network Error Shows Generic Message > should handle non-Error objects gracefully +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-07: Network Error Shows Generic Message > should handle non-Error objects gracefully +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-07: Network Error Shows Generic Message > should handle non-Error objects gracefully +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-08: ACL Toggle Failure Shows Error > should show error when ACL toggle fails +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-08: ACL Toggle Failure Shows Error > should show error when ACL toggle fails +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-08: ACL Toggle Failure Shows Error > should show error when ACL toggle fails +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-09: Multiple Consecutive Failures Show Multiple Toasts > should show separate toast for each failed operation +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-09: Multiple Consecutive Failures Show Multiple Toasts > should show separate toast for each failed operation +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-09: Multiple Consecutive Failures Show Multiple Toasts > should show separate toast for each failed operation +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-10: Optimistic Update Reverts on Error > should revert toggle state when mutation fails +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-10: Optimistic Update Reverts on Error > should revert toggle state when mutation fails +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-10: Optimistic Update Reverts on Error > should revert toggle state when mutation fails +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-10: Optimistic Update Reverts on Error > should revert CrowdSec state on start failure +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-10: Optimistic Update Reverts on Error > should revert CrowdSec state on start failure +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-10: Optimistic Update Reverts on Error > should revert CrowdSec state on start failure +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-10: Optimistic Update Reverts on Error > should revert CrowdSec state on stop failure +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-10: Optimistic Update Reverts on Error > should revert CrowdSec state on stop failure +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.errors.test.tsx > Security Error Handling Tests > EH-10: Optimistic Update Reverts on Error > should revert CrowdSec state on stop failure +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.errors.test.tsx +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.errors.test.tsx +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + + ✓ src/pages/__tests__/Security.errors.test.tsx (13 tests) 2364ms + ✓ should call toast.error() when toggle mutation fails  459ms + ✓ src/components/__tests__/CertificateStatusCard.test.tsx (24 tests) 412ms +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-01: Cerberus Disabled Banner > should not show banner when Cerberus is enabled +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-02: CrowdSec Card Active Status > should show "Enabled" when crowdsec.enabled=true +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-02: CrowdSec Card Active Status > should show "Enabled" when crowdsec.enabled=true +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-02: CrowdSec Card Active Status > should show "Enabled" when crowdsec.enabled=true +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-02: CrowdSec Card Active Status > should show running PID when CrowdSec is running +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-02: CrowdSec Card Active Status > should show running PID when CrowdSec is running +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-02: CrowdSec Card Active Status > should show running PID when CrowdSec is running +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-03: CrowdSec Card Disabled Status > should show "Disabled" when crowdsec.enabled=false +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-03: CrowdSec Card Disabled Status > should show "Disabled" when crowdsec.enabled=false +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-03: CrowdSec Card Disabled Status > should show "Disabled" when crowdsec.enabled=false +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-04: WAF (Coraza) Card Status > should show "Active" when waf.enabled=true +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-04: WAF (Coraza) Card Status > should show "Active" when waf.enabled=true +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-04: WAF (Coraza) Card Status > should show "Active" when waf.enabled=true +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-04: WAF (Coraza) Card Status > should show "Disabled" when waf.enabled=false +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-04: WAF (Coraza) Card Status > should show "Disabled" when waf.enabled=false +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-04: WAF (Coraza) Card Status > should show "Disabled" when waf.enabled=false +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-05: Rate Limiting Card Status > should show badge and text when rate_limit.enabled=true +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-05: Rate Limiting Card Status > should show badge and text when rate_limit.enabled=true +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-05: Rate Limiting Card Status > should show badge and text when rate_limit.enabled=true +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-05: Rate Limiting Card Status > should show "Disabled" badge when rate_limit.enabled=false +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-05: Rate Limiting Card Status > should show "Disabled" badge when rate_limit.enabled=false +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-05: Rate Limiting Card Status > should show "Disabled" badge when rate_limit.enabled=false +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-06: ACL Card Status > should show "Active" when acl.enabled=true +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-06: ACL Card Status > should show "Active" when acl.enabled=true +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-06: ACL Card Status > should show "Active" when acl.enabled=true +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-06: ACL Card Status > should show "Disabled" when acl.enabled=false +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-06: ACL Card Status > should show "Disabled" when acl.enabled=false +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-06: ACL Card Status > should show "Disabled" when acl.enabled=false +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-07: Layer Indicators > should display all layer indicators in correct order +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-07: Layer Indicators > should display all layer indicators in correct order +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-07: Layer Indicators > should display all layer indicators in correct order +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-08: Threat Protection Summaries > should display threat protection descriptions for each card +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-08: Threat Protection Summaries > should display threat protection descriptions for each card +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-08: Threat Protection Summaries > should display threat protection descriptions for each card +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-09: Card Order (Pipeline Sequence) > should maintain card order: CrowdSec → ACL → WAF → Rate Limiting +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-09: Card Order (Pipeline Sequence) > should maintain card order: CrowdSec → ACL → WAF → Rate Limiting +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-09: Card Order (Pipeline Sequence) > should maintain card order: CrowdSec → ACL → WAF → Rate Limiting +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-09: Card Order (Pipeline Sequence) > should maintain card order even after toggle +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-09: Card Order (Pipeline Sequence) > should maintain card order even after toggle +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-09: Card Order (Pipeline Sequence) > should maintain card order even after toggle +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + + ✓ src/pages/__tests__/Uptime.spec.tsx (11 tests) 1654ms + ✓ calls updateMonitor when toggling monitoring  342ms + ✓ opens configure modal and saves changes via updateMonitor  527ms +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-10: Toggle Switches Disabled When Cerberus Off > should disable all service toggles when Cerberus is disabled +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-10: Toggle Switches Disabled When Cerberus Off > should disable all service toggles when Cerberus is disabled +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.dashboard.test.tsx > Security Dashboard - Card Status Tests > SD-10: Toggle Switches Disabled When Cerberus Off > should enable toggles when Cerberus is enabled +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.dashboard.test.tsx +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.dashboard.test.tsx +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + + ✓ src/pages/__tests__/Security.dashboard.test.tsx (18 tests) 2369ms + ✓ should show documentation link in disabled banner  391ms + ✓ src/api/__tests__/dnsProviders.test.ts (30 tests) 25ms + ✓ src/components/__tests__/ProxyHostForm-dns.test.tsx (15 tests) 10335ms + ✓ detects *.example.com as wildcard  928ms + ✓ does not detect sub.example.com as wildcard  541ms + ✓ detects multiple wildcards in comma-separated list  999ms + ✓ detects wildcard at start of comma-separated list  813ms + ✓ shows DNS provider selector when wildcard domain entered  692ms + ✓ shows info alert explaining DNS-01 requirement  379ms + ✓ shows validation error on submit if wildcard without provider  1014ms + ✓ does not show DNS provider selector without wildcard  392ms + ✓ DNS provider selector is present for wildcard domains  421ms + ✓ clears DNS provider when switching to non-wildcard  776ms + ✓ preserves form state during wildcard domain edits  978ms + ✓ includes dns_provider_id null for non-wildcard domains  1221ms + ✓ prevents submission when wildcard present without DNS provider  990ms + ✓ src/hooks/__tests__/usePlugins.test.tsx (19 tests) 1008ms +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-02: Toggling Service Shows CerberusLoader Overlay > should show ConfigReloadOverlay with type="cerberus" when toggling +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-03: Starting CrowdSec Shows "Summoning the guardian..." > should show specific message for CrowdSec start operation +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-03: Starting CrowdSec Shows "Summoning the guardian..." > should show specific message for CrowdSec start operation +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + + ✓ src/components/__tests__/LoadingStates.security.test.tsx (41 tests) 787ms +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-03: Starting CrowdSec Shows "Summoning the guardian..." > should show specific message for CrowdSec start operation +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-04: Stopping CrowdSec Shows "Guardian rests..." > should show specific message for CrowdSec stop operation +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-04: Stopping CrowdSec Shows "Guardian rests..." > should show specific message for CrowdSec stop operation +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-04: Stopping CrowdSec Shows "Guardian rests..." > should show specific message for CrowdSec stop operation +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-05: WAF Config Operations Show Overlay > should show overlay when toggling WAF +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-05: WAF Config Operations Show Overlay > should show overlay when toggling WAF +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-05: WAF Config Operations Show Overlay > should show overlay when toggling WAF +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-06: Rate Limiting Toggle Shows Overlay > should show overlay when toggling rate limiting +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-06: Rate Limiting Toggle Shows Overlay > should show overlay when toggling rate limiting +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-06: Rate Limiting Toggle Shows Overlay > should show overlay when toggling rate limiting +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-07: ACL Toggle Shows Overlay > should show overlay when toggling ACL +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-07: ACL Toggle Shows Overlay > should show overlay when toggling ACL +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-07: ACL Toggle Shows Overlay > should show overlay when toggling ACL +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-08: Overlay Contains CerberusLoader Component > should render CerberusLoader animation within overlay +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-08: Overlay Contains CerberusLoader Component > should render CerberusLoader animation within overlay +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-08: Overlay Contains CerberusLoader Component > should render CerberusLoader animation within overlay +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-09: Overlay Blocks Interactions > should show overlay during toggle operation +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-09: Overlay Blocks Interactions > should show overlay during toggle operation +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-09: Overlay Blocks Interactions > should show overlay during toggle operation +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-09: Overlay Blocks Interactions > should have z-50 overlay that covers content +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-09: Overlay Blocks Interactions > should have z-50 overlay that covers content +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-09: Overlay Blocks Interactions > should have z-50 overlay that covers content +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-10: Overlay Disappears on Mutation Success > should remove overlay after toggle completes successfully +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-10: Overlay Disappears on Mutation Success > should remove overlay after toggle completes successfully +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-10: Overlay Disappears on Mutation Success > should remove overlay after toggle completes successfully +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-10: Overlay Disappears on Mutation Success > should not show overlay when mutation completes instantly +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-10: Overlay Disappears on Mutation Success > should not show overlay when mutation completes instantly +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + + ✓ src/data/__tests__/crowdsecPresets.test.ts (38 tests) 21ms +stdout | src/pages/__tests__/Security.loading.test.tsx > Security Loading Overlay Tests > LS-10: Overlay Disappears on Mutation Success > should not show overlay when mutation completes instantly +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.loading.test.tsx +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.loading.test.tsx +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + + ✓ src/pages/__tests__/Security.loading.test.tsx (12 tests) 2128ms + ✓ should show ConfigReloadOverlay with type="cerberus" when toggling  522ms +Not implemented: navigation to another Document +stderr | src/pages/__tests__/AuditLogs.test.tsx >  > handles export error +Export error: Error: Export failed + at /projects/Charon/frontend/src/pages/__tests__/AuditLogs.test.tsx:324:7 + at file:///projects/Charon/frontend/node_modules/@vitest/runner/dist/index.js:145:11 + at file:///projects/Charon/frontend/node_modules/@vitest/runner/dist/index.js:915:26 + at file:///projects/Charon/frontend/node_modules/@vitest/runner/dist/index.js:1243:20 + at new Promise () + at runWithTimeout (file:///projects/Charon/frontend/node_modules/@vitest/runner/dist/index.js:1209:10) + at file:///projects/Charon/frontend/node_modules/@vitest/runner/dist/index.js:1653:37 + at Traces.$ (file:///projects/Charon/frontend/node_modules/vitest/dist/chunks/traces.CCmnQaNT.js:142:27) + at trace (file:///projects/Charon/frontend/node_modules/vitest/dist/chunks/test.B8ej_ZHS.js:239:21) + at runTest (file:///projects/Charon/frontend/node_modules/@vitest/runner/dist/index.js:1653:12) + + ✓ src/pages/__tests__/AuditLogs.test.tsx (14 tests) 3286ms + ✓ toggles filter panel  369ms + ✓ clears all filters  327ms + ✓ closes detail modal  382ms + ✓ handles pagination  419ms + ✓ src/hooks/__tests__/useSecurity.test.tsx (19 tests) 1100ms + ✓ src/pages/__tests__/Plugins.test.tsx (18 tests) 1415ms +stdout | src/api/logs.test.ts > logs api > connects to live logs websocket and handles lifecycle events +Connecting to WebSocket: ws://localhost/api/v1/logs/live?level=error&source=cerberus +WebSocket connection established +WebSocket connection closed { code: 1000, reason: '', wasClean: true } + +stderr | src/api/logs.test.ts > logs api > connects to live logs websocket and handles lifecycle events +WebSocket error: Event { isTrusted: [Getter] } + +stdout | src/api/logs.test.ts > connectSecurityLogs > connects to cerberus logs websocket endpoint +Connecting to Cerberus logs WebSocket: ws://localhost/api/v1/cerberus/logs/ws? + +stdout | src/api/logs.test.ts > connectSecurityLogs > passes source filter to websocket url +Connecting to Cerberus logs WebSocket: ws://localhost/api/v1/cerberus/logs/ws?source=waf + +stdout | src/api/logs.test.ts > connectSecurityLogs > passes level filter to websocket url +Connecting to Cerberus logs WebSocket: ws://localhost/api/v1/cerberus/logs/ws?level=error + +stdout | src/api/logs.test.ts > connectSecurityLogs > passes ip filter to websocket url +Connecting to Cerberus logs WebSocket: ws://localhost/api/v1/cerberus/logs/ws?ip=192.168 + +stdout | src/api/logs.test.ts > connectSecurityLogs > passes host filter to websocket url +Connecting to Cerberus logs WebSocket: ws://localhost/api/v1/cerberus/logs/ws?host=example.com + +stdout | src/api/logs.test.ts > connectSecurityLogs > passes blocked_only filter to websocket url +Connecting to Cerberus logs WebSocket: ws://localhost/api/v1/cerberus/logs/ws?blocked_only=true + +stdout | src/api/logs.test.ts > connectSecurityLogs > receives and parses security log entries +Connecting to Cerberus logs WebSocket: ws://localhost/api/v1/cerberus/logs/ws? +Cerberus logs WebSocket connection established + +stdout | src/api/logs.test.ts > connectSecurityLogs > receives blocked security log entries +Connecting to Cerberus logs WebSocket: ws://localhost/api/v1/cerberus/logs/ws? +Cerberus logs WebSocket connection established + +stdout | src/api/logs.test.ts > connectSecurityLogs > handles onOpen callback +Connecting to Cerberus logs WebSocket: ws://localhost/api/v1/cerberus/logs/ws? +Cerberus logs WebSocket connection established + +stdout | src/api/logs.test.ts > connectSecurityLogs > handles onError callback +Connecting to Cerberus logs WebSocket: ws://localhost/api/v1/cerberus/logs/ws? + +stderr | src/api/logs.test.ts > connectSecurityLogs > handles onError callback +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } + +stdout | src/api/logs.test.ts > connectSecurityLogs > handles onClose callback +Connecting to Cerberus logs WebSocket: ws://localhost/api/v1/cerberus/logs/ws? +Cerberus logs WebSocket closed { code: 1000, reason: '', wasClean: true } + +stdout | src/api/logs.test.ts > connectSecurityLogs > returns disconnect function that closes websocket +Connecting to Cerberus logs WebSocket: ws://localhost/api/v1/cerberus/logs/ws? +Cerberus logs WebSocket connection established +Cerberus logs WebSocket closed { code: 1000, reason: '', wasClean: true } + +stdout | src/api/logs.test.ts > connectSecurityLogs > handles JSON parse errors gracefully +Connecting to Cerberus logs WebSocket: ws://localhost/api/v1/cerberus/logs/ws? +Cerberus logs WebSocket connection established + +stdout | src/api/logs.test.ts > connectSecurityLogs > uses wss protocol when on https +Connecting to Cerberus logs WebSocket: wss://secure.example.com/api/v1/cerberus/logs/ws? + +stdout | src/api/logs.test.ts > connectSecurityLogs > combines multiple filters in websocket url +Connecting to Cerberus logs WebSocket: ws://localhost/api/v1/cerberus/logs/ws?source=waf&level=warn&ip=10.0.0&host=example.com&blocked_only=true + + ✓ src/api/logs.test.ts (19 tests) 21ms + ✓ src/hooks/__tests__/useSecurityHeaders.test.tsx (15 tests) 811ms + ✓ src/hooks/__tests__/useImport.test.tsx (8 tests) 562ms + ✓ src/pages/__tests__/SecurityHeaders.test.tsx (10 tests) 1665ms + ✓ should open create form dialog  501ms + ✓ should delete profile with backup  392ms + ✓ src/components/ui/__tests__/DataTable.test.tsx (19 tests) 1247ms + ✓ handles row selection - single row  343ms + ✓ src/components/__tests__/SecurityHeaderProfileForm.test.tsx (17 tests) 2298ms + ✓ should submit form with valid data  383ms + ✓ should calculate security score on form changes  556ms + ✓ src/components/__tests__/SecurityNotificationSettingsModal.test.tsx (13 tests) 1900ms + ✓ submits updated settings  584ms + ✓ handles email recipients input  447ms + ✓ src/components/__tests__/Layout.test.tsx (16 tests) 1431ms + ✓ renders all navigation items  542ms +stdout | src/pages/__tests__/Security.spec.tsx > Security page > renders per-service toggles and calls updateSetting on change +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.spec.tsx > Security page > calls updateSetting when toggling ACL +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.spec.tsx > Security page > calls updateSetting when toggling ACL +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.spec.tsx > Security page > calls updateSetting when toggling ACL +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.spec.tsx > Security page > calls start/stop endpoints for CrowdSec via toggle +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.spec.tsx > Security page > calls start/stop endpoints for CrowdSec via toggle +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.spec.tsx > Security page > calls start/stop endpoints for CrowdSec via toggle +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.spec.tsx > Security page > calls start/stop endpoints for CrowdSec via toggle +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.spec.tsx > Security page > calls start/stop endpoints for CrowdSec via toggle +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.spec.tsx > Security page > calls start/stop endpoints for CrowdSec via toggle +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.spec.tsx > Security page > disables service toggles when cerberus is off +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.spec.tsx > Security page > disables service toggles when cerberus is off +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + +stdout | src/pages/__tests__/Security.spec.tsx > Security page > displays correct WAF threat protection summary when enabled +Connecting to Cerberus logs WebSocket: ws://localhost:3000/api/v1/cerberus/logs/ws? + +stdout | src/pages/__tests__/Security.spec.tsx +Cerberus logs WebSocket closed { code: 1006, reason: '', wasClean: false } +security log viewer disconnected + +stderr | src/pages/__tests__/Security.spec.tsx +Cerberus logs WebSocket error: Event { isTrusted: [Getter] } +security log viewer error: Event { isTrusted: [Getter] } + + ✓ src/pages/__tests__/SMTPSettings.test.tsx (10 tests) 3944ms + ✓ saves SMTP settings successfully  1119ms + ✓ sends test email  429ms + ✓ surfaces backend validation errors on save  621ms + ✓ disables test connection until required fields are set and shows error toast on failure  731ms + ✓ handles test email failures and keeps input value intact  584ms + ✓ src/pages/__tests__/Security.spec.tsx (6 tests) 1168ms + ✓ calls updateSetting when toggling ACL  315ms + ✓ calls start/stop endpoints for CrowdSec via toggle  395ms + ✓ src/hooks/__tests__/useCredentials.test.tsx (16 tests) 232ms + ✓ src/hooks/__tests__/useRemoteServers.test.tsx (10 tests) 615ms + ✓ src/pages/__tests__/Login.overlay.audit.test.tsx (7 tests) 3282ms + ✓ shows coin-themed overlay during login  785ms + ✓ ATTACK: rapid fire login attempts are blocked by overlay  668ms + ✓ clears overlay on login error  414ms + ✓ ATTACK: XSS in login credentials does not break overlay  465ms + ✓ ATTACK: network timeout does not leave overlay stuck  446ms + ✓ src/api/__tests__/security.test.ts (16 tests) 24ms + ✓ src/api/auditLogs.test.ts (14 tests) 20ms + ✓ src/hooks/__tests__/useNotifications.test.tsx (9 tests) 540ms + ✓ src/components/__tests__/CSPBuilder.test.tsx (13 tests) 1596ms + ✓ should add a directive  329ms + ✓ src/pages/__tests__/EncryptionManagement.test.tsx (14 tests) 1765ms + ✓ shows confirmation dialog when rotation is triggered  542ms + ✓ executes rotation when confirmed  321ms + ✓ src/components/__tests__/WebSocketStatusCard.test.tsx (8 tests) 661ms + ✓ should render loading state  351ms + ✓ src/hooks/__tests__/useManualChallenge.test.tsx (11 tests) 247ms + ✓ src/components/__tests__/ImportReviewTable.test.tsx (9 tests) 888ms + ✓ src/pages/__tests__/RateLimiting.spec.tsx (9 tests) 589ms + ✓ src/hooks/__tests__/useProxyHosts.test.tsx (8 tests) 562ms + ✓ src/api/__tests__/users.test.ts (10 tests) 22ms + ✓ src/components/__tests__/DNSDetectionResult.test.tsx (10 tests) 477ms + ✓ src/api/__tests__/manualChallenge.test.ts (14 tests) 20ms +stdout | src/api/__tests__/logs-websocket.test.ts > logs API - connectLiveLogs > creates WebSocket connection with correct URL +Connecting to WebSocket: ws://localhost:8080/api/v1/logs/live? + +stdout | src/api/__tests__/logs-websocket.test.ts > logs API - connectLiveLogs > uses wss protocol when page is https +Connecting to WebSocket: wss://example.com/api/v1/logs/live? + +stdout | src/api/__tests__/logs-websocket.test.ts > logs API - connectLiveLogs > includes filters in query parameters +Connecting to WebSocket: ws://localhost:8080/api/v1/logs/live?level=error&source=waf + +stdout | src/api/__tests__/logs-websocket.test.ts > logs API - connectLiveLogs > calls onMessage callback when message is received +Connecting to WebSocket: ws://localhost:8080/api/v1/logs/live? + +stdout | src/api/__tests__/logs-websocket.test.ts > logs API - connectLiveLogs > handles JSON parse errors gracefully +Connecting to WebSocket: ws://localhost:8080/api/v1/logs/live? + +stdout | src/api/__tests__/logs-websocket.test.ts > logs API - connectLiveLogs > returns a close function that closes the WebSocket +Connecting to WebSocket: ws://localhost:8080/api/v1/logs/live? + +stdout | src/api/__tests__/logs-websocket.test.ts > logs API - connectLiveLogs > does not throw when closing already closed connection +Connecting to WebSocket: ws://localhost:8080/api/v1/logs/live? + +stdout | src/api/__tests__/logs-websocket.test.ts > logs API - connectLiveLogs > handles missing optional callbacks +Connecting to WebSocket: ws://localhost:8080/api/v1/logs/live? +Connecting to WebSocket: ws://localhost:8080/api/v1/logs/live? + +stderr | src/api/__tests__/logs-websocket.test.ts > logs API - connectLiveLogs > handles missing optional callbacks +WebSocket error: Event { isTrusted: [Getter] } + +stdout | src/api/__tests__/logs-websocket.test.ts > logs API - connectLiveLogs > processes multiple messages in sequence +Connecting to WebSocket: ws://localhost:8080/api/v1/logs/live? + +stdout | src/api/__tests__/logs-websocket.test.ts +WebSocket connection closed { code: 1000, reason: '', wasClean: true } + + ✓ src/api/__tests__/logs-websocket.test.ts (11 tests | 2 skipped) 41ms + ✓ src/hooks/__tests__/useDNSDetection.test.tsx (10 tests) 704ms +stderr | src/pages/__tests__/ProxyHosts-coverage-isolated.test.tsx > ProxyHosts page - coverage targets (isolated) > bulk apply merges host data and calls updateHost +Each child in a list should have a unique "key" prop. + +Check the render method of `Primitive.p`. It was passed a child from ProxyHosts. See https://react.dev/link/warning-keys for more information. + + ✓ src/pages/__tests__/AcceptInvite.test.tsx (8 tests) 1991ms + ✓ shows password mismatch error  508ms + ✓ submits form and shows success  739ms + ✓ shows error on submit failure  546ms + ✓ src/pages/__tests__/ProxyHosts-coverage-isolated.test.tsx (3 tests) 2312ms + ✓ renders SSL staging badge, websocket badge  652ms + ✓ opens domain link in new window when linkBehavior is new_window  333ms + ✓ bulk apply merges host data and calls updateHost  1325ms + ✓ src/components/__tests__/RemoteServerForm.test.tsx (9 tests) 1166ms + ✓ submits form with correct data  458ms + ✓ src/api/__tests__/settings.test.ts (16 tests) 19ms + ✓ src/components/ui/__tests__/Skeleton.test.tsx (18 tests) 338ms +stderr | src/pages/__tests__/ProxyHosts-progress.test.tsx > ProxyHosts progress apply > shows progress when applying multiple ACLs +Each child in a list should have a unique "key" prop. + +Check the render method of `Primitive.p`. It was passed a child from ProxyHosts. See https://react.dev/link/warning-keys for more information. + + ✓ src/api/notifications.test.ts (5 tests) 12ms + ✓ src/pages/__tests__/ProxyHosts-progress.test.tsx (2 tests) 2093ms + ✓ shows progress when applying multiple ACLs  1936ms +stderr | src/pages/__tests__/ProxyHosts-bulk-apply.test.tsx > ProxyHosts - Bulk Apply Settings > shows Bulk Apply button when hosts selected and opens modal +Each child in a list should have a unique "key" prop. + +Check the render method of `Primitive.p`. It was passed a child from ProxyHosts. See https://react.dev/link/warning-keys for more information. + + ✓ src/data/__tests__/securityPresets.test.ts (24 tests) 14ms + ✓ src/pages/__tests__/ProxyHosts-bulk-apply.test.tsx (3 tests) 4329ms + ✓ shows Bulk Apply button when hosts selected and opens modal  1784ms + ✓ applies selected settings to all selected hosts by calling updateProxyHost merged payload  1684ms + ✓ cancels bulk apply modal when Cancel clicked  858ms + ✓ src/components/ui/__tests__/Alert.test.tsx (18 tests) 814ms + ✓ renders with default variant  331ms + ✓ src/hooks/__tests__/useAccessLists.test.tsx (6 tests) 352ms + ✓ src/components/ui/__tests__/StatsCard.test.tsx (14 tests) 397ms + ✓ src/components/__tests__/NotificationCenter.test.tsx (6 tests) 777ms + ✓ src/api/__tests__/accessLists.test.ts (7 tests) 10ms + ✓ src/components/ui/__tests__/Input.test.tsx (16 tests) 463ms + ✓ src/components/dialogs/__tests__/ImportSuccessModal.test.tsx (13 tests) 206ms + ✓ src/hooks/__tests__/useProxyHosts-bulk.test.tsx (5 tests) 515ms + ✓ src/components/__tests__/CertificateList.test.tsx (4 tests) 504ms + ✓ deletes custom certificate when confirmed  366ms + ✓ src/hooks/__tests__/useDocker.test.tsx (8 tests) 2288ms + ✓ handles API errors  1011ms + ✓ extracts details from 503 service unavailable error  1014ms + ✓ src/pages/__tests__/Setup.test.tsx (5 tests) 965ms + ✓ submits form successfully  487ms + ✓ displays error on submission failure  367ms + ✓ src/components/__tests__/SecurityScoreDisplay.test.tsx (13 tests) 207ms + ✓ src/api/__tests__/crowdsec.test.ts (9 tests) 15ms + ✓ src/api/__tests__/dnsDetection.test.ts (8 tests) 15ms + ✓ src/api/__tests__/remoteServers.test.ts (9 tests) 8ms + ✓ src/api/__tests__/notifications.test.ts (4 tests) 13ms + ✓ src/api/__tests__/uptime.test.ts (8 tests) 11ms +stderr | src/pages/__tests__/ProxyHosts-bulk-apply-progress.test.tsx > ProxyHosts - Bulk Apply progress UI > shows applying progress while updateProxyHost resolves +Each child in a list should have a unique "key" prop. + +Check the render method of `Primitive.p`. It was passed a child from ProxyHosts. See https://react.dev/link/warning-keys for more information. + + ✓ src/pages/__tests__/ProxyHosts-bulk-apply-progress.test.tsx (1 test) 975ms + ✓ shows applying progress while updateProxyHost resolves  971ms + ✓ src/pages/__tests__/CrowdSecConfig.test.tsx (3 tests) 619ms + ✓ shows info banner directing to Security Dashboard  308ms +stderr | src/pages/__tests__/ProxyHosts-bulk-apply-all-settings.test.tsx > ProxyHosts - Bulk Apply all settings coverage > renders all bulk apply setting labels and allows toggling +Each child in a list should have a unique "key" prop. + +Check the render method of `Primitive.p`. It was passed a child from ProxyHosts. See https://react.dev/link/warning-keys for more information. + + ✓ src/hooks/__tests__/useDomains.test.tsx (6 tests) 409ms + ✓ src/pages/__tests__/ProxyHosts-bulk-apply-all-settings.test.tsx (1 test) 1568ms + ✓ renders all bulk apply setting labels and allows toggling  1565ms + ✓ src/pages/__tests__/Login.test.tsx (4 tests) 501ms + ✓ src/components/__tests__/LoadingStates-overlays.test.tsx (12 tests) 509ms + ✓ src/api/users.test.ts (3 tests) 12ms + ✓ src/components/__tests__/ProxyHostForm-uptime.test.tsx (1 test) 1571ms + ✓ submits host and requests uptime sync when Add Uptime is checked  1569ms + ✓ src/components/__tests__/AccessListSelector.test.tsx (3 tests) 294ms + ✓ src/api/__tests__/websocket.test.ts (6 tests) 12ms + ✓ src/pages/__tests__/DNS.test.tsx (7 tests) 509ms + ✓ src/api/__tests__/docker.test.ts (6 tests) 10ms + ✓ src/api/__tests__/proxyHosts-bulk.test.ts (5 tests) 11ms + ✓ src/api/__tests__/proxyHosts.test.ts (6 tests) 12ms + ✓ src/components/__tests__/PasswordStrengthMeter.test.tsx (4 tests) 62ms + ✓ src/hooks/__tests__/useLanguage.test.tsx (5 tests) 61ms + ✓ src/api/__tests__/system.test.ts (5 tests) 6ms + ✓ src/pages/__tests__/Dashboard.test.tsx (2 tests) 168ms + ✓ src/pages/__tests__/ImportCrowdSec.test.tsx (2 tests) 343ms + ✓ src/__tests__/i18n.test.ts (7 tests) 30ms + ✓ src/utils/__tests__/compareHosts.test.ts (4 tests) 4ms + ✓ src/components/__tests__/LanguageSelector.test.tsx (3 tests) 438ms + ✓ renders language selector with all options  379ms + ✓ src/api/__tests__/certificates.test.ts (3 tests) 9ms + ✓ src/pages/__tests__/ImportCrowdSec.spec.tsx (1 test) 214ms + ✓ src/utils/__tests__/passwordStrength.test.ts (6 tests) 6ms + ✓ src/api/__tests__/logs.http.test.ts (2 tests) 6ms + ✓ src/api/__tests__/domains.test.ts (3 tests) 7ms + ✓ src/utils/__tests__/toast.test.ts (2 tests) 8ms + ✓ src/api/__tests__/backups.test.ts (4 tests) 7ms + ✓ src/components/__tests__/SystemStatus.test.tsx (1 test) 35ms + ✓ src/hooks/__tests__/useAuth.test.tsx (2 tests) 36ms + ✓ src/api/featureFlags.test.ts (2 tests) 8ms + ✓ src/api/__tests__/setup.test.ts (2 tests) 8ms + ✓ src/test/setup.spec.ts (2 tests) 3ms + ✓ src/hooks/__tests__/useTheme.test.tsx (1 test) 26ms + + Test Files  125 passed (125) + Tests  1488 passed | 2 skipped (1490) + Start at  16:26:27 + Duration  123.58s (transform 5.09s, setup 24.75s, import 41.19s, tests 174.41s, environment 86.94s) + + % Coverage report from v8 +-------------------|---------|----------|---------|---------|------------------- +File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s +-------------------|---------|----------|---------|---------|------------------- +All files | 85.04 | 78.4 | 79.3 | 85.76 | + src | 100 | 100 | 100 | 100 | + i18n.ts | 100 | 100 | 100 | 100 | + src/api | 89.82 | 74.77 | 84.14 | 89.12 | + accessLists.ts | 100 | 100 | 100 | 100 | + auditLogs.ts | 100 | 87.5 | 100 | 100 | 73,76,131,133 + backups.ts | 100 | 100 | 100 | 100 | + certificates.ts | 100 | 100 | 100 | 100 | + client.ts | 41.17 | 8.33 | 25 | 41.17 | 18-21,36,41,44-49 + ...Enrollment.ts | 80 | 100 | 66.66 | 80 | 50 + credentials.ts | 0 | 100 | 0 | 0 | 53-147 + crowdsec.ts | 81.81 | 100 | 72.72 | 81.81 | 114-135 + dnsDetection.ts | 100 | 100 | 100 | 100 | + dnsProviders.ts | 100 | 100 | 100 | 100 | + docker.ts | 100 | 100 | 100 | 100 | + domains.ts | 100 | 100 | 100 | 100 | + featureFlags.ts | 100 | 100 | 100 | 100 | + logs.ts | 100 | 80.48 | 100 | 100 | 67-73,258 + ...lChallenge.ts | 100 | 100 | 100 | 100 | + notifications.ts | 100 | 50 | 100 | 100 | 96-166 + presets.ts | 100 | 100 | 100 | 100 | + proxyHosts.ts | 91.3 | 50 | 87.5 | 91.3 | 174-181 + remoteServers.ts | 100 | 100 | 100 | 100 | + security.ts | 100 | 100 | 100 | 100 | + ...ityHeaders.ts | 10 | 100 | 10 | 10 | 89-186 + settings.ts | 100 | 100 | 100 | 100 | + setup.ts | 100 | 100 | 100 | 100 | + system.ts | 84.61 | 100 | 80 | 84.61 | 70-71 + uptime.ts | 90 | 100 | 85.71 | 90 | 88-89 + users.ts | 100 | 100 | 100 | 100 | + websocket.ts | 100 | 100 | 100 | 100 | + src/components | 77.29 | 76.57 | 69.46 | 78.6 | + ...tSelector.tsx | 85.71 | 76.92 | 80 | 83.33 | 22 + CSPBuilder.tsx | 93.9 | 78.94 | 100 | 94.66 | 93,120,151-155 + ...icateList.tsx | 75.43 | 60 | 66.66 | 79.24 | ...2,61-65,92-103 + ...tatusCard.tsx | 93.75 | 88.46 | 100 | 95.83 | 62 + ...alManager.tsx | 50 | 48.31 | 36.11 | 51.56 | ...83-497,520-582 + ...ionResult.tsx | 94.73 | 94.11 | 100 | 94.73 | 76 + ...rSelector.tsx | 100 | 100 | 100 | 100 | + ...viewTable.tsx | 85.71 | 68.91 | 80 | 86.66 | ...,75,85,166,348 + ...eSelector.tsx | 100 | 100 | 100 | 100 | + Layout.tsx | 84.31 | 89.23 | 66.66 | 83.67 | ...51,274,315-334 + ...LogViewer.tsx | 90.47 | 85.71 | 96.87 | 93.04 | ...81-182,267-270 + ...ingStates.tsx | 66.66 | 85.71 | 50 | 66.66 | 2-8,289-323 + ...ionCenter.tsx | 100 | 88 | 100 | 100 | 36,42,91 + ...ngthMeter.tsx | 88.88 | 83.33 | 100 | 88.23 | 21,30 + ...cyBuilder.tsx | 32.81 | 19.35 | 20.83 | 35 | ...79,191,205-249 + ...yHostForm.tsx | 76.07 | 73.6 | 61.61 | 78.81 | ...1105,1129-1189 + ...erverForm.tsx | 84.21 | 87.3 | 72.72 | 88.88 | 59-60,151-162 + ...ofileForm.tsx | 60.97 | 90.66 | 48.14 | 58.97 | ...68,308,342-430 + ...ingsModal.tsx | 90.47 | 83.33 | 83.33 | 90 | 153-170 + ...reDisplay.tsx | 100 | 87.8 | 100 | 100 | 143-151,208 + SystemStatus.tsx | 100 | 100 | 100 | 100 | + ThemeToggle.tsx | 100 | 50 | 100 | 100 | 8-9 + ...tatusCard.tsx | 100 | 94.28 | 100 | 100 | 125-126 + ...onents/dialogs | 100 | 87.8 | 100 | 100 | + ...nupDialog.tsx | 100 | 80 | 100 | 100 | 76-87 + ...cessModal.tsx | 100 | 92.3 | 100 | 100 | 60,68 + .../dns-providers | 90 | 74.28 | 94.11 | 91.76 | + ...Challenge.tsx | 90 | 74.28 | 94.11 | 91.76 | ...80,196,243-244 + ...ponents/layout | 100 | 100 | 100 | 100 | + PageShell.tsx | 100 | 100 | 100 | 100 | + src/components/ui | 97.35 | 93.93 | 92.06 | 97.31 | + Alert.tsx | 100 | 85.71 | 100 | 100 | 70-71 + Badge.tsx | 100 | 100 | 100 | 100 | + Button.tsx | 100 | 90 | 100 | 100 | 103 + Card.tsx | 100 | 100 | 100 | 100 | + Checkbox.tsx | 100 | 100 | 100 | 100 | + DataTable.tsx | 100 | 97.22 | 100 | 100 | 141,212 + Dialog.tsx | 100 | 100 | 100 | 100 | + EmptyState.tsx | 100 | 90.9 | 100 | 100 | 62 + Input.tsx | 100 | 100 | 100 | 100 | + Label.tsx | 100 | 100 | 100 | 100 | + NativeSelect.tsx | 100 | 50 | 100 | 100 | 19 + Progress.tsx | 100 | 57.14 | 100 | 100 | 47-49 + Select.tsx | 91.66 | 85.71 | 71.42 | 91.66 | 123,161 + Skeleton.tsx | 100 | 100 | 100 | 100 | + StatsCard.tsx | 100 | 100 | 100 | 100 | + Switch.tsx | 100 | 100 | 100 | 100 | + Tabs.tsx | 70 | 100 | 0 | 70 | 11,27,47 + Textarea.tsx | 100 | 50 | 100 | 100 | 18 + Tooltip.tsx | 100 | 100 | 100 | 100 | + index.ts | 0 | 0 | 0 | 0 | + src/context | 92.59 | 66.66 | 77.77 | 96.15 | + ...ntextValue.ts | 100 | 100 | 100 | 100 | + ...geContext.tsx | 100 | 100 | 100 | 100 | + ...ntextValue.ts | 100 | 100 | 100 | 100 | + ThemeContext.tsx | 83.33 | 50 | 60 | 90.9 | 18 + ...ntextValue.ts | 100 | 100 | 100 | 100 | + src/data | 100 | 100 | 100 | 100 | + ...secPresets.ts | 100 | 100 | 100 | 100 | + ...ityPresets.ts | 100 | 100 | 100 | 100 | + src/hooks | 95.65 | 83.96 | 93.53 | 95.87 | + ...ccessLists.ts | 80.76 | 100 | 73.68 | 80.76 | 21,37,54,69,79 + useAuditLogs.ts | 42.85 | 20 | 38.46 | 42.85 | 16-19,48-72 + useAuth.ts | 100 | 100 | 100 | 100 | + ...rtificates.ts | 100 | 100 | 100 | 100 | + ...Enrollment.ts | 87.5 | 100 | 83.33 | 87.5 | 24 + ...redentials.ts | 100 | 100 | 100 | 100 | + ...SDetection.ts | 100 | 100 | 100 | 100 | + ...SProviders.ts | 100 | 100 | 100 | 100 | + useDocker.ts | 100 | 90.9 | 100 | 100 | 19 + useDomains.ts | 100 | 100 | 100 | 100 | + useEncryption.ts | 100 | 100 | 100 | 100 | + useImport.ts | 96.96 | 81.81 | 100 | 100 | 41-57,94-95 + useLanguage.ts | 100 | 100 | 100 | 100 | + ...lChallenge.ts | 100 | 100 | 100 | 100 | + ...ifications.ts | 100 | 87.5 | 100 | 100 | 44 + usePlugins.ts | 100 | 100 | 100 | 100 | + useProxyHosts.ts | 100 | 91.66 | 100 | 100 | 39 + ...oteServers.ts | 100 | 100 | 100 | 100 | + useSecurity.ts | 100 | 100 | 100 | 100 | + ...ityHeaders.ts | 97.14 | 100 | 96.15 | 97.14 | 85 + useTheme.ts | 100 | 100 | 100 | 100 | + ...cketStatus.ts | 100 | 100 | 100 | 100 | + src/locales/de | 0 | 0 | 0 | 0 | + translation.json | 0 | 0 | 0 | 0 | + src/locales/en | 0 | 0 | 0 | 0 | + translation.json | 0 | 0 | 0 | 0 | + src/locales/es | 0 | 0 | 0 | 0 | + translation.json | 0 | 0 | 0 | 0 | + src/locales/fr | 0 | 0 | 0 | 0 | + translation.json | 0 | 0 | 0 | 0 | + src/locales/zh | 0 | 0 | 0 | 0 | + translation.json | 0 | 0 | 0 | 0 | + src/pages | 83.18 | 76.96 | 75.18 | 83.82 | + AcceptInvite.tsx | 87.23 | 86.2 | 85.71 | 86.95 | ...3,56-57,66,113 + AuditLogs.tsx | 84.37 | 88.37 | 67.85 | 86.44 | ...00-328,372-383 + ...SecConfig.tsx | 81.71 | 75.33 | 69.23 | 82.53 | ...1209,1228-1235 + DNS.tsx | 100 | 100 | 100 | 100 | + Dashboard.tsx | 75.6 | 63.63 | 62.5 | 80 | 15,56-57,65-69 + ...anagement.tsx | 83.67 | 77.04 | 81.25 | 85.1 | 150,172-180,438 + ...tCrowdSec.tsx | 81.48 | 50 | 87.5 | 88 | 24-25,45 + Login.tsx | 96.77 | 90 | 83.33 | 96.77 | 105 + Plugins.tsx | 60.37 | 77.41 | 68.75 | 58.82 | ...01,262-269,381 + ProxyHosts.tsx | 94.41 | 82.84 | 96.59 | 95.23 | ...,855,1138-1139 + RateLimiting.tsx | 90.62 | 68.57 | 70 | 90.62 | 44,170-180 + SMTPSettings.tsx | 88.46 | 64.28 | 80 | 88.46 | 72,88,162-186,207 + Security.tsx | 84.25 | 93.1 | 66.66 | 84.61 | ...88,543,596-612 + ...tyHeaders.tsx | 64.61 | 77.08 | 55.17 | 64.51 | ...99-231,287-324 + Setup.tsx | 97.5 | 95.83 | 100 | 97.43 | 51 + ...mSettings.tsx | 82.35 | 72.85 | 73.07 | 81.48 | ...91,303,433-548 + Uptime.tsx | 65.04 | 60.37 | 59.64 | 62.16 | ...74,503-512,569 + UsersPage.tsx | 76.92 | 61.79 | 70.45 | 78.37 | ...85,496-497,631 + WafConfig.tsx | 89.61 | 83.11 | 87.87 | 91.78 | ...85,358,416,447 + src/test-utils | 100 | 100 | 100 | 100 | + ...eryClient.tsx | 100 | 100 | 100 | 100 | + src/testUtils | 100 | 100 | 100 | 100 | + ...kProxyHost.ts | 100 | 100 | 100 | 100 | + src/utils | 96.49 | 83.33 | 100 | 97.4 | + cn.ts | 100 | 100 | 100 | 100 | + compareHosts.ts | 100 | 63.63 | 100 | 100 | 12-28 + ...dsecExport.ts | 100 | 90.9 | 100 | 100 | 9 + ...rdStrength.ts | 91.89 | 94.73 | 100 | 91.89 | 70-72 + ...stsHelpers.ts | 98.03 | 96.15 | 100 | 98 | 60 + toast.ts | 100 | 100 | 100 | 100 | + validation.ts | 93.54 | 75 | 100 | 100 | 30,47 +-------------------|---------|----------|---------|---------|-------------------