ae89d84e07
Complete middleware integration with comprehensive testing: 1. Middleware Integration - Added PreferencesMiddleware to middleware chain in routes - Order: Recovery → Logger → SecurityHeaders → Preferences → Mux - Reads all preference cookies once per request - Stores in context for handlers to access 2. Handler Updates - cv_pages.go: Home handler uses middleware.GetPreferences() - cv_htmx.go: All toggle handlers use middleware preferences - Eliminated manual cookie reading in handlers - Migration logic handled entirely by middleware 3. Comprehensive Middleware Tests - Created preferences_test.go with 10+ test functions - Tests: default values, migrations, cookie setting, context access - Verified: extended→long, true→show, false→hide migrations - All tests passing Benefits: - Performance: Cookies read once per request (not multiple times) - Consistency: All handlers get same preference values - Maintainability: Migration logic centralized in middleware - Testability: Easy to mock preferences via context Testing: - All unit tests pass (handlers + middleware) - Build succeeds - No breaking changes
345 lines
9.2 KiB
Go
345 lines
9.2 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// TestPreferencesMiddleware tests that middleware reads cookies and stores in context
|
|
func TestPreferencesMiddleware(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cookies map[string]string
|
|
expectedPrefs *Preferences
|
|
description string
|
|
}{
|
|
{
|
|
name: "Default preferences (no cookies)",
|
|
cookies: map[string]string{},
|
|
expectedPrefs: &Preferences{
|
|
CVLength: "short",
|
|
CVIcons: "show",
|
|
CVLanguage: "en",
|
|
CVTheme: "default",
|
|
ColorTheme: "light",
|
|
},
|
|
description: "Should use default values when no cookies present",
|
|
},
|
|
{
|
|
name: "All preferences set",
|
|
cookies: map[string]string{
|
|
"cv-length": "long",
|
|
"cv-icons": "hide",
|
|
"cv-language": "es",
|
|
"cv-theme": "clean",
|
|
"color-theme": "dark",
|
|
},
|
|
expectedPrefs: &Preferences{
|
|
CVLength: "long",
|
|
CVIcons: "hide",
|
|
CVLanguage: "es",
|
|
CVTheme: "clean",
|
|
ColorTheme: "dark",
|
|
},
|
|
description: "Should read all preference cookies correctly",
|
|
},
|
|
{
|
|
name: "Migration: extended → long",
|
|
cookies: map[string]string{
|
|
"cv-length": "extended",
|
|
},
|
|
expectedPrefs: &Preferences{
|
|
CVLength: "long", // Migrated from "extended"
|
|
CVIcons: "show",
|
|
CVLanguage: "en",
|
|
CVTheme: "default",
|
|
ColorTheme: "light",
|
|
},
|
|
description: "Should migrate 'extended' to 'long'",
|
|
},
|
|
{
|
|
name: "Migration: true → show",
|
|
cookies: map[string]string{
|
|
"cv-icons": "true",
|
|
},
|
|
expectedPrefs: &Preferences{
|
|
CVLength: "short",
|
|
CVIcons: "show", // Migrated from "true"
|
|
CVLanguage: "en",
|
|
CVTheme: "default",
|
|
ColorTheme: "light",
|
|
},
|
|
description: "Should migrate 'true' to 'show'",
|
|
},
|
|
{
|
|
name: "Migration: false → hide",
|
|
cookies: map[string]string{
|
|
"cv-icons": "false",
|
|
},
|
|
expectedPrefs: &Preferences{
|
|
CVLength: "short",
|
|
CVIcons: "hide", // Migrated from "false"
|
|
CVLanguage: "en",
|
|
CVTheme: "default",
|
|
ColorTheme: "light",
|
|
},
|
|
description: "Should migrate 'false' to 'hide'",
|
|
},
|
|
{
|
|
name: "Partial preferences",
|
|
cookies: map[string]string{
|
|
"cv-length": "long",
|
|
"cv-language": "es",
|
|
},
|
|
expectedPrefs: &Preferences{
|
|
CVLength: "long",
|
|
CVIcons: "show", // Default
|
|
CVLanguage: "es",
|
|
CVTheme: "default", // Default
|
|
ColorTheme: "light", // Default
|
|
},
|
|
description: "Should combine cookies with defaults",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create request with cookies
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
for name, value := range tt.cookies {
|
|
req.AddCookie(&http.Cookie{
|
|
Name: name,
|
|
Value: value,
|
|
})
|
|
}
|
|
|
|
// Create test handler that checks context
|
|
var capturedPrefs *Preferences
|
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
capturedPrefs = GetPreferences(r)
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
// Wrap with middleware
|
|
wrappedHandler := PreferencesMiddleware(testHandler)
|
|
|
|
// Execute request
|
|
w := httptest.NewRecorder()
|
|
wrappedHandler.ServeHTTP(w, req)
|
|
|
|
// Verify preferences
|
|
if capturedPrefs == nil {
|
|
t.Fatal("Preferences not found in context")
|
|
}
|
|
|
|
if capturedPrefs.CVLength != tt.expectedPrefs.CVLength {
|
|
t.Errorf("CVLength: got %q, want %q", capturedPrefs.CVLength, tt.expectedPrefs.CVLength)
|
|
}
|
|
if capturedPrefs.CVIcons != tt.expectedPrefs.CVIcons {
|
|
t.Errorf("CVIcons: got %q, want %q", capturedPrefs.CVIcons, tt.expectedPrefs.CVIcons)
|
|
}
|
|
if capturedPrefs.CVLanguage != tt.expectedPrefs.CVLanguage {
|
|
t.Errorf("CVLanguage: got %q, want %q", capturedPrefs.CVLanguage, tt.expectedPrefs.CVLanguage)
|
|
}
|
|
if capturedPrefs.CVTheme != tt.expectedPrefs.CVTheme {
|
|
t.Errorf("CVTheme: got %q, want %q", capturedPrefs.CVTheme, tt.expectedPrefs.CVTheme)
|
|
}
|
|
if capturedPrefs.ColorTheme != tt.expectedPrefs.ColorTheme {
|
|
t.Errorf("ColorTheme: got %q, want %q", capturedPrefs.ColorTheme, tt.expectedPrefs.ColorTheme)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetPreferencesWithoutMiddleware tests fallback behavior
|
|
func TestGetPreferencesWithoutMiddleware(t *testing.T) {
|
|
// Create request without middleware
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
|
|
// GetPreferences should return defaults
|
|
prefs := GetPreferences(req)
|
|
|
|
if prefs.CVLength != "short" {
|
|
t.Errorf("Expected default CVLength 'short', got %q", prefs.CVLength)
|
|
}
|
|
if prefs.CVIcons != "show" {
|
|
t.Errorf("Expected default CVIcons 'show', got %q", prefs.CVIcons)
|
|
}
|
|
if prefs.CVLanguage != "en" {
|
|
t.Errorf("Expected default CVLanguage 'en', got %q", prefs.CVLanguage)
|
|
}
|
|
if prefs.CVTheme != "default" {
|
|
t.Errorf("Expected default CVTheme 'default', got %q", prefs.CVTheme)
|
|
}
|
|
if prefs.ColorTheme != "light" {
|
|
t.Errorf("Expected default ColorTheme 'light', got %q", prefs.ColorTheme)
|
|
}
|
|
}
|
|
|
|
// TestSetPreferenceCookie tests cookie setting
|
|
func TestSetPreferenceCookie(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cookieName string
|
|
cookieValue string
|
|
}{
|
|
{
|
|
name: "Set cv-length cookie",
|
|
cookieName: "cv-length",
|
|
cookieValue: "long",
|
|
},
|
|
{
|
|
name: "Set cv-icons cookie",
|
|
cookieName: "cv-icons",
|
|
cookieValue: "hide",
|
|
},
|
|
{
|
|
name: "Set cv-language cookie",
|
|
cookieName: "cv-language",
|
|
cookieValue: "es",
|
|
},
|
|
{
|
|
name: "Set cv-theme cookie",
|
|
cookieName: "cv-theme",
|
|
cookieValue: "clean",
|
|
},
|
|
{
|
|
name: "Set color-theme cookie",
|
|
cookieName: "color-theme",
|
|
cookieValue: "dark",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
|
|
SetPreferenceCookie(w, tt.cookieName, tt.cookieValue)
|
|
|
|
// Get cookies from response
|
|
cookies := w.Result().Cookies()
|
|
if len(cookies) != 1 {
|
|
t.Fatalf("Expected 1 cookie, got %d", len(cookies))
|
|
}
|
|
|
|
cookie := cookies[0]
|
|
|
|
// Verify cookie attributes
|
|
if cookie.Name != tt.cookieName {
|
|
t.Errorf("Cookie name: got %q, want %q", cookie.Name, tt.cookieName)
|
|
}
|
|
if cookie.Value != tt.cookieValue {
|
|
t.Errorf("Cookie value: got %q, want %q", cookie.Value, tt.cookieValue)
|
|
}
|
|
if cookie.Path != "/" {
|
|
t.Errorf("Cookie path: got %q, want %q", cookie.Path, "/")
|
|
}
|
|
if cookie.MaxAge != 365*24*60*60 {
|
|
t.Errorf("Cookie MaxAge: got %d, want %d (1 year)", cookie.MaxAge, 365*24*60*60)
|
|
}
|
|
if !cookie.HttpOnly {
|
|
t.Error("Cookie should be HttpOnly")
|
|
}
|
|
if cookie.SameSite != http.SameSiteStrictMode {
|
|
t.Errorf("Cookie SameSite: got %v, want %v", cookie.SameSite, http.SameSiteStrictMode)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetPreferenceCookie tests cookie reading helper
|
|
func TestGetPreferenceCookie(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cookieName string
|
|
cookieValue string
|
|
defaultValue string
|
|
expectedValue string
|
|
}{
|
|
{
|
|
name: "Cookie exists",
|
|
cookieName: "cv-length",
|
|
cookieValue: "long",
|
|
defaultValue: "short",
|
|
expectedValue: "long",
|
|
},
|
|
{
|
|
name: "Cookie doesn't exist",
|
|
cookieName: "cv-length",
|
|
cookieValue: "",
|
|
defaultValue: "short",
|
|
expectedValue: "short",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
|
|
if tt.cookieValue != "" {
|
|
req.AddCookie(&http.Cookie{
|
|
Name: tt.cookieName,
|
|
Value: tt.cookieValue,
|
|
})
|
|
}
|
|
|
|
value := getPreferenceCookie(req, tt.cookieName, tt.defaultValue)
|
|
|
|
if value != tt.expectedValue {
|
|
t.Errorf("Expected %q, got %q", tt.expectedValue, value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMiddlewareChain tests middleware can be chained
|
|
func TestMiddlewareChain(t *testing.T) {
|
|
// Create multiple handlers in chain
|
|
finalHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
prefs := GetPreferences(r)
|
|
w.Header().Set("X-CV-Length", prefs.CVLength)
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
// Wrap with middleware
|
|
wrappedHandler := PreferencesMiddleware(finalHandler)
|
|
|
|
// Create request with cookie
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.AddCookie(&http.Cookie{Name: "cv-length", Value: "long"})
|
|
|
|
w := httptest.NewRecorder()
|
|
wrappedHandler.ServeHTTP(w, req)
|
|
|
|
// Verify preferences were passed through chain
|
|
if w.Header().Get("X-CV-Length") != "long" {
|
|
t.Errorf("Expected X-CV-Length header 'long', got %q", w.Header().Get("X-CV-Length"))
|
|
}
|
|
}
|
|
|
|
// TestMultipleMigrations tests multiple migrations in one request
|
|
func TestMultipleMigrations(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req.AddCookie(&http.Cookie{Name: "cv-length", Value: "extended"})
|
|
req.AddCookie(&http.Cookie{Name: "cv-icons", Value: "true"})
|
|
|
|
var capturedPrefs *Preferences
|
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
capturedPrefs = GetPreferences(r)
|
|
})
|
|
|
|
wrappedHandler := PreferencesMiddleware(testHandler)
|
|
|
|
w := httptest.NewRecorder()
|
|
wrappedHandler.ServeHTTP(w, req)
|
|
|
|
// Both migrations should occur
|
|
if capturedPrefs.CVLength != "long" {
|
|
t.Errorf("CVLength: expected 'long' (migrated from 'extended'), got %q", capturedPrefs.CVLength)
|
|
}
|
|
if capturedPrefs.CVIcons != "show" {
|
|
t.Errorf("CVIcons: expected 'show' (migrated from 'true'), got %q", capturedPrefs.CVIcons)
|
|
}
|
|
}
|