Files
Charon/docs/plans/archive/patch-coverage-codecov.md
akanealw eec8c28fb3
Some checks are pending
Go Benchmark / Performance Regression Check (push) Waiting to run
Cerberus Integration / Cerberus Security Stack Integration (push) Waiting to run
Upload Coverage to Codecov / Backend Codecov Upload (push) Waiting to run
Upload Coverage to Codecov / Frontend Codecov Upload (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (go) (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (javascript-typescript) (push) Waiting to run
CrowdSec Integration / CrowdSec Bouncer Integration (push) Waiting to run
Docker Build, Publish & Test / build-and-push (push) Waiting to run
Docker Build, Publish & Test / Security Scan PR Image (push) Blocked by required conditions
Quality Checks / Auth Route Protection Contract (push) Waiting to run
Quality Checks / Codecov Trigger/Comment Parity Guard (push) Waiting to run
Quality Checks / Backend (Go) (push) Waiting to run
Quality Checks / Frontend (React) (push) Waiting to run
Rate Limit integration / Rate Limiting Integration (push) Waiting to run
Security Scan (PR) / Trivy Binary Scan (push) Waiting to run
Supply Chain Verification (PR) / Verify Supply Chain (push) Waiting to run
WAF integration / Coraza WAF Integration (push) Waiting to run
changed perms
2026-04-22 18:19:14 +00:00

18 KiB
Executable File
Raw Permalink Blame History

Patch Coverage Only (Codecov)

This plan is strictly about adding deterministic tests (and only micro-hooks if absolutely required) to push patch coverage ≥85%.

Whats failing

Codecov patch coverage is failing at 82.64%. The failure is specifically driven by missing coverage on newly-changed lines across 10 flagged backend files (listed below).

10 flagged backend files (scope boundary)

  1. backend/internal/caddy/config.go
  2. backend/internal/services/dns_provider_service.go
  3. backend/internal/caddy/manager.go
  4. backend/internal/utils/url_testing.go
  5. backend/internal/network/safeclient.go
  6. backend/internal/api/routes/routes.go
  7. backend/internal/services/notification_service.go
  8. backend/internal/crypto/encryption.go
  9. backend/internal/api/handlers/settings_handler.go
  10. backend/internal/security/url_validator.go

Phase 0 — Extract missing patch line ranges (required, exact steps)

Goal: for each of the 10 files, record the exact line ranges Codecov considers “missing” on the patch, then map each range to a specific branch and a specific new test.

A) From GitHub PR Codecov view (fastest, authoritative)

  1. Open the PR in GitHub.
  2. Find the Codecov status check / comment, then open the Codecov “Files changed” (or “View report”) link.
  3. In Codecov, switch to Patch (not Project).
  4. Filter to the 10 backend files listed above.
  5. For each file:
    • Click the file.
    • Identify red (uncovered) and yellow (partial) lines.
    • Note the exact line numbers/ranges as shown in Codecov.
  6. Paste those ranges into the table template in “Phase 0 Output”.

B) From local go tool cover -html (best for understanding branches)

  1. Run the VS Code task shell: Test: Backend with Coverage.
  2. Generate an HTML view:
    • cd backend
    • go tool cover -html=coverage.txt -o ../test-results/backend_cover.html
  3. Open test-results/backend_cover.html in a browser.
  4. For each of the 10 files:
    • Use in-page search for the filename (or navigate via the coverage page UI).
    • Identify red/uncovered spans and record the exact line ranges.
  5. Cross-check that those spans overlap with the PR diff (next section) so youre targeting patch lines, not legacy debt.

C) From git diff main...HEAD (ground truth for “patch” lines)

  1. List hunks for a file:
    • git diff -U0 main...HEAD -- backend/internal/caddy/config.go
  2. For each hunk header like:
    • @@ -OLDSTART,OLDCOUNT +NEWSTART,NEWCOUNT @@
  3. Convert it into a line range on the new file:
    • If NEWCOUNT is 0, that hunk adds no new lines.
    • Otherwise the patch line range is:
      • NEWSTART through NEWSTART + NEWCOUNT - 1
  4. Repeat for each of the 10 files.

Phase 0 Output: table to paste missing line ranges into

Paste and fill this as you extract line ranges (Codecov is the source of truth):

File Missing patch line ranges (Codecov) Partial patch line ranges (Codecov) Notes (branch / error path)
backend/internal/caddy/config.go TBD TBD
backend/internal/services/dns_provider_service.go TBD TBD
backend/internal/caddy/manager.go TBD TBD
backend/internal/utils/url_testing.go TBD TBD
backend/internal/network/safeclient.go TBD TBD
backend/internal/api/routes/routes.go TBD TBD
backend/internal/services/notification_service.go TBD TBD
backend/internal/crypto/encryption.go TBD TBD
backend/internal/api/handlers/settings_handler.go TBD TBD
backend/internal/security/url_validator.go TBD TBD

File-by-file plan (tests only, implementation-ready)

1) backend/internal/caddy/config.go

Targeted branches (functions/paths in this file)

  • GenerateConfig:
    • wildcard vs non-wildcard split (hasWildcard(cleanDomains))
    • DNS-provider policy creation vs HTTP-challenge policy creation
    • sslProvider switch: letsencrypt vs zerossl vs default/both
    • acmeStaging CA override branch
    • “DNS provider config missing → skip policy” (dnsProviderMap lookup miss)
    • IP filtering for ACME issuers (net.ParseIP branch)
  • getCrowdSecAPIKey: env-var priority / fallback branches
  • hasWildcard: true/false detection

Tests to add (exact paths + exact test function names)

  • Add file: backend/internal/caddy/config_patch_coverage_test.go
    • TestGenerateConfig_DNSChallenge_LetsEncrypt_StagingCAAndPropagationTimeout
    • TestGenerateConfig_DNSChallenge_ZeroSSL_IssuerShape
    • TestGenerateConfig_DNSChallenge_SkipsPolicyWhenProviderConfigMissing
    • TestGenerateConfig_HTTPChallenge_ExcludesIPDomains
    • TestGetCrowdSecAPIKey_EnvPriority
    • TestHasWildcard_TrueFalse

Determinism

  • Use pure in-memory inputs; no Docker/Caddy process calls.
  • Avoid comparing raw JSON strings; unmarshal and assert keys/values.
  • When asserting lists (subjects), use set semantics or sort before compare.

2) backend/internal/services/dns_provider_service.go

Targeted branches (functions/paths in this file)

  • (*dnsProviderService).Create:
    • invalid provider type
    • credential validation error
    • defaults (PropagationTimeout == 0, PollingInterval == 0)
    • IsDefault true → unset previous defaults branch
  • (*dnsProviderService).Update:
    • credentials nil/empty vs non-empty
    • IsDefault toggle branches
  • (*dnsProviderService).Delete: RowsAffected == 0 not found
  • (*dnsProviderService).Test: decryption failure returns a structured TestResult (no error)
  • (*dnsProviderService).GetDecryptedCredentials: decrypt error vs invalid JSON error

Tests to add (exact paths + exact test function names)

  • Update file: backend/internal/services/dns_provider_service_test.go
    • TestDNSProviderServiceCreate_DefaultsAndUnsetsExistingDefault
    • TestDNSProviderServiceUpdate_DoesNotOverwriteCredentialsWhenEmpty
    • TestDNSProviderServiceDelete_NotFound
    • TestDNSProviderServiceTest_DecryptionError_ReturnsResultNotErr
    • TestDNSProviderServiceGetDecryptedCredentials_InvalidJSON_ReturnsErrDecryptionFailed

Determinism

  • Use in-memory sqlite via existing test helpers.
  • Avoid map ordering assumptions in comparisons.

3) backend/internal/caddy/manager.go

Targeted branches (functions/paths in this file)

  • (*Manager).ApplyConfig:
    • DNS provider load present vs empty (len(dnsProviders) > 0)
    • encryption key discovery:
      • CHARON_ENCRYPTION_KEY set
      • fallback keys (ENCRYPTION_KEY, CERBERUS_ENCRYPTION_KEY)
      • no key → warning branch
    • decrypt/parse branches per provider:
      • CredentialsEncrypted == "" skip
      • decrypt error skip
      • JSON unmarshal error skip

Tests to add (exact paths + exact test function names)

  • Add file: backend/internal/caddy/manager_patch_coverage_test.go
    • TestManagerApplyConfig_DNSProviders_NoKey_SkipsDecryption
    • TestManagerApplyConfig_DNSProviders_UsesFallbackEnvKeys
    • TestManagerApplyConfig_DNSProviders_SkipsDecryptOrJSONFailures

Determinism

  • Do not hit real filesystem/Caddy Admin API; override generateConfigFunc / validateConfigFunc to capture inputs and short-circuit.
  • Do not use t.Parallel() unless you fully isolate and restore package-level hooks.
  • Use t.Setenv for env changes.

4) backend/internal/utils/url_testing.go

Targeted branches (functions/paths in this file)

  • TestURLConnectivity:
    • scheme validation (http/https only)
    • production path (no transport): validation error mapping
    • test path (custom transport): “skip DNS/IP validation” branch
    • redirect handling: validateRedirectTarget (localhost allowlist vs validation failure)
  • ssrfSafeDialer: address parse error, DNS resolution error, “any private IP blocks” branch

Tests to add (exact paths + exact test function names)

  • Update file: backend/internal/utils/url_testing_test.go
    • TestValidateRedirectTarget_AllowsLocalhost
    • TestValidateRedirectTarget_BlocksInvalidExternalRedirect
    • TestURLConnectivity_TestPath_ReconstructsURLAndSkipsDNSValidation

Determinism

  • Use httptest.Server + injected http.RoundTripper to avoid real DNS/network.
  • Avoid timing flakiness: assert “latency > 0” only when a local server is used.

5) backend/internal/network/safeclient.go

Targeted branches (functions/paths in this file)

  • IsPrivateIP: fast-path checks and CIDR-block scan
  • safeDialer:
    • bad host:port parse
    • localhost allow/deny branch (AllowLocalhost)
    • DNS lookup error / no IPs
    • “any private IP blocks” validation branch
  • validateRedirectTarget: missing hostname, private IP block

Tests to add (exact paths + exact test function names)

  • Update file: backend/internal/network/safeclient_test.go
    • TestValidateRedirectTarget_MissingHostname_ReturnsErr
    • TestValidateRedirectTarget_BlocksPrivateIPRedirect
    • TestNewSafeHTTPClient_WithMaxRedirects_EnforcesRedirectLimit

Determinism

  • Prefer redirect tests built with httptest.Server.
  • Keep tests self-contained by controlling Location headers.

6) backend/internal/api/routes/routes.go

Targeted branches (functions/paths in this file)

  • Register:
    • DNS providers are conditionally registered only when cfg.EncryptionKey != "".
    • Branch when crypto.NewEncryptionService(cfg.EncryptionKey) fails (routes should remain unavailable).
    • Branch when encryption service initializes successfully (routes should exist).

Tests to add (exact paths + exact test function names)

  • Update file: backend/internal/api/routes/routes_test.go
    • TestRegister_DNSProviders_NotRegisteredWhenEncryptionKeyMissing
    • TestRegister_DNSProviders_NotRegisteredWhenEncryptionKeyInvalid
    • TestRegister_DNSProviders_RegisteredWhenEncryptionKeyValid

Determinism

  • Validate route presence by inspecting router.Routes() and asserting "/dns-providers" paths exist or do not exist.
  • Use a known-good base64 32-byte key literal for “valid” cases.

7) backend/internal/services/notification_service.go

Targeted branches (functions/paths in this file)

  • normalizeURL: Discord webhook URL normalization vs passthrough
  • SendExternal:
    • event-type preference filtering branches
    • http/https destination validation using security.ValidateExternalURL (skip invalid destinations)
    • JSON-template path vs shoutrrr path
  • sendJSONPayload:
    • template selection (minimal/detailed/custom/default)
    • template size limit
    • URL validation failure branch
    • template parse/execute error branches
    • JSON unmarshal error
    • service-specific validation branches (discord/slack/gotify)

Tests to add (exact paths + exact test function names)

  • Update file: backend/internal/services/notification_service_json_test.go
    • TestNormalizeURL_DiscordWebhook_ConvertsToDiscordScheme
    • TestSendExternal_SkipsInvalidHTTPDestination
    • TestSendJSONPayload_InvalidTemplate_ReturnsErr

Determinism

  • Avoid real external calls: use httptest.Server and validation-only failures.
  • For SendExternal (async goroutine), use an atomic counter + short time.After guard to assert “no send happened”.

8) backend/internal/crypto/encryption.go

Targeted branches (functions/paths in this file)

  • NewEncryptionService: invalid base64, wrong key length
  • (*EncryptionService).Encrypt: nonce generation
  • (*EncryptionService).Decrypt: invalid base64, ciphertext too short, auth/tag failure (gcm.Open)

Tests to add (exact paths + exact test function names)

  • Update file: backend/internal/crypto/encryption_test.go
    • TestDecrypt_InvalidCiphertext (extend table cases if patch adds new error branches)
    • TestDecrypt_TamperedCiphertext (ensure it hits the new/changed patch lines)

Determinism

  • Avoid any network/file IO.
  • Do not rely on ciphertext equality; only assert round-trips and error substrings.

9) backend/internal/api/handlers/settings_handler.go

Targeted branches (functions/handlers in this file)

  • ValidatePublicURL:
    • admin-only gate (403)
    • JSON bind error (400)
    • invalid URL format (400)
    • success response includes optional warning field
  • TestPublicURL:
    • admin-only gate (403)
    • JSON bind error (400)
    • format validation error (400)
    • SSRF validation failure returns 200 with reachable=false
    • connectivity error returns 200 with reachable=false
    • success path returns 200 with reachable and latency

Tests to add (exact paths + exact test function names)

  • Update file: backend/internal/api/handlers/settings_handler_test.go
    • TestSettingsHandler_ValidatePublicURL_NonAdmin_Returns403
    • TestSettingsHandler_TestPublicURL_BindError_Returns400
    • TestSettingsHandler_TestPublicURL_SSRFValidationFailure_Returns200ReachableFalse

Determinism

  • Use gin test mode + httptest recorder; dont hit real DNS.
  • For TestPublicURL, choose inputs that fail at validation (e.g., http://10.0.0.1) so the handler exits before any real network.

10) backend/internal/security/url_validator.go

Targeted branches (functions/paths in this file)

  • ValidateExternalURL:
    • scheme enforcement (https-only unless WithAllowHTTP)
    • credentials-in-URL rejection
    • hostname length guard and suspicious pattern rejection
    • port parsing and privileged-port blocking (non-standard ports <1024)
    • WithAllowLocalhost early-return path for localhost/127.0.0.1/::1
    • DNS resolution failure path
    • private IP block path (including IPv4-mapped IPv6 detection)

Tests to add (exact paths + exact test function names)

  • Update file: backend/internal/security/url_validator_test.go
    • TestValidateExternalURL_HostnameTooLong_ReturnsErr
    • TestValidateExternalURL_SuspiciousHostnamePattern_ReturnsErr
    • TestValidateExternalURL_PrivilegedPortBlocked_ReturnsErr
    • TestValidateExternalURL_URLWithCredentials_ReturnsErr

Determinism

  • Prefer validation-only failures (length/pattern/credentials/port) to avoid reliance on DNS.
  • Avoid asserting exact DNS error strings; assert on stable substrings for validation errors.

Patch Coverage Hit List (consolidated)

Fill the missing ranges from Phase 0, then implement only the tests listed here.

File Missing patch line ranges Test(s) that hit it
backend/internal/caddy/config.go TBD: fill from Codecov TestGenerateConfig_DNSChallenge_LetsEncrypt_StagingCAAndPropagationTimeout; TestGenerateConfig_DNSChallenge_ZeroSSL_IssuerShape; TestGenerateConfig_DNSChallenge_SkipsPolicyWhenProviderConfigMissing; TestGenerateConfig_HTTPChallenge_ExcludesIPDomains; TestGetCrowdSecAPIKey_EnvPriority; TestHasWildcard_TrueFalse
backend/internal/services/dns_provider_service.go TBD: fill from Codecov TestDNSProviderServiceCreate_DefaultsAndUnsetsExistingDefault; TestDNSProviderServiceUpdate_DoesNotOverwriteCredentialsWhenEmpty; TestDNSProviderServiceDelete_NotFound; TestDNSProviderServiceTest_DecryptionError_ReturnsResultNotErr; TestDNSProviderServiceGetDecryptedCredentials_InvalidJSON_ReturnsErrDecryptionFailed
backend/internal/caddy/manager.go TBD: fill from Codecov TestManagerApplyConfig_DNSProviders_NoKey_SkipsDecryption; TestManagerApplyConfig_DNSProviders_UsesFallbackEnvKeys; TestManagerApplyConfig_DNSProviders_SkipsDecryptOrJSONFailures
backend/internal/utils/url_testing.go TBD: fill from Codecov TestValidateRedirectTarget_AllowsLocalhost; TestValidateRedirectTarget_BlocksInvalidExternalRedirect; TestURLConnectivity_TestPath_ReconstructsURLAndSkipsDNSValidation
backend/internal/network/safeclient.go TBD: fill from Codecov TestValidateRedirectTarget_MissingHostname_ReturnsErr; TestValidateRedirectTarget_BlocksPrivateIPRedirect; TestNewSafeHTTPClient_WithMaxRedirects_EnforcesRedirectLimit
backend/internal/api/routes/routes.go TBD: fill from Codecov TestRegister_DNSProviders_NotRegisteredWhenEncryptionKeyMissing; TestRegister_DNSProviders_NotRegisteredWhenEncryptionKeyInvalid; TestRegister_DNSProviders_RegisteredWhenEncryptionKeyValid
backend/internal/services/notification_service.go TBD: fill from Codecov TestNormalizeURL_DiscordWebhook_ConvertsToDiscordScheme; TestSendExternal_SkipsInvalidHTTPDestination; TestSendJSONPayload_InvalidTemplate_ReturnsErr
backend/internal/crypto/encryption.go TBD: fill from Codecov TestDecrypt_InvalidCiphertext; TestDecrypt_TamperedCiphertext
backend/internal/api/handlers/settings_handler.go TBD: fill from Codecov TestSettingsHandler_ValidatePublicURL_NonAdmin_Returns403; TestSettingsHandler_TestPublicURL_BindError_Returns400; TestSettingsHandler_TestPublicURL_SSRFValidationFailure_Returns200ReachableFalse
backend/internal/security/url_validator.go TBD: fill from Codecov TestValidateExternalURL_HostnameTooLong_ReturnsErr; TestValidateExternalURL_SuspiciousHostnamePattern_ReturnsErr; TestValidateExternalURL_PrivilegedPortBlocked_ReturnsErr; TestValidateExternalURL_URLWithCredentials_ReturnsErr

Minimal refactors (only if required)

Do not refactor unless Phase 0 shows patch-missing lines are in branches that are effectively unreachable from tests.

  • backend/internal/services/dns_provider_service.go: if json.Marshal failure branches in Create/Update are patch-missing and cannot be triggered via normal inputs, add a micro-hook (package-level var marshalJSON = json.Marshal) and restore it in tests.
  • backend/internal/caddy/manager.go: if any error branches depend on OS/JSON behaviors, use the existing package-level hooks (e.g., readFileFunc, writeFileFunc, jsonMarshalFunc, generateConfigFunc) and always restore them with defer.

Notes:

  • Avoid t.Parallel() in any test that touches package-level vars/hooks unless you fully isolate state.
  • Any global override must be restored, even on failure paths.

Validation checklist (run after tests are implemented)

  • shell: Test: Backend with Coverage
  • shell: Lint: Pre-commit (All Files)
  • shell: Security: CodeQL Go Scan (CI-Aligned) [~60s]
  • shell: Security: CodeQL JS Scan (CI-Aligned) [~90s]
  • shell: Security: Trivy Scan