# QA Remediation Plan **Date:** December 23, 2025 **Status:** 🔴 CRITICAL - Required for Beta Release **Current State:** - Coverage: **84.7%** (Target: ≥85%) - **0.3% gap** - Test Failures: **7 test scenarios (21 total sub-test failures)** - Root Cause: SSRF protection correctly blocking localhost/private IPs in tests --- ## Executive Summary The QA audit identified two blockers preventing merge: 1. **Test Failures:** 7 tests fail because SSRF protection (working correctly) blocks localhost URLs used by `httptest` servers 2. **Coverage Gap:** 0.3% below the 85% threshold due to low coverage in `internal/utils` (51.5%) **Estimated Total Effort:** 4-6 hours **Approach:** Mock HTTP transport for tests + add targeted test coverage **Risk Level:** Low (fixes do not weaken security) --- ## Priority 1: Fix Test Failures (3-4 hours) ### Overview All 7 failing tests are caused by the new SSRF protection correctly blocking `httptest.NewServer()` and `httptest.NewTLSServer()`, which bind to `127.0.0.1`. The tests need to be refactored to use mock HTTP transports instead of real HTTP servers. ### Failing Tests Breakdown #### 1.1 URL Connectivity Tests (6 scenarios, 18 sub-tests) **Location:** `backend/internal/utils/url_connectivity_test.go` **Failing Tests:** - `TestTestURLConnectivity_Success` (line 17) - `TestTestURLConnectivity_Redirect` (line 35) - `TestTestURLConnectivity_TooManyRedirects` (line 56) - `TestTestURLConnectivity_StatusCodes` (11 sub-tests, line 70) - 200 OK, 201 Created, 204 No Content - 301 Moved Permanently, 302 Found - 400 Bad Request, 401 Unauthorized, 403 Forbidden - 404 Not Found, 500 Internal Server Error, 503 Service Unavailable - `TestTestURLConnectivity_InvalidURL` (3 of 4 sub-tests, line 114) - Empty URL, Invalid scheme, No scheme - `TestTestURLConnectivity_Timeout` (line 143) **Error Message:** ``` access to private IP addresses is blocked (resolved to 127.0.0.1) ``` **Analysis:** - These tests use `httptest.NewServer()` which creates servers on `127.0.0.1` - SSRF protection correctly identifies and blocks these private IPs - Tests need to use mock transport that bypasses DNS resolution and HTTP connection **Solution Approach:** Create a test helper that injects a custom `http.RoundTripper` into the `TestURLConnectivity` function: ```go // Option A: Add test-only parameter (least invasive) func TestURLConnectivity(rawURL string, transport ...http.RoundTripper) (bool, float64, error) { // Use custom transport if provided, otherwise use default } // Option B: Refactor for dependency injection (cleaner) type ConnectivityTester struct { Transport http.RoundTripper Timeout time.Duration } ``` **Specific Changes Required:** 1. **Modify `url_testing.go`:** - Add optional `http.RoundTripper` parameter to `TestURLConnectivity()` - Use provided transport if available, otherwise use default client - This allows tests to mock HTTP without triggering SSRF checks 2. **Update all 6 failing test functions:** - Replace `httptest.NewServer()` with mock `http.RoundTripper` - Use `httptest.NewRequest()` for building test requests - Mock response with custom `http.Response` objects **Example Mock Transport Pattern:** ```go // Test helper for mocking HTTP responses type mockTransport struct { statusCode int headers http.Header body string err error } func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { if m.err != nil { return nil, m.err } resp := &http.Response{ StatusCode: m.statusCode, Header: m.headers, Body: io.NopCloser(strings.NewReader(m.body)), Request: req, } return resp, nil } ``` **Files to Modify:** - `backend/internal/utils/url_testing.go` (add transport parameter) - `backend/internal/utils/url_connectivity_test.go` (update all 6 failing tests) **Estimated Effort:** 2-3 hours --- #### 1.2 Settings Handler Test (1 scenario) **Location:** `backend/internal/api/handlers/settings_handler_test.go` **Failing Test:** - `TestSettingsHandler_TestPublicURL_Success` (line 662) **Error Details:** ``` Line 673: Expected: true, Actual: false (reachable) Line 674: Expected not nil (latency) Line 675: Expected not nil (message) ``` **Root Cause:** - Test calls `/settings/test-url` endpoint which internally calls `TestURLConnectivity()` - The test is trying to validate a localhost URL, which is blocked by SSRF protection - Unlike the utils tests, this is testing the full HTTP handler flow **Solution Approach:** Use test doubles (mock/spy) for the `TestURLConnectivity` function within the handler test: ```go // Option A: Extract URL validator as interface for mocking type URLValidator interface { TestURLConnectivity(url string) (bool, float64, error) } // Option B: Use test server with overridden connectivity function // (requires refactoring handler to accept injected validator) ``` **Specific Changes Required:** 1. **Refactor `settings_handler.go`:** - Extract URL validation logic into a testable interface - Allow dependency injection of validator for testing 2. **Update `settings_handler_test.go`:** - Create mock validator that returns success for test URLs - Inject mock into handler during test setup - Alternatively: use a real public URL (e.g., `https://httpbin.org/status/200`) **Alternative Quick Fix (if refactoring is too invasive):** - Change test to use a real public URL instead of localhost - Add comment explaining why we use external URL for this specific test - Pros: No code changes to production code - Cons: Test depends on external service availability **Recommended Approach:** Use the quick fix for immediate unblocking, plan interface refactoring for post-release cleanup. **Files to Modify:** - `backend/internal/api/handlers/settings_handler_test.go` (line 662-676) - Optionally: `backend/internal/api/handlers/settings_handler.go` (if using DI approach) **Estimated Effort:** 1 hour (quick fix) or 2 hours (full refactoring) --- ## Priority 2: Increase Test Coverage to ≥85% (2 hours) ### Current Coverage by Package **Packages Below Target:** - `internal/utils`: **51.5%** (biggest gap) - `internal/services`: 83.5% (close but below) - `cmd/seed`: 62.5% **Total Coverage:** 84.7% (need +0.3% minimum, target +1% for safety margin) ### Coverage Strategy Focus on `internal/utils` package as it has the largest gap and fewest lines of code to cover. #### 2.1 Missing Coverage in `internal/utils` **Files in package:** - `url.go` (likely low coverage) - `url_testing.go` (covered by existing tests) **Action Items:** 1. **Audit `url.go` for uncovered functions:** ```bash cd backend && go test -coverprofile=coverage.out ./internal/utils go tool cover -html=coverage.out -o utils_coverage.html # Open HTML to identify uncovered lines ``` 2. **Add tests for uncovered functions in `url.go`:** - Look for utility functions related to URL parsing, validation, or manipulation - Write targeted unit tests for each uncovered function - Aim for 80-90% coverage of `url.go` to bring package average above 70% **Expected Impact:** - Adding 5-10 tests for `url.go` functions should increase package coverage from 51.5% to ~75% - This alone should push total coverage from 84.7% to **~85.5%** (exceeding target) **Files to Modify:** - Add new test file: `backend/internal/utils/url_test.go` - Or expand: `backend/internal/utils/url_connectivity_test.go` **Estimated Effort:** 1.5 hours --- #### 2.2 Additional Coverage (if needed) If `url.go` tests don't push coverage high enough, target these next: **Option A: `internal/services` (83.5% → 86%)** - Review coverage HTML report for services with lowest coverage - Add edge case tests for error handling paths - Focus on: `access_list_service.go`, `backup_service.go`, `certificate_service.go` **Option B: `cmd/seed` (62.5% → 75%)** - Add tests for seeding logic and error handling - Mock database interactions - Test seed data validation **Recommended:** Start with `internal/utils`, only proceed to Option A/B if necessary. **Estimated Effort:** 0.5-1 hour (if required) --- ## Implementation Plan & Sequence ### Phase 1: Coverage First (Get to ≥85%) **Rationale:** Easier to run tests without failures constantly appearing **Steps:** 1. ✅ Audit `internal/utils/url.go` for uncovered functions 2. ✅ Write unit tests for all uncovered functions in `url.go` 3. ✅ Run coverage report: `go test -coverprofile=coverage.out ./...` 4. ✅ Verify coverage ≥85.5% (safety margin) **Exit Criteria:** Total coverage ≥85% --- ### Phase 2: Fix Test Failures (Make all tests pass) #### Step 2A: Fix Utils Tests (Priority 1.1) 1. ✅ Add `http.RoundTripper` parameter to `TestURLConnectivity()` 2. ✅ Create mock transport helper in test file 3. ✅ Update all 6 failing test functions to use mock transport 4. ✅ Run tests: `go test ./internal/utils -v` 5. ✅ Verify all tests pass **Exit Criteria:** All utils tests pass #### Step 2B: Fix Settings Handler Test (Priority 1.2) 1. ✅ Choose approach: Quick fix (public URL) or DI refactoring 2. ✅ Implement fix in `settings_handler_test.go` 3. ✅ Run tests: `go test ./internal/api/handlers -v -run TestSettingsHandler_TestPublicURL` 4. ✅ Verify test passes **Exit Criteria:** Settings handler test passes --- ### Phase 3: Validation & Sign-Off 1. ✅ Run full test suite: `go test ./...` 2. ✅ Run coverage: `go test -coverprofile=coverage.out ./...` 3. ✅ Verify: - All tests passing (FAIL count = 0) - Coverage ≥85% 4. ✅ Run pre-commit hooks: `.github/skills/scripts/skill-runner.sh qa-precommit-all` 5. ✅ Generate final QA report 6. ✅ Commit changes with descriptive message **Exit Criteria:** Clean test run, coverage ≥85%, pre-commit passes --- ## Acceptance Criteria - [x] **All tests must pass** (0 failures) - [x] **Coverage must be ≥85%** (preferably ≥85.5% for buffer) - [x] **SSRF protection must remain intact** (no security regression) - [x] **Pre-commit hooks must pass** (linting, formatting) - [x] **No changes to production code security logic** (only test code modified) --- ## Risk Assessment ### Low Risk Items ✅ - Adding unit tests for `url.go` (no production code changes) - Mock transport in `url_connectivity_test.go` (test-only changes) - Quick fix for settings handler test (minimal change) ### Medium Risk Items ⚠️ - Dependency injection refactoring in settings handler (if chosen) - **Mitigation:** Test thoroughly, use quick fix as backup ### High Risk Items 🔴 - None identified ### Security Considerations - **CRITICAL:** Do NOT add localhost to SSRF allowlist - **CRITICAL:** Do NOT disable SSRF checks in production code - **CRITICAL:** Mock transport must only be available in test builds - All fixes target test code, not production security logic --- ## Alternative Approaches Considered ### ❌ Approach 1: Add test allowlist to SSRF protection **Why Rejected:** Weakens security, could leak to production ### ❌ Approach 2: Use build tags to disable SSRF in tests **Why Rejected:** Too risky, could accidentally disable in production ### ❌ Approach 3: Skip failing tests temporarily **Why Rejected:** Violates project standards, hides real issues ### ✅ Approach 4: Mock HTTP transport (SELECTED) **Why Selected:** Industry standard, no security impact, clean separation of concerns --- ## Dependencies & Blockers **Dependencies:** - None (all work can proceed immediately) **Potential Blockers:** - If `url.go` has fewer functions than expected, may need to add tests to `services` package - **Mitigation:** Identified backup options in Section 2.2 --- ## Testing Strategy ### Unit Tests - ✅ All new tests must follow existing patterns in `*_test.go` files - ✅ Use table-driven tests for multiple scenarios - ✅ Mock external dependencies (HTTP, DNS) ### Integration Tests - ⚠️ No integration test changes required (SSRF tests pass) - ✅ Verify SSRF protection still blocks localhost in production ### Regression Tests - ✅ Run full suite before and after changes - ✅ Compare coverage reports to ensure no decrease - ✅ Verify no new failures introduced --- ## Rollback Plan If any issues arise during implementation: 1. **Revert to last known good state:** ```bash git checkout HEAD -- backend/internal/utils/ git checkout HEAD -- backend/internal/api/handlers/settings_handler_test.go ``` 2. **Re-run tests to confirm stability:** ```bash go test ./... ``` 3. **Escalate to team lead** if issues persist --- ## Success Metrics ### Before Remediation - Coverage: 84.7% - Test Failures: 21 - Failing Test Scenarios: 7 - QA Status: ⚠️ CONDITIONAL PASS ### After Remediation - Coverage: ≥85.5% - Test Failures: 0 - Failing Test Scenarios: 0 - QA Status: ✅ PASS --- ## Deliverables 1. **Code Changes:** - `backend/internal/utils/url_testing.go` (add transport parameter) - `backend/internal/utils/url_connectivity_test.go` (mock transport) - `backend/internal/utils/url_test.go` (new tests for url.go) - `backend/internal/api/handlers/settings_handler_test.go` (fix failing test) 2. **Documentation:** - Updated test documentation explaining mock transport pattern - Code comments in test files explaining SSRF protection handling 3. **Reports:** - Final QA report with updated metrics - Coverage report showing ≥85% --- ## Post-Remediation Tasks ### Immediate (Before Merge) - [ ] Update QA report with final metrics - [ ] Update PR description with remediation summary - [ ] Request final code review ### Short-Term (Post-Merge) - [ ] Document mock transport pattern in testing guidelines - [ ] Add linter rule to prevent `httptest.NewServer()` in SSRF-tested code paths - [ ] Create tech debt ticket for settings handler DI refactoring (if using quick fix) ### Long-Term - [ ] Evaluate test coverage targets for individual packages - [ ] Consider increasing global coverage target to 90% - [ ] Add automated coverage regression detection to CI --- ## Timeline **Day 1 (4-6 hours):** - Hour 1-2: Increase coverage in `internal/utils` - Hour 3-4: Fix URL connectivity tests (mock transport) - Hour 5: Fix settings handler test - Hour 6: Final validation and QA report **Estimated Completion:** Same day (December 23, 2025) --- ## Questions & Answers **Q: Why not just disable SSRF protection for tests?** A: Would create security vulnerability risk and violate secure coding standards. **Q: Can we use a real localhost exception just for tests?** A: No. Build-time exceptions can leak to production and create attack vectors. **Q: Why mock transport instead of using a test container?** A: Faster, no external dependencies, standard Go testing practice. **Q: What if coverage doesn't reach 85% after utils tests?** A: Backup plan documented in Section 2.2 (services or seed package). --- ## Approvals **Prepared By:** GitHub Copilot (Automated QA Agent) **Reviewed By:** [Pending] **Approved By:** [Pending] **Status:** 🟡 DRAFT - Awaiting Implementation --- **Document Version:** 1.0 **Last Updated:** December 23, 2025 **Location:** `/projects/Charon/docs/plans/qa_remediation.md`