18 KiB
Executable File
Patch Coverage Only (Codecov)
This plan is strictly about adding deterministic tests (and only micro-hooks if absolutely required) to push patch coverage ≥85%.
What’s 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)
backend/internal/caddy/config.gobackend/internal/services/dns_provider_service.gobackend/internal/caddy/manager.gobackend/internal/utils/url_testing.gobackend/internal/network/safeclient.gobackend/internal/api/routes/routes.gobackend/internal/services/notification_service.gobackend/internal/crypto/encryption.gobackend/internal/api/handlers/settings_handler.gobackend/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)
- Open the PR in GitHub.
- Find the Codecov status check / comment, then open the Codecov “Files changed” (or “View report”) link.
- In Codecov, switch to Patch (not Project).
- Filter to the 10 backend files listed above.
- For each file:
- Click the file.
- Identify red (uncovered) and yellow (partial) lines.
- Note the exact line numbers/ranges as shown in Codecov.
- Paste those ranges into the table template in “Phase 0 Output”.
B) From local go tool cover -html (best for understanding branches)
- Run the VS Code task
shell: Test: Backend with Coverage. - Generate an HTML view:
cd backendgo tool cover -html=coverage.txt -o ../test-results/backend_cover.html
- Open
test-results/backend_cover.htmlin a browser. - 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.
- Cross-check that those spans overlap with the PR diff (next section) so you’re targeting patch lines, not legacy debt.
C) From git diff main...HEAD (ground truth for “patch” lines)
- List hunks for a file:
git diff -U0 main...HEAD -- backend/internal/caddy/config.go
- For each hunk header like:
@@ -OLDSTART,OLDCOUNT +NEWSTART,NEWCOUNT @@
- Convert it into a line range on the new file:
- If
NEWCOUNTis 0, that hunk adds no new lines. - Otherwise the patch line range is:
NEWSTARTthroughNEWSTART + NEWCOUNT - 1
- If
- 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
sslProviderswitch:letsencryptvszerosslvs default/bothacmeStagingCA override branch- “DNS provider config missing → skip policy” (
dnsProviderMaplookup miss) - IP filtering for ACME issuers (
net.ParseIPbranch)
- wildcard vs non-wildcard split (
getCrowdSecAPIKey: env-var priority / fallback brancheshasWildcard: true/false detection
Tests to add (exact paths + exact test function names)
- Add file:
backend/internal/caddy/config_patch_coverage_test.goTestGenerateConfig_DNSChallenge_LetsEncrypt_StagingCAAndPropagationTimeoutTestGenerateConfig_DNSChallenge_ZeroSSL_IssuerShapeTestGenerateConfig_DNSChallenge_SkipsPolicyWhenProviderConfigMissingTestGenerateConfig_HTTPChallenge_ExcludesIPDomainsTestGetCrowdSecAPIKey_EnvPriorityTestHasWildcard_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) IsDefaulttrue → unset previous defaults branch
(*dnsProviderService).Update:- credentials nil/empty vs non-empty
IsDefaulttoggle branches
(*dnsProviderService).Delete:RowsAffected == 0not found(*dnsProviderService).Test: decryption failure returns a structuredTestResult(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.goTestDNSProviderServiceCreate_DefaultsAndUnsetsExistingDefaultTestDNSProviderServiceUpdate_DoesNotOverwriteCredentialsWhenEmptyTestDNSProviderServiceDelete_NotFoundTestDNSProviderServiceTest_DecryptionError_ReturnsResultNotErrTestDNSProviderServiceGetDecryptedCredentials_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_KEYset- 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
- DNS provider load present vs empty (
Tests to add (exact paths + exact test function names)
- Add file:
backend/internal/caddy/manager_patch_coverage_test.goTestManagerApplyConfig_DNSProviders_NoKey_SkipsDecryptionTestManagerApplyConfig_DNSProviders_UsesFallbackEnvKeysTestManagerApplyConfig_DNSProviders_SkipsDecryptOrJSONFailures
Determinism
- Do not hit real filesystem/Caddy Admin API; override
generateConfigFunc/validateConfigFuncto capture inputs and short-circuit. - Do not use
t.Parallel()unless you fully isolate and restore package-level hooks. - Use
t.Setenvfor env changes.
4) backend/internal/utils/url_testing.go
Targeted branches (functions/paths in this file)
TestURLConnectivity:- scheme validation (
http/httpsonly) - production path (no transport): validation error mapping
- test path (custom transport): “skip DNS/IP validation” branch
- redirect handling:
validateRedirectTarget(localhost allowlist vs validation failure)
- scheme validation (
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.goTestValidateRedirectTarget_AllowsLocalhostTestValidateRedirectTarget_BlocksInvalidExternalRedirectTestURLConnectivity_TestPath_ReconstructsURLAndSkipsDNSValidation
Determinism
- Use
httptest.Server+ injectedhttp.RoundTripperto 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 scansafeDialer:- bad
host:portparse - localhost allow/deny branch (
AllowLocalhost) - DNS lookup error / no IPs
- “any private IP blocks” validation branch
- bad
validateRedirectTarget: missing hostname, private IP block
Tests to add (exact paths + exact test function names)
- Update file:
backend/internal/network/safeclient_test.goTestValidateRedirectTarget_MissingHostname_ReturnsErrTestValidateRedirectTarget_BlocksPrivateIPRedirectTestNewSafeHTTPClient_WithMaxRedirects_EnforcesRedirectLimit
Determinism
- Prefer redirect tests built with
httptest.Server. - Keep tests self-contained by controlling
Locationheaders.
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).
- DNS providers are conditionally registered only when
Tests to add (exact paths + exact test function names)
- Update file:
backend/internal/api/routes/routes_test.goTestRegister_DNSProviders_NotRegisteredWhenEncryptionKeyMissingTestRegister_DNSProviders_NotRegisteredWhenEncryptionKeyInvalidTestRegister_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 passthroughSendExternal:- 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)
- template selection (
Tests to add (exact paths + exact test function names)
- Update file:
backend/internal/services/notification_service_json_test.goTestNormalizeURL_DiscordWebhook_ConvertsToDiscordSchemeTestSendExternal_SkipsInvalidHTTPDestinationTestSendJSONPayload_InvalidTemplate_ReturnsErr
Determinism
- Avoid real external calls: use
httptest.Serverand validation-only failures. - For
SendExternal(async goroutine), use anatomiccounter + shorttime.Afterguard 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.goTestDecrypt_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
reachableandlatency
Tests to add (exact paths + exact test function names)
- Update file:
backend/internal/api/handlers/settings_handler_test.goTestSettingsHandler_ValidatePublicURL_NonAdmin_Returns403TestSettingsHandler_TestPublicURL_BindError_Returns400TestSettingsHandler_TestPublicURL_SSRFValidationFailure_Returns200ReachableFalse
Determinism
- Use gin test mode + httptest recorder; don’t 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)
WithAllowLocalhostearly-return path for localhost/127.0.0.1/::1- DNS resolution failure path
- private IP block path (including IPv4-mapped IPv6 detection)
- scheme enforcement (https-only unless
Tests to add (exact paths + exact test function names)
- Update file:
backend/internal/security/url_validator_test.goTestValidateExternalURL_HostnameTooLong_ReturnsErrTestValidateExternalURL_SuspiciousHostnamePattern_ReturnsErrTestValidateExternalURL_PrivilegedPortBlocked_ReturnsErrTestValidateExternalURL_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: ifjson.Marshalfailure branches inCreate/Updateare patch-missing and cannot be triggered via normal inputs, add a micro-hook (package-levelvar 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 withdefer.
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 Coverageshell: 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