diff --git a/.github/agents/Managment.agent.md b/.github/agents/Managment.agent.md index 892f644e..5a279e0b 100644 --- a/.github/agents/Managment.agent.md +++ b/.github/agents/Managment.agent.md @@ -72,22 +72,30 @@ You are "lazy" in the smartest way possible. You never do what a subordinate can The task is not complete until ALL of the following pass with zero issues: -1. **Coverage Tests (MANDATORY - Verify Explicitly)**: +1. **Playwright E2E Tests (MANDATORY - Run First)**: + - **Run**: `npx playwright test --project=chromium` from project root + - **Why First**: If the app is broken at E2E level, unit tests may need updates. Catch integration issues early. + - **Scope**: Run tests relevant to modified features (e.g., `tests/manual-dns-provider.spec.ts`) + - **On Failure**: Trace root cause through frontend → backend flow before proceeding + - **Base URL**: Uses `PLAYWRIGHT_BASE_URL` or default from `playwright.config.js` + - All E2E tests must pass before proceeding to unit tests + +2. **Coverage Tests (MANDATORY - Verify Explicitly)**: - **Backend**: Ensure `Backend_Dev` ran VS Code task "Test: Backend with Coverage" or `scripts/go-test-coverage.sh` - **Frontend**: Ensure `Frontend_Dev` ran VS Code task "Test: Frontend with Coverage" or `scripts/frontend-test-coverage.sh` - **Why**: These are in manual stage of pre-commit for performance. Subagents MUST run them via VS Code tasks or scripts. - Minimum coverage: 85% for both backend and frontend. - All tests must pass with zero failures. -2. **Type Safety (Frontend)**: +3. **Type Safety (Frontend)**: - Ensure `Frontend_Dev` ran VS Code task "Lint: TypeScript Check" or `npm run type-check` - **Why**: This check is in manual stage of pre-commit for performance. Subagents MUST run it explicitly. -3. **Pre-commit Hooks**: Ensure `QA_Security` ran `pre-commit run --all-files` (fast hooks only; coverage was verified in step 1) +4. **Pre-commit Hooks**: Ensure `QA_Security` ran `pre-commit run --all-files` (fast hooks only; coverage was verified in step 2) -4. **Security Scans**: Ensure `QA_Security` ran CodeQL and Trivy with zero Critical or High severity issues +5. **Security Scans**: Ensure `QA_Security` ran CodeQL and Trivy with zero Critical or High severity issues -5. **Linting**: All language-specific linters must pass +6. **Linting**: All language-specific linters must pass **Your Role**: You delegate implementation to subagents, but YOU are responsible for verifying they completed the Definition of Done. Do not accept "DONE" from a subagent until you have confirmed they ran coverage tests, type checks, and security scans explicitly. diff --git a/.github/agents/Planning.agent.md b/.github/agents/Planning.agent.md index 1ff9bc08..3ae64213 100644 --- a/.github/agents/Planning.agent.md +++ b/.github/agents/Planning.agent.md @@ -67,9 +67,12 @@ Your goal is to design the **User Experience** first, then engineer the **Backen } ``` -### 🕵️ Phase 1: QA & Security +### 🕵️ Phase 1: Playwright E2E Tests (Run First) - 1. Build tests for coverage of perposed code additions and chages based on how the code SHOULD work + 1. Run `npx playwright test --project=chromium` to verify app functions correctly + 2. If tests fail, trace root cause through frontend → backend flow + 3. Write/update Playwright tests for new features in `tests/*.spec.ts` + 4. Build unit tests for coverage of proposed code additions and changes based on how the code SHOULD work ### 🏗️ Phase 2: Backend Implementation (Go) @@ -89,15 +92,19 @@ Your goal is to design the **User Experience** first, then engineer the **Backen ### 🕵️ Phase 3: QA & Security - 1. Edge Cases: {List specific scenarios to test} - 2. **Coverage Tests (MANDATORY)**: + 1. **Playwright E2E Tests (MANDATORY - Run First)**: + - Run `npx playwright test --project=chromium` from project root + - All E2E tests must pass BEFORE running unit tests + - If E2E fails, trace root cause and fix before proceeding + 2. Edge Cases: {List specific scenarios to test} + 3. **Coverage Tests (MANDATORY - After E2E passes)**: - Backend: Run VS Code task "Test: Backend with Coverage" or execute `scripts/go-test-coverage.sh` - Frontend: Run VS Code task "Test: Frontend with Coverage" or execute `scripts/frontend-test-coverage.sh` - Minimum coverage: 85% for both backend and frontend - **Critical**: These are in manual stage of pre-commit for performance. Agents MUST run them via VS Code tasks or scripts before marking tasks complete. - 3. Security: Run CodeQL and Trivy scans. Triage and fix any new errors or warnings. - 4. **Type Safety (Frontend)**: Run VS Code task "Lint: TypeScript Check" or execute `cd frontend && npm run type-check` - 5. Linting: Run `pre-commit` hooks on all files and triage anything not auto-fixed. + 4. Security: Run CodeQL and Trivy scans. Triage and fix any new errors or warnings. + 5. **Type Safety (Frontend)**: Run VS Code task "Lint: TypeScript Check" or execute `cd frontend && npm run type-check` + 6. Linting: Run `pre-commit` hooks on all files and triage anything not auto-fixed. ### 📚 Phase 4: Documentation diff --git a/.github/agents/QA_Security.agent.md b/.github/agents/QA_Security.agent.md index 46e158e7..156e98a5 100644 --- a/.github/agents/QA_Security.agent.md +++ b/.github/agents/QA_Security.agent.md @@ -70,13 +70,21 @@ When Trivy or CodeQLreports CVEs in container dependencies (especially Caddy tra The task is not complete until ALL of the following pass with zero issues: -1. **Security Scans**: +1. **Playwright E2E Tests (MANDATORY - Run First)**: + - **Run**: `npx playwright test --project=chromium` from project root + - **Why First**: If the app is broken at E2E level, unit tests may need updates. Catch integration issues early. + - **Scope**: Run tests relevant to modified features (e.g., `tests/manual-dns-provider.spec.ts`) + - **On Failure**: Trace root cause through frontend → backend flow, report to Management or Dev subagent + - **Base URL**: Uses `PLAYWRIGHT_BASE_URL` or default `http://100.98.12.109:8080` + - All E2E tests must pass before proceeding + +2. **Security Scans**: - CodeQL: Run VS Code task "Security: CodeQL All (CI-Aligned)" or individual Go/JS tasks - Trivy: Run VS Code task "Security: Trivy Scan" - Go Vulnerabilities: Run VS Code task "Security: Go Vulnerability Check" - Zero Critical/High issues allowed -2. **Coverage Tests (MANDATORY - Run Explicitly)**: +3. **Coverage Tests (MANDATORY - Run Explicitly)**: - **MANDATORY**: Patch coverage must cover 100% of new/modified code. This prevents CodeCov Report failing CI. - **Backend**: Run VS Code task "Test: Backend with Coverage" or execute `scripts/go-test-coverage.sh` - **Frontend**: Run VS Code task "Test: Frontend with Coverage" or execute `scripts/frontend-test-coverage.sh` @@ -84,14 +92,14 @@ The task is not complete until ALL of the following pass with zero issues: - Minimum coverage: 85% for both backend and frontend. - All tests must pass with zero failures. -3. **Type Safety (Frontend)**: +4. **Type Safety (Frontend)**: - Run VS Code task "Lint: TypeScript Check" or execute `cd frontend && npm run type-check` - **Why**: This check is in manual stage of pre-commit for performance. You MUST run it explicitly. - Fix all type errors immediately. -4. **Pre-commit Hooks**: Run `pre-commit run --all-files` (this runs fast hooks only; coverage was verified in step 1) +5. **Pre-commit Hooks**: Run `pre-commit run --all-files` (this runs fast hooks only; coverage was verified in step 3) -5. **Linting (MANDATORY - Run All Explicitly)**: +6. **Linting (MANDATORY - Run All Explicitly)**: - **Backend GolangCI-Lint**: Run VS Code task "Lint: GolangCI-Lint (Docker)" - This is the FULL linter suite including gocritic, bodyclose, etc. - **Why**: "Lint: Go Vet" only runs `go vet`, NOT the full golangci-lint suite. CI runs golangci-lint, so you MUST run this task to match CI behavior. - **Command**: `cd backend && docker run --rm -v $(pwd):/app:ro -w /app golangci/golangci-lint:latest golangci-lint run -v` diff --git a/.github/instructions/copilot-instructions.md b/.github/instructions/copilot-instructions.md index 67df4632..b0002363 100644 --- a/.github/instructions/copilot-instructions.md +++ b/.github/instructions/copilot-instructions.md @@ -114,7 +114,15 @@ Before proposing ANY code change or fix, you must build a mental map of the feat Before marking an implementation task as complete, perform the following in order: -1. **Security Scans** (MANDATORY - Zero Tolerance): +1. **Playwright E2E Tests** (MANDATORY - Run First): + - **Run**: `npx playwright test --project=chromium` from project root + - **Why First**: If the app is broken at E2E level, unit tests may need updates. Catch integration issues early. + - **Scope**: Run tests relevant to modified features (e.g., `tests/manual-dns-provider.spec.ts`) + - **On Failure**: Trace root cause through frontend → backend flow before proceeding + - **Base URL**: Uses `PLAYWRIGHT_BASE_URL` or default from `playwright.config.js` + - All E2E tests must pass before proceeding to unit tests + +2. **Security Scans** (MANDATORY - Zero Tolerance): - **CodeQL Go Scan**: Run VS Code task "Security: CodeQL Go Scan (CI-Aligned)" OR `pre-commit run codeql-go-scan --all-files` - Must use `security-and-quality` suite (CI-aligned) - **Zero high/critical (error-level) findings allowed** @@ -136,12 +144,12 @@ Before marking an implementation task as complete, perform the following in orde - Database creation: `--threads=0 --overwrite` - Analysis: `--sarif-add-baseline-file-info` -2. **Pre-Commit Triage**: Run `pre-commit run --all-files`. +3. **Pre-Commit Triage**: Run `pre-commit run --all-files`. - If errors occur, **fix them immediately**. - If logic errors occur, analyze and propose a fix. - Do not output code that violates pre-commit standards. -3. **Staticcheck BLOCKING Validation**: Pre-commit hooks automatically run fast linters including staticcheck. +4. **Staticcheck BLOCKING Validation**: Pre-commit hooks automatically run fast linters including staticcheck. - **CRITICAL:** Staticcheck errors are BLOCKING - you MUST fix them before commit succeeds. - Manual verification: Run VS Code task "Lint: Staticcheck (Fast)" or `make lint-fast` - To check only staticcheck: `make lint-staticcheck-only` @@ -149,7 +157,7 @@ Before marking an implementation task as complete, perform the following in orde - If pre-commit fails: Fix the reported issues, then retry commit - **Do NOT** use `--no-verify` to bypass this check unless emergency hotfix -4. **Coverage Testing** (MANDATORY - Non-negotiable): +5. **Coverage Testing** (MANDATORY - Non-negotiable): - **MANDATORY**: Patch coverage must cover 100% of modified lines (Codecov Patch view must be green). If patch coverage fails, add targeted tests for the missing patch line ranges. - **Backend Changes**: Run the VS Code task "Test: Backend with Coverage" or execute `scripts/go-test-coverage.sh`. - Minimum coverage: 85% (set via `CHARON_MIN_COVERAGE` or `CPM_MIN_COVERAGE`). @@ -162,22 +170,21 @@ Before marking an implementation task as complete, perform the following in orde - **Critical**: Coverage tests are NOT run by default pre-commit hooks (they are in manual stage for performance). You MUST run them explicitly via VS Code tasks or scripts before completing any task. - **Why**: CI enforces coverage in GitHub Actions. Local verification prevents CI failures and maintains code quality. -4. **Type Safety** (Frontend only): +6. **Type Safety** (Frontend only): - Run the VS Code task "Lint: TypeScript Check" or execute `cd frontend && npm run type-check`. - Fix all type errors immediately. This is non-negotiable. - This check is also in manual stage for performance but MUST be run before completion. -5. **Verify Build**: Ensure the backend compiles and the frontend builds without errors. +7. **Verify Build**: Ensure the backend compiles and the frontend builds without errors. - Backend: `cd backend && go build ./...` - Frontend: `cd frontend && npm run build` -6. **Fixed and New Code Testing**: - - Ensure all existing and new unit tests pass with zero failures using Playwright MCP. - - When fasilures and Errors are found, deep-dive into root causes. Using the correct `subAgent`, update the working plan, review the implementation, and fix the issues. +8. **Fixed and New Code Testing**: + - Ensure all existing and new unit tests pass with zero failures. + - When failures and errors are found, deep-dive into root causes. Using the correct `subAgent`, update the working plan, review the implementation, and fix the issues. - No issue is out of scope for investigation and resolution. All issues must be addressed before task completion. - -6. **Clean Up**: Ensure no debug print statements or commented-out blocks remain. +9. **Clean Up**: Ensure no debug print statements or commented-out blocks remain. - Remove `console.log`, `fmt.Println`, and similar debugging statements. - Delete commented-out code blocks. - Remove unused imports. diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md index 76a97630..025bcf0a 100644 --- a/.github/instructions/testing.instructions.md +++ b/.github/instructions/testing.instructions.md @@ -4,6 +4,16 @@ description: 'Strict protocols for test execution, debugging, and coverage valid --- # Testing Protocols +## 0. E2E Verification First (Playwright) + +**MANDATORY**: Before running unit tests, verify the application functions correctly end-to-end. + +* **Run Playwright E2E Tests**: Execute `npx playwright test --project=chromium` from the project root. +* **Why First**: If the application is broken at the E2E level, unit tests may need updates. Playwright catches integration issues early. +* **Base URL**: Tests use `PLAYWRIGHT_BASE_URL` env var or default from `playwright.config.js` (Tailscale IP: `http://100.98.12.109:8080`). +* **On Failure**: Analyze failures, trace root cause through frontend → backend flow, then fix before proceeding to unit tests. +* **Scope**: Run relevant test files for the feature being modified (e.g., `tests/manual-dns-provider.spec.ts`). + ## 1. Execution Environment * **No Truncation:** Never use pipe commands (e.g., `head`, `tail`) or flags that limit stdout/stderr. If a test hangs, it likely requires an interactive input or is caught in a loop; analyze the full output to identify the block. * **Task-Based Execution:** Do not manually construct test strings. Use existing project tasks (e.g., `npm test`, `go test ./...`). If a specific sub-module requires frequent testing, generate a new task definition in the project's configuration file (e.g., `.vscode/tasks.json`) before proceeding. diff --git a/.vscode/settings.json b/.vscode/settings.json index c6645352..99eaab2f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,10 @@ }, "go.useLanguageServer": true, "go.lintOnSave": "workspace", - "go.vetOnSave": "workspace" + "go.vetOnSave": "workspace", + "yaml.validate": false, + "yaml.schemaStore.enable": false, + "files.exclude": {}, + "search.exclude": {}, + "files.associations": {} } diff --git a/backend/.golangci-fast.yml b/backend/.golangci-fast.yml index 128d4048..4421a4fb 100644 --- a/backend/.golangci-fast.yml +++ b/backend/.golangci-fast.yml @@ -24,11 +24,4 @@ linters-settings: - (net/http.ResponseWriter).Write issues: - exclude-rules: - # Exclude test files to match main config behavior - - path: _test\.go - linters: - - staticcheck - - errcheck - - govet - - ineffassign + exclude-generated-strict: true diff --git a/backend/.golangci.yml b/backend/.golangci.yml index a7ac5f64..7f831622 100644 --- a/backend/.golangci.yml +++ b/backend/.golangci.yml @@ -1,5 +1,4 @@ -version: "2" - +# golangci-lint configuration run: timeout: 5m tests: true @@ -15,7 +14,7 @@ linters: - unused - errcheck -linters-config: +linters-settings: gocritic: enabled-tags: - diagnostic @@ -52,19 +51,17 @@ linters-config: - os.WriteFile - os.Remove - (*gorm.io/gorm.DB).AutoMigrate + # Additional test cleanup functions + - (*database/sql.Rows).Close + - (gorm.io/gorm.Migrator).DropTable + - (*net/http.Response.Body).Close + - json.Unmarshal + - (*github.com/Wikid82/charon/backend/models.User).SetPassword + - (*github.com/Wikid82/charon/backend/internal/services.NotificationService).CreateProvider + - (*github.com/Wikid82/charon/backend/internal/services.ProxyHostService).Create issues: - exclude-generated-strict: true - exclude-dirs-use-default: false - exclude: - # Exclude some linters from running on tests - - path: _test\.go - linters: - - errcheck - - gosec - - govet - - ineffassign - - staticcheck + exclude-rules: # Exclude gosec file permission warnings - 0644/0755 are intentional for config/data dirs - linters: - gosec diff --git a/backend/cmd/api/main_test.go b/backend/cmd/api/main_test.go index 1986b7da..da506402 100644 --- a/backend/cmd/api/main_test.go +++ b/backend/cmd/api/main_test.go @@ -41,7 +41,7 @@ func TestResetPasswordCommand_Succeeds(t *testing.T) { t.Fatalf("seed user: %v", err) } - cmd := exec.Command(os.Args[0], "-test.run=TestResetPasswordCommand_Succeeds") + cmd := exec.Command(os.Args[0], "-test.run=TestResetPasswordCommand_Succeeds") //nolint:gosec // G204: Test subprocess pattern using os.Args[0] is safe cmd.Dir = tmp cmd.Env = append(os.Environ(), "CHARON_TEST_RUN_MAIN=1", @@ -87,7 +87,7 @@ func TestMigrateCommand_Succeeds(t *testing.T) { t.Fatal("SecurityConfig table should not exist yet") } - cmd := exec.Command(os.Args[0], "-test.run=TestMigrateCommand_Succeeds") + cmd := exec.Command(os.Args[0], "-test.run=TestMigrateCommand_Succeeds") //nolint:gosec // G204: Test subprocess pattern using os.Args[0] is safe cmd.Dir = tmp cmd.Env = append(os.Environ(), "CHARON_TEST_RUN_MAIN=1", @@ -147,7 +147,7 @@ func TestStartupVerification_MissingTables(t *testing.T) { // Close and reopen to simulate startup scenario sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() db, err = database.Connect(dbPath) if err != nil { diff --git a/backend/cmd/seed/main.go b/backend/cmd/seed/main.go index d3b23063..9b98120f 100644 --- a/backend/cmd/seed/main.go +++ b/backend/cmd/seed/main.go @@ -107,11 +107,12 @@ func main() { for _, server := range remoteServers { result := db.Where("host = ? AND port = ?", server.Host, server.Port).FirstOrCreate(&server) - if result.Error != nil { + switch { + case result.Error != nil: logger.Log().WithField("server", server.Name).WithError(result.Error).Error("Failed to seed remote server") - } else if result.RowsAffected > 0 { + case result.RowsAffected > 0: logger.Log().WithField("server", server.Name).Infof("✓ Created remote server: %s (%s:%d)", server.Name, server.Host, server.Port) - } else { + default: logger.Log().WithField("server", server.Name).Info("Remote server already exists") } } @@ -161,11 +162,12 @@ func main() { for _, host := range proxyHosts { result := db.Where("domain_names = ?", host.DomainNames).FirstOrCreate(&host) - if result.Error != nil { + switch { + case result.Error != nil: logger.Log().WithField("host", util.SanitizeForLog(host.DomainNames)).WithError(result.Error).Error("Failed to seed proxy host") - } else if result.RowsAffected > 0 { + case result.RowsAffected > 0: logger.Log().WithField("host", util.SanitizeForLog(host.DomainNames)).Infof("✓ Created proxy host: %s -> %s://%s:%d", host.DomainNames, host.ForwardScheme, host.ForwardHost, host.ForwardPort) - } else { + default: logger.Log().WithField("host", util.SanitizeForLog(host.DomainNames)).Info("Proxy host already exists") } } @@ -194,11 +196,12 @@ func main() { for _, setting := range settings { result := db.Where("key = ?", setting.Key).FirstOrCreate(&setting) - if result.Error != nil { + switch { + case result.Error != nil: logger.Log().WithField("setting", setting.Key).WithError(result.Error).Error("Failed to seed setting") - } else if result.RowsAffected > 0 { + case result.RowsAffected > 0: logger.Log().WithField("setting", setting.Key).Infof("✓ Created setting: %s = %s", setting.Key, setting.Value) - } else { + default: logger.Log().WithField("setting", setting.Key).Info("Setting already exists") } } diff --git a/backend/final_lint.txt b/backend/final_lint.txt new file mode 100644 index 00000000..01580ee6 --- /dev/null +++ b/backend/final_lint.txt @@ -0,0 +1,112 @@ +internal/api/handlers/security_handler_audit_test.go:581:18: Error return value of `json.Unmarshal` is not checked (errcheck) + json.Unmarshal(w.Body.Bytes(), &resp) + ^ +internal/api/handlers/security_handler_coverage_test.go:525:16: Error return value of `json.Unmarshal` is not checked (errcheck) + json.Unmarshal(w.Body.Bytes(), &tokenResp) + ^ +internal/api/handlers/security_handler_coverage_test.go:589:16: Error return value of `json.Unmarshal` is not checked (errcheck) + json.Unmarshal(w.Body.Bytes(), &resp) + ^ +internal/caddy/config_test.go:1794:14: Error return value of `os.Unsetenv` is not checked (errcheck) + os.Unsetenv(v) + ^ +internal/config/config_test.go:74:11: Error return value of `os.Setenv` is not checked (errcheck) + os.Setenv("CPM_DB_PATH", filepath.Join(tempDir, "db", "test.db")) + ^ +internal/config/config_test.go:75:11: Error return value of `os.Setenv` is not checked (errcheck) + os.Setenv("CPM_IMPORT_DIR", filepath.Join(tempDir, "imports")) + ^ +internal/config/config_test.go:82:11: Error return value of `os.Setenv` is not checked (errcheck) + os.Setenv("CPM_CADDY_CONFIG_DIR", filepath.Join(tempDir, "caddy")) + ^ +internal/config/config_test.go:175:19: Error return value of `os.Unsetenv` is not checked (errcheck) + defer os.Unsetenv("CHARON_ACME_STAGING") + ^ +internal/config/config_test.go:196:19: Error return value of `os.Unsetenv` is not checked (errcheck) + defer os.Unsetenv("CHARON_DEBUG") + ^ +internal/services/dns_provider_service_test.go:1493:13: Error return value of `sqlDB.Close` is not checked (errcheck) + sqlDB.Close() + ^ +internal/services/dns_provider_service_test.go:1531:13: Error return value of `sqlDB.Close` is not checked (errcheck) + sqlDB.Close() + ^ +internal/services/dns_provider_service_test.go:1549:13: Error return value of `sqlDB.Close` is not checked (errcheck) + sqlDB.Close() + ^ +cmd/seed/seed_smoke_test.go:21:12: G301: Expect directory permissions to be 0750 or less (gosec) + if err := os.MkdirAll("data", 0o755); err != nil { + ^ +internal/api/handlers/manual_challenge_handler.go:649:15: G115: integer overflow conversion int -> uint (gosec) + return uint(v) + ^ +internal/api/handlers/manual_challenge_handler.go:651:15: G115: integer overflow conversion int64 -> uint (gosec) + return uint(v) + ^ +internal/api/handlers/security_handler_rules_decisions_test.go:162:92: G115: integer overflow conversion uint -> int (gosec) + req = httptest.NewRequest(http.MethodDelete, "/api/v1/security/rulesets/"+strconv.Itoa(int(rs.ID)), http.NoBody) + ^ +internal/caddy/config.go:463:16: G602: slice index out of range (gosec) + host := hosts[i] + ^ +internal/config/config.go:68:12: G301: Expect directory permissions to be 0750 or less (gosec) + if err := os.MkdirAll(filepath.Dir(cfg.DatabasePath), 0o755); err != nil { + ^ +internal/config/config.go:72:12: G301: Expect directory permissions to be 0750 or less (gosec) + if err := os.MkdirAll(cfg.CaddyConfigDir, 0o755); err != nil { + ^ +internal/config/config_test.go:67:12: G304: Potential file inclusion via variable (gosec) + f, err := os.Create(filePath) + ^ +internal/config/config_test.go:148:12: G304: Potential file inclusion via variable (gosec) + f, err := os.Create(blockingFile) + ^ +internal/crowdsec/hub_cache.go:82:12: G306: Expect WriteFile permissions to be 0600 or less (gosec) + if err := os.WriteFile(archivePath, archive, 0o640); err != nil { + ^ +internal/crowdsec/hub_cache.go:86:12: G306: Expect WriteFile permissions to be 0600 or less (gosec) + if err := os.WriteFile(previewPath, []byte(preview), 0o640); err != nil { + ^ +internal/crowdsec/hub_cache.go:105:12: G306: Expect WriteFile permissions to be 0600 or less (gosec) + if err := os.WriteFile(metaPath, raw, 0o640); err != nil { + ^ +internal/crowdsec/hub_cache.go:127:15: G304: Potential file inclusion via variable (gosec) + data, err := os.ReadFile(metaPath) + ^ +internal/crowdsec/hub_sync.go:1016:16: G110: Potential DoS vulnerability via decompression bomb (gosec) + if _, err := io.Copy(f, tr); err != nil { + ^ +internal/database/database_test.go:181:12: G302: Expect file permissions to be 0600 or less (gosec) + f, err := os.OpenFile(dbPath, os.O_RDWR, 0o644) + ^ +internal/database/errors_test.go:187:12: G302: Expect file permissions to be 0600 or less (gosec) + f, err := os.OpenFile(dbPath, os.O_RDWR, 0o644) + ^ +internal/services/backup_service.go:316:12: G305: File traversal when extracting zip/tar archive (gosec) + fpath := filepath.Join(dest, f.Name) + ^ +internal/services/backup_service.go:345:12: G110: Potential DoS vulnerability via decompression bomb (gosec) + _, err = io.Copy(outFile, rc) + ^ +internal/services/backup_service_test.go:469:6: G302: Expect file permissions to be 0600 or less (gosec) + _ = os.Chmod(service.BackupDir, 0o444) + ^ +internal/services/uptime_service_test.go:58:13: G112: Potential Slowloris Attack because ReadHeaderTimeout is not configured in the http.Server (gosec) + server := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }), + } +internal/services/uptime_service_test.go:831:14: G112: Potential Slowloris Attack because ReadHeaderTimeout is not configured in the http.Server (gosec) + server := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }), + } +internal/util/crypto_test.go:63:2: G101: Potential hardcoded credentials (gosec) + secret := "a]3kL9#mP2$vN7@qR5*wX1&yT4^uI8%oE0!" + ^ +34 issues: +* errcheck: 12 +* gosec: 22 +exit status 1 diff --git a/backend/fix_all_errcheck.sh b/backend/fix_all_errcheck.sh new file mode 100755 index 00000000..82dc1e71 --- /dev/null +++ b/backend/fix_all_errcheck.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Fix all errcheck errors iteratively + +for i in {1..10}; do + echo "=== Iteration $i ===" + + # Run linter and extract just file:line for errcheck errors + errors=$(go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest run --config .golangci.yml ./... 2>&1 | grep "errcheck" | grep -oP '.*\.go:\d+' | sort -u) + + if [ -z "$errors" ]; then + echo "NO MORE ERRCHECK ERRORS FOUND!" + break + fi + + echo "Found $(echo "$errors" | wc -l) error locations" + + # Fix each one + while IFS=: read -r file line; do + # Check what function is on that line + func=$(sed -n "${line}p" "$file" | grep -oP '(db\.AutoMigrate|json\.Unmarshal|os\.Setenv|os\.Unsetenv|sqlDB\.Close|w\.Write)') + + if [ "$func" = "w.Write" ]; then + # w.Write returns 2 values + sed -i "${line}s/w\.Write/_, _ = w.Write/" "$file" + elif [ -n "$func" ]; then + sed -i "${line}s/${func}/_ = ${func}/" "$file" + fi + + echo "Fixed $file:$line" + done <<< "$errors" +done diff --git a/backend/fix_all_lint_errors.sh b/backend/fix_all_lint_errors.sh new file mode 100755 index 00000000..41c2cccf --- /dev/null +++ b/backend/fix_all_lint_errors.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Fix ALL errcheck and typecheck errors iteratively until ZERO remain + +MAX_ITER=20 +for i in $(seq 1 $MAX_ITER); do + echo "=== ITERATION $i ===" + + # Run full linter + go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest run --config .golangci.yml ./... 2>&1 > lint_iter_$i.txt + + # Extract errcheck errors + errcheck_errors=$(grep "errcheck" lint_iter_$i.txt | grep -oP '.*\.go:\d+' | sort -u) + + # Extract typecheck/defer errors + typecheck_errors=$(grep "typecheck.*unexpected = at end of statement\|expected '==', found '='" lint_iter_$i.txt | grep -oP '.*\.go:\d+' | head -1 | cut -d: -f1-2) + + errcheck_count=$(echo "$errcheck_errors" | grep -v '^$' | wc -l) + typecheck_count=$(echo "$typecheck_errors" | grep -v '^$' | wc -l) + + echo "Found $errcheck_count errcheck errors and $typecheck_count typecheck errors" + + if [ "$errcheck_count" -eq 0 ] && [ "$typecheck_count" -eq 0 ]; then + echo "✅ ✅ ✅ NO MORE ERRORS! SUCCESS! ✅ ✅ ✅" + break + fi + + # Fix errcheck errors + if [ "$errcheck_count" -gt 0 ]; then + while IFS=: read -r file line; do + [ -z "$file" ] && continue + func=$(sed -n "${line}p" "$file" | grep -oP '(db\.AutoMigrate|json\.Unmarshal|os\.Setenv|os\.Unsetenv|sqlDB\.Close|w\.Write)') + + if [ "$func" = "w.Write" ]; then + sed -i "${line}s/w\.Write/_, _ = w.Write/" "$file" + echo "Fixed $file:$line (w.Write)" + elif [ -n "$func" ]; then + sed -i "${line}s/${func}/_ = ${func}/" "$file" + echo "Fixed $file:$line ($func)" + fi + done <<< "$errcheck_errors" + fi + + # Fix typecheck/defer errors + if [ "$typecheck_count" -gt 0 ]; then + while IFS=: read -r file line; do + [ -z "$file" ] && continue + # Check if it's a defer with blank identifier + content=$(sed -n "${line}p" "$file") + if echo "$content" | grep -q "defer _ = os\.Unsetenv"; then + # Extract the argument + arg=$(echo "$content" | grep -oP 'os\.Unsetenv\([^)]+\)') + # Replace with proper defer wrapper + sed -i "${line}s|defer _ = ${arg}|defer func() { _ = ${arg} }()|" "$file" + echo "Fixed defer Unsetenv at $file:$line" + elif echo "$content" | grep -q "defer _ = os\.Setenv"; then + arg=$(echo "$content" | grep -oP 'os\.Setenv\([^)]+,[^)]+\)') + sed -i "${line}s|defer _ = ${arg}|defer func() { _ = ${arg} }()|" "$file" + echo "Fixed defer Setenv at $file:$line" + fi + done <<< "$typecheck_errors" + fi +done + +if [ $i -eq $MAX_ITER ]; then + echo "❌ Reached maximum iterations!" + exit 1 +fi diff --git a/backend/full_lint_output.txt b/backend/full_lint_output.txt new file mode 100644 index 00000000..585fc240 --- /dev/null +++ b/backend/full_lint_output.txt @@ -0,0 +1,129 @@ +internal/api/handlers/notification_coverage_test.go:22:16: Error return value of `db.AutoMigrate` is not checked (errcheck) + db.AutoMigrate(&models.Notification{}, &models.NotificationProvider{}, &models.NotificationTemplate{}) + ^ +internal/api/handlers/pr_coverage_test.go:404:16: Error return value of `db.AutoMigrate` is not checked (errcheck) + db.AutoMigrate(&models.SecurityAudit{}, &models.DNSProvider{}) + ^ +internal/api/handlers/pr_coverage_test.go:438:16: Error return value of `db.AutoMigrate` is not checked (errcheck) + db.AutoMigrate(&models.SecurityAudit{}, &models.DNSProvider{}) + ^ +internal/api/handlers/settings_handler_test.go:895:16: Error return value of `json.Unmarshal` is not checked (errcheck) + json.Unmarshal(w.Body.Bytes(), &resp) + ^ +internal/api/handlers/settings_handler_test.go:923:16: Error return value of `json.Unmarshal` is not checked (errcheck) + json.Unmarshal(w.Body.Bytes(), &resp) + ^ +internal/api/handlers/settings_handler_test.go:1081:16: Error return value of `json.Unmarshal` is not checked (errcheck) + json.Unmarshal(w.Body.Bytes(), &resp) + ^ +internal/caddy/manager_additional_test.go:1467:11: Error return value of `w.Write` is not checked (errcheck) + w.Write([]byte(`{"apps":{"http":{}}}`)) + ^ +internal/caddy/manager_additional_test.go:1522:11: Error return value of `w.Write` is not checked (errcheck) + w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) + ^ +internal/caddy/manager_test.go:133:11: Error return value of `w.Write` is not checked (errcheck) + w.Write([]byte(`{"apps": {"http": {}}}`)) + ^ +internal/config/config_test.go:56:11: Error return value of `os.Setenv` is not checked (errcheck) + os.Setenv("CHARON_DB_PATH", charonDB) + ^ +internal/config/config_test.go:57:11: Error return value of `os.Setenv` is not checked (errcheck) + os.Setenv("CPM_DB_PATH", cpmDB) + ^ +internal/config/config_test.go:72:11: Error return value of `os.Setenv` is not checked (errcheck) + os.Setenv("CPM_CADDY_CONFIG_DIR", filePath) + ^ +internal/config/config_test.go:157:14: Error return value of `os.Unsetenv` is not checked (errcheck) + os.Unsetenv("CHARON_DB_PATH") + ^ +internal/config/config_test.go:158:14: Error return value of `os.Unsetenv` is not checked (errcheck) + os.Unsetenv("CHARON_CADDY_CONFIG_DIR") + ^ +internal/config/config_test.go:159:14: Error return value of `os.Unsetenv` is not checked (errcheck) + os.Unsetenv("CHARON_IMPORT_DIR") + ^ +internal/database/errors_test.go:230:13: Error return value of `sqlDB.Close` is not checked (errcheck) + sqlDB.Close() + ^ +internal/services/dns_provider_service_test.go:1446:13: Error return value of `sqlDB.Close` is not checked (errcheck) + sqlDB.Close() + ^ +internal/services/dns_provider_service_test.go:1466:13: Error return value of `sqlDB.Close` is not checked (errcheck) + sqlDB.Close() + ^ +cmd/seed/seed_smoke_test.go:21:12: G301: Expect directory permissions to be 0750 or less (gosec) + if err := os.MkdirAll("data", 0o755); err != nil { + ^ +internal/api/handlers/manual_challenge_handler.go:649:15: G115: integer overflow conversion int -> uint (gosec) + return uint(v) + ^ +internal/api/handlers/manual_challenge_handler.go:651:15: G115: integer overflow conversion int64 -> uint (gosec) + return uint(v) + ^ +internal/api/handlers/security_handler_rules_decisions_test.go:162:92: G115: integer overflow conversion uint -> int (gosec) + req = httptest.NewRequest(http.MethodDelete, "/api/v1/security/rulesets/"+strconv.Itoa(int(rs.ID)), http.NoBody) + ^ +internal/caddy/config.go:463:16: G602: slice index out of range (gosec) + host := hosts[i] + ^ +internal/config/config.go:68:12: G301: Expect directory permissions to be 0750 or less (gosec) + if err := os.MkdirAll(filepath.Dir(cfg.DatabasePath), 0o755); err != nil { + ^ +internal/config/config.go:72:12: G301: Expect directory permissions to be 0750 or less (gosec) + if err := os.MkdirAll(cfg.CaddyConfigDir, 0o755); err != nil { + ^ +internal/config/config_test.go:67:12: G304: Potential file inclusion via variable (gosec) + f, err := os.Create(filePath) + ^ +internal/config/config_test.go:148:12: G304: Potential file inclusion via variable (gosec) + f, err := os.Create(blockingFile) + ^ +internal/crowdsec/hub_cache.go:82:12: G306: Expect WriteFile permissions to be 0600 or less (gosec) + if err := os.WriteFile(archivePath, archive, 0o640); err != nil { + ^ +internal/crowdsec/hub_cache.go:86:12: G306: Expect WriteFile permissions to be 0600 or less (gosec) + if err := os.WriteFile(previewPath, []byte(preview), 0o640); err != nil { + ^ +internal/crowdsec/hub_cache.go:105:12: G306: Expect WriteFile permissions to be 0600 or less (gosec) + if err := os.WriteFile(metaPath, raw, 0o640); err != nil { + ^ +internal/crowdsec/hub_cache.go:127:15: G304: Potential file inclusion via variable (gosec) + data, err := os.ReadFile(metaPath) + ^ +internal/crowdsec/hub_sync.go:1016:16: G110: Potential DoS vulnerability via decompression bomb (gosec) + if _, err := io.Copy(f, tr); err != nil { + ^ +internal/database/database_test.go:181:12: G302: Expect file permissions to be 0600 or less (gosec) + f, err := os.OpenFile(dbPath, os.O_RDWR, 0o644) + ^ +internal/database/errors_test.go:187:12: G302: Expect file permissions to be 0600 or less (gosec) + f, err := os.OpenFile(dbPath, os.O_RDWR, 0o644) + ^ +internal/services/backup_service.go:316:12: G305: File traversal when extracting zip/tar archive (gosec) + fpath := filepath.Join(dest, f.Name) + ^ +internal/services/backup_service.go:345:12: G110: Potential DoS vulnerability via decompression bomb (gosec) + _, err = io.Copy(outFile, rc) + ^ +internal/services/backup_service_test.go:469:6: G302: Expect file permissions to be 0600 or less (gosec) + _ = os.Chmod(service.BackupDir, 0o444) + ^ +internal/services/uptime_service_test.go:58:13: G112: Potential Slowloris Attack because ReadHeaderTimeout is not configured in the http.Server (gosec) + server := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }), + } +internal/services/uptime_service_test.go:831:14: G112: Potential Slowloris Attack because ReadHeaderTimeout is not configured in the http.Server (gosec) + server := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }), + } +internal/util/crypto_test.go:63:2: G101: Potential hardcoded credentials (gosec) + secret := "a]3kL9#mP2$vN7@qR5*wX1&yT4^uI8%oE0!" + ^ +40 issues: +* errcheck: 18 +* gosec: 22 diff --git a/backend/internal/api/handlers/access_list_handler_coverage_test.go b/backend/internal/api/handlers/access_list_handler_coverage_test.go index afc98556..a06a27e8 100644 --- a/backend/internal/api/handlers/access_list_handler_coverage_test.go +++ b/backend/internal/api/handlers/access_list_handler_coverage_test.go @@ -16,7 +16,7 @@ import ( func TestAccessListHandler_SetGeoIPService(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - db.AutoMigrate(&models.AccessList{}) + _ = db.AutoMigrate(&models.AccessList{}) handler := NewAccessListHandler(db) @@ -30,7 +30,7 @@ func TestAccessListHandler_SetGeoIPService(t *testing.T) { func TestAccessListHandler_SetGeoIPService_Nil(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - db.AutoMigrate(&models.AccessList{}) + _ = db.AutoMigrate(&models.AccessList{}) handler := NewAccessListHandler(db) @@ -151,7 +151,7 @@ func TestAccessListHandler_Get_DBError(t *testing.T) { func TestAccessListHandler_Delete_InternalError(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) // Migrate AccessList but not ProxyHost to cause internal error on delete - db.AutoMigrate(&models.AccessList{}) + _ = db.AutoMigrate(&models.AccessList{}) gin.SetMode(gin.TestMode) router := gin.New() diff --git a/backend/internal/api/handlers/additional_coverage_test.go b/backend/internal/api/handlers/additional_coverage_test.go index 57fda9f2..19cc7daf 100644 --- a/backend/internal/api/handlers/additional_coverage_test.go +++ b/backend/internal/api/handlers/additional_coverage_test.go @@ -22,7 +22,7 @@ import ( func setupImportCoverageDB(t *testing.T) *gorm.DB { t.Helper() db := OpenTestDB(t) - db.AutoMigrate(&models.ImportSession{}, &models.ProxyHost{}, &models.Domain{}) + _ = db.AutoMigrate(&models.ImportSession{}, &models.ProxyHost{}, &models.Domain{}) return db } @@ -90,7 +90,7 @@ func TestImportHandler_Commit_SessionNotFound(t *testing.T) { func setupRemoteServerCoverageDB2(t *testing.T) *gorm.DB { t.Helper() db := OpenTestDB(t) - db.AutoMigrate(&models.RemoteServer{}) + _ = db.AutoMigrate(&models.RemoteServer{}) return db } @@ -106,7 +106,7 @@ func TestRemoteServerHandler_TestConnection_Unreachable(t *testing.T) { Host: "192.0.2.1", // TEST-NET - not routable Port: 65535, } - svc.Create(server) + _ = svc.Create(server) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -124,7 +124,7 @@ func TestRemoteServerHandler_TestConnection_Unreachable(t *testing.T) { func setupSecurityCoverageDB3(t *testing.T) *gorm.DB { t.Helper() db := OpenTestDB(t) - db.AutoMigrate( + _ = db.AutoMigrate( &models.SecurityConfig{}, &models.SecurityDecision{}, &models.SecurityRuleSet{}, @@ -140,7 +140,7 @@ func TestSecurityHandler_GetConfig_InternalError(t *testing.T) { h := NewSecurityHandler(config.SecurityConfig{}, db, nil) // Drop table to cause internal error (not ErrSecurityConfigNotFound) - db.Migrator().DropTable(&models.SecurityConfig{}) + _ = db.Migrator().DropTable(&models.SecurityConfig{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -183,7 +183,7 @@ func TestSecurityHandler_GenerateBreakGlass_Error(t *testing.T) { h := NewSecurityHandler(config.SecurityConfig{}, db, nil) // Drop the config table so generate fails - db.Migrator().DropTable(&models.SecurityConfig{}) + _ = db.Migrator().DropTable(&models.SecurityConfig{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -202,7 +202,7 @@ func TestSecurityHandler_ListDecisions_Error(t *testing.T) { h := NewSecurityHandler(config.SecurityConfig{}, db, nil) // Drop decisions table - db.Migrator().DropTable(&models.SecurityDecision{}) + _ = db.Migrator().DropTable(&models.SecurityDecision{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -221,7 +221,7 @@ func TestSecurityHandler_ListRuleSets_Error(t *testing.T) { h := NewSecurityHandler(config.SecurityConfig{}, db, nil) // Drop rulesets table - db.Migrator().DropTable(&models.SecurityRuleSet{}) + _ = db.Migrator().DropTable(&models.SecurityRuleSet{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -240,7 +240,7 @@ func TestSecurityHandler_UpsertRuleSet_Error(t *testing.T) { h := NewSecurityHandler(config.SecurityConfig{}, db, nil) // Drop table to cause upsert to fail - db.Migrator().DropTable(&models.SecurityRuleSet{}) + _ = db.Migrator().DropTable(&models.SecurityRuleSet{}) body, _ := json.Marshal(map[string]any{ "name": "test-ruleset", @@ -265,7 +265,7 @@ func TestSecurityHandler_CreateDecision_LogError(t *testing.T) { h := NewSecurityHandler(config.SecurityConfig{}, db, nil) // Drop decisions table to cause log to fail - db.Migrator().DropTable(&models.SecurityDecision{}) + _ = db.Migrator().DropTable(&models.SecurityDecision{}) body, _ := json.Marshal(map[string]any{ "ip": "192.168.1.1", @@ -290,7 +290,7 @@ func TestSecurityHandler_DeleteRuleSet_Error(t *testing.T) { h := NewSecurityHandler(config.SecurityConfig{}, db, nil) // Drop table to cause delete to fail (not NotFound but table error) - db.Migrator().DropTable(&models.SecurityRuleSet{}) + _ = db.Migrator().DropTable(&models.SecurityRuleSet{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -321,7 +321,7 @@ func TestCrowdsec_ImportConfig_EmptyUpload(t *testing.T) { fw, _ := mw.CreateFormFile("file", "empty.tar.gz") // Write nothing to make file empty _ = fw - mw.Close() + _ = mw.Close() w := httptest.NewRecorder() req := httptest.NewRequest("POST", "/api/v1/admin/crowdsec/import", buf) @@ -451,10 +451,10 @@ func setupLogsDownloadTest(t *testing.T) (h *LogsHandler, logsDir string) { t.Helper() tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") - os.MkdirAll(dataDir, 0o755) + _ = os.MkdirAll(dataDir, 0o755) logsDir = filepath.Join(dataDir, "logs") - os.MkdirAll(logsDir, 0o755) + _ = os.MkdirAll(logsDir, 0o755) dbPath := filepath.Join(dataDir, "charon.db") cfg := &config.Config{DatabasePath: dbPath} @@ -499,7 +499,7 @@ func TestLogsHandler_Download_Success(t *testing.T) { h, logsDir := setupLogsDownloadTest(t) // Create a log file to download - os.WriteFile(filepath.Join(logsDir, "test.log"), []byte("log content"), 0o644) + _ = os.WriteFile(filepath.Join(logsDir, "test.log"), []byte("log content"), 0o644) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -557,11 +557,11 @@ func TestBackupHandler_List_ServiceError(t *testing.T) { // Create a temp dir with invalid permission for backup dir tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") - os.MkdirAll(dataDir, 0o755) + _ = os.MkdirAll(dataDir, 0o755) // Create database file so config is valid dbPath := filepath.Join(dataDir, "charon.db") - os.WriteFile(dbPath, []byte("test"), 0o644) + _ = os.WriteFile(dbPath, []byte("test"), 0o644) cfg := &config.Config{ DatabasePath: dbPath, @@ -571,8 +571,8 @@ func TestBackupHandler_List_ServiceError(t *testing.T) { h := NewBackupHandler(svc) // Make backup dir a file to cause ReadDir error - os.RemoveAll(svc.BackupDir) - os.WriteFile(svc.BackupDir, []byte("not a dir"), 0o644) + _ = os.RemoveAll(svc.BackupDir) + _ = os.WriteFile(svc.BackupDir, []byte("not a dir"), 0o644) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -589,10 +589,10 @@ func TestBackupHandler_Delete_PathTraversal(t *testing.T) { tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") - os.MkdirAll(dataDir, 0o755) + _ = os.MkdirAll(dataDir, 0o755) dbPath := filepath.Join(dataDir, "charon.db") - os.WriteFile(dbPath, []byte("test"), 0o644) + _ = os.WriteFile(dbPath, []byte("test"), 0o644) cfg := &config.Config{ DatabasePath: dbPath, @@ -619,10 +619,10 @@ func TestBackupHandler_Delete_InternalError2(t *testing.T) { tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") - os.MkdirAll(dataDir, 0o755) + _ = os.MkdirAll(dataDir, 0o755) dbPath := filepath.Join(dataDir, "charon.db") - os.WriteFile(dbPath, []byte("test"), 0o644) + _ = os.WriteFile(dbPath, []byte("test"), 0o644) cfg := &config.Config{ DatabasePath: dbPath, @@ -634,13 +634,13 @@ func TestBackupHandler_Delete_InternalError2(t *testing.T) { // Create a backup backupsDir := filepath.Join(dataDir, "backups") - os.MkdirAll(backupsDir, 0o755) + _ = os.MkdirAll(backupsDir, 0o755) backupFile := filepath.Join(backupsDir, "test.zip") - os.WriteFile(backupFile, []byte("backup"), 0o644) + _ = os.WriteFile(backupFile, []byte("backup"), 0o644) // Remove write permissions to cause delete error - os.Chmod(backupsDir, 0o555) - defer os.Chmod(backupsDir, 0o755) + _ = os.Chmod(backupsDir, 0o555) + defer func() { _ = os.Chmod(backupsDir, 0o755) }() w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -697,7 +697,7 @@ func TestRemoteServerHandler_TestConnectionCustom_Unreachable2(t *testing.T) { func setupAuthCoverageDB(t *testing.T) *gorm.DB { t.Helper() db := OpenTestDB(t) - db.AutoMigrate(&models.User{}, &models.Setting{}) + _ = db.AutoMigrate(&models.User{}, &models.Setting{}) return db } @@ -743,7 +743,7 @@ func TestBackupHandler_Create_Error(t *testing.T) { // Use a path where database file doesn't exist tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") - os.MkdirAll(dataDir, 0o755) + _ = os.MkdirAll(dataDir, 0o755) // Don't create the database file - this will cause CreateBackup to fail dbPath := filepath.Join(dataDir, "charon.db") @@ -772,7 +772,7 @@ func TestBackupHandler_Create_Error(t *testing.T) { func setupSettingsCoverageDB(t *testing.T) *gorm.DB { t.Helper() db := OpenTestDB(t) - db.AutoMigrate(&models.Setting{}) + _ = db.AutoMigrate(&models.Setting{}) return db } @@ -783,7 +783,7 @@ func TestSettingsHandler_GetSettings_Error(t *testing.T) { h := NewSettingsHandler(db) // Drop table to cause error - db.Migrator().DropTable(&models.Setting{}) + _ = db.Migrator().DropTable(&models.Setting{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -825,7 +825,7 @@ func TestRemoteServerHandler_TestConnection_Reachable(t *testing.T) { Host: "127.0.0.1", Port: 22, // SSH port typically listening on localhost } - svc.Create(server) + _ = svc.Create(server) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) diff --git a/backend/internal/api/handlers/audit_log_handler_test.go b/backend/internal/api/handlers/audit_log_handler_test.go index f6220f8b..a965f0e1 100644 --- a/backend/internal/api/handlers/audit_log_handler_test.go +++ b/backend/internal/api/handlers/audit_log_handler_test.go @@ -376,7 +376,7 @@ func TestAuditLogHandler_ServiceErrors(t *testing.T) { // Close the database to trigger error sqlDB, err := db.DB() assert.NoError(t, err) - sqlDB.Close() + _ = sqlDB.Close() w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -621,14 +621,14 @@ func TestAuditLogHandler_Get_InternalError(t *testing.T) { // Create a fresh DB and immediately close it to simulate internal error db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) assert.NoError(t, err) - db.AutoMigrate(&models.SecurityAudit{}) + _ = db.AutoMigrate(&models.SecurityAudit{}) securityService := services.NewSecurityService(db) handler := NewAuditLogHandler(securityService) // Close the DB to force internal error (not "not found") sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) diff --git a/backend/internal/api/handlers/auth_handler_test.go b/backend/internal/api/handlers/auth_handler_test.go index 2d77e13b..26c0efcc 100644 --- a/backend/internal/api/handlers/auth_handler_test.go +++ b/backend/internal/api/handlers/auth_handler_test.go @@ -23,7 +23,7 @@ func setupAuthHandler(t *testing.T) (*AuthHandler, *gorm.DB) { dbName := "file:" + t.Name() + "?mode=memory&cache=shared" db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{}) require.NoError(t, err) - db.AutoMigrate(&models.User{}, &models.Setting{}) + _ = db.AutoMigrate(&models.User{}, &models.Setting{}) cfg := config.Config{JWTSecret: "test-secret"} authService := services.NewAuthService(db, cfg) @@ -40,7 +40,7 @@ func TestAuthHandler_Login(t *testing.T) { Email: "test@example.com", Name: "Test User", } - user.SetPassword("password123") + _ = user.SetPassword("password123") db.Create(user) gin.SetMode(gin.TestMode) @@ -64,8 +64,8 @@ func TestAuthHandler_Login(t *testing.T) { func TestSetSecureCookie_HTTPS_Strict(t *testing.T) { gin.SetMode(gin.TestMode) - os.Setenv("CHARON_ENV", "production") - defer os.Unsetenv("CHARON_ENV") + _ = os.Setenv("CHARON_ENV", "production") + defer func() { _ = os.Unsetenv("CHARON_ENV") }() recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) req := httptest.NewRequest("POST", "https://example.com/login", http.NoBody) @@ -218,7 +218,7 @@ func TestAuthHandler_Me(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, float64(user.ID), resp["user_id"]) assert.Equal(t, "admin", resp["role"]) assert.Equal(t, "Me User", resp["name"]) @@ -253,7 +253,7 @@ func TestAuthHandler_ChangePassword(t *testing.T) { Email: "change@example.com", Name: "Change User", } - user.SetPassword("oldpassword") + _ = user.SetPassword("oldpassword") db.Create(user) gin.SetMode(gin.TestMode) @@ -288,7 +288,7 @@ func TestAuthHandler_ChangePassword_WrongOld(t *testing.T) { t.Parallel() handler, db := setupAuthHandler(t) user := &models.User{UUID: uuid.NewString(), Email: "wrong@example.com"} - user.SetPassword("correct") + _ = user.SetPassword("correct") db.Create(user) gin.SetMode(gin.TestMode) @@ -344,7 +344,7 @@ func setupAuthHandlerWithDB(t *testing.T) (*AuthHandler, *gorm.DB) { dbName := "file:" + t.Name() + "?mode=memory&cache=shared" db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{}) require.NoError(t, err) - db.AutoMigrate(&models.User{}, &models.Setting{}, &models.ProxyHost{}) + _ = db.AutoMigrate(&models.User{}, &models.Setting{}, &models.ProxyHost{}) cfg := config.Config{JWTSecret: "test-secret"} authService := services.NewAuthService(db, cfg) @@ -401,7 +401,7 @@ func TestAuthHandler_Verify_ValidToken(t *testing.T) { Role: "user", Enabled: true, } - user.SetPassword("password123") + _ = user.SetPassword("password123") db.Create(user) // Generate token @@ -432,7 +432,7 @@ func TestAuthHandler_Verify_BearerToken(t *testing.T) { Role: "admin", Enabled: true, } - user.SetPassword("password123") + _ = user.SetPassword("password123") db.Create(user) token, _ := handler.authService.GenerateToken(user) @@ -460,7 +460,7 @@ func TestAuthHandler_Verify_DisabledUser(t *testing.T) { Name: "Disabled User", Role: "user", } - user.SetPassword("password123") + _ = user.SetPassword("password123") db.Create(user) // Explicitly disable after creation to bypass GORM's default:true behavior db.Model(user).Update("enabled", false) @@ -502,7 +502,7 @@ func TestAuthHandler_Verify_ForwardAuthDenied(t *testing.T) { Enabled: true, PermissionMode: models.PermissionModeDenyAll, } - user.SetPassword("password123") + _ = user.SetPassword("password123") db.Create(user) token, _ := handler.authService.GenerateToken(user) @@ -533,7 +533,7 @@ func TestAuthHandler_VerifyStatus_NotAuthenticated(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, false, resp["authenticated"]) } @@ -551,7 +551,7 @@ func TestAuthHandler_VerifyStatus_InvalidToken(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, false, resp["authenticated"]) } @@ -566,7 +566,7 @@ func TestAuthHandler_VerifyStatus_Authenticated(t *testing.T) { Role: "user", Enabled: true, } - user.SetPassword("password123") + _ = user.SetPassword("password123") db.Create(user) token, _ := handler.authService.GenerateToken(user) @@ -582,7 +582,7 @@ func TestAuthHandler_VerifyStatus_Authenticated(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, true, resp["authenticated"]) userObj := resp["user"].(map[string]any) assert.Equal(t, "status@example.com", userObj["email"]) @@ -598,7 +598,7 @@ func TestAuthHandler_VerifyStatus_DisabledUser(t *testing.T) { Name: "Disabled User 2", Role: "user", } - user.SetPassword("password123") + _ = user.SetPassword("password123") db.Create(user) // Explicitly disable after creation to bypass GORM's default:true behavior db.Model(user).Update("enabled", false) @@ -616,7 +616,7 @@ func TestAuthHandler_VerifyStatus_DisabledUser(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, false, resp["authenticated"]) } @@ -668,7 +668,7 @@ func TestAuthHandler_GetAccessibleHosts_AllowAll(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) hosts := resp["hosts"].([]any) assert.Len(t, hosts, 2) } @@ -705,7 +705,7 @@ func TestAuthHandler_GetAccessibleHosts_DenyAll(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) hosts := resp["hosts"].([]any) assert.Len(t, hosts, 0) } @@ -745,7 +745,7 @@ func TestAuthHandler_GetAccessibleHosts_PermittedHosts(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) hosts := resp["hosts"].([]any) assert.Len(t, hosts, 1) } @@ -834,7 +834,7 @@ func TestAuthHandler_CheckHostAccess_Allowed(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, true, resp["can_access"]) } @@ -867,6 +867,6 @@ func TestAuthHandler_CheckHostAccess_Denied(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, false, resp["can_access"]) } diff --git a/backend/internal/api/handlers/backup_handler_test.go b/backend/internal/api/handlers/backup_handler_test.go index 8016c4b2..41f151de 100644 --- a/backend/internal/api/handlers/backup_handler_test.go +++ b/backend/internal/api/handlers/backup_handler_test.go @@ -69,7 +69,7 @@ func setupBackupTest(t *testing.T) (*gin.Engine, *services.BackupService, string func TestBackupLifecycle(t *testing.T) { router, _, tmpDir := setupBackupTest(t) - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // 1. List backups (should be empty) req := httptest.NewRequest(http.MethodGet, "/api/v1/backups", http.NoBody) @@ -124,7 +124,7 @@ func TestBackupLifecycle(t *testing.T) { router.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) var list []any - json.Unmarshal(resp.Body.Bytes(), &list) + _ = json.Unmarshal(resp.Body.Bytes(), &list) require.Empty(t, list) // 8. Delete non-existent backup @@ -148,18 +148,18 @@ func TestBackupLifecycle(t *testing.T) { func TestBackupHandler_Errors(t *testing.T) { router, svc, tmpDir := setupBackupTest(t) - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // 1. List Error (remove backup dir to cause ReadDir error) // Note: Service now handles missing dir gracefully by returning empty list - os.RemoveAll(svc.BackupDir) + _ = os.RemoveAll(svc.BackupDir) req := httptest.NewRequest(http.MethodGet, "/api/v1/backups", http.NoBody) resp := httptest.NewRecorder() router.ServeHTTP(resp, req) require.Equal(t, http.StatusOK, resp.Code) var list []any - json.Unmarshal(resp.Body.Bytes(), &list) + _ = json.Unmarshal(resp.Body.Bytes(), &list) require.Empty(t, list) // 4. Delete Error (Not Found) @@ -171,7 +171,7 @@ func TestBackupHandler_Errors(t *testing.T) { func TestBackupHandler_List_Success(t *testing.T) { router, _, tmpDir := setupBackupTest(t) - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Create a backup first req := httptest.NewRequest(http.MethodPost, "/api/v1/backups", http.NoBody) @@ -194,7 +194,7 @@ func TestBackupHandler_List_Success(t *testing.T) { func TestBackupHandler_Create_Success(t *testing.T) { router, _, tmpDir := setupBackupTest(t) - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() req := httptest.NewRequest(http.MethodPost, "/api/v1/backups", http.NoBody) resp := httptest.NewRecorder() @@ -202,14 +202,14 @@ func TestBackupHandler_Create_Success(t *testing.T) { require.Equal(t, http.StatusCreated, resp.Code) var result map[string]string - json.Unmarshal(resp.Body.Bytes(), &result) + _ = json.Unmarshal(resp.Body.Bytes(), &result) require.NotEmpty(t, result["filename"]) require.Contains(t, result["filename"], "backup_") } func TestBackupHandler_Download_Success(t *testing.T) { router, _, tmpDir := setupBackupTest(t) - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Create backup req := httptest.NewRequest(http.MethodPost, "/api/v1/backups", http.NoBody) @@ -218,7 +218,7 @@ func TestBackupHandler_Download_Success(t *testing.T) { require.Equal(t, http.StatusCreated, resp.Code) var result map[string]string - json.Unmarshal(resp.Body.Bytes(), &result) + _ = json.Unmarshal(resp.Body.Bytes(), &result) filename := result["filename"] // Download it @@ -231,7 +231,7 @@ func TestBackupHandler_Download_Success(t *testing.T) { func TestBackupHandler_PathTraversal(t *testing.T) { router, _, tmpDir := setupBackupTest(t) - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Try path traversal in Delete req := httptest.NewRequest(http.MethodDelete, "/api/v1/backups/../../../etc/passwd", http.NoBody) @@ -254,7 +254,7 @@ func TestBackupHandler_PathTraversal(t *testing.T) { func TestBackupHandler_Download_InvalidPath(t *testing.T) { router, _, tmpDir := setupBackupTest(t) - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Request with path traversal attempt req := httptest.NewRequest(http.MethodGet, "/api/v1/backups/../invalid/download", http.NoBody) @@ -266,11 +266,11 @@ func TestBackupHandler_Download_InvalidPath(t *testing.T) { func TestBackupHandler_Create_ServiceError(t *testing.T) { router, svc, tmpDir := setupBackupTest(t) - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Remove write permissions on backup dir to force create error - os.Chmod(svc.BackupDir, 0o444) - defer os.Chmod(svc.BackupDir, 0o755) + _ = os.Chmod(svc.BackupDir, 0o444) + defer func() { _ = os.Chmod(svc.BackupDir, 0o755) }() req := httptest.NewRequest(http.MethodPost, "/api/v1/backups", http.NoBody) resp := httptest.NewRecorder() @@ -281,7 +281,7 @@ func TestBackupHandler_Create_ServiceError(t *testing.T) { func TestBackupHandler_Delete_InternalError(t *testing.T) { router, svc, tmpDir := setupBackupTest(t) - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Create a backup first req := httptest.NewRequest(http.MethodPost, "/api/v1/backups", http.NoBody) @@ -290,12 +290,12 @@ func TestBackupHandler_Delete_InternalError(t *testing.T) { require.Equal(t, http.StatusCreated, resp.Code) var result map[string]string - json.Unmarshal(resp.Body.Bytes(), &result) + _ = json.Unmarshal(resp.Body.Bytes(), &result) filename := result["filename"] // Make backup dir read-only to cause delete error (not NotExist) - os.Chmod(svc.BackupDir, 0o444) - defer os.Chmod(svc.BackupDir, 0o755) + _ = os.Chmod(svc.BackupDir, 0o444) + defer func() { _ = os.Chmod(svc.BackupDir, 0o755) }() req = httptest.NewRequest(http.MethodDelete, "/api/v1/backups/"+filename, http.NoBody) resp = httptest.NewRecorder() @@ -306,7 +306,7 @@ func TestBackupHandler_Delete_InternalError(t *testing.T) { func TestBackupHandler_Restore_InternalError(t *testing.T) { router, svc, tmpDir := setupBackupTest(t) - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Create a backup first req := httptest.NewRequest(http.MethodPost, "/api/v1/backups", http.NoBody) @@ -315,12 +315,12 @@ func TestBackupHandler_Restore_InternalError(t *testing.T) { require.Equal(t, http.StatusCreated, resp.Code) var result map[string]string - json.Unmarshal(resp.Body.Bytes(), &result) + _ = json.Unmarshal(resp.Body.Bytes(), &result) filename := result["filename"] // Make data dir read-only to cause restore error - os.Chmod(svc.DataDir, 0o444) - defer os.Chmod(svc.DataDir, 0o755) + _ = os.Chmod(svc.DataDir, 0o444) + defer func() { _ = os.Chmod(svc.DataDir, 0o755) }() req = httptest.NewRequest(http.MethodPost, "/api/v1/backups/"+filename+"/restore", http.NoBody) resp = httptest.NewRecorder() diff --git a/backend/internal/api/handlers/cerberus_logs_ws_test.go b/backend/internal/api/handlers/cerberus_logs_ws_test.go index 6ab8229f..cf7dc84e 100644 --- a/backend/internal/api/handlers/cerberus_logs_ws_test.go +++ b/backend/internal/api/handlers/cerberus_logs_ws_test.go @@ -67,8 +67,8 @@ func TestCerberusLogsHandler_SuccessfulConnection(t *testing.T) { // Connect WebSocket conn, resp, err := websocket.DefaultDialer.Dial(wsURL, nil) require.NoError(t, err) - defer resp.Body.Close() - defer conn.Close() + defer func() { _ = resp.Body.Close() }() + defer func() { _ = conn.Close() }() assert.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode) } @@ -83,7 +83,7 @@ func TestCerberusLogsHandler_ReceiveLogEntries(t *testing.T) { // Create the log file file, err := os.Create(logPath) require.NoError(t, err) - defer file.Close() + defer func() { _ = file.Close() }() watcher := services.NewLogWatcher(logPath) err = watcher.Start(context.Background()) @@ -102,7 +102,7 @@ func TestCerberusLogsHandler_ReceiveLogEntries(t *testing.T) { wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws" conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) //nolint:bodyclose // WebSocket Dial response body is consumed by the dial require.NoError(t, err) - defer conn.Close() + defer func() { _ = conn.Close() }() // Give the subscription time to register and watcher to seek to end time.Sleep(300 * time.Millisecond) @@ -124,10 +124,10 @@ func TestCerberusLogsHandler_ReceiveLogEntries(t *testing.T) { require.NoError(t, err) _, err = file.WriteString(string(logJSON) + "\n") require.NoError(t, err) - file.Sync() + _ = file.Sync() // Read the entry from WebSocket - conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) _, msg, err := conn.ReadMessage() require.NoError(t, err) @@ -152,7 +152,7 @@ func TestCerberusLogsHandler_SourceFilter(t *testing.T) { file, err := os.Create(logPath) require.NoError(t, err) - defer file.Close() + defer func() { _ = file.Close() }() watcher := services.NewLogWatcher(logPath) err = watcher.Start(context.Background()) @@ -170,7 +170,7 @@ func TestCerberusLogsHandler_SourceFilter(t *testing.T) { wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws?source=waf" conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) //nolint:bodyclose // WebSocket Dial response body is consumed by the dial require.NoError(t, err) - defer conn.Close() + defer func() { _ = conn.Close() }() time.Sleep(300 * time.Millisecond) @@ -188,7 +188,7 @@ func TestCerberusLogsHandler_SourceFilter(t *testing.T) { normalLog.Request.Host = "example.com" normalJSON, _ := json.Marshal(normalLog) - file.WriteString(string(normalJSON) + "\n") + _, _ = file.WriteString(string(normalJSON) + "\n") // Write a WAF blocked request (should pass filter) wafLog := models.CaddyAccessLog{ @@ -205,11 +205,11 @@ func TestCerberusLogsHandler_SourceFilter(t *testing.T) { wafLog.Request.Host = "example.com" wafJSON, _ := json.Marshal(wafLog) - file.WriteString(string(wafJSON) + "\n") - file.Sync() + _, _ = file.WriteString(string(wafJSON) + "\n") + _ = file.Sync() // Read from WebSocket - should only get WAF entry - conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) _, msg, err := conn.ReadMessage() require.NoError(t, err) @@ -231,7 +231,7 @@ func TestCerberusLogsHandler_BlockedOnlyFilter(t *testing.T) { file, err := os.Create(logPath) require.NoError(t, err) - defer file.Close() + defer func() { _ = file.Close() }() watcher := services.NewLogWatcher(logPath) err = watcher.Start(context.Background()) @@ -249,7 +249,7 @@ func TestCerberusLogsHandler_BlockedOnlyFilter(t *testing.T) { wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws?blocked_only=true" conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) //nolint:bodyclose // WebSocket Dial response body is consumed by the dial require.NoError(t, err) - defer conn.Close() + defer func() { _ = conn.Close() }() time.Sleep(300 * time.Millisecond) @@ -267,7 +267,7 @@ func TestCerberusLogsHandler_BlockedOnlyFilter(t *testing.T) { normalLog.Request.Host = "example.com" normalJSON, _ := json.Marshal(normalLog) - file.WriteString(string(normalJSON) + "\n") + _, _ = file.WriteString(string(normalJSON) + "\n") // Write a rate limited request (should pass filter) blockedLog := models.CaddyAccessLog{ @@ -283,11 +283,11 @@ func TestCerberusLogsHandler_BlockedOnlyFilter(t *testing.T) { blockedLog.Request.Host = "example.com" blockedJSON, _ := json.Marshal(blockedLog) - file.WriteString(string(blockedJSON) + "\n") - file.Sync() + _, _ = file.WriteString(string(blockedJSON) + "\n") + _ = file.Sync() // Read from WebSocket - should only get blocked entry - conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) _, msg, err := conn.ReadMessage() require.NoError(t, err) @@ -308,7 +308,7 @@ func TestCerberusLogsHandler_IPFilter(t *testing.T) { file, err := os.Create(logPath) require.NoError(t, err) - defer file.Close() + defer func() { _ = file.Close() }() watcher := services.NewLogWatcher(logPath) err = watcher.Start(context.Background()) @@ -326,7 +326,7 @@ func TestCerberusLogsHandler_IPFilter(t *testing.T) { wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws?ip=192.168" conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) //nolint:bodyclose // WebSocket Dial response body is consumed by the dial require.NoError(t, err) - defer conn.Close() + defer func() { _ = conn.Close() }() time.Sleep(300 * time.Millisecond) @@ -344,7 +344,7 @@ func TestCerberusLogsHandler_IPFilter(t *testing.T) { log1.Request.Host = "example.com" json1, _ := json.Marshal(log1) - file.WriteString(string(json1) + "\n") + _, _ = file.WriteString(string(json1) + "\n") // Write request from matching IP log2 := models.CaddyAccessLog{ @@ -360,11 +360,11 @@ func TestCerberusLogsHandler_IPFilter(t *testing.T) { log2.Request.Host = "example.com" json2, _ := json.Marshal(log2) - file.WriteString(string(json2) + "\n") - file.Sync() + _, _ = file.WriteString(string(json2) + "\n") + _ = file.Sync() // Read from WebSocket - should only get matching IP entry - conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) _, msg, err := conn.ReadMessage() require.NoError(t, err) @@ -402,7 +402,7 @@ func TestCerberusLogsHandler_ClientDisconnect(t *testing.T) { require.NoError(t, err) // Close the connection - conn.Close() + _ = conn.Close() // Give time for cleanup time.Sleep(100 * time.Millisecond) @@ -419,7 +419,7 @@ func TestCerberusLogsHandler_MultipleClients(t *testing.T) { file, err := os.Create(logPath) require.NoError(t, err) - defer file.Close() + defer func() { _ = file.Close() }() watcher := services.NewLogWatcher(logPath) err = watcher.Start(context.Background()) @@ -441,7 +441,7 @@ func TestCerberusLogsHandler_MultipleClients(t *testing.T) { // Close all connections after test for _, conn := range conns { if conn != nil { - conn.Close() + _ = conn.Close() } } }() @@ -467,12 +467,12 @@ func TestCerberusLogsHandler_MultipleClients(t *testing.T) { logEntry.Request.Host = "example.com" logJSON, _ := json.Marshal(logEntry) - file.WriteString(string(logJSON) + "\n") - file.Sync() + _, _ = file.WriteString(string(logJSON) + "\n") + _ = file.Sync() // All clients should receive the entry for i, conn := range conns { - conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) _, msg, err := conn.ReadMessage() require.NoError(t, err, "Client %d should receive message", i) diff --git a/backend/internal/api/handlers/certificate_handler_coverage_test.go b/backend/internal/api/handlers/certificate_handler_coverage_test.go index 646fafc0..e382e1da 100644 --- a/backend/internal/api/handlers/certificate_handler_coverage_test.go +++ b/backend/internal/api/handlers/certificate_handler_coverage_test.go @@ -35,7 +35,7 @@ func TestCertificateHandler_List_DBError(t *testing.T) { func TestCertificateHandler_Delete_InvalidID(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}) + _ = db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}) gin.SetMode(gin.TestMode) r := gin.New() @@ -54,7 +54,7 @@ func TestCertificateHandler_Delete_InvalidID(t *testing.T) { func TestCertificateHandler_Delete_NotFound(t *testing.T) { // Use unique in-memory DB per test to avoid SQLite locking issues in parallel test runs db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}) + _ = db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}) gin.SetMode(gin.TestMode) r := gin.New() @@ -73,7 +73,7 @@ func TestCertificateHandler_Delete_NotFound(t *testing.T) { func TestCertificateHandler_Delete_NoBackupService(t *testing.T) { // Use unique in-memory DB per test to avoid SQLite locking issues in parallel test runs db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}) + _ = db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}) // Create certificate cert := models.SSLCertificate{UUID: "test-cert-no-backup", Name: "no-backup-cert", Provider: "custom", Domains: "nobackup.example.com"} @@ -111,7 +111,7 @@ func TestCertificateHandler_Delete_CheckUsageDBError(t *testing.T) { // Use unique in-memory DB per test to avoid SQLite locking issues in parallel test runs db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) // Only migrate SSLCertificate, not ProxyHost to cause error when checking usage - db.AutoMigrate(&models.SSLCertificate{}) + _ = db.AutoMigrate(&models.SSLCertificate{}) // Create certificate cert := models.SSLCertificate{UUID: "test-cert-db-err", Name: "db-error-cert", Provider: "custom", Domains: "dberr.example.com"} @@ -134,7 +134,7 @@ func TestCertificateHandler_Delete_CheckUsageDBError(t *testing.T) { func TestCertificateHandler_List_WithCertificates(t *testing.T) { // Use unique in-memory DB per test to avoid SQLite locking issues in parallel test runs db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}) + _ = db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}) // Create certificates db.Create(&models.SSLCertificate{UUID: "cert-1", Name: "Cert 1", Provider: "custom", Domains: "one.example.com"}) @@ -160,7 +160,7 @@ func TestCertificateHandler_Delete_ZeroID(t *testing.T) { // Tests the ID=0 validation check (line 149-152 in certificate_handler.go) // DELETE /api/certificates/0 should return 400 Bad Request db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}) + _ = db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}) gin.SetMode(gin.TestMode) r := gin.New() diff --git a/backend/internal/api/handlers/certificate_handler_test.go b/backend/internal/api/handlers/certificate_handler_test.go index f5fe2b5b..07f2013f 100644 --- a/backend/internal/api/handlers/certificate_handler_test.go +++ b/backend/internal/api/handlers/certificate_handler_test.go @@ -421,10 +421,10 @@ func TestCertificateHandler_Upload_Success(t *testing.T) { t.Fatalf("failed to generate cert: %v", err) } part, _ := writer.CreateFormFile("certificate_file", "cert.pem") - part.Write([]byte(certPEM)) + _, _ = part.Write([]byte(certPEM)) part2, _ := writer.CreateFormFile("key_file", "key.pem") - part2.Write([]byte(keyPEM)) - writer.Close() + _, _ = part2.Write([]byte(keyPEM)) + _ = writer.Close() req := httptest.NewRequest(http.MethodPost, "/api/certificates", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) @@ -459,9 +459,9 @@ func generateSelfSignedCertPEM() (certPEM, keyPEM string, err error) { return "", "", err } certBuf := new(bytes.Buffer) - pem.Encode(certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + _ = pem.Encode(certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) keyBuf := new(bytes.Buffer) - pem.Encode(keyBuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + _ = pem.Encode(keyBuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) certPEM = certBuf.String() keyPEM = keyBuf.String() return certPEM, keyPEM, nil diff --git a/backend/internal/api/handlers/coverage_helpers_test.go b/backend/internal/api/handlers/coverage_helpers_test.go index a3289e0a..f8d765ee 100644 --- a/backend/internal/api/handlers/coverage_helpers_test.go +++ b/backend/internal/api/handlers/coverage_helpers_test.go @@ -71,12 +71,13 @@ func Test_ttlRemainingSeconds(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ttlRemainingSeconds(tt.now, tt.retrievedAt, tt.ttl) - if tt.wantNil { + switch { + case tt.wantNil: assert.Nil(t, result) - } else if tt.wantZero { + case tt.wantZero: require.NotNil(t, result) assert.Equal(t, int64(0), *result) - } else if tt.wantPositive { + case tt.wantPositive: require.NotNil(t, result) assert.Greater(t, *result, int64(0)) } diff --git a/backend/internal/api/handlers/credential_handler_test.go b/backend/internal/api/handlers/credential_handler_test.go index 9509e551..88f91b42 100644 --- a/backend/internal/api/handlers/credential_handler_test.go +++ b/backend/internal/api/handlers/credential_handler_test.go @@ -26,9 +26,9 @@ import ( func setupCredentialHandlerTest(t *testing.T) (*gin.Engine, *gorm.DB, *models.DNSProvider) { // Set encryption key for test - must be done before any service initialization - os.Setenv("CHARON_ENCRYPTION_KEY", "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=") + _ = os.Setenv("CHARON_ENCRYPTION_KEY", "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=") t.Cleanup(func() { - os.Unsetenv("CHARON_ENCRYPTION_KEY") + _ = os.Unsetenv("CHARON_ENCRYPTION_KEY") }) gin.SetMode(gin.TestMode) @@ -42,7 +42,7 @@ func setupCredentialHandlerTest(t *testing.T) (*gin.Engine, *gorm.DB, *models.DN // Close database connection when test completes t.Cleanup(func() { sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() }) err = db.AutoMigrate( diff --git a/backend/internal/api/handlers/crowdsec_coverage_target_test.go b/backend/internal/api/handlers/crowdsec_coverage_target_test.go index ab7ebafe..bc93fba1 100644 --- a/backend/internal/api/handlers/crowdsec_coverage_target_test.go +++ b/backend/internal/api/handlers/crowdsec_coverage_target_test.go @@ -196,8 +196,8 @@ func TestGetLAPIKeyLookup(t *testing.T) { // TestGetLAPIKeyEmpty tests no env vars set func TestGetLAPIKeyEmpty(t *testing.T) { // Ensure no env vars are set - os.Unsetenv("CROWDSEC_API_KEY") - os.Unsetenv("CROWDSEC_BOUNCER_API_KEY") + _ = os.Unsetenv("CROWDSEC_API_KEY") + _ = os.Unsetenv("CROWDSEC_BOUNCER_API_KEY") key := getLAPIKey() require.Equal(t, "", key) diff --git a/backend/internal/api/handlers/crowdsec_exec_test.go b/backend/internal/api/handlers/crowdsec_exec_test.go index e73fe7b5..2fb50305 100644 --- a/backend/internal/api/handlers/crowdsec_exec_test.go +++ b/backend/internal/api/handlers/crowdsec_exec_test.go @@ -52,10 +52,10 @@ while true; do sleep 1; done // Create mock /proc/{pid}/cmdline with "crowdsec" for the started process procPidDir := filepath.Join(mockProc, strconv.Itoa(pid)) - os.MkdirAll(procPidDir, 0o755) + _ = os.MkdirAll(procPidDir, 0o755) // Use a cmdline that contains "crowdsec" to simulate a real CrowdSec process mockCmdline := "/usr/bin/crowdsec\x00-c\x00/etc/crowdsec/config.yaml" - os.WriteFile(filepath.Join(procPidDir, "cmdline"), []byte(mockCmdline), 0o644) + _ = os.WriteFile(filepath.Join(procPidDir, "cmdline"), []byte(mockCmdline), 0o644) // ensure pid file exists and content matches pidB, err := os.ReadFile(e.pidFile(tmp)) @@ -108,7 +108,7 @@ func TestDefaultCrowdsecExecutor_Status_InvalidPid(t *testing.T) { tmpDir := t.TempDir() // Write invalid pid - os.WriteFile(filepath.Join(tmpDir, "crowdsec.pid"), []byte("invalid"), 0o644) + _ = os.WriteFile(filepath.Join(tmpDir, "crowdsec.pid"), []byte("invalid"), 0o644) running, pid, err := exec.Status(context.Background(), tmpDir) @@ -123,7 +123,7 @@ func TestDefaultCrowdsecExecutor_Status_NonExistentProcess(t *testing.T) { // Write a pid that doesn't exist // Use a very high PID that's unlikely to exist - os.WriteFile(filepath.Join(tmpDir, "crowdsec.pid"), []byte("999999999"), 0o644) + _ = os.WriteFile(filepath.Join(tmpDir, "crowdsec.pid"), []byte("999999999"), 0o644) running, pid, err := exec.Status(context.Background(), tmpDir) @@ -147,7 +147,7 @@ func TestDefaultCrowdsecExecutor_Stop_InvalidPid(t *testing.T) { tmpDir := t.TempDir() // Write invalid pid - os.WriteFile(filepath.Join(tmpDir, "crowdsec.pid"), []byte("invalid"), 0o644) + _ = os.WriteFile(filepath.Join(tmpDir, "crowdsec.pid"), []byte("invalid"), 0o644) err := exec.Stop(context.Background(), tmpDir) @@ -164,7 +164,7 @@ func TestDefaultCrowdsecExecutor_Stop_NonExistentProcess(t *testing.T) { tmpDir := t.TempDir() // Write a pid that doesn't exist - os.WriteFile(filepath.Join(tmpDir, "crowdsec.pid"), []byte("999999999"), 0o644) + _ = os.WriteFile(filepath.Join(tmpDir, "crowdsec.pid"), []byte("999999999"), 0o644) err := exec.Stop(context.Background(), tmpDir) @@ -212,11 +212,11 @@ func TestDefaultCrowdsecExecutor_isCrowdSecProcess_ValidProcess(t *testing.T) { // Create a fake PID directory with crowdsec in cmdline pid := 12345 procPidDir := filepath.Join(tmpDir, strconv.Itoa(pid)) - os.MkdirAll(procPidDir, 0o755) + _ = os.MkdirAll(procPidDir, 0o755) // Write cmdline with crowdsec (null-separated like real /proc) cmdline := "/usr/bin/crowdsec\x00-c\x00/etc/crowdsec/config.yaml" - os.WriteFile(filepath.Join(procPidDir, "cmdline"), []byte(cmdline), 0o644) + _ = os.WriteFile(filepath.Join(procPidDir, "cmdline"), []byte(cmdline), 0o644) assert.True(t, exec.isCrowdSecProcess(pid), "Should detect CrowdSec process") } @@ -231,11 +231,11 @@ func TestDefaultCrowdsecExecutor_isCrowdSecProcess_DifferentProcess(t *testing.T // Create a fake PID directory with a different process (like dlv debugger) pid := 12345 procPidDir := filepath.Join(tmpDir, strconv.Itoa(pid)) - os.MkdirAll(procPidDir, 0o755) + _ = os.MkdirAll(procPidDir, 0o755) // Write cmdline with dlv (the original bug case) cmdline := "/usr/local/bin/dlv\x00--telemetry\x00--headless" - os.WriteFile(filepath.Join(procPidDir, "cmdline"), []byte(cmdline), 0o644) + _ = os.WriteFile(filepath.Join(procPidDir, "cmdline"), []byte(cmdline), 0o644) assert.False(t, exec.isCrowdSecProcess(pid), "Should NOT detect dlv as CrowdSec") } @@ -261,10 +261,10 @@ func TestDefaultCrowdsecExecutor_isCrowdSecProcess_EmptyCmdline(t *testing.T) { // Create a fake PID directory with empty cmdline pid := 12345 procPidDir := filepath.Join(tmpDir, strconv.Itoa(pid)) - os.MkdirAll(procPidDir, 0o755) + _ = os.MkdirAll(procPidDir, 0o755) // Write empty cmdline - os.WriteFile(filepath.Join(procPidDir, "cmdline"), []byte(""), 0o644) + _ = os.WriteFile(filepath.Join(procPidDir, "cmdline"), []byte(""), 0o644) assert.False(t, exec.isCrowdSecProcess(pid), "Should return false for empty cmdline") } @@ -281,12 +281,12 @@ func TestDefaultCrowdsecExecutor_Status_PIDReuse_DifferentProcess(t *testing.T) currentPID := os.Getpid() // Write current PID to the crowdsec.pid file (simulating stale PID file) - os.WriteFile(filepath.Join(tmpDir, "crowdsec.pid"), []byte(strconv.Itoa(currentPID)), 0o644) + _ = os.WriteFile(filepath.Join(tmpDir, "crowdsec.pid"), []byte(strconv.Itoa(currentPID)), 0o644) // Create mock /proc entry for current PID but with a non-crowdsec cmdline procPidDir := filepath.Join(mockProc, strconv.Itoa(currentPID)) - os.MkdirAll(procPidDir, 0o755) - os.WriteFile(filepath.Join(procPidDir, "cmdline"), []byte("/usr/local/bin/dlv\x00debug"), 0o644) + _ = os.MkdirAll(procPidDir, 0o755) + _ = os.WriteFile(filepath.Join(procPidDir, "cmdline"), []byte("/usr/local/bin/dlv\x00debug"), 0o644) // Status should return NOT running because the PID is not CrowdSec running, pid, err := exec.Status(context.Background(), tmpDir) @@ -308,12 +308,12 @@ func TestDefaultCrowdsecExecutor_Status_PIDReuse_IsCrowdSec(t *testing.T) { currentPID := os.Getpid() // Write current PID to the crowdsec.pid file - os.WriteFile(filepath.Join(tmpDir, "crowdsec.pid"), []byte(strconv.Itoa(currentPID)), 0o644) + _ = os.WriteFile(filepath.Join(tmpDir, "crowdsec.pid"), []byte(strconv.Itoa(currentPID)), 0o644) // Create mock /proc entry for current PID with crowdsec cmdline procPidDir := filepath.Join(mockProc, strconv.Itoa(currentPID)) - os.MkdirAll(procPidDir, 0o755) - os.WriteFile(filepath.Join(procPidDir, "cmdline"), []byte("/usr/bin/crowdsec\x00-c\x00config.yaml"), 0o644) + _ = os.MkdirAll(procPidDir, 0o755) + _ = os.WriteFile(filepath.Join(procPidDir, "cmdline"), []byte("/usr/bin/crowdsec\x00-c\x00config.yaml"), 0o644) // Status should return running because it IS CrowdSec running, pid, err := exec.Status(context.Background(), tmpDir) @@ -329,7 +329,7 @@ func TestDefaultCrowdsecExecutor_Stop_SignalError(t *testing.T) { // Write a pid for a process that exists but we can't signal (e.g., init process or other user's process) // Use PID 1 which exists but typically can't be signaled by non-root - os.WriteFile(filepath.Join(tmpDir, "crowdsec.pid"), []byte("1"), 0o644) + _ = os.WriteFile(filepath.Join(tmpDir, "crowdsec.pid"), []byte("1"), 0o644) err := exec.Stop(context.Background(), tmpDir) diff --git a/backend/internal/api/handlers/crowdsec_handler_comprehensive_test.go b/backend/internal/api/handlers/crowdsec_handler_comprehensive_test.go index 3a23cfa6..febf99ff 100644 --- a/backend/internal/api/handlers/crowdsec_handler_comprehensive_test.go +++ b/backend/internal/api/handlers/crowdsec_handler_comprehensive_test.go @@ -120,10 +120,10 @@ func TestIsConsoleEnrollmentEnabled(t *testing.T) { envValue: "true", want: true, setupFunc: func() { - os.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") + _ = os.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "true") }, cleanup: func() { - os.Unsetenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT") + _ = os.Unsetenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT") }, }, { @@ -131,10 +131,10 @@ func TestIsConsoleEnrollmentEnabled(t *testing.T) { envValue: "false", want: false, setupFunc: func() { - os.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "false") + _ = os.Setenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT", "false") }, cleanup: func() { - os.Unsetenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT") + _ = os.Unsetenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT") }, }, { @@ -142,7 +142,7 @@ func TestIsConsoleEnrollmentEnabled(t *testing.T) { envValue: "", want: false, setupFunc: func() { - os.Unsetenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT") + _ = os.Unsetenv("FEATURE_CROWDSEC_CONSOLE_ENROLLMENT") }, cleanup: func() {}, }, diff --git a/backend/internal/api/handlers/crowdsec_handler_coverage_test.go b/backend/internal/api/handlers/crowdsec_handler_coverage_test.go index 60b8c555..980486f3 100644 --- a/backend/internal/api/handlers/crowdsec_handler_coverage_test.go +++ b/backend/internal/api/handlers/crowdsec_handler_coverage_test.go @@ -218,7 +218,7 @@ func TestCrowdsec_ExportConfig_NotFound(t *testing.T) { db := setupCrowdDB(t) // Use a non-existent directory nonExistentDir := "/tmp/crowdsec-nonexistent-dir-12345" - os.RemoveAll(nonExistentDir) // Make sure it doesn't exist + _ = os.RemoveAll(nonExistentDir) // Make sure it doesn't exist h := NewCrowdsecHandler(db, &fakeExec{}, "/bin/false", nonExistentDir) // remove any cache dir created during handler init so Export sees missing dir @@ -254,7 +254,7 @@ func TestCrowdsec_ListFiles_EmptyDir(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) // Files may be nil or empty array when dir is empty files := resp["files"] if files != nil { @@ -266,7 +266,7 @@ func TestCrowdsec_ListFiles_NonExistent(t *testing.T) { gin.SetMode(gin.TestMode) db := setupCrowdDB(t) nonExistentDir := "/tmp/crowdsec-nonexistent-dir-67890" - os.RemoveAll(nonExistentDir) + _ = os.RemoveAll(nonExistentDir) h := NewCrowdsecHandler(db, &fakeExec{}, "/bin/false", nonExistentDir) @@ -280,7 +280,7 @@ func TestCrowdsec_ListFiles_NonExistent(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) // Should return empty array (nil) for non-existent dir // The files key should exist _, ok := resp["files"] @@ -330,7 +330,7 @@ func TestCrowdsec_ReadFile_NestedPath(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, "nested content", resp["content"]) } diff --git a/backend/internal/api/handlers/crowdsec_handler_test.go b/backend/internal/api/handlers/crowdsec_handler_test.go index 79aee2e5..8180e426 100644 --- a/backend/internal/api/handlers/crowdsec_handler_test.go +++ b/backend/internal/api/handlers/crowdsec_handler_test.go @@ -106,8 +106,8 @@ func TestImportConfig(t *testing.T) { buf := &bytes.Buffer{} mw := multipart.NewWriter(buf) fw, _ := mw.CreateFormFile("file", "cfg.tar.gz") - fw.Write([]byte("dummy")) - mw.Close() + _, _ = fw.Write([]byte("dummy")) + _ = mw.Close() w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/crowdsec/import", buf) @@ -143,8 +143,8 @@ func TestImportCreatesBackup(t *testing.T) { buf := &bytes.Buffer{} mw := multipart.NewWriter(buf) fw, _ := mw.CreateFormFile("file", "cfg.tar.gz") - fw.Write([]byte("dummy2")) - mw.Close() + _, _ = fw.Write([]byte("dummy2")) + _ = mw.Close() w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/crowdsec/import", buf) @@ -158,7 +158,7 @@ func TestImportCreatesBackup(t *testing.T) { found := false entries, _ := os.ReadDir(filepath.Dir(tmpDir)) for _, e := range entries { - if e.IsDir() && filepath.HasPrefix(e.Name(), filepath.Base(tmpDir)+".backup.") { + if e.IsDir() && strings.HasPrefix(e.Name(), filepath.Base(tmpDir)+".backup.") { found = true break } @@ -1308,7 +1308,7 @@ func TestCrowdsecHandler_ExportConfig_DirNotFound(t *testing.T) { db := setupCrowdDB(t) // Use a non-existent directory nonExistentDir := "/tmp/crowdsec-nonexistent-test-" + t.Name() - os.RemoveAll(nonExistentDir) + _ = os.RemoveAll(nonExistentDir) h := NewCrowdsecHandler(db, &fakeExec{}, "/bin/false", nonExistentDir) // Remove any cache dir created during handler init so Export sees missing dir diff --git a/backend/internal/api/handlers/db_health_handler_test.go b/backend/internal/api/handlers/db_health_handler_test.go index daeefb8a..aa1a8f7c 100644 --- a/backend/internal/api/handlers/db_health_handler_test.go +++ b/backend/internal/api/handlers/db_health_handler_test.go @@ -169,7 +169,7 @@ func TestNewDBHealthHandler(t *testing.T) { // With backup service tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "charon.db") - os.WriteFile(dbPath, []byte("test"), 0o644) + _ = os.WriteFile(dbPath, []byte("test"), 0o644) cfg := &config.Config{DatabasePath: dbPath} backupSvc := services.NewBackupService(cfg) @@ -196,7 +196,7 @@ func TestDBHealthHandler_Check_CorruptedDatabase(t *testing.T) { // Close it sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() // Corrupt the database file corruptDBFile(t, dbPath) @@ -243,14 +243,14 @@ func TestDBHealthHandler_Check_BackupServiceError(t *testing.T) { // Create backup service with unreadable directory tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "charon.db") - os.WriteFile(dbPath, []byte("test"), 0o644) + _ = os.WriteFile(dbPath, []byte("test"), 0o644) cfg := &config.Config{DatabasePath: dbPath} backupService := services.NewBackupService(cfg) // Make backup directory unreadable to trigger error in GetLastBackupTime - os.Chmod(backupService.BackupDir, 0o000) - defer os.Chmod(backupService.BackupDir, 0o755) // Restore for cleanup + _ = os.Chmod(backupService.BackupDir, 0o000) + defer func() { _ = os.Chmod(backupService.BackupDir, 0o755) }() // Restore for cleanup handler := NewDBHealthHandler(db, backupService) @@ -284,7 +284,7 @@ func TestDBHealthHandler_Check_BackupTimeZero(t *testing.T) { // Create backup service with empty backup directory (no backups yet) tmpDir := t.TempDir() dbPath := filepath.Join(tmpDir, "charon.db") - os.WriteFile(dbPath, []byte("test"), 0o644) + _ = os.WriteFile(dbPath, []byte("test"), 0o644) cfg := &config.Config{DatabasePath: dbPath} backupService := services.NewBackupService(cfg) @@ -314,7 +314,7 @@ func corruptDBFile(t *testing.T, dbPath string) { t.Helper() f, err := os.OpenFile(dbPath, os.O_RDWR, 0o644) require.NoError(t, err) - defer f.Close() + defer func() { _ = f.Close() }() // Get file size stat, err := f.Stat() diff --git a/backend/internal/api/handlers/encryption_handler_test.go b/backend/internal/api/handlers/encryption_handler_test.go index d63fb284..0ece61d0 100644 --- a/backend/internal/api/handlers/encryption_handler_test.go +++ b/backend/internal/api/handlers/encryption_handler_test.go @@ -23,7 +23,7 @@ func setupEncryptionTestDB(t *testing.T) *gorm.DB { // Use a unique file-based database for each test to avoid sharing state dbPath := fmt.Sprintf("/tmp/test_encryption_%d.db", time.Now().UnixNano()) t.Cleanup(func() { - os.Remove(dbPath) + _ = os.Remove(dbPath) }) db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ @@ -69,8 +69,8 @@ func TestEncryptionHandler_GetStatus(t *testing.T) { // Generate test keys currentKey, err := crypto.GenerateNewKey() require.NoError(t, err) - os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) - defer os.Unsetenv("CHARON_ENCRYPTION_KEY") + _ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) + defer func() { _ = os.Unsetenv("CHARON_ENCRYPTION_KEY") }() rotationService, err := crypto.NewRotationService(db) require.NoError(t, err) @@ -111,8 +111,8 @@ func TestEncryptionHandler_GetStatus(t *testing.T) { t.Run("status shows next key when configured", func(t *testing.T) { nextKey, err := crypto.GenerateNewKey() require.NoError(t, err) - os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey) - defer os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT") + _ = os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey) + defer func() { _ = os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT") }() rotationService, err := crypto.NewRotationService(db) require.NoError(t, err) @@ -137,7 +137,7 @@ func TestEncryptionHandler_GetStatus(t *testing.T) { // Close the database to trigger an error sqlDB, err := db.DB() require.NoError(t, err) - sqlDB.Close() + _ = sqlDB.Close() rotationService, err := crypto.NewRotationService(db) require.NoError(t, err) @@ -163,11 +163,11 @@ func TestEncryptionHandler_Rotate(t *testing.T) { nextKey, err := crypto.GenerateNewKey() require.NoError(t, err) - os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) - os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey) + _ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) + _ = os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey) defer func() { - os.Unsetenv("CHARON_ENCRYPTION_KEY") - os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT") + _ = os.Unsetenv("CHARON_ENCRYPTION_KEY") + _ = os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT") }() // Create test providers @@ -233,8 +233,8 @@ func TestEncryptionHandler_Rotate(t *testing.T) { }) t.Run("rotation fails without next key", func(t *testing.T) { - os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT") - defer os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey) + _ = os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT") + defer func() { _ = os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey) }() rotationService, err := crypto.NewRotationService(db) require.NoError(t, err) @@ -256,8 +256,8 @@ func TestEncryptionHandler_GetHistory(t *testing.T) { currentKey, err := crypto.GenerateNewKey() require.NoError(t, err) - os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) - defer os.Unsetenv("CHARON_ENCRYPTION_KEY") + _ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) + defer func() { _ = os.Unsetenv("CHARON_ENCRYPTION_KEY") }() rotationService, err := crypto.NewRotationService(db) require.NoError(t, err) @@ -273,7 +273,7 @@ func TestEncryptionHandler_GetHistory(t *testing.T) { EventCategory: "encryption", Details: "{}", } - securityService.LogAudit(audit) + _ = securityService.LogAudit(audit) } // Flush async audit logging @@ -330,7 +330,7 @@ func TestEncryptionHandler_GetHistory(t *testing.T) { t.Run("history error when service fails", func(t *testing.T) { // Create a new DB that will be closed to trigger error dbPath := fmt.Sprintf("/tmp/test_encryption_fail_%d.db", time.Now().UnixNano()) - defer os.Remove(dbPath) + defer func() { _ = os.Remove(dbPath) }() failDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{PrepareStmt: false}) require.NoError(t, err) @@ -338,8 +338,8 @@ func TestEncryptionHandler_GetHistory(t *testing.T) { currentKey, err := crypto.GenerateNewKey() require.NoError(t, err) - os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) - defer os.Unsetenv("CHARON_ENCRYPTION_KEY") + _ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) + defer func() { _ = os.Unsetenv("CHARON_ENCRYPTION_KEY") }() rotationService, err := crypto.NewRotationService(failDB) require.NoError(t, err) @@ -349,7 +349,7 @@ func TestEncryptionHandler_GetHistory(t *testing.T) { // Close the database to trigger errors sqlDB, err := failDB.DB() require.NoError(t, err) - sqlDB.Close() + _ = sqlDB.Close() handler := NewEncryptionHandler(rotationService, failSecurityService) router := setupEncryptionTestRouter(handler, true) @@ -370,8 +370,8 @@ func TestEncryptionHandler_Validate(t *testing.T) { currentKey, err := crypto.GenerateNewKey() require.NoError(t, err) - os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) - defer os.Unsetenv("CHARON_ENCRYPTION_KEY") + _ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) + defer func() { _ = os.Unsetenv("CHARON_ENCRYPTION_KEY") }() rotationService, err := crypto.NewRotationService(db) require.NoError(t, err) @@ -418,8 +418,8 @@ func TestEncryptionHandler_Validate(t *testing.T) { t.Run("validation fails with invalid key configuration", func(t *testing.T) { // Unset the encryption key to trigger validation failure - os.Unsetenv("CHARON_ENCRYPTION_KEY") - defer os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) + _ = os.Unsetenv("CHARON_ENCRYPTION_KEY") + defer func() { _ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) }() // Create rotation service with no key configured rotationService, err := crypto.NewRotationService(db) @@ -464,8 +464,8 @@ func TestEncryptionHandler_IntegrationFlow(t *testing.T) { nextKey, err := crypto.GenerateNewKey() require.NoError(t, err) - os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) - defer os.Unsetenv("CHARON_ENCRYPTION_KEY") + _ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) + defer func() { _ = os.Unsetenv("CHARON_ENCRYPTION_KEY") }() // Create initial provider currentService, err := crypto.NewEncryptionService(currentKey) @@ -505,7 +505,7 @@ func TestEncryptionHandler_IntegrationFlow(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) // Step 3: Configure next key - os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey) + _ = os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey) defer os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT") // Reinitialize rotation service to pick up new key @@ -643,8 +643,8 @@ func TestEncryptionHandler_RefreshKey_RotatesCredentials(t *testing.T) { nextKey, err := crypto.GenerateNewKey() require.NoError(t, err) - os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) - os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey) + _ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) + _ = os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey) defer func() { os.Unsetenv("CHARON_ENCRYPTION_KEY") os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT") @@ -699,7 +699,7 @@ func TestEncryptionHandler_RefreshKey_FailsWithoutProvider(t *testing.T) { // Set only current key, no next key currentKey, err := crypto.GenerateNewKey() require.NoError(t, err) - os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) + _ = os.Setenv("CHARON_ENCRYPTION_KEY", currentKey) defer os.Unsetenv("CHARON_ENCRYPTION_KEY") rotationService, err := crypto.NewRotationService(db) @@ -750,8 +750,8 @@ func TestEncryptionHandler_RefreshKey_InvalidOldKey(t *testing.T) { require.NoError(t, db.Create(&provider).Error) // Now set wrong key and try to rotate - os.Setenv("CHARON_ENCRYPTION_KEY", wrongKey) - os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey) + _ = os.Setenv("CHARON_ENCRYPTION_KEY", wrongKey) + _ = os.Setenv("CHARON_ENCRYPTION_KEY_NEXT", nextKey) defer func() { os.Unsetenv("CHARON_ENCRYPTION_KEY") os.Unsetenv("CHARON_ENCRYPTION_KEY_NEXT") diff --git a/backend/internal/api/handlers/handlers_test.go b/backend/internal/api/handlers/handlers_test.go index 0dd40ade..d44498b5 100644 --- a/backend/internal/api/handlers/handlers_test.go +++ b/backend/internal/api/handlers/handlers_test.go @@ -22,7 +22,7 @@ func setupTestDB(t *testing.T) *gorm.DB { db := handlers.OpenTestDB(t) // Auto migrate all models that handlers depend on - db.AutoMigrate( + _ = db.AutoMigrate( &models.ProxyHost{}, &models.Location{}, &models.RemoteServer{}, diff --git a/backend/internal/api/handlers/import_handler_sanitize_test.go b/backend/internal/api/handlers/import_handler_sanitize_test.go index 8e1d875d..98c2736d 100644 --- a/backend/internal/api/handlers/import_handler_sanitize_test.go +++ b/backend/internal/api/handlers/import_handler_sanitize_test.go @@ -23,7 +23,7 @@ func TestImportUploadSanitizesFilename(t *testing.T) { db := OpenTestDB(t) // Create a fake caddy executable to avoid dependency on system binary fakeCaddy := filepath.Join(tmpDir, "caddy") - os.WriteFile(fakeCaddy, []byte("#!/bin/sh\nexit 0"), 0o755) + _ = os.WriteFile(fakeCaddy, []byte("#!/bin/sh\nexit 0"), 0o755) svc := NewImportHandler(db, fakeCaddy, tmpDir, "") router := gin.New() diff --git a/backend/internal/api/handlers/import_handler_test.go b/backend/internal/api/handlers/import_handler_test.go index 813529f7..73e57591 100644 --- a/backend/internal/api/handlers/import_handler_test.go +++ b/backend/internal/api/handlers/import_handler_test.go @@ -26,7 +26,7 @@ func setupImportTestDB(t *testing.T) *gorm.DB { if err != nil { panic("failed to connect to test database") } - db.AutoMigrate(&models.ImportSession{}, &models.ProxyHost{}, &models.Location{}) + _ = db.AutoMigrate(&models.ImportSession{}, &models.ProxyHost{}, &models.Location{}) return db } @@ -52,7 +52,7 @@ func TestImportHandler_GetStatus(t *testing.T) { // Case 2: No DB session but has mounted Caddyfile tmpDir := t.TempDir() mountPath := filepath.Join(tmpDir, "mounted.caddyfile") - os.WriteFile(mountPath, []byte("example.com"), 0o644) + _ = os.WriteFile(mountPath, []byte("example.com"), 0o644) //nolint:gosec // G306: test file handler2 := handlers.NewImportHandler(db, "echo", "/tmp", mountPath) router2 := gin.New() @@ -115,7 +115,7 @@ func TestImportHandler_GetPreview(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var result map[string]any - json.Unmarshal(w.Body.Bytes(), &result) + _ = json.Unmarshal(w.Body.Bytes(), &result) preview := result["preview"].(map[string]any) hosts := preview["hosts"].([]any) @@ -198,7 +198,7 @@ func TestImportHandler_Upload(t *testing.T) { // Use fake caddy script cwd, _ := os.Getwd() fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy.sh") - os.Chmod(fakeCaddy, 0o755) + _ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions tmpDir := t.TempDir() handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, "") @@ -231,7 +231,7 @@ func TestImportHandler_GetPreview_WithContent(t *testing.T) { // Case: Active session with source file content := "example.com {\n reverse_proxy localhost:8080\n}" sourceFile := filepath.Join(tmpDir, "source.caddyfile") - err := os.WriteFile(sourceFile, []byte(content), 0o644) + err := os.WriteFile(sourceFile, []byte(content), 0o644) //nolint:gosec // G306: test file assert.NoError(t, err) // Case: Active session with source file @@ -320,14 +320,14 @@ func TestCheckMountedImport(t *testing.T) { // Use fake caddy script cwd, _ := os.Getwd() fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy.sh") - os.Chmod(fakeCaddy, 0o755) + _ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions // Case 1: File does not exist err := handlers.CheckMountedImport(db, mountPath, fakeCaddy, tmpDir) assert.NoError(t, err) // Case 2: File exists, not processed - err = os.WriteFile(mountPath, []byte("example.com"), 0o644) + err = os.WriteFile(mountPath, []byte("example.com"), 0o644) //nolint:gosec // G306: test file assert.NoError(t, err) err = handlers.CheckMountedImport(db, mountPath, fakeCaddy, tmpDir) @@ -368,7 +368,7 @@ func TestImportHandler_Upload_Failure(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) // The error message comes from Upload -> ImportFile -> "import failed: ..." assert.Contains(t, resp["error"], "import failed") } @@ -431,10 +431,10 @@ func TestImportHandler_GetPreview_BackupContent(t *testing.T) { // Create backup file backupDir := filepath.Join(tmpDir, "backups") - os.MkdirAll(backupDir, 0o755) + _ = os.MkdirAll(backupDir, 0o755) //nolint:gosec // G301: test dir content := "backup content" backupFile := filepath.Join(backupDir, "source.caddyfile") - os.WriteFile(backupFile, []byte(content), 0o644) + _ = os.WriteFile(backupFile, []byte(content), 0o644) //nolint:gosec // G306: test file // Case: Active session with missing source file but existing backup session := models.ImportSession{ @@ -451,7 +451,7 @@ func TestImportHandler_GetPreview_BackupContent(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var result map[string]any - json.Unmarshal(w.Body.Bytes(), &result) + _ = json.Unmarshal(w.Body.Bytes(), &result) assert.Equal(t, content, result["caddyfile_content"]) } @@ -478,13 +478,13 @@ func TestImportHandler_GetPreview_TransientMount(t *testing.T) { // Create a mounted Caddyfile content := "example.com" - err := os.WriteFile(mountPath, []byte(content), 0o644) + err := os.WriteFile(mountPath, []byte(content), 0o644) //nolint:gosec // G306: test file assert.NoError(t, err) // Use fake caddy script cwd, _ := os.Getwd() fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_hosts.sh") - os.Chmod(fakeCaddy, 0o755) + _ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, mountPath) router := gin.New() @@ -522,7 +522,7 @@ func TestImportHandler_Commit_TransientUpload(t *testing.T) { // Use fake caddy script cwd, _ := os.Getwd() fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_hosts.sh") - os.Chmod(fakeCaddy, 0o755) + _ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, "") router := gin.New() @@ -542,7 +542,7 @@ func TestImportHandler_Commit_TransientUpload(t *testing.T) { // Extract session ID var uploadResp map[string]any - json.Unmarshal(w.Body.Bytes(), &uploadResp) + _ = json.Unmarshal(w.Body.Bytes(), &uploadResp) session := uploadResp["session"].(map[string]any) sessionID := session["id"].(string) @@ -580,13 +580,13 @@ func TestImportHandler_Commit_TransientMount(t *testing.T) { mountPath := filepath.Join(tmpDir, "mounted.caddyfile") // Create a mounted Caddyfile - err := os.WriteFile(mountPath, []byte("mounted.com"), 0o644) + err := os.WriteFile(mountPath, []byte("mounted.com"), 0o644) //nolint:gosec // G306: test file assert.NoError(t, err) // Use fake caddy script cwd, _ := os.Getwd() fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_hosts.sh") - os.Chmod(fakeCaddy, 0o755) + _ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, mountPath) router := gin.New() @@ -627,7 +627,7 @@ func TestImportHandler_Cancel_TransientUpload(t *testing.T) { // Use fake caddy script cwd, _ := os.Getwd() fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_hosts.sh") - os.Chmod(fakeCaddy, 0o755) + _ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, "") router := gin.New() @@ -647,7 +647,7 @@ func TestImportHandler_Cancel_TransientUpload(t *testing.T) { // Extract session ID and file path var uploadResp map[string]any - json.Unmarshal(w.Body.Bytes(), &uploadResp) + _ = json.Unmarshal(w.Body.Bytes(), &uploadResp) session := uploadResp["session"].(map[string]any) sessionID := session["id"].(string) sourceFile := session["source_file"].(string) @@ -794,7 +794,7 @@ func TestImportHandler_UploadMulti(t *testing.T) { // Use fake caddy script cwd, _ := os.Getwd() fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy_hosts.sh") - os.Chmod(fakeCaddy, 0o755) + _ = os.Chmod(fakeCaddy, 0o755) //nolint:gosec // G302: test script needs exec permissions handler := handlers.NewImportHandler(db, fakeCaddy, tmpDir, "") router := gin.New() @@ -816,7 +816,7 @@ func TestImportHandler_UploadMulti(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.NotNil(t, resp["session"]) assert.NotNil(t, resp["preview"]) }) @@ -839,7 +839,7 @@ func TestImportHandler_UploadMulti(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) session := resp["session"].(map[string]any) assert.Equal(t, "transient", session["state"]) }) @@ -893,7 +893,7 @@ func TestImportHandler_UploadMulti(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Contains(t, resp["error"], "empty") }) } diff --git a/backend/internal/api/handlers/logs_handler_coverage_test.go b/backend/internal/api/handlers/logs_handler_coverage_test.go index 9994c213..7e6a0b4b 100644 --- a/backend/internal/api/handlers/logs_handler_coverage_test.go +++ b/backend/internal/api/handlers/logs_handler_coverage_test.go @@ -21,17 +21,17 @@ func TestLogsHandler_Read_FilterBySearch(t *testing.T) { tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") - os.MkdirAll(dataDir, 0o755) + _ = os.MkdirAll(dataDir, 0o755) dbPath := filepath.Join(dataDir, "charon.db") logsDir := filepath.Join(dataDir, "logs") - os.MkdirAll(logsDir, 0o755) + _ = os.MkdirAll(logsDir, 0o755) // Write JSON log lines content := `{"level":"info","ts":1600000000,"msg":"request handled","request":{"method":"GET","host":"example.com","uri":"/api/search","remote_ip":"1.2.3.4"},"status":200} {"level":"error","ts":1600000060,"msg":"error occurred","request":{"method":"POST","host":"example.com","uri":"/api/submit","remote_ip":"5.6.7.8"},"status":500} ` - os.WriteFile(filepath.Join(logsDir, "access.log"), []byte(content), 0o644) + _ = os.WriteFile(filepath.Join(logsDir, "access.log"), []byte(content), 0o644) cfg := &config.Config{DatabasePath: dbPath} svc := services.NewLogService(cfg) @@ -54,16 +54,16 @@ func TestLogsHandler_Read_FilterByHost(t *testing.T) { tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") - os.MkdirAll(dataDir, 0o755) + _ = os.MkdirAll(dataDir, 0o755) dbPath := filepath.Join(dataDir, "charon.db") logsDir := filepath.Join(dataDir, "logs") - os.MkdirAll(logsDir, 0o755) + _ = os.MkdirAll(logsDir, 0o755) content := `{"level":"info","ts":1600000000,"msg":"request handled","request":{"method":"GET","host":"example.com","uri":"/","remote_ip":"1.2.3.4"},"status":200} {"level":"info","ts":1600000001,"msg":"request handled","request":{"method":"GET","host":"other.com","uri":"/","remote_ip":"1.2.3.4"},"status":200} ` - os.WriteFile(filepath.Join(logsDir, "access.log"), []byte(content), 0o644) + _ = os.WriteFile(filepath.Join(logsDir, "access.log"), []byte(content), 0o644) cfg := &config.Config{DatabasePath: dbPath} svc := services.NewLogService(cfg) @@ -84,16 +84,16 @@ func TestLogsHandler_Read_FilterByLevel(t *testing.T) { tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") - os.MkdirAll(dataDir, 0o755) + _ = os.MkdirAll(dataDir, 0o755) dbPath := filepath.Join(dataDir, "charon.db") logsDir := filepath.Join(dataDir, "logs") - os.MkdirAll(logsDir, 0o755) + _ = os.MkdirAll(logsDir, 0o755) content := `{"level":"info","ts":1600000000,"msg":"info message"} {"level":"error","ts":1600000001,"msg":"error message"} ` - os.WriteFile(filepath.Join(logsDir, "access.log"), []byte(content), 0o644) + _ = os.WriteFile(filepath.Join(logsDir, "access.log"), []byte(content), 0o644) cfg := &config.Config{DatabasePath: dbPath} svc := services.NewLogService(cfg) @@ -114,16 +114,16 @@ func TestLogsHandler_Read_FilterByStatus(t *testing.T) { tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") - os.MkdirAll(dataDir, 0o755) + _ = os.MkdirAll(dataDir, 0o755) dbPath := filepath.Join(dataDir, "charon.db") logsDir := filepath.Join(dataDir, "logs") - os.MkdirAll(logsDir, 0o755) + _ = os.MkdirAll(logsDir, 0o755) content := `{"level":"info","ts":1600000000,"msg":"200 OK","request":{"host":"example.com"},"status":200} {"level":"error","ts":1600000001,"msg":"500 Error","request":{"host":"example.com"},"status":500} ` - os.WriteFile(filepath.Join(logsDir, "access.log"), []byte(content), 0o644) + _ = os.WriteFile(filepath.Join(logsDir, "access.log"), []byte(content), 0o644) cfg := &config.Config{DatabasePath: dbPath} svc := services.NewLogService(cfg) @@ -144,16 +144,16 @@ func TestLogsHandler_Read_SortAsc(t *testing.T) { tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") - os.MkdirAll(dataDir, 0o755) + _ = os.MkdirAll(dataDir, 0o755) dbPath := filepath.Join(dataDir, "charon.db") logsDir := filepath.Join(dataDir, "logs") - os.MkdirAll(logsDir, 0o755) + _ = os.MkdirAll(logsDir, 0o755) content := `{"level":"info","ts":1600000000,"msg":"first"} {"level":"info","ts":1600000001,"msg":"second"} ` - os.WriteFile(filepath.Join(logsDir, "access.log"), []byte(content), 0o644) + _ = os.WriteFile(filepath.Join(logsDir, "access.log"), []byte(content), 0o644) cfg := &config.Config{DatabasePath: dbPath} svc := services.NewLogService(cfg) @@ -174,13 +174,13 @@ func TestLogsHandler_List_DirectoryIsFile(t *testing.T) { tmpDir := t.TempDir() dataDir := filepath.Join(tmpDir, "data") - os.MkdirAll(dataDir, 0o755) + _ = os.MkdirAll(dataDir, 0o755) dbPath := filepath.Join(dataDir, "charon.db") logsDir := filepath.Join(dataDir, "logs") // Create logs dir as a file to cause error - os.WriteFile(logsDir, []byte("not a dir"), 0o644) + _ = os.WriteFile(logsDir, []byte("not a dir"), 0o644) cfg := &config.Config{DatabasePath: dbPath} svc := services.NewLogService(cfg) diff --git a/backend/internal/api/handlers/logs_handler_test.go b/backend/internal/api/handlers/logs_handler_test.go index bca59d1f..4311232f 100644 --- a/backend/internal/api/handlers/logs_handler_test.go +++ b/backend/internal/api/handlers/logs_handler_test.go @@ -69,7 +69,7 @@ func setupLogsTest(t *testing.T) (*gin.Engine, *services.LogService, string) { func TestLogsLifecycle(t *testing.T) { router, _, tmpDir := setupLogsTest(t) - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // 1. List logs req := httptest.NewRequest(http.MethodGet, "/api/v1/logs", http.NoBody) @@ -99,9 +99,9 @@ func TestLogsLifecycle(t *testing.T) { require.Equal(t, http.StatusOK, resp.Code) var content struct { - Filename string `json:"filename"` - Logs []any `json:"logs"` - Total int `json:"total"` + Filename string `json:"filename"` + Logs []any `json:"logs"` + Total int `json:"total"` } err = json.Unmarshal(resp.Body.Bytes(), &content) require.NoError(t, err) @@ -127,7 +127,7 @@ func TestLogsLifecycle(t *testing.T) { require.Equal(t, http.StatusNotFound, resp.Code) // 6. List logs error (delete directory) - os.RemoveAll(filepath.Join(tmpDir, "data", "logs")) + _ = os.RemoveAll(filepath.Join(tmpDir, "data", "logs")) req = httptest.NewRequest(http.MethodGet, "/api/v1/logs", http.NoBody) resp = httptest.NewRecorder() router.ServeHTTP(resp, req) @@ -141,7 +141,7 @@ func TestLogsLifecycle(t *testing.T) { func TestLogsHandler_PathTraversal(t *testing.T) { _, _, tmpDir := setupLogsTest(t) - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Manually invoke handler to bypass Gin router cleaning w := httptest.NewRecorder() diff --git a/backend/internal/api/handlers/manual_challenge_handler.go b/backend/internal/api/handlers/manual_challenge_handler.go new file mode 100644 index 00000000..fdec783f --- /dev/null +++ b/backend/internal/api/handlers/manual_challenge_handler.go @@ -0,0 +1,657 @@ +package handlers + +import ( + "context" + "errors" + "net/http" + "strconv" + + "github.com/Wikid82/charon/backend/internal/models" + "github.com/Wikid82/charon/backend/internal/services" + "github.com/gin-gonic/gin" +) + +// ManualChallengeServiceInterface defines the interface for manual challenge operations. +// This allows for easier testing by enabling mock implementations. +type ManualChallengeServiceInterface interface { + CreateChallenge(ctx context.Context, req services.CreateChallengeRequest) (*models.ManualChallenge, error) + GetChallengeForUser(ctx context.Context, challengeID string, userID uint) (*models.ManualChallenge, error) + ListChallengesForProvider(ctx context.Context, providerID, userID uint) ([]models.ManualChallenge, error) + VerifyChallenge(ctx context.Context, challengeID string, userID uint) (*services.VerifyResult, error) + PollChallengeStatus(ctx context.Context, challengeID string, userID uint) (*services.ChallengeStatusResponse, error) + DeleteChallenge(ctx context.Context, challengeID string, userID uint) error +} + +// DNSProviderServiceInterface defines the subset of DNSProviderService needed by ManualChallengeHandler. +type DNSProviderServiceInterface interface { + Get(ctx context.Context, id uint) (*models.DNSProvider, error) +} + +// ManualChallengeHandler handles manual DNS challenge API requests. +type ManualChallengeHandler struct { + challengeService ManualChallengeServiceInterface + providerService DNSProviderServiceInterface +} + +// NewManualChallengeHandler creates a new manual challenge handler. +func NewManualChallengeHandler(challengeService ManualChallengeServiceInterface, providerService DNSProviderServiceInterface) *ManualChallengeHandler { + return &ManualChallengeHandler{ + challengeService: challengeService, + providerService: providerService, + } +} + +// ManualChallengeResponse represents the API response for a manual challenge. +type ManualChallengeResponse struct { + ID string `json:"id"` + ProviderID uint `json:"provider_id"` + FQDN string `json:"fqdn"` + Value string `json:"value"` + Status string `json:"status"` + DNSPropagated bool `json:"dns_propagated"` + CreatedAt string `json:"created_at"` + ExpiresAt string `json:"expires_at"` + LastCheckAt string `json:"last_check_at,omitempty"` + TimeRemainingSeconds int `json:"time_remaining_seconds"` + ErrorMessage string `json:"error_message,omitempty"` +} + +// ErrorResponse represents an error response with a code. +type ErrorResponse struct { + Success bool `json:"success"` + Error struct { + Code string `json:"code"` + Message string `json:"message"` + Details map[string]interface{} `json:"details,omitempty"` + } `json:"error"` +} + +// newErrorResponse creates a standardized error response. +func newErrorResponse(code, message string, details map[string]interface{}) ErrorResponse { + resp := ErrorResponse{Success: false} + resp.Error.Code = code + resp.Error.Message = message + resp.Error.Details = details + return resp +} + +// GetChallenge handles GET /api/v1/dns-providers/:id/manual-challenge/:challengeId +// Returns the status and details of a manual DNS challenge. +func (h *ManualChallengeHandler) GetChallenge(c *gin.Context) { + providerID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_PROVIDER_ID", + "Invalid provider ID", + nil, + )) + return + } + + challengeID := c.Param("challengeId") + if challengeID == "" { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_CHALLENGE_ID", + "Challenge ID is required", + nil, + )) + return + } + + // Get user ID from context (set by auth middleware) + userID := getUserIDFromContext(c) + + // Verify provider exists and user has access + provider, err := h.providerService.Get(c.Request.Context(), uint(providerID)) + if err != nil { + if errors.Is(err, services.ErrDNSProviderNotFound) { + c.JSON(http.StatusNotFound, newErrorResponse( + "PROVIDER_NOT_FOUND", + "DNS provider not found", + nil, + )) + return + } + c.JSON(http.StatusInternalServerError, newErrorResponse( + "INTERNAL_ERROR", + "Failed to retrieve DNS provider", + nil, + )) + return + } + + // Verify provider is manual type + if provider.ProviderType != "manual" { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_PROVIDER_TYPE", + "This endpoint is only available for manual DNS providers", + nil, + )) + return + } + + // Get challenge + challenge, err := h.challengeService.GetChallengeForUser(c.Request.Context(), challengeID, userID) + if err != nil { + if errors.Is(err, services.ErrChallengeNotFound) { + c.JSON(http.StatusNotFound, newErrorResponse( + "CHALLENGE_NOT_FOUND", + "Challenge not found", + map[string]interface{}{"challenge_id": challengeID}, + )) + return + } + if errors.Is(err, services.ErrUnauthorized) { + c.JSON(http.StatusForbidden, newErrorResponse( + "UNAUTHORIZED", + "You do not have access to this challenge", + nil, + )) + return + } + c.JSON(http.StatusInternalServerError, newErrorResponse( + "INTERNAL_ERROR", + "Failed to retrieve challenge", + nil, + )) + return + } + + // Verify challenge belongs to the specified provider + if challenge.ProviderID != uint(providerID) { + c.JSON(http.StatusNotFound, newErrorResponse( + "CHALLENGE_NOT_FOUND", + "Challenge not found for this provider", + nil, + )) + return + } + + c.JSON(http.StatusOK, challengeToResponse(challenge)) +} + +// VerifyChallenge handles POST /api/v1/dns-providers/:id/manual-challenge/:challengeId/verify +// Triggers DNS verification for a challenge. +func (h *ManualChallengeHandler) VerifyChallenge(c *gin.Context) { + providerID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_PROVIDER_ID", + "Invalid provider ID", + nil, + )) + return + } + + challengeID := c.Param("challengeId") + if challengeID == "" { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_CHALLENGE_ID", + "Challenge ID is required", + nil, + )) + return + } + + // Get user ID from context + userID := getUserIDFromContext(c) + + // Verify provider exists and is manual type + provider, err := h.providerService.Get(c.Request.Context(), uint(providerID)) + if err != nil { + if errors.Is(err, services.ErrDNSProviderNotFound) { + c.JSON(http.StatusNotFound, newErrorResponse( + "PROVIDER_NOT_FOUND", + "DNS provider not found", + nil, + )) + return + } + c.JSON(http.StatusInternalServerError, newErrorResponse( + "INTERNAL_ERROR", + "Failed to retrieve DNS provider", + nil, + )) + return + } + + if provider.ProviderType != "manual" { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_PROVIDER_TYPE", + "This endpoint is only available for manual DNS providers", + nil, + )) + return + } + + // Verify ownership before verification + challenge, err := h.challengeService.GetChallengeForUser(c.Request.Context(), challengeID, userID) + if err != nil { + if errors.Is(err, services.ErrChallengeNotFound) { + c.JSON(http.StatusNotFound, newErrorResponse( + "CHALLENGE_NOT_FOUND", + "Challenge not found", + map[string]interface{}{"challenge_id": challengeID}, + )) + return + } + if errors.Is(err, services.ErrUnauthorized) { + c.JSON(http.StatusForbidden, newErrorResponse( + "UNAUTHORIZED", + "You do not have access to this challenge", + nil, + )) + return + } + c.JSON(http.StatusInternalServerError, newErrorResponse( + "INTERNAL_ERROR", + "Failed to retrieve challenge", + nil, + )) + return + } + + if challenge.ProviderID != uint(providerID) { + c.JSON(http.StatusNotFound, newErrorResponse( + "CHALLENGE_NOT_FOUND", + "Challenge not found for this provider", + nil, + )) + return + } + + // Perform verification + result, err := h.challengeService.VerifyChallenge(c.Request.Context(), challengeID, userID) + if err != nil { + if errors.Is(err, services.ErrChallengeExpired) { + c.JSON(http.StatusGone, newErrorResponse( + "CHALLENGE_EXPIRED", + "Challenge has expired", + map[string]interface{}{ + "challenge_id": challengeID, + "expired_at": challenge.ExpiresAt.Format("2006-01-02T15:04:05Z"), + }, + )) + return + } + c.JSON(http.StatusInternalServerError, newErrorResponse( + "INTERNAL_ERROR", + "Failed to verify challenge", + nil, + )) + return + } + + c.JSON(http.StatusOK, result) +} + +// PollChallenge handles GET /api/v1/dns-providers/:id/manual-challenge/:challengeId/poll +// Returns the current status for polling. +func (h *ManualChallengeHandler) PollChallenge(c *gin.Context) { + providerID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_PROVIDER_ID", + "Invalid provider ID", + nil, + )) + return + } + + challengeID := c.Param("challengeId") + if challengeID == "" { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_CHALLENGE_ID", + "Challenge ID is required", + nil, + )) + return + } + + userID := getUserIDFromContext(c) + + // Verify provider exists + provider, err := h.providerService.Get(c.Request.Context(), uint(providerID)) + if err != nil { + if errors.Is(err, services.ErrDNSProviderNotFound) { + c.JSON(http.StatusNotFound, newErrorResponse( + "PROVIDER_NOT_FOUND", + "DNS provider not found", + nil, + )) + return + } + c.JSON(http.StatusInternalServerError, newErrorResponse( + "INTERNAL_ERROR", + "Failed to retrieve DNS provider", + nil, + )) + return + } + + if provider.ProviderType != "manual" { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_PROVIDER_TYPE", + "This endpoint is only available for manual DNS providers", + nil, + )) + return + } + + // Get challenge status + status, err := h.challengeService.PollChallengeStatus(c.Request.Context(), challengeID, userID) + if err != nil { + if errors.Is(err, services.ErrChallengeNotFound) { + c.JSON(http.StatusNotFound, newErrorResponse( + "CHALLENGE_NOT_FOUND", + "Challenge not found", + nil, + )) + return + } + if errors.Is(err, services.ErrUnauthorized) { + c.JSON(http.StatusForbidden, newErrorResponse( + "UNAUTHORIZED", + "You do not have access to this challenge", + nil, + )) + return + } + c.JSON(http.StatusInternalServerError, newErrorResponse( + "INTERNAL_ERROR", + "Failed to get challenge status", + nil, + )) + return + } + + c.JSON(http.StatusOK, status) +} + +// ListChallenges handles GET /api/v1/dns-providers/:id/manual-challenges +// Returns all challenges for a provider. +func (h *ManualChallengeHandler) ListChallenges(c *gin.Context) { + providerID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_PROVIDER_ID", + "Invalid provider ID", + nil, + )) + return + } + + userID := getUserIDFromContext(c) + + // Verify provider exists and is manual type + provider, err := h.providerService.Get(c.Request.Context(), uint(providerID)) + if err != nil { + if errors.Is(err, services.ErrDNSProviderNotFound) { + c.JSON(http.StatusNotFound, newErrorResponse( + "PROVIDER_NOT_FOUND", + "DNS provider not found", + nil, + )) + return + } + c.JSON(http.StatusInternalServerError, newErrorResponse( + "INTERNAL_ERROR", + "Failed to retrieve DNS provider", + nil, + )) + return + } + + if provider.ProviderType != "manual" { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_PROVIDER_TYPE", + "This endpoint is only available for manual DNS providers", + nil, + )) + return + } + + challenges, err := h.challengeService.ListChallengesForProvider(c.Request.Context(), uint(providerID), userID) + if err != nil { + c.JSON(http.StatusInternalServerError, newErrorResponse( + "INTERNAL_ERROR", + "Failed to list challenges", + nil, + )) + return + } + + responses := make([]ManualChallengeResponse, len(challenges)) + for i, ch := range challenges { + responses[i] = *challengeToResponse(&ch) + } + + c.JSON(http.StatusOK, gin.H{ + "challenges": responses, + "total": len(responses), + }) +} + +// DeleteChallenge handles DELETE /api/v1/dns-providers/:id/manual-challenge/:challengeId +// Cancels/deletes a challenge. +func (h *ManualChallengeHandler) DeleteChallenge(c *gin.Context) { + providerID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_PROVIDER_ID", + "Invalid provider ID", + nil, + )) + return + } + + challengeID := c.Param("challengeId") + if challengeID == "" { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_CHALLENGE_ID", + "Challenge ID is required", + nil, + )) + return + } + + userID := getUserIDFromContext(c) + + // Verify provider exists + provider, err := h.providerService.Get(c.Request.Context(), uint(providerID)) + if err != nil { + if errors.Is(err, services.ErrDNSProviderNotFound) { + c.JSON(http.StatusNotFound, newErrorResponse( + "PROVIDER_NOT_FOUND", + "DNS provider not found", + nil, + )) + return + } + c.JSON(http.StatusInternalServerError, newErrorResponse( + "INTERNAL_ERROR", + "Failed to retrieve DNS provider", + nil, + )) + return + } + + if provider.ProviderType != "manual" { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_PROVIDER_TYPE", + "This endpoint is only available for manual DNS providers", + nil, + )) + return + } + + err = h.challengeService.DeleteChallenge(c.Request.Context(), challengeID, userID) + if err != nil { + if errors.Is(err, services.ErrChallengeNotFound) { + c.JSON(http.StatusNotFound, newErrorResponse( + "CHALLENGE_NOT_FOUND", + "Challenge not found", + nil, + )) + return + } + if errors.Is(err, services.ErrUnauthorized) { + c.JSON(http.StatusForbidden, newErrorResponse( + "UNAUTHORIZED", + "You do not have access to this challenge", + nil, + )) + return + } + c.JSON(http.StatusInternalServerError, newErrorResponse( + "INTERNAL_ERROR", + "Failed to delete challenge", + nil, + )) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Challenge deleted successfully", + }) +} + +// CreateChallengeRequest represents the request to create a manual challenge. +type CreateChallengeRequest struct { + FQDN string `json:"fqdn" binding:"required"` + Token string `json:"token"` + Value string `json:"value" binding:"required"` +} + +// CreateChallenge handles POST /api/v1/dns-providers/:id/manual-challenges +// Creates a new manual DNS challenge. +func (h *ManualChallengeHandler) CreateChallenge(c *gin.Context) { + providerID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_PROVIDER_ID", + "Invalid provider ID", + nil, + )) + return + } + + var req CreateChallengeRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_REQUEST", + err.Error(), + nil, + )) + return + } + + userID := getUserIDFromContext(c) + + // Verify provider exists and is manual type + provider, err := h.providerService.Get(c.Request.Context(), uint(providerID)) + if err != nil { + if errors.Is(err, services.ErrDNSProviderNotFound) { + c.JSON(http.StatusNotFound, newErrorResponse( + "PROVIDER_NOT_FOUND", + "DNS provider not found", + nil, + )) + return + } + c.JSON(http.StatusInternalServerError, newErrorResponse( + "INTERNAL_ERROR", + "Failed to retrieve DNS provider", + nil, + )) + return + } + + if provider.ProviderType != "manual" { + c.JSON(http.StatusBadRequest, newErrorResponse( + "INVALID_PROVIDER_TYPE", + "This endpoint is only available for manual DNS providers", + nil, + )) + return + } + + challenge, err := h.challengeService.CreateChallenge(c.Request.Context(), services.CreateChallengeRequest{ + ProviderID: uint(providerID), + UserID: userID, + FQDN: req.FQDN, + Token: req.Token, + Value: req.Value, + }) + if err != nil { + if errors.Is(err, services.ErrChallengeInProgress) { + c.JSON(http.StatusConflict, newErrorResponse( + "CHALLENGE_IN_PROGRESS", + "Another challenge is already in progress for this domain", + map[string]interface{}{"fqdn": req.FQDN}, + )) + return + } + c.JSON(http.StatusInternalServerError, newErrorResponse( + "INTERNAL_ERROR", + "Failed to create challenge", + nil, + )) + return + } + + c.JSON(http.StatusCreated, challengeToResponse(challenge)) +} + +// RegisterRoutes registers all manual challenge routes. +func (h *ManualChallengeHandler) RegisterRoutes(rg *gin.RouterGroup) { + // Routes under /dns-providers/:id + rg.GET("/dns-providers/:id/manual-challenges", h.ListChallenges) + rg.POST("/dns-providers/:id/manual-challenges", h.CreateChallenge) + rg.GET("/dns-providers/:id/manual-challenge/:challengeId", h.GetChallenge) + rg.POST("/dns-providers/:id/manual-challenge/:challengeId/verify", h.VerifyChallenge) + rg.GET("/dns-providers/:id/manual-challenge/:challengeId/poll", h.PollChallenge) + rg.DELETE("/dns-providers/:id/manual-challenge/:challengeId", h.DeleteChallenge) +} + +// Helper functions + +func challengeToResponse(ch *models.ManualChallenge) *ManualChallengeResponse { + resp := &ManualChallengeResponse{ + ID: ch.ID, + ProviderID: ch.ProviderID, + FQDN: ch.FQDN, + Value: ch.Value, + Status: string(ch.Status), + DNSPropagated: ch.DNSPropagated, + CreatedAt: ch.CreatedAt.Format("2006-01-02T15:04:05Z"), + ExpiresAt: ch.ExpiresAt.Format("2006-01-02T15:04:05Z"), + TimeRemainingSeconds: int(ch.TimeRemaining().Seconds()), + ErrorMessage: ch.ErrorMessage, + } + + if ch.LastCheckAt != nil { + resp.LastCheckAt = ch.LastCheckAt.Format("2006-01-02T15:04:05Z") + } + + return resp +} + +// getUserIDFromContext extracts user ID from gin context. +func getUserIDFromContext(c *gin.Context) uint { + // Try to get user_id from context (set by auth middleware) + if userID, exists := c.Get("user_id"); exists { + switch v := userID.(type) { + case uint: + return v + case int: + return uint(v) + case int64: + return uint(v) + case uint64: + return uint(v) + } + } + return 0 +} diff --git a/backend/internal/api/handlers/manual_challenge_handler_test.go b/backend/internal/api/handlers/manual_challenge_handler_test.go new file mode 100644 index 00000000..5eea9410 --- /dev/null +++ b/backend/internal/api/handlers/manual_challenge_handler_test.go @@ -0,0 +1,561 @@ +package handlers + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/Wikid82/charon/backend/internal/models" + "github.com/Wikid82/charon/backend/internal/services" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +// MockManualChallengeService mocks the ManualChallengeServiceInterface for testing. +type MockManualChallengeService struct { + mock.Mock +} + +func (m *MockManualChallengeService) CreateChallenge(ctx context.Context, req services.CreateChallengeRequest) (*models.ManualChallenge, error) { + args := m.Called(ctx, req) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*models.ManualChallenge), args.Error(1) +} + +func (m *MockManualChallengeService) GetChallengeForUser(ctx context.Context, challengeID string, userID uint) (*models.ManualChallenge, error) { + args := m.Called(ctx, challengeID, userID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*models.ManualChallenge), args.Error(1) +} + +func (m *MockManualChallengeService) ListChallengesForProvider(ctx context.Context, providerID, userID uint) ([]models.ManualChallenge, error) { + args := m.Called(ctx, providerID, userID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]models.ManualChallenge), args.Error(1) +} + +func (m *MockManualChallengeService) VerifyChallenge(ctx context.Context, challengeID string, userID uint) (*services.VerifyResult, error) { + args := m.Called(ctx, challengeID, userID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*services.VerifyResult), args.Error(1) +} + +func (m *MockManualChallengeService) PollChallengeStatus(ctx context.Context, challengeID string, userID uint) (*services.ChallengeStatusResponse, error) { + args := m.Called(ctx, challengeID, userID) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*services.ChallengeStatusResponse), args.Error(1) +} + +func (m *MockManualChallengeService) DeleteChallenge(ctx context.Context, challengeID string, userID uint) error { + args := m.Called(ctx, challengeID, userID) + return args.Error(0) +} + +// mockDNSProviderServiceForChallenge is a minimal mock for manual challenge tests. +type mockDNSProviderServiceForChallenge struct { + mock.Mock +} + +func (m *mockDNSProviderServiceForChallenge) Get(ctx context.Context, id uint) (*models.DNSProvider, error) { + args := m.Called(ctx, id) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*models.DNSProvider), args.Error(1) +} + +func setupChallengeTestRouter() *gin.Engine { + gin.SetMode(gin.TestMode) + return gin.New() +} + +func setUserID(c *gin.Context, userID uint) { + c.Set("user_id", userID) +} + +func TestNewManualChallengeHandler(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + + handler := NewManualChallengeHandler(mockService, mockProviderService) + + require.NotNil(t, handler) + assert.Equal(t, mockService, handler.challengeService) + assert.Equal(t, mockProviderService, handler.providerService) +} + +func TestManualChallengeHandler_GetChallenge(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.GetChallenge(c) + }) + + now := time.Now() + challenge := &models.ManualChallenge{ + ID: "test-challenge-id", + ProviderID: 1, + UserID: 1, + FQDN: "_acme-challenge.example.com", + Value: "txtvalue", + Status: models.ChallengeStatusPending, + CreatedAt: now, + ExpiresAt: now.Add(10 * time.Minute), + } + + provider := &models.DNSProvider{ + ID: 1, + ProviderType: "manual", + } + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("GetChallengeForUser", mock.Anything, "test-challenge-id", uint(1)).Return(challenge, nil) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenge/test-challenge-id", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var resp ManualChallengeResponse + err := json.Unmarshal(w.Body.Bytes(), &resp) + require.NoError(t, err) + assert.Equal(t, "test-challenge-id", resp.ID) + assert.Equal(t, uint(1), resp.ProviderID) +} + +func TestManualChallengeHandler_GetChallenge_NotFound(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.GetChallenge(c) + }) + + provider := &models.DNSProvider{ + ID: 1, + ProviderType: "manual", + } + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("GetChallengeForUser", mock.Anything, "nonexistent", uint(1)).Return(nil, services.ErrChallengeNotFound) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenge/nonexistent", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) +} + +func TestManualChallengeHandler_GetChallenge_InvalidProviderType(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.GetChallenge(c) + }) + + provider := &models.DNSProvider{ + ID: 1, + ProviderType: "cloudflare", // Not manual + } + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenge/test", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestManualChallengeHandler_CreateChallenge(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenges", func(c *gin.Context) { + setUserID(c, 1) + handler.CreateChallenge(c) + }) + + now := time.Now() + challenge := &models.ManualChallenge{ + ID: "new-challenge-id", + ProviderID: 1, + UserID: 1, + FQDN: "_acme-challenge.example.com", + Value: "txtvalue", + Status: models.ChallengeStatusPending, + CreatedAt: now, + ExpiresAt: now.Add(10 * time.Minute), + } + + provider := &models.DNSProvider{ + ID: 1, + ProviderType: "manual", + } + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("CreateChallenge", mock.Anything, mock.AnythingOfType("services.CreateChallengeRequest")).Return(challenge, nil) + + body := CreateChallengeRequest{ + FQDN: "_acme-challenge.example.com", + Value: "txtvalue", + } + bodyBytes, _ := json.Marshal(body) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenges", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusCreated, w.Code) +} + +func TestManualChallengeHandler_CreateChallenge_InvalidRequest(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenges", func(c *gin.Context) { + setUserID(c, 1) + handler.CreateChallenge(c) + }) + + provider := &models.DNSProvider{ + ID: 1, + ProviderType: "manual", + } + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + + // Missing required field + body := `{"fqdn": ""}` + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenges", bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestManualChallengeHandler_VerifyChallenge(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenge/:challengeId/verify", func(c *gin.Context) { + setUserID(c, 1) + handler.VerifyChallenge(c) + }) + + now := time.Now() + challenge := &models.ManualChallenge{ + ID: "test-challenge", + ProviderID: 1, + UserID: 1, + FQDN: "_acme-challenge.example.com", + Value: "txtvalue", + Status: models.ChallengeStatusPending, + CreatedAt: now, + ExpiresAt: now.Add(10 * time.Minute), + } + + provider := &models.DNSProvider{ + ID: 1, + ProviderType: "manual", + } + + result := &services.VerifyResult{ + Success: true, + DNSFound: true, + Message: "DNS TXT record verified successfully", + Status: "verified", + } + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("GetChallengeForUser", mock.Anything, "test-challenge", uint(1)).Return(challenge, nil) + mockService.On("VerifyChallenge", mock.Anything, "test-challenge", uint(1)).Return(result, nil) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenge/test-challenge/verify", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestManualChallengeHandler_PollChallenge(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId/poll", func(c *gin.Context) { + setUserID(c, 1) + handler.PollChallenge(c) + }) + + provider := &models.DNSProvider{ + ID: 1, + ProviderType: "manual", + } + + now := time.Now() + status := &services.ChallengeStatusResponse{ + ID: "test-challenge", + Status: "pending", + DNSPropagated: false, + TimeRemainingSeconds: 300, + LastCheckAt: &now, + } + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("PollChallengeStatus", mock.Anything, "test-challenge", uint(1)).Return(status, nil) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenge/test-challenge/poll", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestManualChallengeHandler_ListChallenges(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenges", func(c *gin.Context) { + setUserID(c, 1) + handler.ListChallenges(c) + }) + + provider := &models.DNSProvider{ + ID: 1, + ProviderType: "manual", + } + + now := time.Now() + challenges := []models.ManualChallenge{ + { + ID: "challenge-1", + ProviderID: 1, + UserID: 1, + FQDN: "_acme-challenge.example1.com", + Value: "value1", + Status: models.ChallengeStatusPending, + CreatedAt: now, + ExpiresAt: now.Add(10 * time.Minute), + }, + { + ID: "challenge-2", + ProviderID: 1, + UserID: 1, + FQDN: "_acme-challenge.example2.com", + Value: "value2", + Status: models.ChallengeStatusVerified, + CreatedAt: now, + ExpiresAt: now.Add(10 * time.Minute), + }, + } + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("ListChallengesForProvider", mock.Anything, uint(1), uint(1)).Return(challenges, nil) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenges", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var resp map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &resp) + require.NoError(t, err) + assert.Equal(t, float64(2), resp["total"]) +} + +func TestManualChallengeHandler_DeleteChallenge(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.DELETE("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.DeleteChallenge(c) + }) + + provider := &models.DNSProvider{ + ID: 1, + ProviderType: "manual", + } + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("DeleteChallenge", mock.Anything, "test-challenge", uint(1)).Return(nil) + + req, _ := http.NewRequest("DELETE", "/dns-providers/1/manual-challenge/test-challenge", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestManualChallengeHandler_InvalidProviderID(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.GetChallenge(c) + }) + + req, _ := http.NewRequest("GET", "/dns-providers/invalid/manual-challenge/test", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestManualChallengeHandler_ProviderNotFound(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.GetChallenge(c) + }) + + mockProviderService.On("Get", mock.Anything, uint(999)).Return(nil, services.ErrDNSProviderNotFound) + + req, _ := http.NewRequest("GET", "/dns-providers/999/manual-challenge/test", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) +} + +func TestManualChallengeHandler_RegisterRoutes(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + handler.RegisterRoutes(router.Group("/")) + + // Verify routes are registered by checking that they respond (even with errors) + routes := router.Routes() + paths := make(map[string]bool) + for _, route := range routes { + paths[route.Path] = true + } + + assert.True(t, paths["/dns-providers/:id/manual-challenges"]) + assert.True(t, paths["/dns-providers/:id/manual-challenge/:challengeId"]) + assert.True(t, paths["/dns-providers/:id/manual-challenge/:challengeId/verify"]) + assert.True(t, paths["/dns-providers/:id/manual-challenge/:challengeId/poll"]) +} + +func TestGetUserIDFromContext(t *testing.T) { + tests := []struct { + name string + value interface{} + expected uint + }{ + {"uint value", uint(42), 42}, + {"int value", int(42), 42}, + {"int64 value", int64(42), 42}, + {"uint64 value", uint64(42), 42}, + {"missing value", nil, 0}, + {"invalid type", "42", 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gin.SetMode(gin.TestMode) + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + if tt.value != nil { + c.Set("user_id", tt.value) + } + + result := getUserIDFromContext(c) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestChallengeToResponse(t *testing.T) { + now := time.Now() + lastCheck := now.Add(-1 * time.Minute) + + challenge := &models.ManualChallenge{ + ID: "test-id", + ProviderID: 1, + UserID: 1, + FQDN: "_acme-challenge.example.com", + Value: "txtvalue", + Status: models.ChallengeStatusPending, + DNSPropagated: false, + CreatedAt: now, + ExpiresAt: now.Add(10 * time.Minute), + LastCheckAt: &lastCheck, + ErrorMessage: "test error", + } + + resp := challengeToResponse(challenge) + + assert.Equal(t, "test-id", resp.ID) + assert.Equal(t, uint(1), resp.ProviderID) + assert.Equal(t, "_acme-challenge.example.com", resp.FQDN) + assert.Equal(t, "txtvalue", resp.Value) + assert.Equal(t, "pending", resp.Status) + assert.False(t, resp.DNSPropagated) + assert.NotEmpty(t, resp.CreatedAt) + assert.NotEmpty(t, resp.ExpiresAt) + assert.NotEmpty(t, resp.LastCheckAt) + assert.Equal(t, "test error", resp.ErrorMessage) +} + +func TestNewErrorResponse(t *testing.T) { + details := map[string]interface{}{"key": "value"} + resp := newErrorResponse("TEST_CODE", "Test message", details) + + assert.False(t, resp.Success) + assert.Equal(t, "TEST_CODE", resp.Error.Code) + assert.Equal(t, "Test message", resp.Error.Message) + assert.Equal(t, details, resp.Error.Details) +} diff --git a/backend/internal/api/handlers/misc_coverage_test.go b/backend/internal/api/handlers/misc_coverage_test.go index 665191bd..0b2a5939 100644 --- a/backend/internal/api/handlers/misc_coverage_test.go +++ b/backend/internal/api/handlers/misc_coverage_test.go @@ -18,7 +18,7 @@ import ( func setupDomainCoverageDB(t *testing.T) *gorm.DB { t.Helper() db := OpenTestDB(t) - db.AutoMigrate(&models.Domain{}) + _ = db.AutoMigrate(&models.Domain{}) return db } @@ -28,7 +28,7 @@ func TestDomainHandler_List_Error(t *testing.T) { h := NewDomainHandler(db, nil) // Drop table to cause error - db.Migrator().DropTable(&models.Domain{}) + _ = db.Migrator().DropTable(&models.Domain{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -60,7 +60,7 @@ func TestDomainHandler_Create_DBError(t *testing.T) { h := NewDomainHandler(db, nil) // Drop table to cause error - db.Migrator().DropTable(&models.Domain{}) + _ = db.Migrator().DropTable(&models.Domain{}) body, _ := json.Marshal(map[string]string{"name": "example.com"}) @@ -81,7 +81,7 @@ func TestDomainHandler_Delete_Error(t *testing.T) { h := NewDomainHandler(db, nil) // Drop table to cause error - db.Migrator().DropTable(&models.Domain{}) + _ = db.Migrator().DropTable(&models.Domain{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -98,7 +98,7 @@ func TestDomainHandler_Delete_Error(t *testing.T) { func setupRemoteServerCoverageDB(t *testing.T) *gorm.DB { t.Helper() db := OpenTestDB(t) - db.AutoMigrate(&models.RemoteServer{}) + _ = db.AutoMigrate(&models.RemoteServer{}) return db } @@ -109,7 +109,7 @@ func TestRemoteServerHandler_List_Error(t *testing.T) { h := NewRemoteServerHandler(svc, nil) // Drop table to cause error - db.Migrator().DropTable(&models.RemoteServer{}) + _ = db.Migrator().DropTable(&models.RemoteServer{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -162,7 +162,7 @@ func TestRemoteServerHandler_Update_InvalidJSON(t *testing.T) { // Create a server first server := &models.RemoteServer{Name: "Test", Host: "localhost", Port: 22} - svc.Create(server) + _ = svc.Create(server) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -234,7 +234,7 @@ func TestRemoteServerHandler_TestConnectionCustom_Unreachable(t *testing.T) { func setupUptimeCoverageDB(t *testing.T) *gorm.DB { t.Helper() db := OpenTestDB(t) - db.AutoMigrate(&models.UptimeMonitor{}, &models.UptimeHeartbeat{}) + _ = db.AutoMigrate(&models.UptimeMonitor{}, &models.UptimeHeartbeat{}) return db } @@ -245,7 +245,7 @@ func TestUptimeHandler_List_Error(t *testing.T) { h := NewUptimeHandler(svc) // Drop table to cause error - db.Migrator().DropTable(&models.UptimeMonitor{}) + _ = db.Migrator().DropTable(&models.UptimeMonitor{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -263,7 +263,7 @@ func TestUptimeHandler_GetHistory_Error(t *testing.T) { h := NewUptimeHandler(svc) // Drop history table - db.Migrator().DropTable(&models.UptimeHeartbeat{}) + _ = db.Migrator().DropTable(&models.UptimeHeartbeat{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -299,7 +299,7 @@ func TestUptimeHandler_Sync_Error(t *testing.T) { h := NewUptimeHandler(svc) // Drop table to cause error - db.Migrator().DropTable(&models.UptimeMonitor{}) + _ = db.Migrator().DropTable(&models.UptimeMonitor{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -317,7 +317,7 @@ func TestUptimeHandler_Delete_Error(t *testing.T) { h := NewUptimeHandler(svc) // Drop table to cause error - db.Migrator().DropTable(&models.UptimeMonitor{}) + _ = db.Migrator().DropTable(&models.UptimeMonitor{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) diff --git a/backend/internal/api/handlers/notification_coverage_test.go b/backend/internal/api/handlers/notification_coverage_test.go index f9306ee3..063b5c6f 100644 --- a/backend/internal/api/handlers/notification_coverage_test.go +++ b/backend/internal/api/handlers/notification_coverage_test.go @@ -19,7 +19,7 @@ import ( func setupNotificationCoverageDB(t *testing.T) *gorm.DB { t.Helper() db := OpenTestDB(t) - db.AutoMigrate(&models.Notification{}, &models.NotificationProvider{}, &models.NotificationTemplate{}) + _ = db.AutoMigrate(&models.Notification{}, &models.NotificationProvider{}, &models.NotificationTemplate{}) return db } @@ -32,7 +32,7 @@ func TestNotificationHandler_List_Error(t *testing.T) { h := NewNotificationHandler(svc) // Drop the table to cause error - db.Migrator().DropTable(&models.Notification{}) + _ = db.Migrator().DropTable(&models.Notification{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -51,8 +51,8 @@ func TestNotificationHandler_List_UnreadOnly(t *testing.T) { h := NewNotificationHandler(svc) // Create some notifications - svc.Create(models.NotificationTypeInfo, "Test 1", "Message 1") - svc.Create(models.NotificationTypeInfo, "Test 2", "Message 2") + _, _ = svc.Create(models.NotificationTypeInfo, "Test 1", "Message 1") + _, _ = svc.Create(models.NotificationTypeInfo, "Test 2", "Message 2") w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -70,7 +70,7 @@ func TestNotificationHandler_MarkAsRead_Error(t *testing.T) { h := NewNotificationHandler(svc) // Drop table to cause error - db.Migrator().DropTable(&models.Notification{}) + _ = db.Migrator().DropTable(&models.Notification{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -89,7 +89,7 @@ func TestNotificationHandler_MarkAllAsRead_Error(t *testing.T) { h := NewNotificationHandler(svc) // Drop table to cause error - db.Migrator().DropTable(&models.Notification{}) + _ = db.Migrator().DropTable(&models.Notification{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -109,7 +109,7 @@ func TestNotificationProviderHandler_List_Error(t *testing.T) { h := NewNotificationProviderHandler(svc) // Drop table to cause error - db.Migrator().DropTable(&models.NotificationProvider{}) + _ = db.Migrator().DropTable(&models.NotificationProvider{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -143,7 +143,7 @@ func TestNotificationProviderHandler_Create_DBError(t *testing.T) { h := NewNotificationProviderHandler(svc) // Drop table to cause error - db.Migrator().DropTable(&models.NotificationProvider{}) + _ = db.Migrator().DropTable(&models.NotificationProvider{}) provider := models.NotificationProvider{ Name: "Test", @@ -243,7 +243,7 @@ func TestNotificationProviderHandler_Update_DBError(t *testing.T) { h := NewNotificationProviderHandler(svc) // Drop table to cause error - db.Migrator().DropTable(&models.NotificationProvider{}) + _ = db.Migrator().DropTable(&models.NotificationProvider{}) provider := models.NotificationProvider{ Name: "Test", @@ -271,7 +271,7 @@ func TestNotificationProviderHandler_Delete_Error(t *testing.T) { h := NewNotificationProviderHandler(svc) // Drop table to cause error - db.Migrator().DropTable(&models.NotificationProvider{}) + _ = db.Migrator().DropTable(&models.NotificationProvider{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -388,7 +388,7 @@ func TestNotificationTemplateHandler_List_Error(t *testing.T) { h := NewNotificationTemplateHandler(svc) // Drop table to cause error - db.Migrator().DropTable(&models.NotificationTemplate{}) + _ = db.Migrator().DropTable(&models.NotificationTemplate{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -422,7 +422,7 @@ func TestNotificationTemplateHandler_Create_DBError(t *testing.T) { h := NewNotificationTemplateHandler(svc) // Drop table to cause error - db.Migrator().DropTable(&models.NotificationTemplate{}) + _ = db.Migrator().DropTable(&models.NotificationTemplate{}) tmpl := models.NotificationTemplate{ Name: "Test", @@ -464,7 +464,7 @@ func TestNotificationTemplateHandler_Update_DBError(t *testing.T) { h := NewNotificationTemplateHandler(svc) // Drop table to cause error - db.Migrator().DropTable(&models.NotificationTemplate{}) + _ = db.Migrator().DropTable(&models.NotificationTemplate{}) tmpl := models.NotificationTemplate{ Name: "Test", @@ -490,7 +490,7 @@ func TestNotificationTemplateHandler_Delete_Error(t *testing.T) { h := NewNotificationTemplateHandler(svc) // Drop table to cause error - db.Migrator().DropTable(&models.NotificationTemplate{}) + _ = db.Migrator().DropTable(&models.NotificationTemplate{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) diff --git a/backend/internal/api/handlers/notification_handler_test.go b/backend/internal/api/handlers/notification_handler_test.go index 0d602d25..94c441cc 100644 --- a/backend/internal/api/handlers/notification_handler_test.go +++ b/backend/internal/api/handlers/notification_handler_test.go @@ -25,7 +25,7 @@ func setupNotificationTestDB() *gorm.DB { if err != nil { panic("failed to connect to test database") } - db.AutoMigrate(&models.Notification{}, &models.NotificationProvider{}) + _ = db.AutoMigrate(&models.Notification{}, &models.NotificationProvider{}) return db } @@ -124,7 +124,7 @@ func TestNotificationHandler_MarkAllAsRead_Error(t *testing.T) { // Close DB to force error sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() req, _ := http.NewRequest("POST", "/notifications/read-all", http.NoBody) w := httptest.NewRecorder() @@ -143,7 +143,7 @@ func TestNotificationHandler_DBError(t *testing.T) { // Close DB to force error sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() req, _ := http.NewRequest("POST", "/notifications/1/read", http.NoBody) w := httptest.NewRecorder() diff --git a/backend/internal/api/handlers/plugin_handler_test.go b/backend/internal/api/handlers/plugin_handler_test.go index 96de5a17..4f58b90e 100644 --- a/backend/internal/api/handlers/plugin_handler_test.go +++ b/backend/internal/api/handlers/plugin_handler_test.go @@ -529,7 +529,7 @@ func TestPluginHandler_ListPlugins_ExternalLoadedPlugin(t *testing.T) { Description: "External DNS provider", }, } - dnsprovider.Global().Register(testProvider) + _ = dnsprovider.Global().Register(testProvider) defer dnsprovider.Global().Unregister("external-type") handler := NewPluginHandler(db, pluginLoader) @@ -593,7 +593,7 @@ func TestPluginHandler_GetPlugin_WithProvider(t *testing.T) { DocumentationURL: "https://example.com/docs", }, } - dnsprovider.Global().Register(testProvider) + _ = dnsprovider.Global().Register(testProvider) defer dnsprovider.Global().Unregister("provider-type") handler := NewPluginHandler(db, pluginLoader) @@ -882,7 +882,7 @@ func TestPluginHandler_EnablePlugin_DBUpdateError(t *testing.T) { // Close the underlying connection to simulate DB error sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() router := gin.New() router.POST("/plugins/:id/enable", handler.EnablePlugin) @@ -915,7 +915,7 @@ func TestPluginHandler_DisablePlugin_DBUpdateError(t *testing.T) { // Close the underlying connection to simulate DB error sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() router := gin.New() router.POST("/plugins/:id/disable", handler.DisablePlugin) @@ -948,7 +948,7 @@ func TestPluginHandler_GetPlugin_DBInternalError(t *testing.T) { // Close the underlying connection to simulate DB error sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() router := gin.New() router.GET("/plugins/:id", handler.GetPlugin) @@ -982,7 +982,7 @@ func TestPluginHandler_EnablePlugin_FirstDBLookupError(t *testing.T) { // Close the underlying connection to simulate DB error sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() router := gin.New() router.POST("/plugins/:id/enable", handler.EnablePlugin) @@ -1016,7 +1016,7 @@ func TestPluginHandler_DisablePlugin_FirstDBLookupError(t *testing.T) { // Close the underlying connection to simulate DB error sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() router := gin.New() router.POST("/plugins/:id/disable", handler.DisablePlugin) diff --git a/backend/internal/api/handlers/pr_coverage_test.go b/backend/internal/api/handlers/pr_coverage_test.go index f346264b..db2e69fc 100644 --- a/backend/internal/api/handlers/pr_coverage_test.go +++ b/backend/internal/api/handlers/pr_coverage_test.go @@ -45,7 +45,7 @@ func TestPluginHandler_EnablePlugin_DatabaseUpdateError(t *testing.T) { // Close DB to trigger error during update sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() router := gin.New() router.POST("/plugins/:id/enable", handler.EnablePlugin) @@ -77,7 +77,7 @@ func TestPluginHandler_DisablePlugin_DatabaseUpdateError(t *testing.T) { // Close DB to trigger error during update sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() router := gin.New() router.POST("/plugins/:id/disable", handler.DisablePlugin) @@ -108,7 +108,7 @@ func TestPluginHandler_GetPlugin_DatabaseError(t *testing.T) { // Close DB to trigger database error sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() router := gin.New() router.GET("/plugins/:id", handler.GetPlugin) @@ -130,7 +130,7 @@ func TestPluginHandler_EnablePlugin_DatabaseFirstError(t *testing.T) { // Close DB to trigger error when fetching plugin sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() router := gin.New() router.POST("/plugins/:id/enable", handler.EnablePlugin) @@ -152,7 +152,7 @@ func TestPluginHandler_DisablePlugin_DatabaseFirstError(t *testing.T) { // Close DB to trigger error when fetching plugin sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() router := gin.New() router.POST("/plugins/:id/disable", handler.DisablePlugin) @@ -221,7 +221,7 @@ func TestEncryptionHandler_GetHistory_PaginationBoundary(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var response map[string]any - json.Unmarshal(w.Body.Bytes(), &response) + _ = json.Unmarshal(w.Body.Bytes(), &response) // limit should not exceed 100 assert.LessOrEqual(t, response["limit"].(float64), float64(100)) } @@ -330,7 +330,7 @@ func TestSettingsHandler_TestPublicURL_PrivateIPBlocked_Coverage(t *testing.T) { // Should return 200 but with reachable=false due to SSRF protection assert.Equal(t, http.StatusOK, w.Code) var response map[string]any - json.Unmarshal(w.Body.Bytes(), &response) + _ = json.Unmarshal(w.Body.Bytes(), &response) assert.False(t, response["reachable"].(bool)) } @@ -358,7 +358,7 @@ func TestSettingsHandler_ValidatePublicURL_WithTrailingSlash(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var response map[string]any - json.Unmarshal(w.Body.Bytes(), &response) + _ = json.Unmarshal(w.Body.Bytes(), &response) assert.True(t, response["valid"].(bool)) } @@ -386,7 +386,7 @@ func TestSettingsHandler_ValidatePublicURL_MissingScheme(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) var response map[string]any - json.Unmarshal(w.Body.Bytes(), &response) + _ = json.Unmarshal(w.Body.Bytes(), &response) assert.False(t, response["valid"].(bool)) } @@ -398,10 +398,10 @@ func TestAuditLogHandler_List_PaginationEdgeCases(t *testing.T) { gin.SetMode(gin.TestMode) dbPath := fmt.Sprintf("/tmp/test_audit_pagination_%d.db", time.Now().UnixNano()) - t.Cleanup(func() { os.Remove(dbPath) }) + t.Cleanup(func() { _ = os.Remove(dbPath) }) db, _ := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) - db.AutoMigrate(&models.SecurityAudit{}, &models.DNSProvider{}) + _ = db.AutoMigrate(&models.SecurityAudit{}, &models.DNSProvider{}) // Create test audits for i := 0; i < 10; i++ { @@ -432,10 +432,10 @@ func TestAuditLogHandler_List_CategoryFilter(t *testing.T) { gin.SetMode(gin.TestMode) dbPath := fmt.Sprintf("/tmp/test_audit_category_%d.db", time.Now().UnixNano()) - t.Cleanup(func() { os.Remove(dbPath) }) + t.Cleanup(func() { _ = os.Remove(dbPath) }) db, _ := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) - db.AutoMigrate(&models.SecurityAudit{}, &models.DNSProvider{}) + _ = db.AutoMigrate(&models.SecurityAudit{}, &models.DNSProvider{}) // Create test audits with different categories db.Create(&models.SecurityAudit{ @@ -470,10 +470,10 @@ func TestAuditLogHandler_ListByProvider_DatabaseError(t *testing.T) { gin.SetMode(gin.TestMode) dbPath := fmt.Sprintf("/tmp/test_audit_db_error_%d.db", time.Now().UnixNano()) - t.Cleanup(func() { os.Remove(dbPath) }) + t.Cleanup(func() { _ = os.Remove(dbPath) }) db, _ := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) - db.AutoMigrate(&models.SecurityAudit{}, &models.DNSProvider{}) + _ = db.AutoMigrate(&models.SecurityAudit{}, &models.DNSProvider{}) secService := services.NewSecurityService(db) defer secService.Close() @@ -481,7 +481,7 @@ func TestAuditLogHandler_ListByProvider_DatabaseError(t *testing.T) { // Close DB to trigger error sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() router := gin.New() router.GET("/audit/provider/:id", handler.ListByProvider) @@ -497,10 +497,10 @@ func TestAuditLogHandler_ListByProvider_InvalidProviderID(t *testing.T) { gin.SetMode(gin.TestMode) dbPath := fmt.Sprintf("/tmp/test_audit_invalid_id_%d.db", time.Now().UnixNano()) - t.Cleanup(func() { os.Remove(dbPath) }) + t.Cleanup(func() { _ = os.Remove(dbPath) }) db, _ := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) - db.AutoMigrate(&models.SecurityAudit{}, &models.DNSProvider{}) + _ = db.AutoMigrate(&models.SecurityAudit{}, &models.DNSProvider{}) secService := services.NewSecurityService(db) defer secService.Close() @@ -586,7 +586,7 @@ func setupCredentialHandlerTestWithCtx(t *testing.T) (*gin.Engine, *gorm.DB, *mo t.Cleanup(func() { sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() }) err = db.AutoMigrate( @@ -684,7 +684,7 @@ func TestCredentialHandler_List_DatabaseClosed(t *testing.T) { dbName := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) db, _ := gorm.Open(sqlite.Open(dbName), &gorm.Config{}) - db.AutoMigrate(&models.DNSProvider{}, &models.DNSProviderCredential{}) + _ = db.AutoMigrate(&models.DNSProvider{}, &models.DNSProviderCredential{}) testKey := "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=" encryptor, _ := crypto.NewEncryptionService(testKey) @@ -696,7 +696,7 @@ func TestCredentialHandler_List_DatabaseClosed(t *testing.T) { // Close DB to trigger error sqlDB, _ := db.DB() - sqlDB.Close() + _ = sqlDB.Close() req, _ := http.NewRequest("GET", "/api/v1/dns-providers/1/credentials", nil) w := httptest.NewRecorder() diff --git a/backend/internal/api/handlers/proxy_host_handler_test.go b/backend/internal/api/handlers/proxy_host_handler_test.go index 6163ab5a..dd53c77b 100644 --- a/backend/internal/api/handlers/proxy_host_handler_test.go +++ b/backend/internal/api/handlers/proxy_host_handler_test.go @@ -397,7 +397,7 @@ func TestProxyHostConnection(t *testing.T) { // Start a local listener l, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) - defer l.Close() + defer func() { _ = l.Close() }() addr := l.Addr().(*net.TCPAddr) diff --git a/backend/internal/api/handlers/remote_server_handler_test.go b/backend/internal/api/handlers/remote_server_handler_test.go index afa8d2f1..1e0956e3 100644 --- a/backend/internal/api/handlers/remote_server_handler_test.go +++ b/backend/internal/api/handlers/remote_server_handler_test.go @@ -20,7 +20,7 @@ func setupRemoteServerTest_New(t *testing.T) (*gin.Engine, *handlers.RemoteServe t.Helper() db := setupTestDB(t) // Ensure RemoteServer table exists - db.AutoMigrate(&models.RemoteServer{}) + _ = db.AutoMigrate(&models.RemoteServer{}) ns := services.NewNotificationService(db) handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns) diff --git a/backend/internal/api/handlers/security_handler_audit_test.go b/backend/internal/api/handlers/security_handler_audit_test.go index 904ed9ef..906cdfd1 100644 --- a/backend/internal/api/handlers/security_handler_audit_test.go +++ b/backend/internal/api/handlers/security_handler_audit_test.go @@ -177,7 +177,7 @@ func TestSecurityHandler_UpsertRuleSet_EmptyName(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Contains(t, resp, "error") } @@ -517,7 +517,7 @@ func TestSecurityHandler_Enable_WithoutWhitelist(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]string - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Contains(t, resp["error"], "whitelist") } @@ -578,7 +578,7 @@ func TestSecurityHandler_GetStatus_CrowdSecModeValidation(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) // Invalid modes should be normalized to "disabled" assert.Equal(t, "disabled", resp["crowdsec"]["mode"], diff --git a/backend/internal/api/handlers/security_handler_coverage_test.go b/backend/internal/api/handlers/security_handler_coverage_test.go index 610e9762..6b93130a 100644 --- a/backend/internal/api/handlers/security_handler_coverage_test.go +++ b/backend/internal/api/handlers/security_handler_coverage_test.go @@ -522,7 +522,7 @@ func TestSecurityHandler_Enable_WithValidBreakGlassToken(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var tokenResp map[string]string - json.Unmarshal(w.Body.Bytes(), &tokenResp) + _ = json.Unmarshal(w.Body.Bytes(), &tokenResp) token := tokenResp["token"] // Now try to enable with the token @@ -586,7 +586,7 @@ func TestSecurityHandler_Disable_FromLocalhost(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.False(t, resp["enabled"].(bool)) } @@ -612,7 +612,7 @@ func TestSecurityHandler_Disable_FromRemoteWithToken(t *testing.T) { req, _ := http.NewRequest("POST", "/security/breakglass/generate", http.NoBody) router.ServeHTTP(w, req) var tokenResp map[string]string - json.Unmarshal(w.Body.Bytes(), &tokenResp) + _ = json.Unmarshal(w.Body.Bytes(), &tokenResp) token := tokenResp["token"] // Disable with token diff --git a/backend/internal/api/handlers/security_handler_waf_test.go b/backend/internal/api/handlers/security_handler_waf_test.go index daf90000..5a247478 100644 --- a/backend/internal/api/handlers/security_handler_waf_test.go +++ b/backend/internal/api/handlers/security_handler_waf_test.go @@ -191,7 +191,7 @@ func TestSecurityHandler_AddWAFExclusion_ToExistingConfig(t *testing.T) { router.ServeHTTP(w, req) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) exclusions := resp["exclusions"].([]any) assert.Len(t, exclusions, 2) } @@ -360,7 +360,7 @@ func TestSecurityHandler_DeleteWAFExclusion_Success(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.True(t, resp["deleted"].(bool)) // Verify only one exclusion remains @@ -368,7 +368,7 @@ func TestSecurityHandler_DeleteWAFExclusion_Success(t *testing.T) { req, _ = http.NewRequest("GET", "/security/waf/exclusions", http.NoBody) router.ServeHTTP(w, req) - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) exclusions := resp["exclusions"].([]any) assert.Len(t, exclusions, 1) first := exclusions[0].(map[string]any) @@ -403,7 +403,7 @@ func TestSecurityHandler_DeleteWAFExclusion_WithTarget(t *testing.T) { router.ServeHTTP(w, req) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) exclusions := resp["exclusions"].([]any) assert.Len(t, exclusions, 1) first := exclusions[0].(map[string]any) @@ -514,7 +514,7 @@ func TestSecurityHandler_WAFExclusion_FullWorkflow(t *testing.T) { router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Len(t, resp["exclusions"].([]any), 0) // Step 2: Add first exclusion (full rule removal) diff --git a/backend/internal/api/handlers/settings_handler_test.go b/backend/internal/api/handlers/settings_handler_test.go index 728ebfbc..629a3d74 100644 --- a/backend/internal/api/handlers/settings_handler_test.go +++ b/backend/internal/api/handlers/settings_handler_test.go @@ -122,7 +122,7 @@ func setupSettingsTestDB(t *testing.T) *gorm.DB { if err != nil { panic("failed to connect to test database") } - db.AutoMigrate(&models.Setting{}) + _ = db.AutoMigrate(&models.Setting{}) return db } @@ -281,7 +281,7 @@ func setupSettingsHandlerWithMail(t *testing.T) (*handlers.SettingsHandler, *gor if err != nil { panic("failed to connect to test database") } - db.AutoMigrate(&models.Setting{}) + _ = db.AutoMigrate(&models.Setting{}) return handlers.NewSettingsHandler(db), db } @@ -307,7 +307,7 @@ func TestSettingsHandler_GetSMTPConfig(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, "smtp.example.com", resp["host"]) assert.Equal(t, float64(587), resp["port"]) assert.Equal(t, "********", resp["password"]) // Password should be masked @@ -328,7 +328,7 @@ func TestSettingsHandler_GetSMTPConfig_Empty(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, false, resp["configured"]) } @@ -496,7 +496,7 @@ func TestSettingsHandler_TestSMTPConfig_NotConfigured(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, false, resp["success"]) } @@ -589,7 +589,7 @@ func TestSettingsHandler_SendTestEmail_NotConfigured(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, false, resp["success"]) } @@ -687,7 +687,7 @@ func TestSettingsHandler_ValidatePublicURL_InvalidFormat(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, false, resp["valid"]) }) } @@ -726,7 +726,7 @@ func TestSettingsHandler_ValidatePublicURL_Success(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, true, resp["valid"]) assert.Equal(t, tc.expected, resp["normalized"]) }) @@ -811,7 +811,7 @@ func TestSettingsHandler_TestPublicURL_InvalidURL(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) // BadRequest responses only have 'error' field, not 'reachable' assert.Contains(t, resp["error"].(string), "parse") } @@ -850,7 +850,7 @@ func TestSettingsHandler_TestPublicURL_PrivateIPBlocked(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) // Returns 200 but with reachable=false var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, false, resp["reachable"]) // Verify error message contains relevant security text errorMsg := resp["error"].(string) @@ -892,7 +892,7 @@ func TestSettingsHandler_TestPublicURL_Success(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) // The test verifies the handler works with a real public URL assert.Equal(t, true, resp["reachable"], "example.com should be reachable") @@ -920,7 +920,7 @@ func TestSettingsHandler_TestPublicURL_DNSFailure(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) // Returns 200 but with reachable=false var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.Equal(t, false, resp["reachable"]) // DNS errors contain "dns" or "resolution" keywords (case-insensitive) errorMsg := resp["error"].(string) @@ -1078,7 +1078,7 @@ func TestSettingsHandler_TestPublicURL_EmbeddedCredentials(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any - json.Unmarshal(w.Body.Bytes(), &resp) + _ = json.Unmarshal(w.Body.Bytes(), &resp) assert.False(t, resp["reachable"].(bool)) assert.Contains(t, resp["error"].(string), "credentials") } diff --git a/backend/internal/api/handlers/system_handler.go b/backend/internal/api/handlers/system_handler.go index 8f369e6a..00a7632d 100644 --- a/backend/internal/api/handlers/system_handler.go +++ b/backend/internal/api/handlers/system_handler.go @@ -25,11 +25,12 @@ func (h *SystemHandler) GetMyIP(c *gin.Context) { ip := getClientIP(c.Request) source := "direct" - if c.GetHeader("X-Forwarded-For") != "" { + switch { + case c.GetHeader("X-Forwarded-For") != "": source = "X-Forwarded-For" - } else if c.GetHeader("X-Real-IP") != "" { + case c.GetHeader("X-Real-IP") != "": source = "X-Real-IP" - } else if c.GetHeader("CF-Connecting-IP") != "" { + case c.GetHeader("CF-Connecting-IP") != "": source = "Cloudflare" } diff --git a/backend/internal/api/handlers/testdb_test.go b/backend/internal/api/handlers/testdb_test.go index 88f65244..5b0f9d8a 100644 --- a/backend/internal/api/handlers/testdb_test.go +++ b/backend/internal/api/handlers/testdb_test.go @@ -37,7 +37,7 @@ func TestGetTemplateDB_HasTables(t *testing.T) { var tables []string rows, err := tmpl.Raw("SELECT name FROM sqlite_master WHERE type='table'").Rows() require.NoError(t, err) - defer rows.Close() + defer func() { _ = rows.Close() }() for rows.Next() { var name string @@ -95,7 +95,7 @@ func TestOpenTestDBWithMigrations(t *testing.T) { var tables []string rows, err := db.Raw("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Rows() require.NoError(t, err) - defer rows.Close() + defer func() { _ = rows.Close() }() for rows.Next() { var name string diff --git a/backend/internal/api/handlers/update_handler_test.go b/backend/internal/api/handlers/update_handler_test.go index 457f0e9d..52c5693c 100644 --- a/backend/internal/api/handlers/update_handler_test.go +++ b/backend/internal/api/handlers/update_handler_test.go @@ -20,7 +20,7 @@ func TestUpdateHandler_Check(t *testing.T) { return } w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"tag_name":"v1.0.0","html_url":"https://github.com/example/repo/releases/tag/v1.0.0"}`)) + _, _ = w.Write([]byte(`{"tag_name":"v1.0.0","html_url":"https://github.com/example/repo/releases/tag/v1.0.0"}`)) })) defer server.Close() diff --git a/backend/internal/api/handlers/user_handler_coverage_test.go b/backend/internal/api/handlers/user_handler_coverage_test.go index 179c4a0b..d1fc16af 100644 --- a/backend/internal/api/handlers/user_handler_coverage_test.go +++ b/backend/internal/api/handlers/user_handler_coverage_test.go @@ -16,7 +16,7 @@ import ( func setupUserCoverageDB(t *testing.T) *gorm.DB { t.Helper() db := OpenTestDB(t) - db.AutoMigrate(&models.User{}, &models.Setting{}) + _ = db.AutoMigrate(&models.User{}, &models.Setting{}) return db } @@ -26,7 +26,7 @@ func TestUserHandler_GetSetupStatus_Error(t *testing.T) { h := NewUserHandler(db) // Drop table to cause error - db.Migrator().DropTable(&models.User{}) + _ = db.Migrator().DropTable(&models.User{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -43,7 +43,7 @@ func TestUserHandler_Setup_CheckStatusError(t *testing.T) { h := NewUserHandler(db) // Drop table to cause error - db.Migrator().DropTable(&models.User{}) + _ = db.Migrator().DropTable(&models.User{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -61,7 +61,7 @@ func TestUserHandler_Setup_AlreadyCompleted(t *testing.T) { // Create a user to mark setup as complete user := &models.User{UUID: "uuid-a", Name: "Admin", Email: "admin@test.com", Role: "admin"} - user.SetPassword("password123") + _ = user.SetPassword("password123") db.Create(user) w := httptest.NewRecorder() @@ -108,7 +108,7 @@ func TestUserHandler_RegenerateAPIKey_DBError(t *testing.T) { h := NewUserHandler(db) // Drop table to cause error - db.Migrator().DropTable(&models.User{}) + _ = db.Migrator().DropTable(&models.User{}) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -207,11 +207,11 @@ func TestUserHandler_UpdateProfile_EmailConflict(t *testing.T) { // Create two users user1 := &models.User{UUID: "uuid-1", Name: "User1", Email: "user1@test.com", Role: "admin", APIKey: "key1"} - user1.SetPassword("password123") + _ = user1.SetPassword("password123") db.Create(user1) user2 := &models.User{UUID: "uuid-2", Name: "User2", Email: "user2@test.com", Role: "admin", APIKey: "key2"} - user2.SetPassword("password123") + _ = user2.SetPassword("password123") db.Create(user2) // Try to change user2's email to user1's email @@ -239,7 +239,7 @@ func TestUserHandler_UpdateProfile_EmailChangeNoPassword(t *testing.T) { h := NewUserHandler(db) user := &models.User{UUID: "uuid-u", Name: "User", Email: "user@test.com", Role: "admin"} - user.SetPassword("password123") + _ = user.SetPassword("password123") db.Create(user) // Try to change email without password @@ -266,7 +266,7 @@ func TestUserHandler_UpdateProfile_WrongPassword(t *testing.T) { h := NewUserHandler(db) user := &models.User{UUID: "uuid-u", Name: "User", Email: "user@test.com", Role: "admin"} - user.SetPassword("password123") + _ = user.SetPassword("password123") db.Create(user) // Try to change email with wrong password diff --git a/backend/internal/api/handlers/user_handler_test.go b/backend/internal/api/handlers/user_handler_test.go index 7c1dd70a..a1e862e9 100644 --- a/backend/internal/api/handlers/user_handler_test.go +++ b/backend/internal/api/handlers/user_handler_test.go @@ -24,7 +24,7 @@ func setupUserHandler(t *testing.T) (*UserHandler, *gorm.DB) { dbName := "file:" + t.Name() + "?mode=memory&cache=shared" db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{}) require.NoError(t, err) - db.AutoMigrate(&models.User{}, &models.Setting{}) + _ = db.AutoMigrate(&models.User{}, &models.Setting{}) return NewUserHandler(db), db } @@ -229,7 +229,7 @@ func TestUserHandler_Errors(t *testing.T) { // Update on non-existent record usually returns nil error in GORM unless configured otherwise. // However, let's see if we can force an error by closing DB? No, shared DB. // We can drop the table? - db.Migrator().DropTable(&models.User{}) + _ = db.Migrator().DropTable(&models.User{}) req, _ = http.NewRequest("POST", "/api-key-not-found", http.NoBody) w = httptest.NewRecorder() r.ServeHTTP(w, req) @@ -247,7 +247,7 @@ func TestUserHandler_UpdateProfile(t *testing.T) { Name: "Test User", APIKey: uuid.NewString(), } - user.SetPassword("password123") + _ = user.SetPassword("password123") db.Create(user) gin.SetMode(gin.TestMode) @@ -396,7 +396,7 @@ func setupUserHandlerWithProxyHosts(t *testing.T) (*UserHandler, *gorm.DB) { dbName := "file:" + t.Name() + "?mode=memory&cache=shared" db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{}) require.NoError(t, err) - db.AutoMigrate(&models.User{}, &models.Setting{}, &models.ProxyHost{}) + _ = db.AutoMigrate(&models.User{}, &models.Setting{}, &models.ProxyHost{}) return NewUserHandler(db), db } diff --git a/backend/internal/api/handlers/user_integration_test.go b/backend/internal/api/handlers/user_integration_test.go index 1277c5ad..7b5e6eae 100644 --- a/backend/internal/api/handlers/user_integration_test.go +++ b/backend/internal/api/handlers/user_integration_test.go @@ -22,7 +22,7 @@ func TestUserLoginAfterEmailChange(t *testing.T) { dbName := "file:" + t.Name() + "?mode=memory&cache=shared" db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{}) require.NoError(t, err) - db.AutoMigrate(&models.User{}, &models.Setting{}) + _ = db.AutoMigrate(&models.User{}, &models.Setting{}) // Setup Services and Handlers cfg := config.Config{} diff --git a/backend/internal/api/middleware/auth_test.go b/backend/internal/api/middleware/auth_test.go index c4c9af5f..574e0e42 100644 --- a/backend/internal/api/middleware/auth_test.go +++ b/backend/internal/api/middleware/auth_test.go @@ -19,7 +19,7 @@ func setupAuthService(t *testing.T) *services.AuthService { dbName := "file:" + t.Name() + "?mode=memory&cache=shared" db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{}) require.NoError(t, err) - db.AutoMigrate(&models.User{}) + _ = db.AutoMigrate(&models.User{}) cfg := config.Config{JWTSecret: "test-secret"} return services.NewAuthService(db, cfg) } diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index a28d8b85..5f28a2df 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -24,6 +24,9 @@ import ( "github.com/Wikid82/charon/backend/internal/metrics" "github.com/Wikid82/charon/backend/internal/models" "github.com/Wikid82/charon/backend/internal/services" + + // Import custom DNS providers to register them + _ "github.com/Wikid82/charon/backend/pkg/dnsprovider/custom" ) // Register wires up API routes and performs automatic migrations. @@ -69,6 +72,7 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error { &models.DNSProvider{}, &models.DNSProviderCredential{}, // Multi-credential support (Phase 3) &models.Plugin{}, // Phase 5: DNS provider plugins + &models.ManualChallenge{}, // Phase 1: Manual DNS challenges ); err != nil { return fmt.Errorf("auto migrate: %w", err) } @@ -312,6 +316,11 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error { adminPlugins.POST("/:id/enable", pluginHandler.EnablePlugin) adminPlugins.POST("/:id/disable", pluginHandler.DisablePlugin) adminPlugins.POST("/reload", pluginHandler.ReloadPlugins) + + // Manual DNS Challenges (Phase 1) - For users without automated DNS API access + manualChallengeService := services.NewManualChallengeService(db) + manualChallengeHandler := handlers.NewManualChallengeHandler(manualChallengeService, dnsProviderService) + manualChallengeHandler.RegisterRoutes(protected) } } else { logger.Log().Warn("CHARON_ENCRYPTION_KEY not set - DNS provider and plugin features will be unavailable") diff --git a/backend/internal/api/routes/routes_import_test.go b/backend/internal/api/routes/routes_import_test.go index 3278c03e..0e8707b1 100644 --- a/backend/internal/api/routes/routes_import_test.go +++ b/backend/internal/api/routes/routes_import_test.go @@ -18,7 +18,7 @@ func setupTestImportDB(t *testing.T) *gorm.DB { if err != nil { t.Fatalf("failed to connect to test database: %v", err) } - db.AutoMigrate(&models.ImportSession{}, &models.ProxyHost{}) + _ = db.AutoMigrate(&models.ImportSession{}, &models.ProxyHost{}) return db } diff --git a/backend/internal/api/routes/routes_test.go b/backend/internal/api/routes/routes_test.go index 1b97e79d..b2705092 100644 --- a/backend/internal/api/routes/routes_test.go +++ b/backend/internal/api/routes/routes_test.go @@ -86,7 +86,7 @@ func TestRegister_AutoMigrateFailure(t *testing.T) { // Close underlying SQL connection to force migration failure sqlDB, err := db.DB() require.NoError(t, err) - sqlDB.Close() + _ = sqlDB.Close() cfg := config.Config{ JWTSecret: "test-secret", diff --git a/backend/internal/caddy/client_test.go b/backend/internal/caddy/client_test.go index 3fc664ae..3b264e82 100644 --- a/backend/internal/caddy/client_test.go +++ b/backend/internal/caddy/client_test.go @@ -41,7 +41,7 @@ func TestClient_Load_Success(t *testing.T) { func TestClient_Load_Failure(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"error": "invalid config"}`)) + _, _ = w.Write([]byte(`{"error": "invalid config"}`)) })) defer server.Close() @@ -68,7 +68,7 @@ func TestClient_GetConfig_Success(t *testing.T) { require.Equal(t, "/config/", r.URL.Path) require.Equal(t, http.MethodGet, r.Method) w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(testConfig) + _ = json.NewEncoder(w).Encode(testConfig) })) defer server.Close() @@ -112,7 +112,7 @@ func TestClient_Ping_CreateRequestFailure(t *testing.T) { func TestClient_GetConfig_Failure(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("internal error")) + _, _ = w.Write([]byte("internal error")) })) defer server.Close() @@ -125,7 +125,7 @@ func TestClient_GetConfig_Failure(t *testing.T) { func TestClient_GetConfig_InvalidJSON(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte("invalid json")) + _, _ = w.Write([]byte("invalid json")) })) defer server.Close() diff --git a/backend/internal/caddy/config.go b/backend/internal/caddy/config.go index 423b9b6a..add6ca92 100644 --- a/backend/internal/caddy/config.go +++ b/backend/internal/caddy/config.go @@ -1193,11 +1193,12 @@ func buildWAFHandler(host *models.ProxyHost, rulesets []models.SecurityRuleSet, // Second pass: select by priority if not already selected if selected == nil { - if hostRulesetMatch != nil { + switch { + case hostRulesetMatch != nil: selected = hostRulesetMatch - } else if appMatch != nil { + case appMatch != nil: selected = appMatch - } else if owaspFallback != nil { + case owaspFallback != nil: selected = owaspFallback } } @@ -1427,12 +1428,13 @@ func buildSecurityHeadersHandler(host *models.ProxyHost) (Handler, error) { // Use profile if configured var cfg *models.SecurityHeaderProfile - if host.SecurityHeaderProfile != nil { + switch { + case host.SecurityHeaderProfile != nil: cfg = host.SecurityHeaderProfile - } else if !host.SecurityHeadersEnabled { + case !host.SecurityHeadersEnabled: // No profile and headers disabled - skip return nil, nil - } else { + default: // Use default secure headers cfg = getDefaultSecurityHeaderProfile() } diff --git a/backend/internal/caddy/config_extra_test.go b/backend/internal/caddy/config_extra_test.go index b773a1fa..8c69f742 100644 --- a/backend/internal/caddy/config_extra_test.go +++ b/backend/internal/caddy/config_extra_test.go @@ -277,22 +277,22 @@ func TestGenerateConfig_SecurityPipeline_OmitWhenDisabled(t *testing.T) { func TestGetAccessLogPath(t *testing.T) { // Save and restore env vars origEnv := os.Getenv("CHARON_ENV") - defer os.Setenv("CHARON_ENV", origEnv) + defer func() { _ = os.Setenv("CHARON_ENV", origEnv) }() t.Run("CrowdSecEnabled_UsesStandardPath", func(t *testing.T) { - os.Setenv("CHARON_ENV", "development") + _ = os.Setenv("CHARON_ENV", "development") path := getAccessLogPath("/data/caddy/data", true) require.Equal(t, "/var/log/caddy/access.log", path) }) t.Run("Production_UsesStandardPath", func(t *testing.T) { - os.Setenv("CHARON_ENV", "production") + _ = os.Setenv("CHARON_ENV", "production") path := getAccessLogPath("/data/caddy/data", false) require.Equal(t, "/var/log/caddy/access.log", path) }) t.Run("Development_UsesRelativePath", func(t *testing.T) { - os.Setenv("CHARON_ENV", "development") + _ = os.Setenv("CHARON_ENV", "development") path := getAccessLogPath("/data/caddy/data", false) // Only in development without CrowdSec should it use relative path // Note: This test may fail if /.dockerenv exists (e.g., running in CI container) @@ -307,7 +307,7 @@ func TestGetAccessLogPath(t *testing.T) { }) t.Run("NoEnv_CrowdSecEnabled_UsesStandardPath", func(t *testing.T) { - os.Unsetenv("CHARON_ENV") + _ = os.Unsetenv("CHARON_ENV") path := getAccessLogPath("/tmp/caddy-data", true) require.Equal(t, "/var/log/caddy/access.log", path) }) diff --git a/backend/internal/caddy/config_patch_coverage_test.go b/backend/internal/caddy/config_patch_coverage_test.go index b62251de..fe1e396c 100644 --- a/backend/internal/caddy/config_patch_coverage_test.go +++ b/backend/internal/caddy/config_patch_coverage_test.go @@ -235,14 +235,14 @@ func TestGenerateConfig_HTTPChallenge_ExcludesIPDomains(t *testing.T) { } func TestGetCrowdSecAPIKey_EnvPriority(t *testing.T) { - os.Unsetenv("CROWDSEC_API_KEY") - os.Unsetenv("CROWDSEC_BOUNCER_API_KEY") + _ = os.Unsetenv("CROWDSEC_API_KEY") + _ = os.Unsetenv("CROWDSEC_BOUNCER_API_KEY") t.Setenv("CROWDSEC_BOUNCER_API_KEY", "bouncer") t.Setenv("CROWDSEC_API_KEY", "primary") require.Equal(t, "primary", getCrowdSecAPIKey()) - os.Unsetenv("CROWDSEC_API_KEY") + _ = os.Unsetenv("CROWDSEC_API_KEY") require.Equal(t, "bouncer", getCrowdSecAPIKey()) } diff --git a/backend/internal/caddy/config_security_headers_test.go b/backend/internal/caddy/config_security_headers_test.go index f5a3b22f..b4db3f84 100644 --- a/backend/internal/caddy/config_security_headers_test.go +++ b/backend/internal/caddy/config_security_headers_test.go @@ -214,7 +214,7 @@ func TestBuildCSPString(t *testing.T) { assert.NoError(t, err) // CSP order can vary, so check parts exist if tt.expected != "" { - parts := []string{} + var parts []string if tt.expected == "default-src 'self'" { parts = []string{"default-src 'self'"} } else { diff --git a/backend/internal/caddy/config_test.go b/backend/internal/caddy/config_test.go index 716eab91..b077cdb7 100644 --- a/backend/internal/caddy/config_test.go +++ b/backend/internal/caddy/config_test.go @@ -562,15 +562,15 @@ func TestGetAccessLogPath_DockerEnv(t *testing.T) { // Save original env originalEnv := os.Getenv("CHARON_ENV") - defer os.Setenv("CHARON_ENV", originalEnv) + defer func() { _ = os.Setenv("CHARON_ENV", originalEnv) }() // Set CHARON_ENV=production - os.Setenv("CHARON_ENV", "production") + _ = os.Setenv("CHARON_ENV", "production") path := getAccessLogPath("/tmp/caddy-data", false) require.Equal(t, "/var/log/caddy/access.log", path) // Unset CHARON_ENV - should use development path - os.Unsetenv("CHARON_ENV") + _ = os.Unsetenv("CHARON_ENV") path = getAccessLogPath("/tmp/storage/caddy/data", false) require.Contains(t, path, "logs/access.log") require.Contains(t, path, "/tmp/storage/logs/access.log") @@ -582,14 +582,14 @@ func TestGetAccessLogPath_Development(t *testing.T) { originalEnv := os.Getenv("CHARON_ENV") defer func() { if originalEnv != "" { - os.Setenv("CHARON_ENV", originalEnv) + _ = os.Setenv("CHARON_ENV", originalEnv) } else { - os.Unsetenv("CHARON_ENV") + _ = os.Unsetenv("CHARON_ENV") } }() // Clear CHARON_ENV to simulate dev environment - os.Unsetenv("CHARON_ENV") + _ = os.Unsetenv("CHARON_ENV") // Test with typical dev path storageDir := "/home/user/charon/data/caddy/data" @@ -839,14 +839,14 @@ func TestGenerateConfig_WithCrowdSecApp(t *testing.T) { originalAPIKey := os.Getenv("CROWDSEC_API_KEY") defer func() { if originalAPIKey != "" { - os.Setenv("CROWDSEC_API_KEY", originalAPIKey) + _ = os.Setenv("CROWDSEC_API_KEY", originalAPIKey) } else { - os.Unsetenv("CROWDSEC_API_KEY") + _ = os.Unsetenv("CROWDSEC_API_KEY") } }() // Set test API key - os.Setenv("CROWDSEC_API_KEY", "test-api-key-12345") + _ = os.Setenv("CROWDSEC_API_KEY", "test-api-key-12345") config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "", "", false, true, false, false, false, "", nil, nil, nil, secCfg, nil) require.NoError(t, err) @@ -1791,14 +1791,14 @@ func TestGetCrowdSecAPIKey(t *testing.T) { envVars := []string{"CROWDSEC_API_KEY", "CROWDSEC_BOUNCER_API_KEY", "CERBERUS_SECURITY_CROWDSEC_API_KEY", "CHARON_SECURITY_CROWDSEC_API_KEY", "CPM_SECURITY_CROWDSEC_API_KEY"} for _, v := range envVars { origVars[v] = os.Getenv(v) - os.Unsetenv(v) + _ = os.Unsetenv(v) } defer func() { for k, v := range origVars { if v != "" { os.Setenv(k, v) } else { - os.Unsetenv(k) + _ = os.Unsetenv(k) } } }() @@ -1813,7 +1813,7 @@ func TestGetCrowdSecAPIKey(t *testing.T) { require.Equal(t, "primary-key", result) // Test fallback priority - os.Unsetenv("CROWDSEC_API_KEY") + _ = os.Unsetenv("CROWDSEC_API_KEY") os.Setenv("CROWDSEC_BOUNCER_API_KEY", "bouncer-key") result = getCrowdSecAPIKey() require.Equal(t, "bouncer-key", result) diff --git a/backend/internal/caddy/importer_extra_test.go b/backend/internal/caddy/importer_extra_test.go index af8d454e..57d1b3eb 100644 --- a/backend/internal/caddy/importer_extra_test.go +++ b/backend/internal/caddy/importer_extra_test.go @@ -135,7 +135,7 @@ func TestBackupCaddyfile_Success(t *testing.T) { tmp := t.TempDir() originalFile := filepath.Join(tmp, "Caddyfile") data := []byte("original-data") - os.WriteFile(originalFile, data, 0o644) + _ = os.WriteFile(originalFile, data, 0o644) backupDir := filepath.Join(tmp, "backup") path, err := BackupCaddyfile(originalFile, backupDir) require.NoError(t, err) @@ -195,10 +195,10 @@ func TestImporter_ExtractHosts_DuplicateHost(t *testing.T) { func TestBackupCaddyfile_WriteFailure(t *testing.T) { tmp := t.TempDir() originalFile := filepath.Join(tmp, "Caddyfile") - os.WriteFile(originalFile, []byte("original"), 0o644) + _ = os.WriteFile(originalFile, []byte("original"), 0o644) // Create backup dir and make it readonly to prevent writing (best-effort) backupDir := filepath.Join(tmp, "backup") - os.MkdirAll(backupDir, 0o555) + _ = os.MkdirAll(backupDir, 0o555) _, err := BackupCaddyfile(originalFile, backupDir) // Might error due to write permission; accept both success or failure depending on platform if err != nil { @@ -357,14 +357,14 @@ func TestImporter_ExtractHosts_ForceSplitFallback_PartsSscanfFail(t *testing.T) func TestBackupCaddyfile_WriteErrorDeterministic(t *testing.T) { tmp := t.TempDir() originalFile := filepath.Join(tmp, "Caddyfile") - os.WriteFile(originalFile, []byte("original-data"), 0o644) + _ = os.WriteFile(originalFile, []byte("original-data"), 0o644) backupDir := filepath.Join(tmp, "backup") - os.MkdirAll(backupDir, 0o755) + _ = os.MkdirAll(backupDir, 0o755) // Determine backup path name the function will use pid := fmt.Sprintf("%d", os.Getpid()) // Pre-create a directory at the exact backup path to ensure write fails with EISDIR path := filepath.Join(backupDir, fmt.Sprintf("Caddyfile.%s.backup", pid)) - os.Mkdir(path, 0o755) + _ = os.Mkdir(path, 0o755) _, err := BackupCaddyfile(originalFile, backupDir) require.Error(t, err) } diff --git a/backend/internal/caddy/manager_additional_test.go b/backend/internal/caddy/manager_additional_test.go index 912b1528..d1bf2d88 100644 --- a/backend/internal/caddy/manager_additional_test.go +++ b/backend/internal/caddy/manager_additional_test.go @@ -49,7 +49,7 @@ func TestManager_Rollback_UnmarshalError(t *testing.T) { tmp := t.TempDir() // Write a non-JSON file with .json extension p := filepath.Join(tmp, "config-123.json") - os.WriteFile(p, []byte("not json"), 0o644) + _ = os.WriteFile(p, []byte("not json"), 0o644) manager := NewManager(nil, nil, tmp, "", false, config.SecurityConfig{}) // Reader error should happen before client.Load err := manager.rollback(context.Background()) @@ -61,7 +61,7 @@ func TestManager_Rollback_LoadSnapshotFail(t *testing.T) { // Create a valid JSON file and set client to return error for /load tmp := t.TempDir() p := filepath.Join(tmp, "config-123.json") - os.WriteFile(p, []byte(`{"apps":{"http":{}}}`), 0o644) + _ = os.WriteFile(p, []byte(`{"apps":{"http":{}}}`), 0o644) // Mock client that returns error on Load server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -84,7 +84,7 @@ func TestManager_SaveSnapshot_WriteError(t *testing.T) { // Create a file at path to use as configDir, so writes fail tmp := t.TempDir() notDir := filepath.Join(tmp, "file-not-dir") - os.WriteFile(notDir, []byte("data"), 0o644) + _ = os.WriteFile(notDir, []byte("data"), 0o644) manager := NewManager(nil, nil, notDir, "", false, config.SecurityConfig{}) _, err := manager.saveSnapshot(&Config{}) assert.Error(t, err) @@ -94,10 +94,10 @@ func TestManager_SaveSnapshot_WriteError(t *testing.T) { func TestBackupCaddyfile_MkdirAllFailure(t *testing.T) { tmp := t.TempDir() originalFile := filepath.Join(tmp, "Caddyfile") - os.WriteFile(originalFile, []byte("original"), 0o644) + _ = os.WriteFile(originalFile, []byte("original"), 0o644) // Create a file where the backup dir should be to cause MkdirAll to fail badDir := filepath.Join(tmp, "notadir") - os.WriteFile(badDir, []byte("data"), 0o644) + _ = os.WriteFile(badDir, []byte("data"), 0o644) _, err := BackupCaddyfile(originalFile, badDir) assert.Error(t, err) @@ -123,7 +123,7 @@ func TestManager_ApplyConfig_WithSettings(t *testing.T) { } if r.URL.Path == "/config/" && r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) - w.Write([]byte("{\"apps\":{\"http\":{}}}")) + _, _ = w.Write([]byte("{\"apps\":{\"http\":{}}}")) return } w.WriteHeader(http.StatusNotFound) @@ -178,9 +178,9 @@ func TestManager_RotateSnapshots_DeletesOld(t *testing.T) { for i := 1; i <= 5; i++ { name := fmt.Sprintf("config-%d.json", i) p := filepath.Join(tmp, name) - os.WriteFile(p, []byte("{}"), 0o644) + _ = os.WriteFile(p, []byte("{}"), 0o644) // tweak mod time - os.Chtimes(p, time.Now().Add(time.Duration(i)*time.Second), time.Now().Add(time.Duration(i)*time.Second)) + _ = os.Chtimes(p, time.Now().Add(time.Duration(i)*time.Second), time.Now().Add(time.Duration(i)*time.Second)) } manager := NewManager(nil, nil, tmp, "", false, config.SecurityConfig{}) @@ -208,7 +208,7 @@ func TestManager_ApplyConfig_RotateSnapshotsWarning(t *testing.T) { } if r.URL.Path == "/config/" && r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) - w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) + _, _ = w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) return } w.WriteHeader(http.StatusNotFound) @@ -230,10 +230,10 @@ func TestManager_ApplyConfig_RotateSnapshotsWarning(t *testing.T) { // Create snapshot files: make the oldest a non-empty directory to force delete error; // generate 11 snapshots so rotateSnapshots(10) will attempt to delete 1 d1 := filepath.Join(tmp, "config-1.json") - os.MkdirAll(d1, 0o755) - os.WriteFile(filepath.Join(d1, "inner"), []byte("x"), 0o644) // non-empty + _ = os.MkdirAll(d1, 0o755) + _ = os.WriteFile(filepath.Join(d1, "inner"), []byte("x"), 0o644) // non-empty for i := 2; i <= 11; i++ { - os.WriteFile(filepath.Join(tmp, fmt.Sprintf("config-%d.json", i)), []byte("{}"), 0o644) + _ = os.WriteFile(filepath.Join(tmp, fmt.Sprintf("config-%d.json", i)), []byte("{}"), 0o644) } // Set modification times to ensure config-1.json is oldest for i := 1; i <= 11; i++ { @@ -242,7 +242,7 @@ func TestManager_ApplyConfig_RotateSnapshotsWarning(t *testing.T) { p = d1 } tmo := time.Now().Add(time.Duration(-i) * time.Minute) - os.Chtimes(p, tmo, tmo) + _ = os.Chtimes(p, tmo, tmo) } client := NewClientWithExpectedPort(caddyServer.URL, expectedPortFromURL(t, caddyServer.URL)) @@ -263,7 +263,7 @@ func TestManager_ApplyConfig_LoadFailsAndRollbackFails(t *testing.T) { } if r.URL.Path == "/config/" && r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) - w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) + _, _ = w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) return } w.WriteHeader(http.StatusNotFound) @@ -298,7 +298,7 @@ func TestManager_ApplyConfig_SaveSnapshotFails(t *testing.T) { } if r.URL.Path == "/config/" && r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) - w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) + _, _ = w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) return } w.WriteHeader(http.StatusNotFound) @@ -318,7 +318,7 @@ func TestManager_ApplyConfig_SaveSnapshotFails(t *testing.T) { // Create a file where configDir should be to cause saveSnapshot to fail tmp := t.TempDir() filePath := filepath.Join(tmp, "file-not-dir") - os.WriteFile(filePath, []byte("data"), 0o644) + _ = os.WriteFile(filePath, []byte("data"), 0o644) client := newTestClient(t, caddyServer.URL) manager := NewManager(client, db, filePath, "", false, config.SecurityConfig{}) @@ -343,7 +343,7 @@ func TestManager_ApplyConfig_LoadFailsThenRollbackSucceeds(t *testing.T) { } if r.URL.Path == "/config/" && r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) - w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) + _, _ = w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) return } w.WriteHeader(http.StatusNotFound) @@ -387,8 +387,8 @@ func TestManager_RotateSnapshots_DeleteError(t *testing.T) { // Create three files to remove one for i := 1; i <= 3; i++ { p := filepath.Join(tmp, fmt.Sprintf("config-%d.json", i)) - os.WriteFile(p, []byte("{}"), 0o644) - os.Chtimes(p, time.Now().Add(time.Duration(i)*time.Second), time.Now().Add(time.Duration(i)*time.Second)) + _ = os.WriteFile(p, []byte("{}"), 0o644) + _ = os.Chtimes(p, time.Now().Add(time.Duration(i)*time.Second), time.Now().Add(time.Duration(i)*time.Second)) } manager := NewManager(nil, nil, tmp, "", false, config.SecurityConfig{}) @@ -496,7 +496,7 @@ func TestManager_ApplyConfig_ValidateFails(t *testing.T) { } if r.URL.Path == "/config/" && r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) - w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) + _, _ = w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) return } w.WriteHeader(http.StatusNotFound) @@ -516,7 +516,7 @@ func TestManager_Rollback_ReadFileError(t *testing.T) { manager := NewManager(nil, nil, tmp, "", false, config.SecurityConfig{}) // Create snapshot entries via write p := filepath.Join(tmp, "config-123.json") - os.WriteFile(p, []byte(`{"apps":{"http":{}}}`), 0o644) + _ = os.WriteFile(p, []byte(`{"apps":{"http":{}}}`), 0o644) // Stub readFileFunc to return error origRead := readFileFunc readFileFunc = func(p string) ([]byte, error) { return nil, fmt.Errorf("read error") } @@ -544,7 +544,7 @@ func TestManager_ApplyConfig_RotateSnapshotsWarning_Stderr(t *testing.T) { } if r.URL.Path == "/config/" && r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) - w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) + _, _ = w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) return } w.WriteHeader(http.StatusNotFound) @@ -587,7 +587,7 @@ func TestManager_ApplyConfig_PassesAdminWhitelistToGenerateConfig(t *testing.T) } if r.URL.Path == "/config/" && r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) - w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) + _, _ = w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) return } w.WriteHeader(http.StatusNotFound) @@ -638,7 +638,7 @@ func TestManager_ApplyConfig_PassesRuleSetsToGenerateConfig(t *testing.T) { } if r.URL.Path == "/config/" && r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) - w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) + _, _ = w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) return } w.WriteHeader(http.StatusNotFound) @@ -691,7 +691,7 @@ func TestManager_ApplyConfig_IncludesWAFHandlerWithRuleset(t *testing.T) { } if r.URL.Path == "/config/" && r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) - w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) + _, _ = w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) return } w.WriteHeader(http.StatusNotFound) @@ -787,7 +787,7 @@ func TestManager_ApplyConfig_RulesetWriteFileFailure(t *testing.T) { } if r.URL.Path == "/config/" && r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) - w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) + _, _ = w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) return } w.WriteHeader(http.StatusNotFound) @@ -825,7 +825,7 @@ func TestManager_ApplyConfig_RulesetDirMkdirFailure(t *testing.T) { tmp := t.TempDir() // Create a file at tmp/coraza to cause MkdirAll on tmp/coraza/rulesets to fail corazaFile := filepath.Join(tmp, "coraza") - os.WriteFile(corazaFile, []byte("not a dir"), 0o644) + _ = os.WriteFile(corazaFile, []byte("not a dir"), 0o644) dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()+"rulesets-mkdirfail") db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) @@ -847,7 +847,7 @@ func TestManager_ApplyConfig_RulesetDirMkdirFailure(t *testing.T) { } if r.URL.Path == "/config/" && r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) - w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) + _, _ = w.Write([]byte("{" + "\"apps\":{\"http\":{}}}")) return } w.WriteHeader(http.StatusNotFound) @@ -1041,7 +1041,7 @@ func TestManager_ApplyConfig_PrependsSecRuleEngineDirectives(t *testing.T) { } if r.URL.Path == "/config/" && r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"apps":{"http":{}}}`)) + _, _ = w.Write([]byte(`{"apps":{"http":{}}}`)) return } w.WriteHeader(http.StatusNotFound) @@ -1100,7 +1100,7 @@ SecRule REQUEST_BODY "