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
This commit is contained in:
juanatsap
2025-12-06 17:51:20 +00:00
parent 6ed6c7780b
commit 69012bb1ae
16 changed files with 3900 additions and 865 deletions
+23 -2
View File
@@ -29,6 +29,13 @@ This documentation covers the core Go systems that power the CV site, with a foc
- Protected endpoints and authentication
- API request/response formats
4. **[Go Testing](27-GO-TESTING.md)** (~450 lines)
- Coverage summary by package (100% for config, constants, httputil)
- Test file descriptions and locations
- Testing patterns (table-driven, HTTP handlers, middleware)
- Coverage gap explanations
- Best practices and CI/CD integration
## Quick Navigation
### By Feature
@@ -54,6 +61,14 @@ This documentation covers the core Go systems that power the CV site, with a foc
- [PDF Export](26-GO-ROUTES-API.md#exportpdf---pdf-export)
- [Security Features](26-GO-ROUTES-API.md#security-features)
**Testing:**
- [Coverage Summary](27-GO-TESTING.md#coverage-summary)
- [Test Files](27-GO-TESTING.md#test-files)
- [Running Tests](27-GO-TESTING.md#running-tests)
- [Test Patterns](27-GO-TESTING.md#test-patterns)
- [Coverage Gaps](27-GO-TESTING.md#coverage-gaps)
- [Best Practices](27-GO-TESTING.md#best-practices)
### By Use Case
**Setting Up Validation:**
@@ -71,6 +86,11 @@ This documentation covers the core Go systems that power the CV site, with a foc
2. [Register handlers](26-GO-ROUTES-API.md#route-table)
3. [Apply security](26-GO-ROUTES-API.md#route-specific-middleware)
**Writing Tests:**
1. [Review existing coverage](27-GO-TESTING.md#coverage-summary)
2. [Follow test patterns](27-GO-TESTING.md#test-patterns)
3. [Run and verify](27-GO-TESTING.md#running-tests)
## System Architecture
### Overall Flow
@@ -367,6 +387,7 @@ cv/
│ ├── 24-GO-VALIDATION-SYSTEM.md # Validation docs
│ ├── 25-GO-TEMPLATE-SYSTEM.md # Template docs
│ ├── 26-GO-ROUTES-API.md # Routes/API docs
│ ├── 27-GO-TESTING.md # Testing & coverage
│ └── 00-GO-DOCUMENTATION-INDEX.md # This file
├── internal/
@@ -473,5 +494,5 @@ This documentation is part of the CV site project.
---
**Last Updated:** December 6, 2025
**Total Documentation:** 2,836+ lines across 3 files
**Coverage:** Validation, Templates, Routes, Middleware, Security
**Total Documentation:** 3,300+ lines across 4 files
**Coverage:** Validation, Templates, Routes, Middleware, Security, Testing
+13 -6
View File
@@ -17,12 +17,17 @@ This CV website is built following Go best practices with a focus on:
cv/
├── main.go # Application entry point
└── internal/ # Private packages (cannot be imported by other projects)
├── cache/ # Application-level data caching
├── config/ # Configuration management
├── constants/ # Project-wide constants
├── email/ # Email service (SMTP)
├── fileutil/ # File path utilities
├── handlers/ # HTTP request handlers
├── httputil/ # HTTP response helpers
├── middleware/ # HTTP middleware (security, logging, rate limiting)
├── models/ # Data models and business logic
├── models/ # Data models (cv, ui)
├── pdf/ # PDF generation service
├── services/ # Business services (email, etc.)
├── routes/ # Route configuration
├── templates/ # Template management
└── validation/ # Input validation utilities
```
@@ -41,13 +46,15 @@ Handlers and services receive their dependencies through constructors:
// ✅ Good: Dependencies injected
type CVHandler struct {
templates *templates.Manager
emailService *services.EmailService
emailService *email.Service
dataCache *cache.DataCache
}
func NewCVHandler(tmpl *templates.Manager, addr string, email *services.EmailService) *CVHandler {
func NewCVHandler(tmpl *templates.Manager, addr string, emailSvc *email.Service, dc *cache.DataCache) *CVHandler {
return &CVHandler{
templates: tmpl,
emailService: email, // Can be nil for graceful degradation
emailService: emailSvc, // Can be nil for graceful degradation
dataCache: dc, // Startup-loaded data cache
}
}
@@ -170,7 +177,7 @@ func (h *CVHandler) HandleContact(w http.ResponseWriter, r *http.Request)
- Consistent error handling
- HTMX-aware responses
### Email Service (`internal/services`)
### Email Service (`internal/email`)
**Pattern**: Service layer with dependency injection and interface-based design
+554
View File
@@ -0,0 +1,554 @@
# Go Testing Documentation
Comprehensive guide to the testing infrastructure of the CV site Go backend.
## Table of Contents
1. [Coverage Summary](#coverage-summary)
2. [Test Files](#test-files)
3. [Running Tests](#running-tests)
4. [Test Patterns](#test-patterns)
5. [Coverage Gaps](#coverage-gaps)
6. [Best Practices](#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
```bash
# 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
```bash
# 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
```bash
# 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
```bash
# 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:
```go
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:
```go
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:
```go
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:
```go
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
```go
// 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
```go
// 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
```go
// Good: Create fresh instance per test
func TestRateLimiter(t *testing.T) {
rl := &ContactRateLimiter{
clients: make(map[string]*contactRateLimitEntry),
}
// test code
}
```
### 5. Test Error Messages
```go
if !strings.Contains(body, "Too Many Requests") {
t.Error("Response should contain error message")
}
```
### 6. Use Constants from Production Code
```go
// Good: Use production constants
limit := c.RateLimitContactRequests
// Bad: Hardcode values
limit := 5
```
### 7. Check Response Headers
```go
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
```yaml
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
```bash
#!/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
```
---
## Related Documentation
- [24-GO-VALIDATION-SYSTEM.md](24-GO-VALIDATION-SYSTEM.md) - Validation system details
- [25-GO-TEMPLATE-SYSTEM.md](25-GO-TEMPLATE-SYSTEM.md) - Template system details
- [26-GO-ROUTES-API.md](26-GO-ROUTES-API.md) - Routes and API documentation
- [00-GO-DOCUMENTATION-INDEX.md](00-GO-DOCUMENTATION-INDEX.md) - Go documentation index
---
**Last Updated:** December 6, 2025
**Total Test Files:** 12
**Tested Packages:** 11 (with meaningful coverage)
**Overall Coverage:** ~70-75% for testable code
+69 -2
View File
@@ -4,15 +4,16 @@ This document records key architectural decisions made for this project.
## Table of Contents
- [ADR-001: No Data Caching](#adr-001-no-data-caching)
- [ADR-001: No Data Caching](#adr-001-no-data-caching) *(Superseded by ADR-004)*
- [ADR-002: Static Dates Instead of Git Integration](#adr-002-static-dates-instead-of-git-integration)
- [ADR-003: CI/CD with GitHub Actions](#adr-003-cicd-with-github-actions)
- [ADR-004: Application-Level Data Caching](#adr-004-application-level-data-caching)
---
## ADR-001: No Data Caching
**Status:** Accepted
**Status:** Superseded by [ADR-004](#adr-004-application-level-data-caching)
**Date:** 2025-11-30
### Context
@@ -148,6 +149,72 @@ Steps:
---
## ADR-004: Application-Level Data Caching
**Status:** Accepted
**Date:** 2025-12-06
**Supersedes:** [ADR-001](#adr-001-no-data-caching)
### Context
As the CV site evolved to support multiple languages and increased usage, the original decision (ADR-001) to avoid caching was reconsidered. While the site traffic remains modest, the benefits of eliminating per-request file I/O became clear:
1. **Consistency**: Every request reads the same data
2. **Performance**: Eliminates disk I/O from hot paths
3. **Reliability**: Fail-fast at startup catches data errors early
4. **Simplicity**: No cache invalidation needed (data is static)
### Decision
**Implement application-level data caching with startup-time loading.**
The `internal/cache` package provides:
- `DataCache` struct holding CV and UI data for all supported languages
- Single load at application startup
- Thread-safe read access via `sync.RWMutex`
- Language-keyed retrieval (`GetCV(lang)`, `GetUI(lang)`)
### Implementation
```go
// At startup (main.go)
dataCache, err := cache.New([]string{"en", "es"})
if err != nil {
log.Fatalf("Failed to initialize data cache: %v", err)
}
// In handlers
cv := h.dataCache.GetCV(lang)
ui := h.dataCache.GetUI(lang)
```
### Rationale
1. **Zero Per-Request I/O**: Data loaded once, served from memory
2. **Fail-Fast**: All data issues caught at startup, not runtime
3. **Thread-Safe**: `sync.RWMutex` optimized for read-heavy workloads
4. **Minimal Complexity**: Simple map-based storage, no TTL/invalidation
5. **Testable**: 95.7% test coverage, including concurrency tests
### Consequences
- **Positive:**
- Faster request handling (no disk I/O)
- Earlier error detection (startup validation)
- Consistent data across requests
- Simple, well-tested implementation
- **Considerations:**
- Requires application restart to pick up data changes
- Memory usage increases slightly (minimal - ~KB per language)
- Deep copies required when handlers mutate data
### Documentation
See [23-DATA-CACHE.md](23-DATA-CACHE.md) for complete API reference and usage patterns.
---
## How to Add New Decisions
When making significant architectural decisions, add a new section following this template:
+10 -3
View File
@@ -26,6 +26,9 @@
- [18. Security Audit](18-SECURITY-AUDIT.md) - Comprehensive security audit report (OWASP Top 10)
- [19. Security Implementation](19-SECURITY-IMPLEMENTATION.md) - Detailed security controls documentation
**Testing & Quality**
- [27. Testing](27-GO-TESTING.md) - Comprehensive testing documentation with coverage analysis
**Deployment & Operations**
- [8. Deployment Guide](8-DEPLOYMENT.md) - Production deployment instructions
- [9. Security Policies](9-SECURITY.md) - Security guidelines and vulnerability reporting
@@ -59,6 +62,7 @@
| 17 | [CONTACT-FORM.md](17-CONTACT-FORM.md) | Contact form quick start guide | Backend developers |
| 18 | [SECURITY-AUDIT.md](18-SECURITY-AUDIT.md) | Comprehensive security audit (OWASP Top 10) | Security teams |
| 19 | [SECURITY-IMPLEMENTATION.md](19-SECURITY-IMPLEMENTATION.md) | Security controls implementation details | Backend developers, Security |
| 27 | [GO-TESTING.md](27-GO-TESTING.md) | Comprehensive testing documentation and coverage | Backend developers, QA |
### User & Operations Documentation
@@ -123,6 +127,9 @@
**...report a security issue**
→ See [9-SECURITY.md](9-SECURITY.md) for responsible disclosure process
**...understand or improve test coverage**
→ Read [27-GO-TESTING.md](27-GO-TESTING.md) for coverage analysis and testing patterns
---
## 📦 Archive
@@ -152,6 +159,6 @@ All documentation in this project follows these standards:
---
**Last Updated**: 2025-12-02
**Documentation Status**: ✅ Clean, organized, single doc/ folder
**Total Active Docs**: 19 core documents + archive
**Last Updated**: 2025-12-06
**Documentation Status**: Organized, comprehensive
**Total Active Docs**: 20 core documents + archive