1095 lines
29 KiB
Markdown
1095 lines
29 KiB
Markdown
# Backend Test Coverage Restoration Plan
|
|
|
|
## Executive Summary
|
|
|
|
**Objective:** Restore backend test coverage from current 59.33% patch coverage to **86% locally** (85%+ in CI) with **100% patch coverage** (Codecov requirement).
|
|
|
|
**Current State:**
|
|
- Patch Coverage: 59.33% (98 lines missing coverage)
|
|
- 10 files identified with coverage gaps
|
|
- Priority ranking based on missing lines and business impact
|
|
|
|
**Strategy:** Systematic file-by-file coverage remediation using table-driven tests, interface mocking, and existing test infrastructure patterns.
|
|
|
|
---
|
|
|
|
## 1. Root Cause Analysis
|
|
|
|
### Why Coverage Dropped
|
|
|
|
1. **Recent Feature Additions Without Tests**
|
|
- Caddyfile import enhancements (multi-file, normalization)
|
|
- CrowdSec hub presets and console enrollment
|
|
- Manual challenge DNS provider flow
|
|
- Feature flag batch optimization
|
|
|
|
2. **Complex Error Paths Untested**
|
|
- SSRF validation failures
|
|
- Path traversal edge cases
|
|
- Process lifecycle edge cases (PID recycling)
|
|
- Cache TTL expiry and eviction paths
|
|
|
|
3. **Integration-Heavy Code**
|
|
- External command execution (Caddy, CrowdSec)
|
|
- HTTP client operations with fallback logic
|
|
- File system operations with safety checks
|
|
- Cron scheduler lifecycle
|
|
|
|
### Coverage Gap Breakdown
|
|
|
|
| File | Coverage | Missing | Partials | Priority |
|
|
|------|----------|---------|----------|----------|
|
|
| import_handler.go | 45.83% | 33 lines | 6 branches | 🔴 CRITICAL |
|
|
| crowdsec_handler.go | 21.42% | 5 lines | 6 branches | 🔴 CRITICAL |
|
|
| backup_service.go | 63.33% | 5 lines | 6 branches | 🟡 HIGH |
|
|
| hub_sync.go | 46.66% | 3 lines | 5 branches | 🟡 HIGH |
|
|
| feature_flags_handler.go | 83.33% | 4 lines | 2 branches | 🟢 MEDIUM |
|
|
| importer.go | 76.0% | 22 lines | 4 branches | 🟡 HIGH |
|
|
| security_service.go | 50.0% | 12 lines | 8 branches | 🟡 HIGH |
|
|
| hub_cache.go | 28.57% | 5 lines | 0 branches | 🟡 HIGH |
|
|
| manual_challenge_handler.go | 33.33% | 8 lines | 4 branches | 🟡 HIGH |
|
|
| crowdsec_exec.go | 0% | 1 line | 0 branches | 🟢 LOW |
|
|
|
|
**Total Impact:** 98 missing lines, 35 partial branches
|
|
|
|
---
|
|
|
|
## 2. File-by-File Coverage Plan
|
|
|
|
### File 1: backend/internal/api/handlers/import_handler.go (933 lines)
|
|
|
|
**Current Coverage:** 45.83% (33 missing, 6 partials)
|
|
**Target Coverage:** 100%
|
|
**Complexity:** 🔴 HIGH (complex file handling, path safety, session management)
|
|
|
|
**Existing Test File:** `backend/internal/api/handlers/import_handler_test.go`
|
|
|
|
#### Functions Requiring Coverage
|
|
|
|
1. **Upload() - Lines 85-150**
|
|
- Missing: Normalization success/failure paths
|
|
- Missing: Path traversal validation
|
|
- Partials: Error handling branches
|
|
|
|
2. **UploadMulti() - Lines 155-220**
|
|
- Missing: Multi-file conflict detection
|
|
- Missing: Archive extraction edge cases
|
|
- Partials: File validation failures
|
|
|
|
3. **Commit() - Lines 225-290**
|
|
- Missing: Transient session promotion
|
|
- Missing: Import session cleanup
|
|
- Partials: ProxyHost service failures
|
|
|
|
4. **DetectImports() - Lines 320-380**
|
|
- Missing: Empty Caddyfile handling
|
|
- Missing: Parse failure recovery
|
|
- Partials: Conflict resolution logic
|
|
|
|
5. **safeJoin() helper - Lines 450-475**
|
|
- Missing: Parent directory traversal
|
|
- Missing: Absolute path rejection
|
|
|
|
#### Test Cases Required
|
|
|
|
```go
|
|
// TestUpload_NormalizationSuccess
|
|
// - Upload single-line Caddyfile
|
|
// - Verify NormalizeCaddyfile called
|
|
// - Assert formatted content stored
|
|
|
|
// TestUpload_NormalizationFailure
|
|
// - Mock importer.NormalizeCaddyfile to return error
|
|
// - Verify upload fails with clear error message
|
|
|
|
// TestUpload_PathTraversal
|
|
// - Upload Caddyfile with "../../../etc/passwd"
|
|
// - Verify rejection with security error
|
|
|
|
// TestUploadMulti_ConflictDetection
|
|
// - Upload archive with duplicate domains
|
|
// - Verify conflicts reported in preview
|
|
|
|
// TestCommit_TransientPromotion
|
|
// - Create transient session
|
|
// - Commit session
|
|
// - Verify DB persistence
|
|
|
|
// TestDetectImports_EmptyCaddyfile
|
|
// - Upload empty file
|
|
// - Verify graceful handling with no imports
|
|
|
|
// TestSafeJoin_Traversal
|
|
// - Call safeJoin with "../..", "sensitive.file"
|
|
// - Verify error returned
|
|
|
|
// TestSafeJoin_AbsolutePath
|
|
// - Call safeJoin with "/etc/passwd"
|
|
// - Verify error returned
|
|
```
|
|
|
|
#### Implementation Strategy
|
|
|
|
1. **Mock Importer Interface** (NEW)
|
|
- Create `ImporterInterface` with `NormalizeCaddyfile()`, `ParseCaddyfile()`, `ExtractHosts()`
|
|
- Update handler to accept interface instead of concrete type
|
|
- Use in tests to simulate failures
|
|
|
|
2. **Table-Driven Path Safety Tests**
|
|
- Test matrix of forbidden patterns (`..`, absolute paths, unicode tricks)
|
|
|
|
3. **Session Lifecycle Tests**
|
|
- Test transient → DB promotion
|
|
- Test cleanup on timeout
|
|
- Test concurrent access
|
|
|
|
**Estimated Effort:** 3 developer-days
|
|
|
|
---
|
|
|
|
### File 2: backend/internal/api/handlers/crowdsec_handler.go (1006 lines)
|
|
|
|
**Current Coverage:** 21.42% (5 missing, 6 partials)
|
|
**Target Coverage:** 100%
|
|
**Complexity:** 🔴 HIGH (process lifecycle, LAPI integration, hub operations)
|
|
|
|
**Existing Test File:** `backend/internal/api/handlers/crowdsec_handler_test.go`
|
|
|
|
#### Functions Requiring Coverage
|
|
|
|
1. **Start() - Lines 150-200**
|
|
- Missing: Process already running check
|
|
- Partials: Executor.Start() failure paths
|
|
|
|
2. **Stop() - Lines 205-250**
|
|
- Missing: Clean stop when already stopped
|
|
- Partials: Signal failure recovery
|
|
|
|
3. **ImportConfig() - Lines 350-420**
|
|
- Missing: Archive extraction failures
|
|
- Missing: Backup rollback on error
|
|
- Partials: Path validation edge cases
|
|
|
|
4. **ExportConfig() - Lines 425-480**
|
|
- Missing: Archive creation failures
|
|
- Partials: File read errors
|
|
|
|
5. **ApplyPreset() - Lines 600-680**
|
|
- Missing: Hub cache miss handling
|
|
- Missing: CommandExecutor failure recovery
|
|
- Partials: Rollback on partial apply
|
|
|
|
6. **ConsoleEnroll() - Lines 750-850**
|
|
- Missing: LAPI endpoint validation
|
|
- Missing: Enrollment failure retries
|
|
- Partials: Token parsing errors
|
|
|
|
#### Test Cases Required
|
|
|
|
```go
|
|
// TestStart_AlreadyRunning
|
|
// - Mock Status to return running=true
|
|
// - Call Start
|
|
// - Verify error "already running"
|
|
|
|
// TestStop_IdempotentStop
|
|
// - Stop when already stopped
|
|
// - Verify no error returned
|
|
|
|
// TestImportConfig_ExtractionFailure
|
|
// - Mock tar.gz extraction to fail
|
|
// - Verify rollback executed
|
|
// - Verify original config restored
|
|
|
|
// TestApplyPreset_CacheMiss
|
|
// - Mock HubCache.Load to return ErrCacheMiss
|
|
// - Verify graceful error message
|
|
|
|
// TestConsoleEnroll_InvalidEndpoint
|
|
// - Provide malformed LAPI URL
|
|
// - Verify rejection with validation error
|
|
|
|
// TestConsoleEnroll_TokenParseError
|
|
// - Mock HTTP response with invalid JSON
|
|
// - Verify clear error to user
|
|
```
|
|
|
|
#### Implementation Strategy
|
|
|
|
1. **Enhance Existing Mocks**
|
|
- Add failure modes to `mockCrowdsecExecutor`
|
|
- Add cache miss scenarios to `mockHubService`
|
|
|
|
2. **Process Lifecycle Matrix**
|
|
- Test all state transitions: stopped→started, started→stopped, stopped→stopped
|
|
|
|
3. **LAPI Integration Tests**
|
|
- Mock HTTP responses for enrollment, bans, decisions
|
|
- Test timeout and retry logic
|
|
|
|
**Estimated Effort:** 2.5 developer-days
|
|
|
|
---
|
|
|
|
### File 3: backend/internal/services/backup_service.go (426 lines)
|
|
|
|
**Current Coverage:** 63.33% (5 missing, 6 partials)
|
|
**Target Coverage:** 100%
|
|
**Complexity:** 🟡 MEDIUM (cron scheduling, zip operations)
|
|
|
|
**Existing Test Files:**
|
|
- `backend/internal/services/backup_service_test.go`
|
|
- `backend/internal/services/backup_service_disk_test.go`
|
|
|
|
#### Functions Requiring Coverage
|
|
|
|
1. **Start() - Lines 80-120**
|
|
- Missing: Cron schedule parsing failures
|
|
- Missing: Duplicate start prevention
|
|
|
|
2. **Stop() - Lines 125-145**
|
|
- Missing: Stop when not started (idempotent)
|
|
|
|
3. **CreateBackup() - Lines 200-280**
|
|
- Missing: Disk full scenario
|
|
- Partials: Zip corruption recovery
|
|
|
|
4. **RestoreBackup() - Lines 350-420**
|
|
- Missing: Zip decompression bomb detection
|
|
- Partials: Malformed archive handling
|
|
|
|
5. **GetAvailableSpace() - Lines 450-475**
|
|
- Missing: Syscall failure handling
|
|
|
|
#### Test Cases Required
|
|
|
|
```go
|
|
// TestStart_InvalidCronSchedule
|
|
// - Create service with invalid cron expression
|
|
// - Call Start()
|
|
// - Verify error returned
|
|
|
|
// TestStop_Idempotent
|
|
// - Stop without calling Start first
|
|
// - Verify no panic, no error
|
|
|
|
// TestCreateBackup_DiskFull
|
|
// - Mock syscall.Statfs to return 0 available space
|
|
// - Verify backup fails with "disk full" error
|
|
|
|
// TestRestoreBackup_DecompressionBomb
|
|
// - Create zip with 1GB compressed → 10TB uncompressed
|
|
// - Verify rejection before extraction
|
|
|
|
// TestGetAvailableSpace_SyscallFailure
|
|
// - Mock Statfs to return error
|
|
// - Verify graceful error handling
|
|
```
|
|
|
|
#### Implementation Strategy
|
|
|
|
1. **Cron Lifecycle Tests**
|
|
- Test start/stop multiple times
|
|
- Verify scheduler cleanup on stop
|
|
|
|
2. **Disk Space Mocking**
|
|
- Use interface for syscall operations (new)
|
|
- Mock Statfs for edge cases
|
|
|
|
3. **Archive Safety Tests**
|
|
- Test zip bomb detection
|
|
- Test path traversal in archive entries
|
|
|
|
**Estimated Effort:** 1.5 developer-days
|
|
|
|
---
|
|
|
|
### File 4: backend/internal/crowdsec/hub_sync.go (916 lines)
|
|
|
|
**Current Coverage:** 46.66% (3 missing, 5 partials)
|
|
**Target Coverage:** 100%
|
|
**Complexity:** 🟡 MEDIUM (HTTP client, cache, tar.gz extraction)
|
|
|
|
**Existing Test Files:**
|
|
- `backend/internal/crowdsec/hub_sync_test.go`
|
|
- `backend/internal/crowdsec/hub_pull_apply_test.go`
|
|
- `backend/internal/crowdsec/hub_sync_raw_index_test.go`
|
|
|
|
#### Functions Requiring Coverage
|
|
|
|
1. **FetchIndex() - Lines 120-180**
|
|
- Missing: HTTP timeout handling
|
|
- Partials: JSON parse failures
|
|
|
|
2. **Pull() - Lines 250-350**
|
|
- Missing: Etag cache hit logic
|
|
- Partials: Fallback URL failures
|
|
|
|
3. **Apply() - Lines 400-480**
|
|
- Missing: Tar extraction path traversal
|
|
- Missing: Rollback on partial apply
|
|
- Partials: CommandExecutor failures
|
|
|
|
4. **validateHubURL() - Lines 600-650**
|
|
- Missing: SSRF protection (private IP ranges)
|
|
- Missing: Invalid scheme rejection
|
|
|
|
#### Test Cases Required
|
|
|
|
```go
|
|
// TestFetchIndex_HTTPTimeout
|
|
// - Mock HTTP client to timeout
|
|
// - Verify graceful error with retry suggestion
|
|
|
|
// TestPull_EtagCacheHit
|
|
// - Mock HTTP response with matching Etag
|
|
// - Verify 304 Not Modified handling
|
|
|
|
// TestApply_PathTraversal
|
|
// - Create tar.gz with "../../../etc/passwd"
|
|
// - Verify extraction blocked
|
|
|
|
// TestValidateHubURL_PrivateIP
|
|
// - Call validateHubURL("http://192.168.1.1/preset")
|
|
// - Verify SSRF protection rejects
|
|
|
|
// TestValidateHubURL_InvalidScheme
|
|
// - Call validateHubURL("ftp://example.com/preset")
|
|
// - Verify only http/https allowed
|
|
```
|
|
|
|
#### Implementation Strategy
|
|
|
|
1. **HTTP Client Mocking**
|
|
- Use existing `mockHTTPClient` patterns
|
|
- Add timeout and redirect scenarios
|
|
|
|
2. **SSRF Protection Tests**
|
|
- Test all RFC1918 ranges (10.x, 172.16.x, 192.168.x)
|
|
- Test localhost, link-local (169.254.x)
|
|
|
|
3. **Archive Safety Matrix**
|
|
- Test absolute paths, symlinks, path traversal in tar entries
|
|
|
|
**Estimated Effort:** 2 developer-days
|
|
|
|
---
|
|
|
|
### File 5: backend/internal/api/handlers/feature_flags_handler.go (128 lines)
|
|
|
|
**Current Coverage:** 83.33% (4 missing, 2 partials)
|
|
**Target Coverage:** 100%
|
|
**Complexity:** 🟢 LOW (simple CRUD with batch optimization)
|
|
|
|
**Existing Test Files:**
|
|
- `backend/internal/api/handlers/feature_flags_handler_test.go`
|
|
- `backend/internal/api/handlers/feature_flags_handler_coverage_test.go`
|
|
|
|
#### Functions Requiring Coverage
|
|
|
|
1. **GetFlags() - Lines 50-80**
|
|
- Missing: DB query failure handling
|
|
|
|
2. **UpdateFlags() - Lines 85-120**
|
|
- Partials: Transaction rollback paths
|
|
|
|
#### Test Cases Required
|
|
|
|
```go
|
|
// TestGetFlags_DatabaseError
|
|
// - Mock db.Find() to return error
|
|
// - Verify 500 response with error code
|
|
|
|
// TestUpdateFlags_TransactionRollback
|
|
// - Mock transaction to fail mid-update
|
|
// - Verify rollback executed
|
|
// - Verify no partial updates persisted
|
|
```
|
|
|
|
#### Implementation Strategy
|
|
|
|
1. **Database Error Scenarios**
|
|
- Use existing testDB patterns
|
|
- Inject failures at specific query points
|
|
|
|
**Estimated Effort:** 0.5 developer-days
|
|
|
|
---
|
|
|
|
### File 6: backend/internal/caddy/importer.go (438 lines)
|
|
|
|
**Current Coverage:** 76.0% (22 missing, 4 partials)
|
|
**Target Coverage:** 100%
|
|
**Complexity:** 🟡 MEDIUM (Caddyfile parsing, JSON extraction)
|
|
|
|
**Existing Test Files:**
|
|
- `backend/internal/caddy/importer_test.go`
|
|
- `backend/internal/caddy/importer_subroute_test.go`
|
|
- `backend/internal/caddy/importer_additional_test.go`
|
|
- `backend/internal/caddy/importer_extra_test.go`
|
|
|
|
#### Functions Requiring Coverage
|
|
|
|
1. **NormalizeCaddyfile() - Lines 115-165**
|
|
- Missing: Temp file write failures
|
|
- Missing: caddy fmt timeout handling
|
|
|
|
2. **ParseCaddyfile() - Lines 170-210**
|
|
- Missing: Path traversal validation
|
|
- Missing: caddy adapt failures
|
|
|
|
3. **extractHandlers() - Lines 280-350**
|
|
- Missing: Deep subroute nesting
|
|
|
|
4. **BackupCaddyfile() - Lines 400-435**
|
|
- Missing: Path validation errors
|
|
|
|
#### Test Cases Required
|
|
|
|
```go
|
|
// TestNormalizeCaddyfile_TempFileFailure
|
|
// - Mock os.CreateTemp to return error
|
|
// - Verify error propagated
|
|
|
|
// TestNormalizeCaddyfile_Timeout
|
|
// - Mock executor to timeout
|
|
// - Verify timeout error message
|
|
|
|
// TestParseCaddyfile_PathTraversal
|
|
// - Call with path "../../../etc/Caddyfile"
|
|
// - Verify rejection
|
|
|
|
// TestExtractHandlers_DeepNesting
|
|
// - Parse JSON with subroute → subroute → handler
|
|
// - Verify correct flattening
|
|
|
|
// TestBackupCaddyfile_PathTraversal
|
|
// - Call with originalPath="../../../sensitive"
|
|
// - Verify rejection
|
|
```
|
|
|
|
#### Implementation Strategy
|
|
|
|
1. **Executor Mocking**
|
|
- Use existing `Executor` interface
|
|
- Add timeout simulation
|
|
|
|
2. **Path Safety Test Matrix**
|
|
- Comprehensive traversal patterns
|
|
- Unicode normalization tricks
|
|
|
|
**Estimated Effort:** 1.5 developer-days
|
|
|
|
---
|
|
|
|
### File 7: backend/internal/services/security_service.go (442 lines)
|
|
|
|
**Current Coverage:** 50.0% (12 missing, 8 partials)
|
|
**Target Coverage:** 100%
|
|
**Complexity:** 🟡 MEDIUM (audit logging, goroutine lifecycle, break-glass tokens)
|
|
|
|
**Existing Test File:** `backend/internal/services/security_service_test.go`
|
|
|
|
#### Functions Requiring Coverage
|
|
|
|
1. **Close() - Lines 40-55**
|
|
- Missing: Double-close prevention
|
|
|
|
2. **Flush() - Lines 60-75**
|
|
- Missing: Timeout on slow audit writes
|
|
|
|
3. **Get() - Lines 80-110**
|
|
- Missing: Fallback to first row (backward compat)
|
|
- Partials: RecordNotFound handling
|
|
|
|
4. **Upsert() - Lines 115-180**
|
|
- Missing: Invalid CrowdSec mode rejection
|
|
- Partials: CIDR validation failures
|
|
|
|
5. **processAuditEvents() - Lines 300-350**
|
|
- Missing: Channel close handling
|
|
- Missing: DB error during audit write
|
|
|
|
6. **ListAuditLogs() - Lines 380-430**
|
|
- Partials: Pagination edge cases
|
|
|
|
#### Test Cases Required
|
|
|
|
```go
|
|
// TestClose_DoubleClose
|
|
// - Call Close() twice
|
|
// - Verify no panic, idempotent
|
|
|
|
// TestFlush_SlowWrite
|
|
// - Mock DB write to delay
|
|
// - Verify Flush waits correctly
|
|
|
|
// TestGet_BackwardCompatFallback
|
|
// - DB with no "default" row but has other rows
|
|
// - Verify fallback to first row
|
|
|
|
// TestUpsert_InvalidCrowdSecMode
|
|
// - Call with CrowdSecMode="remote"
|
|
// - Verify rejection error
|
|
|
|
// TestProcessAuditEvents_ChannelClosed
|
|
// - Close channel while processing
|
|
// - Verify graceful shutdown
|
|
|
|
// TestListAuditLogs_EmptyResult
|
|
// - Query with no matching logs
|
|
// - Verify empty array, not error
|
|
```
|
|
|
|
#### Implementation Strategy
|
|
|
|
1. **Goroutine Testing**
|
|
- Use sync.WaitGroup to verify cleanup
|
|
- Test channel closure scenarios
|
|
|
|
2. **CIDR Validation Matrix**
|
|
- Test valid: "192.168.1.0/24", "10.0.0.1"
|
|
- Test invalid: "999.999.999.999/99", "not-an-ip"
|
|
|
|
**Estimated Effort:** 2 developer-days
|
|
|
|
---
|
|
|
|
### File 8: backend/internal/crowdsec/hub_cache.go (234 lines)
|
|
|
|
**Current Coverage:** 28.57% (5 missing, 0 partials)
|
|
**Target Coverage:** 100%
|
|
**Complexity:** 🟢 LOW (cache CRUD with TTL)
|
|
|
|
**Existing Test File:** `backend/internal/crowdsec/hub_cache_test.go`
|
|
|
|
#### Functions Requiring Coverage
|
|
|
|
1. **Store() - Lines 60-105**
|
|
- Missing: Context cancellation handling
|
|
- Missing: Metadata write failure
|
|
|
|
2. **Load() - Lines 110-145**
|
|
- Missing: Expired entry handling
|
|
|
|
3. **Touch() - Lines 200-220**
|
|
- Missing: Update timestamp for TTL extension
|
|
|
|
4. **Size() - Lines 225-240**
|
|
- Missing: Total cache size calculation
|
|
|
|
#### Test Cases Required
|
|
|
|
```go
|
|
// TestStore_ContextCancelled
|
|
// - Cancel context before Store completes
|
|
// - Verify operation aborted
|
|
|
|
// TestLoad_Expired
|
|
// - Store with short TTL
|
|
// - Wait for expiry
|
|
// - Verify ErrCacheExpired returned
|
|
|
|
// TestTouch_ExtendTTL
|
|
// - Store entry with 1 hour TTL
|
|
// - Touch after 30 minutes
|
|
// - Verify TTL reset
|
|
|
|
// TestSize_MultipleEntries
|
|
// - Store 3 presets of known sizes
|
|
// - Call Size()
|
|
// - Verify accurate total
|
|
```
|
|
|
|
#### Implementation Strategy
|
|
|
|
1. **TTL Testing**
|
|
- Use mock time function (`nowFn` field)
|
|
- Avoid time.Sleep in tests
|
|
|
|
2. **Context Testing**
|
|
- Test cancellation at various points
|
|
|
|
**Estimated Effort:** 1 developer-day
|
|
|
|
---
|
|
|
|
### File 9: backend/internal/api/handlers/manual_challenge_handler.go (615 lines)
|
|
|
|
**Current Coverage:** 33.33% (8 missing, 4 partials)
|
|
**Target Coverage:** 100%
|
|
**Complexity:** 🟡 MEDIUM (DNS challenge API with validation)
|
|
|
|
**Existing Test File:** `backend/internal/api/handlers/manual_challenge_handler_test.go`
|
|
|
|
#### Functions Requiring Coverage
|
|
|
|
1. **GetChallenge() - Lines 90-160**
|
|
- Missing: Provider type validation
|
|
- Partials: User authorization checks
|
|
|
|
2. **VerifyChallenge() - Lines 165-240**
|
|
- Missing: Challenge expired handling
|
|
- Partials: Provider ownership check
|
|
|
|
3. **PollChallenge() - Lines 245-310**
|
|
- Partials: Status update failures
|
|
|
|
4. **CreateChallenge() - Lines 420-490**
|
|
- Missing: Duplicate challenge detection
|
|
- Partials: Provider type check
|
|
|
|
#### Test Cases Required
|
|
|
|
```go
|
|
// TestGetChallenge_NonManualProvider
|
|
// - Create challenge on "cloudflare" provider
|
|
// - Call GetChallenge
|
|
// - Verify 400 "invalid provider type"
|
|
|
|
// TestVerifyChallenge_Expired
|
|
// - Create expired challenge (CreatedAt - 2 hours)
|
|
// - Call VerifyChallenge
|
|
// - Verify 410 Gone response
|
|
|
|
// TestCreateChallenge_Duplicate
|
|
// - Create challenge for domain
|
|
// - Create second challenge for same domain
|
|
// - Verify 409 Conflict
|
|
|
|
// TestGetChallenge_Unauthorized
|
|
// - User A creates challenge
|
|
// - User B tries to access
|
|
// - Verify 403 Forbidden
|
|
```
|
|
|
|
#### Implementation Strategy
|
|
|
|
1. **Mock Services**
|
|
- Use existing `ManualChallengeServiceInterface`
|
|
- Add authorization failure scenarios
|
|
|
|
2. **Time-Based Testing**
|
|
- Mock challenge timestamps for expiry tests
|
|
|
|
**Estimated Effort:** 1.5 developer-days
|
|
|
|
- **ROLLBACK immediately** if:
|
|
- Production deployments are affected
|
|
- Core functionality breaks (API, routing, healthchecks)
|
|
- Security posture degrades
|
|
- No clear remediation path within 30 minutes
|
|
|
|
### File 10: backend/internal/api/handlers/crowdsec_exec.go (149 lines)
|
|
|
|
**Current Coverage:** 0% (1 line missing, 0 partials)
|
|
**Target Coverage:** 100%
|
|
**Complexity:** 🟢 LOW (PID process checks)
|
|
|
|
**Existing Test File:** `backend/internal/api/handlers/crowdsec_exec_test.go`
|
|
|
|
#### Functions Requiring Coverage
|
|
|
|
1. **isCrowdSecProcess() - Lines 35-45**
|
|
- Missing: Non-CrowdSec process rejection
|
|
|
|
#### Test Cases Required
|
|
|
|
```go
|
|
// TestIsCrowdSecProcess_ValidProcess
|
|
// - Create mock /proc/{pid}/cmdline with "crowdsec"
|
|
// - Call isCrowdSecProcess
|
|
// - Verify returns true
|
|
|
|
// TestIsCrowdSecProcess_WrongProcess
|
|
// - Create mock /proc/{pid}/cmdline with "nginx"
|
|
// - Verify returns false (PID recycling protection)
|
|
```
|
|
|
|
#### Implementation Strategy
|
|
|
|
1. **Mock /proc filesystem**
|
|
- Use existing `procPath` field for testing
|
|
- Create temp directory with fake cmdline files
|
|
|
|
**Estimated Effort:** 0.5 developer-days
|
|
|
|
- name: Update Dockerfile
|
|
if: steps.checksum.outputs.current != steps.checksum.outputs.old
|
|
run: |
|
|
sed -i "s/ARG GEOLITE2_COUNTRY_SHA256=.*/ARG GEOLITE2_COUNTRY_SHA256=${{ steps.checksum.outputs.current }}/" Dockerfile
|
|
|
|
## 3. Implementation Priority
|
|
|
|
### Phase 1: Critical Files (Week 1)
|
|
**Goal:** Recover 50% of missing coverage
|
|
|
|
1. **import_handler.go** (3 days)
|
|
- Highest missing line count (33 lines)
|
|
- Business-critical import feature
|
|
|
|
2. **crowdsec_handler.go** (2.5 days)
|
|
- Core security module functionality
|
|
- Complex LAPI integration
|
|
|
|
**Deliverable:** +40 lines coverage, patch coverage → 75%
|
|
|
|
---
|
|
|
|
### Phase 2: High-Impact Files (Week 2)
|
|
**Goal:** Recover 35% of missing coverage
|
|
|
|
3. **hub_sync.go** (2 days)
|
|
- SSRF protection critical
|
|
- CrowdSec hub operations
|
|
|
|
4. **security_service.go** (2 days)
|
|
- Audit logging correctness
|
|
- Break-glass token security
|
|
|
|
5. **backup_service.go** (1.5 days)
|
|
- Data integrity critical
|
|
|
|
**Deliverable:** +35 lines coverage, patch coverage → 90%
|
|
|
|
---
|
|
|
|
### Phase 3: Remaining Files (Week 3)
|
|
**Goal:** Achieve 100% patch coverage
|
|
|
|
6. **importer.go** (1.5 days)
|
|
7. **manual_challenge_handler.go** (1.5 days)
|
|
8. **hub_cache.go** (1 day)
|
|
9. **feature_flags_handler.go** (0.5 days)
|
|
10. **crowdsec_exec.go** (0.5 days)
|
|
|
|
**Deliverable:** 100% patch coverage, 86%+ local coverage
|
|
|
|
---
|
|
|
|
## 4. Test Strategy
|
|
|
|
### Testing Principles
|
|
|
|
1. **Table-Driven Tests**
|
|
- Use for input validation and error paths
|
|
- Pattern: `tests := []struct{ name, input, wantErr string }`
|
|
|
|
2. **Interface Mocking**
|
|
- Mock external dependencies (Executor, HTTPClient, DB)
|
|
- Pattern: Define `*Interface` type, create `mock*` struct
|
|
|
|
3. **Test Database Isolation**
|
|
- Use `OpenTestDB()` for in-memory SQLite
|
|
- Clean up with `defer db.Close()`
|
|
|
|
4. **Context Testing**
|
|
- Test cancellation at critical points
|
|
- Use `context.WithCancel()` in tests
|
|
|
|
5. **Error Injection**
|
|
- Mock failures at boundaries (disk full, network timeout, DB error)
|
|
- Verify graceful degradation
|
|
|
|
### Test File Organization
|
|
|
|
```
|
|
backend/internal/
|
|
├── api/handlers/
|
|
│ ├── import_handler.go
|
|
│ ├── import_handler_test.go # Existing
|
|
│ ├── crowdsec_handler.go
|
|
│ ├── crowdsec_handler_test.go # Existing
|
|
│ ├── manual_challenge_handler.go
|
|
│ └── manual_challenge_handler_test.go # Existing
|
|
├── services/
|
|
│ ├── backup_service.go
|
|
│ ├── backup_service_test.go # Existing
|
|
│ ├── security_service.go
|
|
│ └── security_service_test.go # Existing
|
|
└── crowdsec/
|
|
├── hub_sync.go
|
|
├── hub_sync_test.go # Existing
|
|
├── hub_cache.go
|
|
└── hub_cache_test.go # Existing
|
|
```
|
|
|
|
### Mock Patterns
|
|
|
|
#### Example: Mock Importer
|
|
|
|
```go
|
|
// NEW: Define interface in import_handler.go
|
|
type ImporterInterface interface {
|
|
NormalizeCaddyfile(content string) (string, error)
|
|
ParseCaddyfile(path string) ([]byte, error)
|
|
ExtractHosts(json []byte) (*ImportResult, error)
|
|
}
|
|
|
|
// In test file
|
|
type mockImporter struct {
|
|
normalizeErr error
|
|
parseErr error
|
|
}
|
|
|
|
func (m *mockImporter) NormalizeCaddyfile(content string) (string, error) {
|
|
if m.normalizeErr != nil {
|
|
return "", m.normalizeErr
|
|
}
|
|
return content, nil
|
|
}
|
|
```
|
|
|
|
#### Example: Table-Driven Path Safety
|
|
|
|
```go
|
|
func TestSafeJoin_PathTraversal(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
base string
|
|
path string
|
|
wantErr bool
|
|
}{
|
|
{"relative safe", "/tmp", "file.txt", false},
|
|
{"parent traversal", "/tmp", "../etc/passwd", true},
|
|
{"double parent", "/tmp", "../../root", true},
|
|
{"absolute", "/tmp", "/etc/passwd", true},
|
|
{"hidden parent", "/tmp", "a/../../../etc", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := safeJoin(tt.base, tt.path)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("wantErr=%v, got err=%v", tt.wantErr, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
### Coverage Validation
|
|
|
|
After each phase, run:
|
|
|
|
```bash
|
|
# Local coverage
|
|
go test ./backend/... -coverprofile=coverage.out
|
|
go tool cover -func=coverage.out | grep total
|
|
|
|
# Expected output after Phase 3:
|
|
# total: (statements) 86.0%
|
|
|
|
# Codecov patch coverage (CI)
|
|
# Expected: 100% for all modified lines
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Definition of Done
|
|
|
|
### Coverage Metrics
|
|
|
|
- [x] **Local Coverage:** 86%+ (via `go test -cover`)
|
|
- [x] **CI Coverage:** 85%+ (Codecov reports slightly lower)
|
|
- [x] **Patch Coverage:** 100% (no modified line uncovered)
|
|
|
|
### Code Quality
|
|
|
|
- [x] All tests pass on first run (no flakes)
|
|
- [x] No skipped tests (`.Skip()` only for valid reasons)
|
|
- [x] No truncated test output (avoid `head`, `tail`)
|
|
- [x] Table-driven tests for input validation
|
|
- [x] Mock interfaces for external dependencies
|
|
|
|
### Documentation
|
|
|
|
- [x] Test file comments explain complex scenarios
|
|
- [x] Test names follow convention: `Test<Function>_<Scenario>`
|
|
- [x] Failure messages are actionable
|
|
|
|
### Security
|
|
|
|
- [x] GORM Security Scanner passes (manual stage)
|
|
- [x] Path traversal tests in place
|
|
- [x] SSRF protection validated
|
|
- [x] No hardcoded credentials in tests
|
|
|
|
### CI Integration
|
|
|
|
- [x] All tests pass in CI pipeline
|
|
- [x] Codecov patch coverage gate passes
|
|
- [x] No new linting errors introduced
|
|
|
|
---
|
|
|
|
## 6. Continuous Validation
|
|
|
|
### Pre-Commit Checks
|
|
|
|
```bash
|
|
# Run before each commit
|
|
make test-backend-coverage
|
|
|
|
# Expected output:
|
|
# ✅ Coverage: 86.2%
|
|
# ✅ All tests passed
|
|
```
|
|
|
|
### Daily Monitoring
|
|
|
|
- Check Codecov dashboard for regression
|
|
- Review failed CI runs immediately
|
|
- Monitor test execution time (should be <5 min for all backend tests)
|
|
|
|
### Weekly Review
|
|
|
|
- Analyze coverage trends (should not drop below 85%)
|
|
- Identify new untested code from PRs
|
|
- Update this plan if new files need coverage
|
|
|
|
---
|
|
|
|
## 7. Risk Mitigation
|
|
|
|
### Identified Risks
|
|
|
|
1. **Flaky Tests**
|
|
- **Mitigation:** Avoid time.Sleep, use mocks for time
|
|
- **Detection:** Run tests 10x locally before commit
|
|
|
|
2. **Mock Drift**
|
|
- **Mitigation:** Keep mocks in sync with interfaces
|
|
- **Detection:** CI will fail if interface changes
|
|
|
|
3. **Coverage Regression**
|
|
- **Mitigation:** Codecov patch coverage gate (100%)
|
|
- **Detection:** Automated on every PR
|
|
|
|
4. **Test Maintenance Burden**
|
|
- **Mitigation:** Use table-driven tests to reduce duplication
|
|
- **Detection:** Review test LOC vs production LOC ratio (should be <2:1)
|
|
|
|
---
|
|
|
|
## 8. Rollout Plan
|
|
|
|
### Week 1: Critical Files (10 points)
|
|
- Day 1-2: import_handler.go tests
|
|
- Day 3-4: crowdsec_handler.go tests
|
|
- Day 5: Integration testing, coverage validation
|
|
- **Checkpoint:** Coverage → 75%
|
|
|
|
### Week 2: High-Impact Files (20 points)
|
|
- Day 1-2: hub_sync.go + security_service.go
|
|
- Day 3-4: backup_service.go
|
|
- Day 5: Integration testing, coverage validation
|
|
- **Checkpoint:** Coverage → 90%
|
|
|
|
### Week 3: Remaining Files (20 points)
|
|
- Day 1: importer.go + manual_challenge_handler.go
|
|
- Day 2: hub_cache.go + feature_flags_handler.go + crowdsec_exec.go
|
|
- Day 3: Full regression testing
|
|
- Day 4: Documentation, PR prep
|
|
- Day 5: Code review iterations
|
|
- **Checkpoint:** Coverage → 86%+, patch coverage 100%
|
|
|
|
---
|
|
|
|
## 9. Success Criteria
|
|
|
|
### Quantitative Metrics
|
|
|
|
| Metric | Current | Target | Status |
|
|
|--------|---------|--------|--------|
|
|
| Local Coverage | ~60% | 86%+ | ❌ |
|
|
| CI Coverage | ~58% | 85%+ | ❌ |
|
|
| Patch Coverage | 59.33% | 100% | ❌ |
|
|
| Missing Lines | 98 | 0 | ❌ |
|
|
| Partial Branches | 35 | 0 | ❌ |
|
|
|
|
### Qualitative Outcomes
|
|
|
|
- [x] All critical business logic paths tested
|
|
- [x] Security validation (SSRF, path traversal) enforced by tests
|
|
- [x] Error handling coverage comprehensive
|
|
- [x] Flake-free test suite
|
|
- [x] Fast test execution (<5 min total)
|
|
|
|
---
|
|
|
|
## 10. Post-Implementation
|
|
|
|
### Maintenance
|
|
|
|
1. **New Code Requirements**
|
|
- All new functions must have tests
|
|
- Pre-commit hooks enforce coverage
|
|
- PR reviews check patch coverage
|
|
|
|
2. **Regression Prevention**
|
|
- Codecov threshold: 85% enforced
|
|
- Weekly coverage reports
|
|
- Coverage trends dashboard
|
|
|
|
3. **Test Debt Tracking**
|
|
- Label low-coverage files in backlog
|
|
- Quarterly coverage audits
|
|
- Test optimization sprints
|
|
|
|
**Debug Steps:**
|
|
1. **Check specific stage build:**
|
|
```bash
|
|
# Test specific stage
|
|
docker build --target backend-builder -t test-backend .
|
|
docker build --target frontend-builder -t test-frontend .
|
|
```
|
|
|
|
## Appendix A: Test Execution Commands
|
|
|
|
```bash
|
|
# Run all backend tests
|
|
go test ./backend/...
|
|
|
|
# Run with coverage
|
|
go test ./backend/... -coverprofile=coverage.out
|
|
|
|
# View coverage report in browser
|
|
go tool cover -html=coverage.out
|
|
|
|
# Run specific file's tests
|
|
go test -v ./backend/internal/api/handlers/import_handler_test.go
|
|
|
|
# Run with race detector (slower but catches concurrency bugs)
|
|
go test -race ./backend/...
|
|
|
|
# Generate coverage for CI
|
|
go test ./backend/... -coverprofile=coverage.out -covermode=atomic
|
|
```
|
|
|
|
---
|
|
|
|
## Appendix B: Reference Test Files
|
|
|
|
Best examples to follow from existing codebase:
|
|
|
|
1. **Table-Driven:** `backend/internal/caddy/importer_test.go`
|
|
2. **Mock Interfaces:** `backend/internal/api/handlers/auth_handler_test.go`
|
|
3. **Test DB Usage:** `backend/internal/testutil/testdb_test.go`
|
|
4. **Error Injection:** `backend/internal/services/proxyhost_service_test.go`
|
|
5. **Context Testing:** `backend/internal/network/safeclient_test.go`
|
|
|
|
---
|
|
|
|
## Sign-Off
|
|
|
|
**Plan Version:** 1.0
|
|
**Created:** 2025-01-XX
|
|
**Status:** APPROVED FOR IMPLEMENTATION
|
|
|
|
**Next Steps:**
|
|
1. Review plan with team
|
|
2. Begin Phase 1 implementation
|
|
3. Daily standup on progress
|
|
4. Weekly coverage checkpoint reviews
|