00e28906e6
- Fix golangci-lint errcheck errors by using t.Setenv() instead of os.Setenv() - Add CSS bundle build step to deploy workflow for production - Add graceful fallback to modular CSS if bundle doesn't exist - Remove unused os import from preferences_test.go
444 lines
11 KiB
Go
444 lines
11 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)
|
|
}
|
|
}
|
|
|
|
// TestIsProductionMode tests the production mode detection function
|
|
func TestIsProductionMode(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
envValue string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "Production environment",
|
|
envValue: "production",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Prod shorthand",
|
|
envValue: "prod",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Development environment",
|
|
envValue: "development",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "Empty environment",
|
|
envValue: "",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "Staging environment",
|
|
envValue: "staging",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "Case sensitivity - PRODUCTION",
|
|
envValue: "PRODUCTION",
|
|
expected: false, // Case sensitive
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// t.Setenv automatically restores the original value after the test
|
|
t.Setenv("GO_ENV", tt.envValue)
|
|
|
|
result := isProductionMode()
|
|
|
|
if result != tt.expected {
|
|
t.Errorf("isProductionMode() with GO_ENV=%q: got %v, want %v",
|
|
tt.envValue, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSetPreferenceCookieSecureFlag tests that Secure flag is set correctly based on environment
|
|
func TestSetPreferenceCookieSecureFlag(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
envValue string
|
|
expectedSecure bool
|
|
}{
|
|
{
|
|
name: "Production mode sets Secure=true",
|
|
envValue: "production",
|
|
expectedSecure: true,
|
|
},
|
|
{
|
|
name: "Development mode sets Secure=false",
|
|
envValue: "development",
|
|
expectedSecure: false,
|
|
},
|
|
{
|
|
name: "Empty env sets Secure=false",
|
|
envValue: "",
|
|
expectedSecure: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// t.Setenv automatically restores the original value after the test
|
|
t.Setenv("GO_ENV", tt.envValue)
|
|
|
|
w := httptest.NewRecorder()
|
|
SetPreferenceCookie(w, "test-cookie", "test-value")
|
|
|
|
cookies := w.Result().Cookies()
|
|
if len(cookies) != 1 {
|
|
t.Fatalf("Expected 1 cookie, got %d", len(cookies))
|
|
}
|
|
|
|
if cookies[0].Secure != tt.expectedSecure {
|
|
t.Errorf("Cookie Secure flag with GO_ENV=%q: got %v, want %v",
|
|
tt.envValue, cookies[0].Secure, tt.expectedSecure)
|
|
}
|
|
})
|
|
}
|
|
}
|