Files
cv-site/doc/27-GO-TESTING.md
juanatsap 69012bb1ae test: add comprehensive Go test suite with ~75% coverage
New test files:
- config/config_test.go (100% coverage)
- constants/constants_test.go (100% coverage)
- httputil/response_test.go (100% coverage)
- validation/rules_test.go (91.9% coverage)
- middleware/logger_test.go, security_test.go, security_logger_test.go
- handlers/errors_test.go

Updated documentation:
- doc/27-GO-TESTING.md: Complete testing guide
- doc/00-GO-DOCUMENTATION-INDEX.md: Added testing section
- doc/01-ARCHITECTURE.md: Updated package structure
- doc/DECISIONS.md: Added ADR-004 caching decision
- PROJECT-MEMORY.md: Added Go testing section
2025-12-06 17:51:20 +00:00

14 KiB

Go Testing Documentation

Comprehensive guide to the testing infrastructure of the CV site Go backend.

Table of Contents

  1. Coverage Summary
  2. Test Files
  3. Running Tests
  4. Test Patterns
  5. Coverage Gaps
  6. Best Practices

Coverage Summary

Current test coverage as of December 2025:

Package Coverage Status Notes
internal/config 100% Excellent Fully tested configuration loading
internal/constants 100% Excellent All constants and validation values
internal/httputil 100% Excellent All response helper functions
internal/cache 95.7% Excellent Application-level data caching
internal/validation 91.9% Excellent Validation rules and error handling
internal/middleware 87.5% Good Security, rate limiting, preferences
internal/fileutil 88.9% Good File path utilities
internal/models/ui 85.7% Good UI configuration models
internal/models/cv 83.3% Good CV data models
internal/handlers 62.9% Fair HTTP handlers (PDF requires Chrome)
internal/email 58.0% Fair Email requires SMTP connection
internal/pdf 0% N/A Requires Chrome/chromedp
internal/templates 0% N/A File-system dependent
internal/routes 0% N/A Integration testing required
internal/models 0% N/A Interface-only package

Overall Project Coverage: ~70-75% (for testable packages)


Test Files

High-Coverage Packages (90%+)

internal/config/config_test.go

Tests for application configuration loading:

  • Environment variable parsing
  • Default value handling
  • Port configuration
  • SMTP settings validation

internal/constants/constants_test.go

Tests for constant values and validation:

  • Language constants (English, Spanish)
  • CV theme constants (default, clean)
  • CV length constants (short, long)
  • Color theme constants (light, dark)
  • Rate limit configurations
  • HTTP header constants

internal/httputil/response_test.go

Tests for HTTP response helpers:

  • JSON() - Generic JSON response
  • JSONOk() - Success JSON response
  • JSONCached() - Cached JSON response
  • HTML() - HTML response with proper headers
  • NoContent() - 204 No Content response

Good-Coverage Packages (80-90%)

internal/middleware/csrf_test.go

CSRF protection testing:

  • Token generation (generateToken)
  • Token validation (validateToken)
  • GetToken() from request context
  • Middleware protection flow
  • HTMX request handling

internal/middleware/logger_test.go

Request logging testing:

  • responseWriter implementation
  • WriteHeader() status capture
  • Write() body capture
  • Middleware integration

internal/middleware/contact_rate_limit_test.go

Rate limiting testing:

  • NewContactRateLimiter() initialization
  • allow() function behavior
  • Middleware blocking behavior
  • HTMX error responses
  • X-Forwarded-For header handling
  • X-Real-IP header handling
  • GetStats() statistics

internal/middleware/security_logger_test.go

Security logging and preferences testing:

  • LogSecurityEvent() function
  • getSeverity() mapping
  • SecurityLogger middleware
  • isSecurityRelevantPath() detection
  • Preferences helper functions:
    • GetLanguage(), GetCVLength(), GetCVIcons()
    • GetCVTheme(), GetColorTheme()
    • IsLongCV(), IsShortCV()
    • ShowIcons(), HideIcons()
    • IsCleanTheme(), IsDefaultTheme()
    • IsDarkMode(), IsLightMode()

internal/validation/rules_test.go

Validation rules testing:

  • ruleOptional - Optional field handling
  • ruleTrim - Whitespace trimming marker
  • ruleSanitize - HTML sanitization marker
  • ruleMin - Minimum length validation (UTF-8 aware)
  • ruleTiming - Bot detection timing
  • FieldError.Error() - Error formatting
  • ValidationErrors.HasErrors() - Error checking
  • ValidationErrors.GetFieldErrors() - Field-specific errors

internal/handlers/errors_test.go

Error handling testing:

  • AppError.Error() - Error message formatting
  • NewAppError() - Error constructor
  • HandleError() with JSON requests
  • HandleError() with HTMX requests
  • HandleError() with standard requests
  • Internal error message hiding
  • Error constructors:
    • NotFoundError(), BadRequestError()
    • InternalError(), TemplateError()
    • DataLoadError()
  • DomainError type testing
  • Method chaining (WithError(), WithField())

Running Tests

Basic Commands

# Run all tests
go test ./...

# Run with coverage
go test -cover ./...

# Run specific package
go test ./internal/middleware/...

# Run with verbose output
go test -v ./internal/validation/...

# Run with race detection
go test -race ./...

Coverage Report

# Generate coverage profile
go test -coverprofile=coverage.out ./internal/...

# View in terminal
go tool cover -func=coverage.out

# Generate HTML report
go tool cover -html=coverage.out -o coverage.html

# Open HTML report (macOS)
open coverage.html

Package-Specific Testing

# Config tests
go test -v ./internal/config/

# Middleware tests
go test -v ./internal/middleware/

# Validation tests
go test -v ./internal/validation/

# Handler tests
go test -v ./internal/handlers/

# All tests with coverage summary
go test -cover ./internal/... 2>&1 | grep -E "^ok|coverage:"

Running Individual Tests

# Run tests matching a pattern
go test -v -run "TestCSRF" ./internal/middleware/

# Run specific test function
go test -v -run "TestRuleMin" ./internal/validation/

# Run subtests
go test -v -run "TestValidationErrors_GetFieldErrors/Get_multiple" ./internal/validation/

Test Patterns

Table-Driven Tests

Most tests use Go's table-driven test pattern for comprehensive coverage:

func TestRuleMin(t *testing.T) {
    tests := []struct {
        name     string
        field    string
        value    string
        param    string
        hasError bool
    }{
        {"Valid - meets minimum", "msg", "hello", "5", false},
        {"Valid - exceeds minimum", "msg", "hello world", "5", false},
        {"Invalid - too short", "msg", "hi", "5", true},
        {"UTF-8 aware - valid", "name", "Jose", "4", false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := ruleMin(tt.field, tt.value, tt.param)
            if (result != nil) != tt.hasError {
                t.Errorf("ruleMin(%q, %q, %q) error = %v, wantError %v",
                    tt.field, tt.value, tt.param, result != nil, tt.hasError)
            }
        })
    }
}

HTTP Handler Testing

Using net/http/httptest for handler tests:

func TestHandleError_JSON(t *testing.T) {
    appErr := NewAppError(nil, "Bad request", http.StatusBadRequest, false)

    req := httptest.NewRequest(http.MethodGet, "/", nil)
    req.Header.Set(c.HeaderAccept, c.ContentTypeJSON)
    rec := httptest.NewRecorder()

    HandleError(rec, req, appErr)

    if rec.Code != http.StatusBadRequest {
        t.Errorf("Status = %d, want %d", rec.Code, http.StatusBadRequest)
    }

    var response ErrorResponse
    if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
        t.Fatalf("Failed to parse JSON response: %v", err)
    }
}

Context-Based Testing

Testing preferences via request context:

func TestPreferencesHelperFunctions(t *testing.T) {
    prefs := &Preferences{
        CVLength:   c.CVLengthLong,
        CVIcons:    c.CVIconsShow,
        CVLanguage: c.LangSpanish,
        CVTheme:    c.CVThemeClean,
        ColorTheme: c.ColorThemeDark,
    }

    req := httptest.NewRequest(http.MethodGet, "/", nil)
    ctx := context.WithValue(req.Context(), PreferencesKey, prefs)
    reqWithPrefs := req.WithContext(ctx)

    t.Run("GetLanguage", func(t *testing.T) {
        result := GetLanguage(reqWithPrefs)
        if result != c.LangSpanish {
            t.Errorf("GetLanguage() = %q, want %q", result, c.LangSpanish)
        }
    })
}

Middleware Testing

Testing middleware chains:

func TestContactRateLimiter_Middleware_Blocked(t *testing.T) {
    rl := &ContactRateLimiter{
        clients: make(map[string]*contactRateLimitEntry),
    }

    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    })

    protected := rl.Middleware(handler)

    // Exhaust the rate limit
    limit := c.RateLimitContactRequests
    for i := 0; i < limit; i++ {
        req := httptest.NewRequest(http.MethodPost, "/api/contact", nil)
        req.RemoteAddr = "192.168.1.1:12345"
        rec := httptest.NewRecorder()
        protected.ServeHTTP(rec, req)
    }

    // Next request should be blocked
    req := httptest.NewRequest(http.MethodPost, "/api/contact", nil)
    req.RemoteAddr = "192.168.1.1:12345"
    rec := httptest.NewRecorder()

    protected.ServeHTTP(rec, req)

    if rec.Code != http.StatusTooManyRequests {
        t.Errorf("Status = %d, want %d", rec.Code, http.StatusTooManyRequests)
    }
}

Coverage Gaps

Why Some Packages Have 0% Coverage

internal/pdf (0%)

  • Reason: Requires Chrome browser via chromedp
  • Solution: Would need headless Chrome in CI/CD
  • Alternative: Mock the chromedp interface (significant refactoring)

internal/templates (0%)

  • Reason: File-system dependent template loading
  • Solution: Could use embedded test templates
  • Impact: Low priority - simple template wrapper

internal/routes (0%)

  • Reason: Integration-level routing setup
  • Solution: End-to-end testing with running server
  • Alternative: Test individual handlers instead

internal/models (0%)

  • Reason: Contains only interface definitions
  • Impact: None - interfaces have no executable code

Partial Coverage Explanations

internal/middleware (87.5%)

Uncovered code includes:

  • Background goroutine cleanup functions (tickers)
  • Production-only file logging (/var/log/)
  • Edge cases in recovery middleware

internal/email (58.0%)

Uncovered code includes:

  • Actual SMTP connection and sending
  • TLS handshake code
  • Network error handling Note: Would require SMTP mock server

internal/handlers (62.9%)

Uncovered code includes:

  • PDF generation handlers (need Chrome)
  • Some HTMX-specific response paths
  • Error paths for template loading failures

Best Practices

1. Use Table-Driven Tests

// Good: Table-driven
tests := []struct {
    name     string
    input    string
    expected bool
}{
    {"valid", "test@example.com", true},
    {"invalid", "not-an-email", false},
}

for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        // test code
    })
}

2. Test Edge Cases

Always test:

  • Empty inputs
  • Maximum length inputs
  • Unicode/UTF-8 characters
  • Invalid parameters
  • Boundary conditions

3. Use Descriptive Test Names

// Good
t.Run("UTF-8 aware - valid Japanese characters", ...)
t.Run("Invalid - exceeds maximum length", ...)

// Bad
t.Run("test1", ...)
t.Run("case2", ...)

4. Isolate Tests

// Good: Create fresh instance per test
func TestRateLimiter(t *testing.T) {
    rl := &ContactRateLimiter{
        clients: make(map[string]*contactRateLimitEntry),
    }
    // test code
}

5. Test Error Messages

if !strings.Contains(body, "Too Many Requests") {
    t.Error("Response should contain error message")
}

6. Use Constants from Production Code

// Good: Use production constants
limit := c.RateLimitContactRequests

// Bad: Hardcode values
limit := 5

7. Check Response Headers

contentType := rec.Header().Get(c.HeaderContentType)
if contentType != c.ContentTypeJSON {
    t.Errorf("Content-Type = %q, want %q", contentType, c.ContentTypeJSON)
}

Test File Structure

internal/
├── cache/
│   └── cache_test.go           # Data caching tests
├── config/
│   └── config_test.go          # Configuration tests
├── constants/
│   └── constants_test.go       # Constants validation
├── email/
│   └── email_test.go           # Email service tests
├── fileutil/
│   └── fileutil_test.go        # File utilities tests
├── handlers/
│   └── errors_test.go          # Error handling tests
├── httputil/
│   └── response_test.go        # HTTP response tests
├── middleware/
│   ├── csrf_test.go            # CSRF protection tests
│   ├── logger_test.go          # Logging middleware tests
│   ├── contact_rate_limit_test.go  # Rate limiting tests
│   └── security_logger_test.go     # Security logging tests
├── models/
│   ├── cv/
│   │   └── cv_test.go          # CV model tests
│   └── ui/
│       └── ui_test.go          # UI model tests
└── validation/
    ├── validator_test.go       # Core validator tests
    └── rules_test.go           # Validation rules tests

CI/CD Integration

GitHub Actions Example

name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.23'

      - name: Run tests
        run: go test -v -race -coverprofile=coverage.out ./...

      - name: Check coverage
        run: |
          go tool cover -func=coverage.out | grep total | awk '{print $3}'

Pre-commit Hook

#!/bin/bash
# .git/hooks/pre-commit

echo "Running tests..."
go test ./internal/... -cover

if [ $? -ne 0 ]; then
    echo "Tests failed. Commit aborted."
    exit 1
fi


Last Updated: December 6, 2025 Total Test Files: 12 Tested Packages: 11 (with meaningful coverage) Overall Coverage: ~70-75% for testable code