- Add 16 comprehensive tests for user_handler.go covering PreviewInviteURL, getAppName, email normalization, permission/role defaults, and edge cases - Add 14 unit tests for url.go functions (GetBaseURL, ConstructURL, NormalizeURL) - Refactor URL connectivity tests to use mock HTTP transport pattern - Fix 21 test failures caused by SSRF protection blocking localhost - Maintain full SSRF security - no production code security changes - Coverage increased from 66.67% to 86.1% (exceeds 85% target) - All security scans pass with zero Critical/High vulnerabilities - 38 SSRF protection tests verified passing Technical details: - Added optional http.RoundTripper parameter to TestURLConnectivity() - Created mockTransport for test isolation without network calls - Changed settings handler test to use public URL for validation - Verified no regressions in existing test suite Closes: Coverage gap identified in Codecov report See: docs/plans/user_handler_coverage_fix.md See: docs/plans/qa_remediation.md See: docs/reports/qa_report_final.md
16 KiB
User Handler Coverage Fix Plan
Current State
File: backend/internal/api/handlers/user_handler.go
Current Coverage: 66.67% patch coverage (Codecov report)
Target Coverage: 100% of new/changed lines, minimum 85% overall
Missing Coverage: 3 lines (2 missing, 1 partial)
Coverage Analysis
Based on the coverage report analysis, the following functions have gaps:
Functions with Incomplete Coverage
-
PreviewInviteURL - 0.0% coverage
- Lines: 509-543
- Status: Completely untested
- Route:
POST /api/v1/users/preview-invite-url(protected, admin-only)
-
getAppName - 0.0% coverage
- Lines: 547-552
- Status: Helper function, completely untested
- Usage: Called internally by InviteUser
-
generateSecureToken - 75.0% coverage (1 line missing)
- Lines: 386-390
- Missing: Error path when
rand.Read()fails - Current Test: TestGenerateSecureToken only tests success path
-
Setup - 76.9% coverage
- Lines: 74-136
- Partial Coverage: Some error paths not fully tested
-
CreateUser - 75.7% coverage
- Lines: 295-384
- Partial Coverage: Some error scenarios not covered
-
InviteUser - 74.5% coverage
- Lines: 395-501
- Partial Coverage: Some edge cases and error paths missing
-
UpdateUser - 75.0% coverage
- Lines: 608-668
- Partial Coverage: Email conflict error path may be missing
-
UpdateProfile - 87.1% coverage
- Lines: 192-247
- Partial Coverage: Database error paths not fully covered
-
AcceptInvite - 81.8% coverage
- Lines: 814-862
- Partial Coverage: Some error scenarios not tested
Detailed Test Plan
Priority 1: Zero Coverage Functions
1.1 PreviewInviteURL Tests
Function Purpose: Returns a preview of what an invite URL would look like with current settings
Test File: backend/internal/api/handlers/user_handler_test.go
Test Cases to Add:
func TestUserHandler_PreviewInviteURL_NonAdmin(t *testing.T)
- Setup: User with "user" role
- Action: POST /users/preview-invite-url
- Expected: HTTP 403 Forbidden
- Assertion: Error message "Admin access required"
func TestUserHandler_PreviewInviteURL_InvalidJSON(t *testing.T)
- Setup: Admin user
- Action: POST with invalid JSON body
- Expected: HTTP 400 Bad Request
- Assertion: Binding error message
func TestUserHandler_PreviewInviteURL_Success_Unconfigured(t *testing.T)
- Setup: Admin user, no app.public_url setting
- Action: POST with valid email
- Expected: HTTP 200 OK
- Assertions:
is_configuredis falsewarningis truewarning_messagecontains "not configured"preview_urlcontains fallback URL
func TestUserHandler_PreviewInviteURL_Success_Configured(t *testing.T)
- Setup: Admin user, app.public_url setting exists
- Action: POST with valid email
- Expected: HTTP 200 OK
- Assertions:
is_configuredis truewarningis falsepreview_urlcontains configured URLbase_urlmatches configured setting
Mock Requirements:
- Need to create Setting model with key "app.public_url"
- Test both with and without configured URL
1.2 getAppName Tests
Function Purpose: Helper function to retrieve app name from settings
Test File: backend/internal/api/handlers/user_handler_test.go
Test Cases to Add:
func TestGetAppName_Default(t *testing.T)
- Setup: Empty database
- Action: Call getAppName(db)
- Expected: Returns "Charon"
func TestGetAppName_FromSettings(t *testing.T)
- Setup: Create Setting with key "app_name", value "MyCustomApp"
- Action: Call getAppName(db)
- Expected: Returns "MyCustomApp"
func TestGetAppName_EmptyValue(t *testing.T)
- Setup: Create Setting with key "app_name", empty value
- Action: Call getAppName(db)
- Expected: Returns "Charon" (fallback)
Mock Requirements:
- Models.Setting with key "app_name"
Priority 2: Partial Coverage - Error Paths
2.1 generateSecureToken Error Path
Current Coverage: 75% (missing error branch)
Test Case to Add:
func TestGenerateSecureToken_ReadError(t *testing.T)
- Challenge:
crypto/rand.Read()rarely fails in normal conditions - Approach: This is difficult to test without mocking the rand.Reader
- Alternative: Document that this error path is for catastrophic system failure
- Decision: Consider this acceptable uncovered code OR refactor to accept an io.Reader for dependency injection
Recommendation: Accept this as untestable without significant refactoring. Document with comment.
2.2 Setup Database Error Paths
Current Coverage: 76.9%
Missing Test Cases:
func TestUserHandler_Setup_TransactionFailure(t *testing.T)
- Setup: Mock DB transaction failure
- Action: POST /setup with valid data
- Challenge: SQLite doesn't easily simulate transaction failures
- Alternative: Test with closed DB connection or dropped table
func TestUserHandler_Setup_PasswordHashError(t *testing.T)
- Setup: Valid request but password hashing fails
- Challenge: bcrypt.GenerateFromPassword rarely fails
- Decision: May be acceptable uncovered code
Recommendation: Focus on testable paths; document hard-to-test error scenarios.
2.3 CreateUser Missing Scenarios
Current Coverage: 75.7%
Test Cases to Add:
func TestUserHandler_CreateUser_PasswordHashError(t *testing.T)
- Setup: Valid request
- Action: Attempt to create user with password that causes hash failure
- Challenge: Hard to trigger without mocking
- Decision: Document as edge case
func TestUserHandler_CreateUser_DatabaseCheckError(t *testing.T)
- Setup: Drop users table before email check
- Action: POST /users
- Expected: HTTP 500 "Failed to check email"
func TestUserHandler_CreateUser_AssociationError(t *testing.T)
- Setup: Valid permitted_hosts with non-existent host IDs
- Action: POST /users with invalid host IDs
- Expected: Transaction should fail or hosts should be empty
2.4 InviteUser Missing Scenarios
Current Coverage: 74.5%
Test Cases to Add:
func TestUserHandler_InviteUser_TokenGenerationError(t *testing.T)
- Challenge: Hard to force crypto/rand failure
- Decision: Document as edge case
func TestUserHandler_InviteUser_DisableUserError(t *testing.T)
- Setup: Create user, then cause Update to fail
- Action: POST /users/invite
- Expected: Transaction rollback
func TestUserHandler_InviteUser_MailServiceConfigured(t *testing.T)
- Setup: Configure MailService with valid SMTP settings
- Action: POST /users/invite
- Expected: email_sent should be true (or handle SMTP error)
- Challenge: Requires SMTP mock or test server
2.5 UpdateUser Email Conflict
Current Coverage: 75.0%
Existing Test: TestUserHandler_UpdateUser_Success covers basic update Potential Gap: Email conflict check might not hit error path
Test Case to Verify/Add:
func TestUserHandler_UpdateUser_EmailConflict(t *testing.T)
- Setup: Create two users
- Action: Try to update user1's email to user2's email
- Expected: HTTP 409 Conflict
- Assertion: Error message "Email already in use"
2.6 UpdateProfile Database Errors
Current Coverage: 87.1%
Test Cases to Add:
func TestUserHandler_UpdateProfile_EmailCheckError(t *testing.T)
- Setup: Valid user, drop table before email check
- Action: PUT /profile with new email
- Expected: HTTP 500 "Failed to check email availability"
func TestUserHandler_UpdateProfile_UpdateError(t *testing.T)
- Setup: Valid user, close DB before update
- Action: PUT /profile
- Expected: HTTP 500 "Failed to update profile"
2.7 AcceptInvite Missing Coverage
Current Coverage: 81.8%
Existing Tests Cover:
- Invalid JSON
- Invalid token
- Expired token (with status update)
- Already accepted
- Success
Potential Gap: SetPassword error path
Test Case to Consider:
func TestUserHandler_AcceptInvite_PasswordHashError(t *testing.T)
- Challenge: Hard to trigger bcrypt failure
- Decision: Document as edge case
Priority 3: Boundary Conditions and Edge Cases
3.1 Email Normalization
Test Cases to Add:
func TestUserHandler_CreateUser_EmailNormalization(t *testing.T)
- Setup: Admin user
- Action: Create user with email "User@Example.COM"
- Expected: Email stored as "user@example.com"
func TestUserHandler_InviteUser_EmailNormalization(t *testing.T)
- Setup: Admin user
- Action: Invite user with mixed-case email
- Expected: Email stored lowercase
3.2 Permission Mode Defaults
Test Cases to Verify:
func TestUserHandler_CreateUser_DefaultPermissionMode(t *testing.T)
- Setup: Admin user
- Action: Create user without specifying permission_mode
- Expected: permission_mode defaults to "allow_all"
func TestUserHandler_InviteUser_DefaultPermissionMode(t *testing.T)
- Setup: Admin user
- Action: Invite user without specifying permission_mode
- Expected: permission_mode defaults to "allow_all"
3.3 Role Defaults
Test Cases to Verify:
func TestUserHandler_CreateUser_DefaultRole(t *testing.T)
- Setup: Admin user
- Action: Create user without specifying role
- Expected: role defaults to "user"
func TestUserHandler_InviteUser_DefaultRole(t *testing.T)
- Setup: Admin user
- Action: Invite user without specifying role
- Expected: role defaults to "user"
Priority 4: Integration Scenarios
4.1 Permitted Hosts Edge Cases
Test Cases to Add:
func TestUserHandler_CreateUser_EmptyPermittedHosts(t *testing.T)
- Setup: Admin, permission_mode "deny_all", empty permitted_hosts
- Action: Create user
- Expected: User created with deny_all mode, no permitted hosts
func TestUserHandler_CreateUser_NonExistentPermittedHosts(t *testing.T)
- Setup: Admin, permission_mode "deny_all", non-existent host IDs [999, 1000]
- Action: Create user
- Expected: User created but no hosts associated (or error)
Implementation Strategy
Phase 1: Add Missing Tests (Priority 1)
- Implement PreviewInviteURL test suite (4 tests)
- Implement getAppName test suite (3 tests)
- Run coverage and verify these reach 100%
Expected Impact: +7 test cases, ~35 lines of untested code covered
Phase 2: Error Path Coverage (Priority 2)
- Add database error simulation tests where feasible
- Document hard-to-test error paths with code comments
- Focus on testable scenarios (table drops, closed connections)
Expected Impact: +8-10 test cases, improved error path coverage
Phase 3: Edge Cases and Defaults (Priority 3)
- Add email normalization tests
- Add default value tests
- Verify role and permission defaults
Expected Impact: +6 test cases, better validation of business logic
Phase 4: Integration Edge Cases (Priority 4)
- Add permitted hosts edge case tests
- Test association behavior with invalid data
Expected Impact: +2 test cases, comprehensive coverage
Success Criteria
Minimum Requirements (85% Coverage)
- PreviewInviteURL: 100% coverage (4 tests)
- getAppName: 100% coverage (3 tests)
- generateSecureToken: 100% or documented as untestable
- All other functions: ≥85% coverage
- Total user_handler.go coverage: ≥85%
Stretch Goal (100% Coverage)
- All testable code paths covered
- Untestable code paths documented with
// Coverage: Untestable without mockingcomments - All error paths tested or documented
- All edge cases and boundary conditions tested
Test Execution Plan
Step 1: Run Baseline Coverage
cd backend
go test -coverprofile=baseline_coverage.txt -run "TestUserHandler" ./internal/api/handlers
go tool cover -func=baseline_coverage.txt | grep user_handler.go
Step 2: Implement Priority 1 Tests
- Add PreviewInviteURL tests
- Add getAppName tests
- Run coverage and verify improvement
Step 3: Iterate Through Priorities
- Implement each priority group
- Run coverage after each group
- Adjust plan based on results
Step 4: Final Coverage Report
go test -coverprofile=final_coverage.txt -run "TestUserHandler" ./internal/api/handlers
go tool cover -func=final_coverage.txt | grep user_handler.go
go tool cover -html=final_coverage.txt -o user_handler_coverage.html
Step 5: Validate Against Codecov
- Push changes to branch
- Verify Codecov report shows ≥85% patch coverage
- Verify no coverage regressions
Mock and Setup Requirements
Database Models
models.Usermodels.Settingmodels.ProxyHost
Test Helpers
setupUserHandler(t)- Creates test DB with User and Setting tablessetupUserHandlerWithProxyHosts(t)- Includes ProxyHost table- Admin middleware mock:
c.Set("role", "admin") - User ID middleware mock:
c.Set("userID", uint(1))
Additional Mocks Needed
- SMTP server mock for email testing (optional, can verify email_sent=false)
- Settings helper for creating app.public_url and app_name settings
Testing Best Practices to Follow
- Use Table-Driven Tests: For similar scenarios with different inputs
- Descriptive Test Names: Follow pattern
TestUserHandler_Function_Scenario - Arrange-Act-Assert: Clear separation of setup, action, and verification
- Unique Database Names: Use
t.Name()in DB connection string - Test Isolation: Each test should be independent
- Assertions: Use
testify/assertfor clear failure messages - Error Messages: Verify exact error messages, not just status codes
Code Style Consistency
Existing Patterns to Maintain
- Use
gin.SetMode(gin.TestMode)at test start - Use
httptest.NewRecorder()for response capture - Marshal request bodies with
json.Marshal() - Set
Content-Type: application/jsonheader - Use
require.NoError()for setup failures - Use
assert.Equal()for assertions
Test Organization
- Group related tests with
t.Run()when appropriate - Keep tests in same file as existing tests
- Use clear comments for complex setup
Acceptance Checklist
- All Priority 1 tests implemented and passing
- All Priority 2 testable scenarios implemented
- All Priority 3 edge cases tested
- Coverage report shows ≥85% for user_handler.go
- No existing tests broken
- All new tests follow existing patterns
- Code review checklist satisfied
- Codecov patch coverage ≥85%
- Documentation updated if needed
Notes and Considerations
Hard-to-Test Scenarios
- crypto/rand.Read() failure: Extremely rare, requires system-level failure
- bcrypt password hashing failure: Rare, usually only with invalid cost
- SMTP email sending: Requires mock server or test credentials
Recommendations
- Document untestable error paths with comments
- Focus test effort on realistic failure scenarios
- Use table drops and closed connections for DB errors
- Consider refactoring hard-to-test code if coverage is critical
Future Improvements
- Consider dependency injection for crypto/rand and bcrypt
- Add integration tests with real SMTP mock server
- Add performance tests for password hashing
- Consider property-based testing for email normalization
Related Files
- Handler:
backend/internal/api/handlers/user_handler.go - Tests:
backend/internal/api/handlers/user_handler_test.go - Routes:
backend/internal/api/routes/routes.go - Models:
backend/internal/models/user.go - Utils:
backend/internal/utils/url.go
Timeline Estimate
- Phase 1 (Priority 1): 2-3 hours
- Phase 2 (Priority 2): 3-4 hours
- Phase 3 (Priority 3): 1-2 hours
- Phase 4 (Priority 4): 1 hour
- Total: 7-10 hours
Plan created: 2025-12-23 Target completion: Before merge to main branch Owner: Development team