555 lines
14 KiB
Markdown
555 lines
14 KiB
Markdown
|
|
# 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
|