fix: Add unit tests for emergency bypass and backup service validation

This commit is contained in:
GitHub Actions
2026-02-18 17:33:20 +00:00
parent 5ee63ad381
commit 983ec7a42e
8 changed files with 293 additions and 0 deletions

View File

@@ -381,6 +381,46 @@ func TestEmergencySecurityReset_ClearsBlockDecisions(t *testing.T) {
assert.Equal(t, "allow", remaining[0].Action)
}
func TestEmergencySecurityReset_MiddlewarePrevalidatedBypass(t *testing.T) {
db := setupEmergencyTestDB(t)
handler := NewEmergencyHandler(db)
gin.SetMode(gin.TestMode)
router := gin.New()
router.POST("/api/v1/emergency/security-reset", func(c *gin.Context) {
c.Set("emergency_bypass", true)
handler.SecurityReset(c)
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
}
func TestEmergencySecurityReset_MiddlewareBypass_ResetFailure(t *testing.T) {
db := setupEmergencyTestDB(t)
handler := NewEmergencyHandler(db)
stdDB, err := db.DB()
require.NoError(t, err)
require.NoError(t, stdDB.Close())
gin.SetMode(gin.TestMode)
router := gin.New()
router.POST("/api/v1/emergency/security-reset", func(c *gin.Context) {
c.Set("emergency_bypass", true)
handler.SecurityReset(c)
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/emergency/security-reset", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
require.Equal(t, http.StatusInternalServerError, w.Code)
}
func TestLogEnhancedAudit(t *testing.T) {
// Setup
db := setupEmergencyTestDB(t)

View File

@@ -33,6 +33,30 @@ func TestEmergencyBypass_NoToken(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
}
func TestEmergencyBypass_InvalidClientIP(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("CHARON_EMERGENCY_TOKEN", "test-token-that-meets-minimum-length-requirement-32-chars")
router := gin.New()
managementCIDRs := []string{"127.0.0.0/8"}
router.Use(EmergencyBypass(managementCIDRs, nil))
router.GET("/test", func(c *gin.Context) {
_, exists := c.Get("emergency_bypass")
assert.False(t, exists, "Emergency bypass flag should not be set for invalid client IP")
c.JSON(http.StatusOK, gin.H{"message": "ok"})
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
req.Header.Set(EmergencyTokenHeader, "test-token-that-meets-minimum-length-requirement-32-chars")
req.RemoteAddr = "invalid-remote-addr"
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestEmergencyBypass_ValidToken(t *testing.T) {
// Test that valid token from allowed IP sets bypass flag
gin.SetMode(gin.TestMode)

View File

@@ -244,3 +244,22 @@ func TestMiddleware_ACLDisabledDoesNotBlock(t *testing.T) {
// Disabled ACL should not block
require.False(t, ctx.IsAborted())
}
func TestMiddleware_EmergencyBypassSkipsChecks(t *testing.T) {
t.Parallel()
db := setupDB(t)
c := cerberus.New(config.SecurityConfig{CerberusEnabled: true, ACLMode: "enabled"}, db)
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
req := httptest.NewRequest(http.MethodGet, "/admin/secure", nil)
req.RemoteAddr = "203.0.113.10:1234"
ctx.Request = req
ctx.Set("emergency_bypass", true)
mw := c.Middleware()
mw(ctx)
require.False(t, ctx.IsAborted(), "middleware should short-circuit when emergency_bypass=true")
}

View File

@@ -2,6 +2,9 @@ package crowdsec
import (
"context"
"errors"
"os"
"path/filepath"
"testing"
"time"
@@ -168,6 +171,22 @@ func TestHubCacheLoadInvalidSlug(t *testing.T) {
require.Error(t, err)
}
func TestHubCacheLoadMetadataReadError(t *testing.T) {
t.Parallel()
baseDir := t.TempDir()
cache, err := NewHubCache(baseDir, time.Hour)
require.NoError(t, err)
slugDir := filepath.Join(baseDir, "crowdsecurity", "demo")
require.NoError(t, os.MkdirAll(slugDir, 0o750))
require.NoError(t, os.Mkdir(filepath.Join(slugDir, "metadata.json"), 0o750))
_, err = cache.Load(context.Background(), "crowdsecurity/demo")
require.Error(t, err)
require.False(t, errors.Is(err, ErrCacheMiss))
}
func TestHubCacheExistsContextCanceled(t *testing.T) {
t.Parallel()
cache, err := NewHubCache(t.TempDir(), time.Hour)

View File

@@ -1713,6 +1713,41 @@ func TestHubHTTPErrorCanFallback(t *testing.T) {
})
}
func TestHubServiceFetchWithFallbackStopsOnNonFallbackError(t *testing.T) {
t.Parallel()
svc := NewHubService(nil, nil, t.TempDir())
attempts := 0
svc.HTTPClient = &http.Client{Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
attempts++
return newResponse(http.StatusBadRequest, "bad request"), nil
})}
_, _, err := svc.fetchWithFallback(context.Background(), []string{"https://hub.crowdsec.net/a", "https://raw.githubusercontent.com/crowdsecurity/hub/master/b"})
require.Error(t, err)
require.Equal(t, 1, attempts)
}
func TestHubServiceFetchWithFallbackRetriesWhenErrorCanFallback(t *testing.T) {
t.Parallel()
svc := NewHubService(nil, nil, t.TempDir())
attempts := 0
svc.HTTPClient = &http.Client{Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
attempts++
if attempts == 1 {
return newResponse(http.StatusServiceUnavailable, "unavailable"), nil
}
return newResponse(http.StatusOK, "ok"), nil
})}
data, used, err := svc.fetchWithFallback(context.Background(), []string{"https://hub.crowdsec.net/a", "https://raw.githubusercontent.com/crowdsecurity/hub/master/b"})
require.NoError(t, err)
require.Equal(t, "ok", string(data))
require.Equal(t, "https://raw.githubusercontent.com/crowdsecurity/hub/master/b", used)
require.Equal(t, 2, attempts)
}
// TestValidateHubURL_EdgeCases tests additional edge cases for SSRF protection
func TestValidateHubURL_EdgeCases(t *testing.T) {
t.Parallel()

View File

@@ -2,6 +2,7 @@ package services
import (
"archive/zip"
"bytes"
"os"
"path/filepath"
"strings"
@@ -90,3 +91,49 @@ func TestBackupService_ExtractDatabaseFromBackup_ExtractWalFailure(t *testing.T)
_, err = svc.extractDatabaseFromBackup(zipPath)
require.Error(t, err)
}
func TestBackupService_UnzipWithSkip_RejectsPathTraversal(t *testing.T) {
tmp := t.TempDir()
destDir := filepath.Join(tmp, "data")
require.NoError(t, os.MkdirAll(destDir, 0o700))
zipPath := filepath.Join(tmp, "path-traversal.zip")
zipFile := openZipInTempDir(t, tmp, zipPath)
writer := zip.NewWriter(zipFile)
entry, err := writer.Create("../escape.txt")
require.NoError(t, err)
_, err = entry.Write([]byte("evil"))
require.NoError(t, err)
require.NoError(t, writer.Close())
require.NoError(t, zipFile.Close())
svc := &BackupService{DataDir: destDir, DatabaseName: "charon.db"}
err = svc.unzipWithSkip(zipPath, destDir, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid file path in archive")
}
func TestBackupService_UnzipWithSkip_RejectsExcessiveUncompressedSize(t *testing.T) {
tmp := t.TempDir()
destDir := filepath.Join(tmp, "data")
require.NoError(t, os.MkdirAll(destDir, 0o700))
zipPath := filepath.Join(tmp, "oversized.zip")
zipFile := openZipInTempDir(t, tmp, zipPath)
writer := zip.NewWriter(zipFile)
entry, err := writer.Create("huge.bin")
require.NoError(t, err)
_, err = entry.Write(bytes.Repeat([]byte("a"), 101*1024*1024))
require.NoError(t, err)
require.NoError(t, writer.Close())
require.NoError(t, zipFile.Close())
svc := &BackupService{DataDir: destDir, DatabaseName: "charon.db"}
err = svc.unzipWithSkip(zipPath, destDir, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "exceeded decompression limit")
}

View File

@@ -190,6 +190,27 @@ func TestCheckMonitor_TCPFailure(t *testing.T) {
require.NotEmpty(t, hb.Message)
}
func TestCreateMonitor_AppliesDefaultIntervalAndRetries(t *testing.T) {
db := setupUnitTestDB(t)
svc := NewUptimeService(db, nil)
monitor, err := svc.CreateMonitor("defaults", "http://example.com", "http", 0, 0)
require.NoError(t, err)
require.Equal(t, 60, monitor.Interval)
require.Equal(t, 3, monitor.MaxRetries)
require.Equal(t, "pending", monitor.Status)
require.True(t, monitor.Enabled)
}
func TestCreateMonitor_TCPRequiresHostPort(t *testing.T) {
db := setupUnitTestDB(t)
svc := NewUptimeService(db, nil)
_, err := svc.CreateMonitor("bad-tcp", "example.com", "tcp", 60, 2)
require.Error(t, err)
require.Contains(t, err.Error(), "TCP URL must be in host:port format")
}
// TestCheckMonitor_UnknownType tests unknown monitor type
func TestCheckMonitor_UnknownType(t *testing.T) {
db := setupUnitTestDB(t)

View File

@@ -647,3 +647,91 @@ After user approval of this plan:
2. Execute PR-2 (quality/open findings) second.
3. Execute PR-3 (hygiene/config hardening) third.
4. Submit final supervisor review with linked evidence and closure checklist.
## Patch-Coverage Uplift Addendum (CodeQL Remediation Branch)
### Scope
Input baseline (`docs/plans/codecove_patch_report.md`): 18 uncovered patch lines across 9 backend files.
Goal: close uncovered branches with minimal, branch-specific tests only (no broad refactors).
### 1) Exact test files to add/update
- Update `backend/internal/api/handlers/emergency_handler_test.go`
- Update `backend/internal/api/handlers/proxy_host_handler_update_test.go`
- Update `backend/internal/crowdsec/hub_sync_test.go`
- Update `backend/internal/api/handlers/crowdsec_pull_apply_integration_test.go`
- Update `backend/internal/services/backup_service_wave3_test.go`
- Update `backend/internal/services/uptime_service_unit_test.go`
- Update `backend/internal/api/middleware/emergency_test.go`
- Update `backend/internal/cerberus/cerberus_middleware_test.go`
- Update `backend/internal/crowdsec/hub_cache_test.go`
### 2) Minimal branch-execution scenarios
#### `backend/internal/api/handlers/emergency_handler.go` (3 lines)
- Add middleware-prevalidated reset test: set `emergency_bypass=true` in context and assert `SecurityReset` takes middleware path and returns success.
- Add reset failure-path test: force module-disable failure (closed DB/failed upsert) and assert HTTP 500 path executes.
#### `backend/internal/api/handlers/proxy_host_handler.go` (3 lines)
- Add update payload case with `security_header_profile_id` as valid string to execute string-conversion success path.
- Add update payload case with invalid string to execute string parse failure branch.
- Add update payload case with unsupported type (boolean/object) to execute unsupported-type branch.
#### `backend/internal/crowdsec/hub_sync.go` (3 lines)
- Add apply scenario where cache metadata exists but archive read fails, forcing refresh path and post-refresh archive read.
- Add fallback fetch scenario with first endpoint returning fallback-eligible error, second endpoint success.
- Add fallback-stop scenario with non-fallback error to execute early break path.
#### `backend/internal/api/handlers/crowdsec_handler.go` (2 lines)
- Add apply test where cached meta exists but archive/preview file stat fails to execute missing-file log branches before apply.
- Add pull/apply branch case that exercises cache-miss diagnostics and response payload path.
#### `backend/internal/services/backup_service.go` (2 lines)
- Add unzip-with-skip test with oversized decompressed entry to execute decompression-limit rejection branch.
- Add unzip-with-skip error-path test that validates extraction abort handling for invalid archive entry flow.
#### `backend/internal/services/uptime_service.go` (2 lines)
- Add `CreateMonitor` test with `interval<=0` and `max_retries<=0` to execute defaulting branches.
- Add TCP monitor validation case with invalid `host:port` input to execute TCP validation error path.
#### `backend/internal/api/middleware/emergency.go` (1 line)
- Add malformed client IP test (`RemoteAddr` unparsable) with token present to execute invalid-IP branch and confirm bypass is not set.
#### `backend/internal/cerberus/cerberus.go` (1 line)
- Add middleware test with `emergency_bypass=true` in gin context and ACL enabled to execute bypass short-circuit branch.
#### `backend/internal/crowdsec/hub_cache.go` (1 line)
- Add cache-load test that causes non-ENOENT metadata read failure (e.g., invalid metadata path state) to execute hard read-error branch (not `ErrCacheMiss`).
### 3) Verification commands (targeted + patch report)
Run targeted backend tests only:
```bash
cd /projects/Charon
go test ./backend/internal/api/handlers -run 'TestEmergency|TestProxyHostUpdate|TestPullThenApply|TestApplyWithoutPull|TestApplyRollbackWhenCacheMissingAndRepullFails'
go test ./backend/internal/crowdsec -run 'TestPull|TestApply|TestFetchWith|TestHubCache'
go test ./backend/internal/services -run 'TestBackupService_UnzipWithSkip|TestCreateMonitor|TestUpdateMonitor|TestDeleteMonitor'
go test ./backend/internal/api/middleware -run 'TestEmergencyBypass'
go test ./backend/internal/cerberus -run 'TestMiddleware_'
```
Generate local patch coverage report artifacts:
```bash
cd /projects/Charon
bash scripts/local-patch-report.sh
```
Expected artifacts:
- `test-results/local-patch-report.md`
- `test-results/local-patch-report.json`
### 4) Acceptance criteria
- Patch coverage increases from `79.31034%` to `>= 90%` for this remediation branch.
- Missing patch lines decrease from `18` to `<= 6` (target `0` if all branches are feasibly testable).
- All nine listed backend files show reduced missing-line counts in local patch report output.
- Targeted test commands pass with zero failures.