# Security Coverage QA Test Plan ## Overview This document outlines the comprehensive test plan to achieve **100% code coverage** for all security-related functionality in Charon. Security is a critical selling point for a reverse proxy, and this plan ensures all security code paths are thoroughly tested. **Target Coverage:** 100% for all security-related code **Minimum Threshold:** 85% overall (enforced by pre-commit) --- ## Current Coverage Status (Updated 2025-12-12) ### Coverage Summary | Package | Coverage | Status | |---------|----------|--------| | `cerberus` | **100.0%** | ✅ Complete | | `middleware` | **98.1%** | ✅ Complete | | `handlers` | **84.1%** | ✅ Above threshold | | `crowdsec` | **82.1%** | 🟡 Below threshold | | `services` | **82.0%** | 🟡 Below threshold | ### Tests Added This Session #### CrowdSec Handler Tests (`crowdsec_handler_test.go`) - ✅ Console enrollment tests (disabled, service unavailable, invalid payload, success, missing agent name) - ✅ Console status tests (disabled, unavailable, success, after enrollment) - ✅ `isConsoleEnrollmentEnabled` tests (DB variants, env variants, defaults) - ✅ `actorFromContext` tests (with userID, numeric userID, no user) - ✅ `ttlRemainingSeconds` tests (normal, expired, zero time, zero TTL) - ✅ `hubEndpoints` tests (nil, deduplicates, multiple, skips empty) #### CrowdSec Package Tests - ✅ `ExecuteWithEnv` - 100% coverage - ✅ `formatEnv` - 100% coverage - ✅ `hubHTTPError.Error()` - 100% coverage - ✅ `hubHTTPError.Unwrap()` - 100% coverage - ✅ `hubHTTPError.CanFallback()` - 100% coverage - ✅ `deriveKey` - 100% coverage - ✅ `normalizeEnrollmentKey` - 100% coverage - ✅ `redactSecret` - 100% coverage - ✅ Encryption round-trip tests #### Cerberus Middleware Tests (`cerberus_test.go`) - ✅ `IsEnabled` - All branches covered (config, DB setting, legacy setting, modes) - ✅ `Middleware` - 100% coverage achieved - Disabled state (skip checks) - WAF enabled (metrics tracking) - ACL enabled - no lists - ACL enabled - disabled list - ACL enabled - blocked by blacklist - CrowdSec local mode (metrics tracking) #### Access List Handler Tests - ✅ `SetGeoIPService` - 100% coverage (was 0%) - ✅ `TestIP` - 100% coverage (was 89.5%) - ✅ Internal error path covered #### Security Service Tests - ✅ `DeleteRuleSet` not found case - ✅ `ListDecisions` unlimited and limited variants - ✅ `LogDecision` nil and prefilled UUID - ✅ `ListRuleSets` empty database - ✅ `Upsert` invalid CrowdSec mode variants ### Backend Security Handlers | File | Function | Current | Target | Priority | |------|----------|---------|--------|----------| | `console_enroll.go` | `ExecuteWithEnv` | **0.0%** | 100% | 🔴 CRITICAL | | `console_enroll.go` | `formatEnv` | **0.0%** | 100% | 🔴 CRITICAL | | `console_enroll.go` | `Status` | **0.0%** | 100% | 🔴 CRITICAL | | `console_enroll.go` | `TTL` | **0.0%** | 100% | 🔴 CRITICAL | | `console_enroll.go` | `Unwrap` | **0.0%** | 100% | 🔴 CRITICAL | | `hub_sync.go` | `emptyDir` | **30.8%** | 100% | 🔴 CRITICAL | | `hub_sync.go` | `backupExisting` | **36.4%** | 100% | 🔴 CRITICAL | | `hub_sync.go` | `extractTarGz` | **65.9%** | 100% | 🟡 HIGH | | `hub_sync.go` | `Pull` | **65.4%** | 100% | 🟡 HIGH | ### Backend Services Package | File | Function | Current | Target | Priority | |------|----------|---------|--------|----------| | `access_list_service.go` | `testGeoIP` | **9.1%** | 100% | 🔴 CRITICAL | | `security_service.go` | `GenerateBreakGlassToken` | **73.7%** | 100% | 🟡 HIGH | | `security_service.go` | `DeleteRuleSet` | **75.0%** | 100% | 🟡 HIGH | | `security_service.go` | `ListRuleSets` | **75.0%** | 100% | 🟡 HIGH | ### Backend Cerberus Package | File | Function | Current | Target | Priority | |------|----------|---------|--------|----------| | `cerberus.go` | `Middleware` | **81.8%** | 100% | 🟢 MEDIUM | ### Frontend Security Components | File | Current | Target | Priority | |------|---------|--------|----------| | `api/consoleEnrollment.ts` | **0.0%** | 100% | 🔴 CRITICAL | | `crowdsec.ts` | **81.81%** | 100% | 🟢 MEDIUM | | `pages/Security.tsx` | **82.35%** | 100% | 🟢 MEDIUM | | `pages/WafConfig.tsx` | **89.47%** | 100% | 🟢 MEDIUM | | `pages/CrowdSecConfig.tsx` | **85.96%** | 100% | 🟢 MEDIUM | | `pages/RateLimiting.tsx` | **90.32%** | 100% | 🟢 MEDIUM | --- ## Test Cases by Component ### 1. CrowdSec Console Enrollment Tests (Handler) #### `ConsoleEnroll` (0% → 100%) ```go // TEST CASE 1.1: Successful enrollment // Input: Valid enrollment key, valid accept_tos // Expected: 200 OK, enrollment initiated, correct response body // Validates: Happy path enrollment flow // TEST CASE 1.2: Missing enrollment key // Input: Empty enrollment_key field // Expected: 400 Bad Request with validation error // Validates: Input validation // TEST CASE 1.3: TOS not accepted // Input: Valid key, accept_tos=false // Expected: 400 Bad Request with TOS error // Validates: Business rule enforcement // TEST CASE 1.4: Feature disabled in config // Input: Valid request but console enrollment disabled // Expected: 400 Bad Request with feature disabled error // Validates: Feature flag checking // TEST CASE 1.5: Enrollment execution error // Input: Valid request, execution fails // Expected: 500 Internal Server Error with appropriate message // Validates: Error handling ``` #### `ConsoleStatus` (0% → 100%) ```go // TEST CASE 1.6: Get status when enrolled // Input: GET request when console is enrolled // Expected: 200 OK with enrolled=true, expiry info // Validates: Enrolled state reporting // TEST CASE 1.7: Get status when not enrolled // Input: GET request when console is not enrolled // Expected: 200 OK with enrolled=false // Validates: Unenrolled state reporting // TEST CASE 1.8: Feature disabled // Input: GET request when feature disabled // Expected: 400 Bad Request // Validates: Feature flag checking ``` #### `isConsoleEnrollmentEnabled` (0% → 100%) ```go // TEST CASE 1.9: Enabled in config // Input: Config with CROWDSEC_CONSOLE_ENROLLMENT_KEY set // Expected: Returns true // Validates: Positive flag detection // TEST CASE 1.10: Disabled in config // Input: Config without enrollment key // Expected: Returns false // Validates: Negative flag detection ``` #### `actorFromContext` (0% → 100%) ```go // TEST CASE 1.11: User in context // Input: Gin context with authenticated user // Expected: Returns username // Validates: User extraction from context // TEST CASE 1.12: No user in context // Input: Gin context without user // Expected: Returns "system" or empty string // Validates: Fallback behavior ``` ### 2. CrowdSec LAPI Tests (Handler) #### `GetLAPIDecisions` (40% → 100%) ```go // TEST CASE 2.1: Successful decisions retrieval // Input: Valid LAPI connection // Expected: 200 OK with decisions list // Validates: Happy path // TEST CASE 2.2: LAPI unavailable // Input: LAPI service not running // Expected: 503 Service Unavailable // Validates: Service dependency handling // TEST CASE 2.3: Empty decisions list // Input: Valid LAPI with no active decisions // Expected: 200 OK with empty array // Validates: Empty state handling // TEST CASE 2.4: Connection timeout // Input: LAPI slow to respond // Expected: 504 Gateway Timeout // Validates: Timeout handling // TEST CASE 2.5: Invalid API key // Input: Invalid LAPI key // Expected: 401 Unauthorized // Validates: Authentication error handling ``` #### `CheckLAPIHealth` (41.4% → 100%) ```go // TEST CASE 2.6: LAPI healthy // Input: Healthy LAPI endpoint // Expected: 200 OK with healthy=true // Validates: Health check success // TEST CASE 2.7: LAPI unhealthy // Input: Unhealthy LAPI endpoint // Expected: 200 OK with healthy=false, error details // Validates: Health check failure reporting // TEST CASE 2.8: Connection refused // Input: LAPI not listening // Expected: Appropriate error response // Validates: Connection error handling ``` ### 3. Security Handler Tests #### `LookupGeoIP` (38.9% → 100%) ```go // TEST CASE 3.1: Successful IP lookup // Input: Valid public IP address // Expected: 200 OK with country, city, coordinates // Validates: GeoIP resolution // TEST CASE 3.2: Private IP address // Input: 192.168.1.1 or 10.0.0.1 // Expected: 200 OK with "Private Address" response // Validates: Private IP handling // TEST CASE 3.3: Invalid IP format // Input: "not-an-ip" // Expected: 400 Bad Request // Validates: Input validation // TEST CASE 3.4: GeoIP database not loaded // Input: Valid IP, GeoIP service unavailable // Expected: 503 Service Unavailable // Validates: Dependency handling // TEST CASE 3.5: Localhost/loopback // Input: 127.0.0.1 // Expected: 200 OK with "Localhost" response // Validates: Loopback handling ``` #### `ReloadGeoIP` (58.3% → 100%) ```go // TEST CASE 3.6: Successful reload // Input: POST to reload with valid database path // Expected: 200 OK with success message // Validates: Hot reload functionality // TEST CASE 3.7: Database file not found // Input: POST with invalid database path // Expected: 500 Internal Server Error // Validates: File error handling // TEST CASE 3.8: Corrupt database file // Input: POST with corrupt MaxMind database // Expected: 500 Internal Server Error with parse error // Validates: Database validation ``` #### `UpdateConfig` (69.2% → 100%) ```go // TEST CASE 3.9: Update all security settings // Input: Complete SecurityConfig object // Expected: 200 OK with updated config // Validates: Full config update // TEST CASE 3.10: Partial update // Input: Partial config (only rate_limit changes) // Expected: 200 OK with merged config // Validates: Partial update handling // TEST CASE 3.11: Invalid config values // Input: Negative rate limit values // Expected: 400 Bad Request // Validates: Config validation // TEST CASE 3.12: Enable WAF // Input: waf.enabled = true // Expected: 200 OK, WAF activated // Validates: WAF toggle // TEST CASE 3.13: Update ACL settings // Input: acl.default_action = "deny" // Expected: 200 OK, ACL updated // Validates: ACL config update ``` ### 4. CrowdSec Package Tests #### `ExecuteWithEnv` (0% → 100%) ```go // TEST CASE 4.1: Execute command successfully // Input: Valid command with env vars // Expected: Success, command executed // Validates: Environment variable passing // TEST CASE 4.2: Execute with missing env // Input: Command requiring env vars not set // Expected: Error with clear message // Validates: Missing env handling // TEST CASE 4.3: Command execution failure // Input: Command that returns non-zero exit // Expected: Error with exit code and stderr // Validates: Error propagation ``` #### `backupExisting` (36.4% → 100%) ```go // TEST CASE 4.4: Backup when directory exists // Input: Existing directory with files // Expected: Backup created with timestamp // Validates: Backup creation // TEST CASE 4.5: Backup when directory doesn't exist // Input: Non-existent directory // Expected: No error, no backup created // Validates: Missing directory handling // TEST CASE 4.6: Permission denied during backup // Input: Read-only filesystem // Expected: Error with permission message // Validates: Permission error handling ``` #### `emptyDir` (30.8% → 100%) ```go // TEST CASE 4.7: Empty directory with files // Input: Directory with files and subdirectories // Expected: All contents removed, directory remains // Validates: Recursive deletion // TEST CASE 4.8: Empty non-existent directory // Input: Directory that doesn't exist // Expected: Error or no-op (depending on design) // Validates: Missing directory handling // TEST CASE 4.9: Empty directory with symlinks // Input: Directory containing symlinks // Expected: Symlinks removed, targets untouched // Validates: Symlink handling ``` #### `extractTarGz` (65.9% → 100%) ```go // TEST CASE 4.10: Extract valid tarball // Input: Valid .tar.gz file // Expected: Contents extracted to destination // Validates: Basic extraction // TEST CASE 4.11: Extract corrupted tarball // Input: Corrupted .tar.gz file // Expected: Error with corruption message // Validates: Corruption detection // TEST CASE 4.12: Extract with path traversal attempt // Input: Malicious tarball with "../" paths // Expected: Error or sanitized extraction // Validates: Security - path traversal prevention // TEST CASE 4.13: Extract to non-existent directory // Input: Destination doesn't exist // Expected: Directory created, contents extracted // Validates: Auto-directory creation ``` ### 5. Services Package Tests #### `testGeoIP` (9.1% → 100%) ```go // TEST CASE 5.1: Test with valid GeoIP database // Input: Valid MaxMind database path // Expected: Test passes, returns success // Validates: Database validation // TEST CASE 5.2: Test with missing database // Input: Non-existent database path // Expected: Error indicating file not found // Validates: Missing file handling // TEST CASE 5.3: Test with wrong file format // Input: Non-MaxMind file (e.g., text file) // Expected: Error indicating invalid format // Validates: Format validation // TEST CASE 5.4: Test with empty path // Input: Empty string for database path // Expected: Appropriate error or default behavior // Validates: Empty input handling ``` #### `GenerateBreakGlassToken` (73.7% → 100%) ```go // TEST CASE 5.5: Generate valid token // Input: Request to generate token // Expected: Token with proper expiry, stored in DB // Validates: Token generation // TEST CASE 5.6: Generate when existing token exists // Input: Request when unexpired token exists // Expected: Existing token returned or new one generated // Validates: Token reuse policy // TEST CASE 5.7: Token expiry configuration // Input: Custom expiry duration // Expected: Token has correct expiry time // Validates: Expiry configuration ``` ### 6. Cerberus Package Tests #### `Middleware` (81.8% → 100%) ```go // TEST CASE 6.1: Request when Cerberus disabled // Input: Request with Cerberus disabled in config // Expected: Request passes through unmodified // Validates: Bypass when disabled // TEST CASE 6.2: Request blocked by WAF // Input: Request matching WAF rule // Expected: 403 Forbidden with WAF block reason // Validates: WAF integration // TEST CASE 6.3: Request blocked by ACL // Input: Request from blocked IP // Expected: 403 Forbidden with ACL block reason // Validates: ACL integration // TEST CASE 6.4: Request blocked by rate limit // Input: Request exceeding rate limit // Expected: 429 Too Many Requests // Validates: Rate limiting integration // TEST CASE 6.5: Request allowed through all checks // Input: Legitimate request passing all checks // Expected: Request proceeds to handler // Validates: Pass-through behavior // TEST CASE 6.6: Break glass token bypass // Input: Request with valid break-glass token // Expected: Request bypasses security checks // Validates: Emergency bypass // TEST CASE 6.7: GeoIP blocking // Input: Request from blocked country // Expected: 403 Forbidden with geo block reason // Validates: GeoIP integration ``` --- ## Frontend Test Cases ### 7. Console Enrollment API Tests #### `api/consoleEnrollment.ts` (0% → 100%) ```typescript // TEST CASE 7.1: enrollConsole - successful enrollment // Input: Valid enrollment key, accepted TOS // Expected: Success response with enrollment status // Validates: API call construction and response handling // TEST CASE 7.2: enrollConsole - network error // Input: Network failure during enrollment // Expected: Error thrown with appropriate message // Validates: Error handling // TEST CASE 7.3: getConsoleStatus - enrolled state // Input: GET request when enrolled // Expected: Returns enrolled status with details // Validates: Status parsing // TEST CASE 7.4: getConsoleStatus - not enrolled // Input: GET request when not enrolled // Expected: Returns not enrolled status // Validates: Negative state handling ``` ### 8. Security Page Tests #### `pages/Security.tsx` (82.35% → 100%) ```typescript // TEST CASE 8.1: Render security dashboard // Input: Component mount with mock data // Expected: All security sections displayed // Validates: Basic rendering // TEST CASE 8.2: Toggle Cerberus enabled // Input: Click Cerberus toggle // Expected: API called, state updated // Validates: Toggle interaction // TEST CASE 8.3: Enable rate limiting with presets // Input: Select rate limit preset // Expected: Rate limit config applied // Validates: Preset application // TEST CASE 8.4: Generate break glass token // Input: Click generate token button // Expected: Token displayed, copy functionality works // Validates: Token generation UI // TEST CASE 8.5: Error state display // Input: API returns error // Expected: Error message displayed to user // Validates: Error handling UI ``` ### 9. WAF Config Tests #### `pages/WafConfig.tsx` (89.47% → 100%) ```typescript // TEST CASE 9.1: Render WAF configuration // Input: Component mount with mock WAF config // Expected: All WAF settings displayed // Validates: Basic rendering // TEST CASE 9.2: Add WAF exclusion // Input: Enter exclusion pattern and save // Expected: Exclusion added to list // Validates: Exclusion creation // TEST CASE 9.3: Delete WAF exclusion // Input: Click delete on existing exclusion // Expected: Exclusion removed after confirmation // Validates: Exclusion deletion // TEST CASE 9.4: Rule set management // Input: Toggle rule set enabled/disabled // Expected: Rule set state updated // Validates: Rule set toggling ``` --- ## Test Execution Order ### Phase 1: Critical (0% Coverage) - IMMEDIATE 1. CrowdSec Console tests (ConsoleEnroll, ConsoleStatus) 2. `testGeoIP` service tests 3. `SetGeoIPService` handler tests 4. CrowdSec package tests (ExecuteWithEnv, formatEnv, Status) ### Phase 2: High Priority (<70% Coverage) - WEEK 1 1. `LookupGeoIP` handler tests 2. `GetLAPIDecisions` handler tests 3. `CheckLAPIHealth` handler tests 4. `ReloadGeoIP` handler tests 5. `emptyDir` and `backupExisting` tests ### Phase 3: Medium Priority (70-90% Coverage) - WEEK 2 1. Remaining handler coverage gaps 2. Cerberus middleware edge cases 3. Frontend console enrollment tests 4. Security.tsx remaining coverage 5. WafConfig.tsx remaining coverage --- ## QA Workflow ### For Each Test Case 1. **QA writes test** following expected behavior 2. **If test passes**: Mark as ✅ complete 3. **If test fails due to missing code behavior**: Create DEV issue to implement behavior 4. **If test fails due to bug**: Create DEV issue to fix bug ### Dev Handoff Format ```markdown ## Test Failure Report **Test Case ID:** 3.4 **Test Name:** LookupGeoIP_GeoIPServiceUnavailable **Expected Behavior:** Return 503 Service Unavailable with error message **Actual Behavior:** Returns 500 with generic error / panics / returns nil **Test Code:** [Include test code] **Required Fix:** - Add nil check for GeoIP service - Return appropriate HTTP status code - Include helpful error message for debugging ``` --- ## Coverage Commands ```bash # Backend full coverage cd backend && go test -coverprofile=coverage.out ./... go tool cover -func=coverage.out | grep -E "(security|crowdsec|access_list|cerberus)" # Backend specific package go test -coverprofile=handlers.out ./internal/api/handlers/... go tool cover -func=handlers.out # Frontend coverage cd frontend && npm run test:coverage ``` --- ## Definition of Done - [ ] All 0% coverage functions have tests - [ ] All security handlers ≥95% coverage - [ ] All CrowdSec package functions ≥95% coverage - [ ] All security services ≥95% coverage - [ ] Cerberus middleware 100% coverage - [ ] Frontend security components ≥95% coverage - [ ] All tests pass - [ ] Pre-commit passes with ≥85% overall coverage - [ ] No TODO comments in security code - [ ] All error paths tested