319 lines
8.1 KiB
Go
319 lines
8.1 KiB
Go
|
|
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")
|
||
|
|
}
|
||
|
|
}
|