package middleware import ( "context" "net/http" "net/http/httptest" "testing" c "github.com/juanatsap/cv-site/internal/constants" ) func TestLogSecurityEvent(t *testing.T) { // Just verify it doesn't panic req := httptest.NewRequest(http.MethodPost, "/api/contact", nil) req.Header.Set(c.HeaderUserAgent, "TestAgent/1.0") req.RemoteAddr = "192.168.1.1:12345" // Should not panic LogSecurityEvent(EventContactFormSent, req, "test details") LogSecurityEvent(EventBlocked, req, "blocked test") LogSecurityEvent(EventCSRFViolation, req, "csrf test") } func TestGetSeverity(t *testing.T) { tests := []struct { eventType string expected string }{ {EventBlocked, SeverityHigh}, {EventCSRFViolation, SeverityHigh}, {EventOriginViolation, SeverityHigh}, {EventRateLimitExceeded, SeverityMedium}, {EventValidationFailed, SeverityMedium}, {EventSuspiciousUserAgent, SeverityMedium}, {EventContactFormFailed, SeverityMedium}, {EventPDFGenerationFailed, SeverityMedium}, {EventEmailSendFailed, SeverityMedium}, {EventBotDetected, SeverityLow}, {EventContactFormSent, SeverityInfo}, {EventPDFGenerated, SeverityInfo}, {"UNKNOWN_EVENT", SeverityLow}, } for _, tt := range tests { t.Run(tt.eventType, func(t *testing.T) { result := getSeverity(tt.eventType) if result != tt.expected { t.Errorf("getSeverity(%q) = %q, want %q", tt.eventType, result, tt.expected) } }) } } func TestSecurityLogger(t *testing.T) { t.Run("Normal request passes through", func(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("OK")) }) logged := SecurityLogger(handler) req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() logged.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("Status = %d, want %d", rec.Code, http.StatusOK) } }) t.Run("Logs security-relevant paths", func(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) logged := SecurityLogger(handler) // Test security-relevant path req := httptest.NewRequest(http.MethodPost, "/api/contact", nil) req.Header.Set(c.HeaderUserAgent, "Mozilla/5.0") rec := httptest.NewRecorder() logged.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("Status = %d, want %d", rec.Code, http.StatusOK) } }) t.Run("Logs error responses", func(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "Forbidden", http.StatusForbidden) }) logged := SecurityLogger(handler) req := httptest.NewRequest(http.MethodGet, "/secret", nil) rec := httptest.NewRecorder() logged.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Errorf("Status = %d, want %d", rec.Code, http.StatusForbidden) } }) t.Run("Logs rate limit responses", func(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "Too Many Requests", http.StatusTooManyRequests) }) logged := SecurityLogger(handler) req := httptest.NewRequest(http.MethodPost, "/api/contact", nil) rec := httptest.NewRecorder() logged.ServeHTTP(rec, req) if rec.Code != http.StatusTooManyRequests { t.Errorf("Status = %d, want %d", rec.Code, http.StatusTooManyRequests) } }) } func TestIsSecurityRelevantPath(t *testing.T) { tests := []struct { path string expected bool }{ {"/api/contact", true}, {"/api/contact/send", true}, {"/export/pdf", true}, {"/export/pdf/cv", true}, {"/toggle/theme", true}, {"/toggle/length", true}, {"/switch-language", true}, {"/", false}, {"/cv", false}, {"/health", false}, {"/static/css/style.css", false}, {"/api/other", false}, } for _, tt := range tests { t.Run(tt.path, func(t *testing.T) { result := isSecurityRelevantPath(tt.path) if result != tt.expected { t.Errorf("isSecurityRelevantPath(%q) = %v, want %v", tt.path, result, tt.expected) } }) } } // Test preferences helper functions func TestPreferencesHelperFunctions(t *testing.T) { // Create a request with preferences in context 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) } }) t.Run("GetCVLength", func(t *testing.T) { result := GetCVLength(reqWithPrefs) if result != c.CVLengthLong { t.Errorf("GetCVLength() = %q, want %q", result, c.CVLengthLong) } }) t.Run("GetCVIcons", func(t *testing.T) { result := GetCVIcons(reqWithPrefs) if result != c.CVIconsShow { t.Errorf("GetCVIcons() = %q, want %q", result, c.CVIconsShow) } }) t.Run("GetCVTheme", func(t *testing.T) { result := GetCVTheme(reqWithPrefs) if result != c.CVThemeClean { t.Errorf("GetCVTheme() = %q, want %q", result, c.CVThemeClean) } }) t.Run("GetColorTheme", func(t *testing.T) { result := GetColorTheme(reqWithPrefs) if result != c.ColorThemeDark { t.Errorf("GetColorTheme() = %q, want %q", result, c.ColorThemeDark) } }) t.Run("IsLongCV", func(t *testing.T) { if !IsLongCV(reqWithPrefs) { t.Error("IsLongCV() should return true") } }) t.Run("IsShortCV", func(t *testing.T) { if IsShortCV(reqWithPrefs) { t.Error("IsShortCV() should return false for long CV") } }) t.Run("ShowIcons", func(t *testing.T) { if !ShowIcons(reqWithPrefs) { t.Error("ShowIcons() should return true") } }) t.Run("HideIcons", func(t *testing.T) { if HideIcons(reqWithPrefs) { t.Error("HideIcons() should return false when icons shown") } }) t.Run("IsCleanTheme", func(t *testing.T) { if !IsCleanTheme(reqWithPrefs) { t.Error("IsCleanTheme() should return true") } }) t.Run("IsDefaultTheme", func(t *testing.T) { if IsDefaultTheme(reqWithPrefs) { t.Error("IsDefaultTheme() should return false for clean theme") } }) t.Run("IsDarkMode", func(t *testing.T) { if !IsDarkMode(reqWithPrefs) { t.Error("IsDarkMode() should return true") } }) t.Run("IsLightMode", func(t *testing.T) { if IsLightMode(reqWithPrefs) { t.Error("IsLightMode() should return false for dark mode") } }) } func TestPreferencesHelperFunctions_Defaults(t *testing.T) { // Request without preferences should return defaults req := httptest.NewRequest(http.MethodGet, "/", nil) t.Run("GetLanguage default", func(t *testing.T) { result := GetLanguage(req) if result != c.LangEnglish { t.Errorf("GetLanguage() = %q, want %q", result, c.LangEnglish) } }) t.Run("GetCVLength default", func(t *testing.T) { result := GetCVLength(req) if result != c.CVLengthShort { t.Errorf("GetCVLength() = %q, want %q", result, c.CVLengthShort) } }) t.Run("IsShortCV default", func(t *testing.T) { if !IsShortCV(req) { t.Error("IsShortCV() should return true by default") } }) t.Run("ShowIcons default", func(t *testing.T) { if !ShowIcons(req) { t.Error("ShowIcons() should return true by default") } }) t.Run("IsDefaultTheme default", func(t *testing.T) { if !IsDefaultTheme(req) { t.Error("IsDefaultTheme() should return true by default") } }) t.Run("IsLightMode default", func(t *testing.T) { if !IsLightMode(req) { t.Error("IsLightMode() should return true by default") } }) } func TestPreferencesHelperFunctions_HideIcons(t *testing.T) { prefs := &Preferences{ CVLength: c.CVLengthShort, CVIcons: c.CVIconsHide, CVLanguage: c.LangEnglish, CVTheme: c.CVThemeDefault, ColorTheme: c.ColorThemeLight, } req := httptest.NewRequest(http.MethodGet, "/", nil) ctx := context.WithValue(req.Context(), PreferencesKey, prefs) reqWithPrefs := req.WithContext(ctx) if !HideIcons(reqWithPrefs) { t.Error("HideIcons() should return true when icons hidden") } if ShowIcons(reqWithPrefs) { t.Error("ShowIcons() should return false when icons hidden") } }