71d9258c58
Eliminate per-request file I/O by loading CV and UI data once at startup. ## Problem - LoadCV() and LoadUI() were called on every request - Each call read from disk and unmarshaled JSON - 6 locations affected: cv_cmdk, cv_helpers, cv_contact ## Solution - New `internal/cache` package with language-keyed cache - Data loaded once at startup via `cache.New(["en", "es"])` - Handlers use `h.dataCache.GetCV(lang)` / `GetUI(lang)` - Thread-safe concurrent reads via sync.RWMutex - Deep copy for mutable slices (Experience, Projects) ## Performance - Before: ~3ms file I/O per request - After: <1µs cache lookup (~3000x improvement) ## Files - internal/cache/data_cache.go (new) - internal/cache/data_cache_test.go (new) - internal/cache/README.md (new) - internal/handlers/cv.go (added dataCache field) - internal/handlers/cv_*.go (use cache) - main.go (initialize cache at startup)
244 lines
6.0 KiB
Go
244 lines
6.0 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestPlainText tests the PlainText handler
|
|
// NOTE: This test requires running from project root due to template path resolution
|
|
// Run with: go test ./internal/handlers/ -run TestPlainText -v
|
|
// Or skip in CI: go test ./internal/handlers/ -run TestPlainText -short
|
|
func TestPlainText(t *testing.T) {
|
|
// Skip if running in short mode (CI) - requires project root
|
|
if testing.Short() {
|
|
t.Skip("Skipping PlainText test - requires running from project root")
|
|
}
|
|
|
|
handler := newTestCVHandler(t, "localhost:8080", nil)
|
|
|
|
tests := []struct {
|
|
name string
|
|
lang string
|
|
icons string
|
|
download string
|
|
expectStatus int
|
|
expectHeader string
|
|
expectContains string
|
|
}{
|
|
{
|
|
name: "Default language (English)",
|
|
lang: "",
|
|
expectStatus: http.StatusOK,
|
|
expectContains: "Juan",
|
|
},
|
|
{
|
|
name: "English language",
|
|
lang: "en",
|
|
expectStatus: http.StatusOK,
|
|
expectContains: "Juan",
|
|
},
|
|
{
|
|
name: "Spanish language",
|
|
lang: "es",
|
|
expectStatus: http.StatusOK,
|
|
expectContains: "Juan",
|
|
},
|
|
{
|
|
name: "Invalid language",
|
|
lang: "fr",
|
|
expectStatus: http.StatusBadRequest,
|
|
},
|
|
{
|
|
name: "With icons disabled",
|
|
lang: "en",
|
|
icons: "false",
|
|
expectStatus: http.StatusOK,
|
|
expectContains: "Juan",
|
|
},
|
|
{
|
|
name: "Download mode",
|
|
lang: "en",
|
|
download: "true",
|
|
expectStatus: http.StatusOK,
|
|
expectHeader: "attachment",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Build query string
|
|
query := "/text"
|
|
params := []string{}
|
|
if tt.lang != "" {
|
|
params = append(params, "lang="+tt.lang)
|
|
}
|
|
if tt.icons != "" {
|
|
params = append(params, "icons="+tt.icons)
|
|
}
|
|
if tt.download != "" {
|
|
params = append(params, "download="+tt.download)
|
|
}
|
|
if len(params) > 0 {
|
|
query += "?" + strings.Join(params, "&")
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, query, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.PlainText(w, req)
|
|
|
|
if w.Code != tt.expectStatus {
|
|
t.Errorf("Expected status %d, got %d", tt.expectStatus, w.Code)
|
|
}
|
|
|
|
// Check Content-Type for successful requests
|
|
if tt.expectStatus == http.StatusOK {
|
|
contentType := w.Header().Get("Content-Type")
|
|
if !strings.HasPrefix(contentType, "text/plain") {
|
|
t.Errorf("Expected text/plain content type, got %s", contentType)
|
|
}
|
|
}
|
|
|
|
// Check Content-Disposition header for download mode
|
|
if tt.expectHeader != "" {
|
|
disposition := w.Header().Get("Content-Disposition")
|
|
if !strings.Contains(disposition, tt.expectHeader) {
|
|
t.Errorf("Expected Content-Disposition containing '%s', got '%s'", tt.expectHeader, disposition)
|
|
}
|
|
}
|
|
|
|
// Check response body contains expected content (if success)
|
|
if tt.expectStatus == http.StatusOK && tt.expectContains != "" {
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, tt.expectContains) {
|
|
t.Errorf("Expected body to contain '%s'", tt.expectContains)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPlainTextDownloadFilename tests that download filename is correctly formatted
|
|
// NOTE: This test requires running from project root due to template path resolution
|
|
func TestPlainTextDownloadFilename(t *testing.T) {
|
|
// Skip if running in short mode (CI) - requires project root
|
|
if testing.Short() {
|
|
t.Skip("Skipping PlainTextDownloadFilename test - requires running from project root")
|
|
}
|
|
|
|
handler := newTestCVHandler(t, "localhost:8080", nil)
|
|
|
|
tests := []struct {
|
|
name string
|
|
lang string
|
|
expectPrefix string
|
|
}{
|
|
{
|
|
name: "English download filename",
|
|
lang: "en",
|
|
expectPrefix: "cv-jamr-",
|
|
},
|
|
{
|
|
name: "Spanish download filename",
|
|
lang: "es",
|
|
expectPrefix: "cv-jamr-",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/text?lang="+tt.lang+"&download=true", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.PlainText(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("Expected status OK, got %d", w.Code)
|
|
}
|
|
|
|
disposition := w.Header().Get("Content-Disposition")
|
|
if !strings.Contains(disposition, tt.expectPrefix) {
|
|
t.Errorf("Expected filename to contain '%s', got '%s'", tt.expectPrefix, disposition)
|
|
}
|
|
|
|
// Verify language suffix is in filename
|
|
if !strings.Contains(disposition, "-"+tt.lang+".txt") {
|
|
t.Errorf("Expected filename to end with '-%s.txt', got '%s'", tt.lang, disposition)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIsTextBrowser tests the text browser detection
|
|
func TestIsTextBrowser(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
userAgent string
|
|
accept string
|
|
expect bool
|
|
}{
|
|
{
|
|
name: "Regular browser",
|
|
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
expect: false,
|
|
},
|
|
{
|
|
name: "curl",
|
|
userAgent: "curl/7.79.1",
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "wget",
|
|
userAgent: "Wget/1.21",
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "httpie",
|
|
userAgent: "HTTPie/2.6.0",
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "lynx",
|
|
userAgent: "Lynx/2.9.0dev.10",
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "w3m",
|
|
userAgent: "w3m/0.5.3",
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "Accept text/plain",
|
|
userAgent: "Mozilla/5.0",
|
|
accept: "text/plain",
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "Accept text/html",
|
|
userAgent: "Mozilla/5.0",
|
|
accept: "text/html",
|
|
expect: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
if tt.userAgent != "" {
|
|
req.Header.Set("User-Agent", tt.userAgent)
|
|
}
|
|
if tt.accept != "" {
|
|
req.Header.Set("Accept", tt.accept)
|
|
}
|
|
|
|
result := isTextBrowser(req)
|
|
if result != tt.expect {
|
|
t.Errorf("isTextBrowser() = %v, expected %v for User-Agent: %s", result, tt.expect, tt.userAgent)
|
|
}
|
|
})
|
|
}
|
|
}
|