c89b67a06d
- Merge lang package into constants (add IsValidLang, ValidateLang, AllLangs) - Rename internal/services to internal/email for consistency with pdf package - Rename types to avoid redundancy: EmailService→Service, EmailConfig→Config - Update all imports and references across codebase - Delete internal/lang directory (functions moved to constants)
189 lines
5.8 KiB
Go
189 lines
5.8 KiB
Go
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)
|
|
}
|
|
}
|