package handlers import ( "fmt" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" c "github.com/juanatsap/cv-site/internal/constants" "github.com/juanatsap/cv-site/internal/email" ) // MockEmailService implements a mock email sender for testing type MockEmailService struct { SendCalled bool LastEmailData *email.ContactFormData ShouldFail bool FailError error } func (m *MockEmailService) SendContactForm(data *email.ContactFormData) error { m.SendCalled = true m.LastEmailData = data if m.ShouldFail { return m.FailError } return nil } // TestHandleContact_ValidSubmission tests successful form submission func TestHandleContact_ValidSubmission(t *testing.T) { if testing.Short() { t.Skip("Skipping contact handler test - requires running from project root") } handler := newTestCVHandler(t, "localhost:8080", nil) // Create form data with valid timing (5 seconds ago) formLoadedAt := time.Now().Add(-5 * time.Second).UnixMilli() formData := url.Values{} formData.Set("email", "test@example.com") formData.Set("name", "Test User") formData.Set("company", "Test Company") formData.Set("subject", "Test Subject") formData.Set("message", "This is a test message with more than 10 characters") formData.Set("website", "") // Honeypot should be empty formData.Set("form_loaded_at", fmt.Sprintf("%d", formLoadedAt)) req := httptest.NewRequest(http.MethodPost, "/api/contact?lang=en", strings.NewReader(formData.Encode())) req.Header.Set(c.HeaderContentType, c.ContentTypeFormURLEnc) w := httptest.NewRecorder() handler.HandleContact(w, req) // Should return OK (email service is nil, so it logs warning and continues) if w.Code != http.StatusOK { t.Errorf("Expected status OK, got %d: %s", w.Code, w.Body.String()) } } // TestHandleContact_MissingFields tests validation for missing required fields func TestHandleContact_MissingFields(t *testing.T) { if testing.Short() { t.Skip("Skipping contact handler test - requires running from project root") } handler := newTestCVHandler(t, "localhost:8080", nil) tests := []struct { name string formData url.Values expectError string }{ { name: "Missing email", formData: url.Values{ "message": []string{"This is a valid message"}, "form_loaded_at": []string{fmt.Sprintf("%d", time.Now().Add(-5*time.Second).UnixMilli())}, }, expectError: "email", }, { name: "Missing message", formData: url.Values{ "email": []string{"test@example.com"}, "form_loaded_at": []string{fmt.Sprintf("%d", time.Now().Add(-5*time.Second).UnixMilli())}, }, expectError: "message", }, { name: "Message too short", formData: url.Values{ "email": []string{"test@example.com"}, "message": []string{"Short"}, "form_loaded_at": []string{fmt.Sprintf("%d", time.Now().Add(-5*time.Second).UnixMilli())}, }, expectError: "short", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/api/contact?lang=en", strings.NewReader(tt.formData.Encode())) req.Header.Set(c.HeaderContentType, c.ContentTypeFormURLEnc) w := httptest.NewRecorder() handler.HandleContact(w, req) // Should return OK (error is in response body, not status) // This is because HTMX handles error display body := w.Body.String() if !strings.Contains(strings.ToLower(body), tt.expectError) { t.Logf("Response body: %s", body) } }) } } // TestHandleContact_HoneypotDetection tests bot detection via honeypot func TestHandleContact_HoneypotDetection(t *testing.T) { if testing.Short() { t.Skip("Skipping contact handler test - requires running from project root") } handler := newTestCVHandler(t, "localhost:8080", nil) formLoadedAt := time.Now().Add(-5 * time.Second).UnixMilli() formData := url.Values{} formData.Set("email", "bot@spam.com") formData.Set("message", "This is spam message from a bot") formData.Set("website", "http://spam-site.com") // Honeypot filled = bot formData.Set("form_loaded_at", fmt.Sprintf("%d", formLoadedAt)) req := httptest.NewRequest(http.MethodPost, "/api/contact?lang=en", strings.NewReader(formData.Encode())) req.Header.Set(c.HeaderContentType, c.ContentTypeFormURLEnc) w := httptest.NewRecorder() handler.HandleContact(w, req) // Bot gets silent success (200 OK) to avoid revealing detection if w.Code != http.StatusOK { t.Errorf("Expected silent success for bot, got %d", w.Code) } } // TestHandleContact_TimingCheck tests bot detection via timing func TestHandleContact_TimingCheck(t *testing.T) { if testing.Short() { t.Skip("Skipping contact handler test - requires running from project root") } handler := newTestCVHandler(t, "localhost:8080", nil) // Form filled too quickly (1 second ago - bots are fast) formLoadedAt := time.Now().Add(-1 * time.Second).UnixMilli() formData := url.Values{} formData.Set("email", "bot@spam.com") formData.Set("message", "This is spam message from a fast bot") formData.Set("form_loaded_at", fmt.Sprintf("%d", formLoadedAt)) req := httptest.NewRequest(http.MethodPost, "/api/contact?lang=en", strings.NewReader(formData.Encode())) req.Header.Set(c.HeaderContentType, c.ContentTypeFormURLEnc) w := httptest.NewRecorder() handler.HandleContact(w, req) // Bot gets silent success (200 OK) to avoid revealing detection if w.Code != http.StatusOK { t.Errorf("Expected silent success for bot, got %d", w.Code) } } // TestHandleContact_MethodNotAllowed tests that GET requests are rejected func TestHandleContact_MethodNotAllowed(t *testing.T) { handler := newTestCVHandler(t, "localhost:8080", nil) req := httptest.NewRequest(http.MethodGet, "/api/contact", nil) w := httptest.NewRecorder() handler.HandleContact(w, req) if w.Code != http.StatusMethodNotAllowed { t.Errorf("Expected MethodNotAllowed, got %d", w.Code) } }