diff --git a/internal/handlers/contact.go b/internal/handlers/contact.go
index 89d2317..23dc4ed 100644
--- a/internal/handlers/contact.go
+++ b/internal/handlers/contact.go
@@ -11,14 +11,20 @@ import (
"github.com/juanatsap/cv-site/internal/templates"
)
+// EmailSender is an interface for sending contact form emails
+// This allows for easy mocking in tests
+type EmailSender interface {
+ SendContactForm(data *services.ContactFormData) error
+}
+
// ContactHandler handles contact form submissions
type ContactHandler struct {
templates *templates.Manager
- emailService *services.EmailService
+ emailService EmailSender
}
// NewContactHandler creates a new contact handler
-func NewContactHandler(tmpl *templates.Manager, emailService *services.EmailService) *ContactHandler {
+func NewContactHandler(tmpl *templates.Manager, emailService EmailSender) *ContactHandler {
return &ContactHandler{
templates: tmpl,
emailService: emailService,
@@ -139,23 +145,28 @@ func (h *ContactHandler) renderSuccess(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
+ // Fallback HTML for when templates aren't available (e.g., in tests)
+ fallbackHTML := `
+
Message Sent!
+
Thank you for your message. I'll get back to you soon.
+
`
+
+ // Check if templates are properly initialized
+ if !h.templates.IsInitialized() {
+ _, _ = w.Write([]byte(fallbackHTML))
+ return
+ }
+
tmpl, err := h.templates.Render("contact-success")
if err != nil {
log.Printf("ERROR loading success template: %v", err)
- // Fallback to simple HTML
- _, _ = w.Write([]byte(`
-
Message Sent!
-
Thank you for your message. I'll get back to you soon.
-
`))
+ _, _ = w.Write([]byte(fallbackHTML))
return
}
if err := tmpl.Execute(w, nil); err != nil {
- log.Printf("ERROR rendering success template: %v", err)
- _, _ = w.Write([]byte(`
-
Message Sent!
-
Thank you for your message. I'll get back to you soon.
-
`))
+ log.Printf("ERROR rendering error template: %v", err)
+ _, _ = w.Write([]byte(fallbackHTML))
}
}
@@ -164,6 +175,15 @@ func (h *ContactHandler) renderError(w http.ResponseWriter, r *http.Request, mes
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusBadRequest)
+ // Fallback HTML for when templates aren't available (e.g., in tests)
+ fallbackHTML := ``
+
+ // Check if templates are properly initialized
+ if !h.templates.IsInitialized() {
+ _, _ = w.Write([]byte(fallbackHTML))
+ return
+ }
+
data := map[string]interface{}{
"Message": message,
}
@@ -171,20 +191,13 @@ func (h *ContactHandler) renderError(w http.ResponseWriter, r *http.Request, mes
tmpl, err := h.templates.Render("contact-error")
if err != nil {
log.Printf("ERROR loading error template: %v", err)
- // Fallback to simple HTML
- _, _ = w.Write([]byte(`
-
Error
-
` + message + `
-
`))
+ _, _ = w.Write([]byte(fallbackHTML))
return
}
if err := tmpl.Execute(w, data); err != nil {
log.Printf("ERROR rendering error template: %v", err)
- _, _ = w.Write([]byte(`
-
Error
-
` + message + `
-
`))
+ _, _ = w.Write([]byte(fallbackHTML))
}
}
diff --git a/internal/templates/template.go b/internal/templates/template.go
index e75b6f7..780d6cc 100644
--- a/internal/templates/template.go
+++ b/internal/templates/template.go
@@ -17,6 +17,12 @@ type Manager struct {
mu sync.RWMutex
}
+// IsInitialized returns true if the template manager has been properly initialized
+// with a config. Empty Manager structs (e.g., in tests) will return false.
+func (m *Manager) IsInitialized() bool {
+ return m != nil && m.config != nil
+}
+
// NewManager creates a new template manager
func NewManager(cfg *config.TemplateConfig) (*Manager, error) {
m := &Manager{
diff --git a/tests/security/contact_security_test.go b/tests/security/contact_security_test.go
index ff0f40d..1f32305 100644
--- a/tests/security/contact_security_test.go
+++ b/tests/security/contact_security_test.go
@@ -15,6 +15,18 @@ import (
"github.com/juanatsap/cv-site/internal/templates"
)
+// mockEmailSender is a mock implementation of handlers.EmailSender for testing
+type mockEmailSender struct{}
+
+func (m *mockEmailSender) SendContactForm(data *services.ContactFormData) error {
+ // Validate like the real service would
+ if err := data.Validate(); err != nil {
+ return fmt.Errorf("validation failed: %w", err)
+ }
+ // Always succeed - don't actually send
+ return nil
+}
+
// setupTestServer creates a test server with the contact handler and security middleware
func setupTestServer(t *testing.T) http.Handler {
t.Helper()
@@ -22,18 +34,8 @@ func setupTestServer(t *testing.T) http.Handler {
// Create a minimal template manager for testing
tmpl := &templates.Manager{}
- // Create a mock email service configuration
- emailConfig := &services.EmailConfig{
- SMTPHost: "localhost",
- SMTPPort: "1025",
- SMTPUser: "test",
- SMTPPassword: "test",
- FromEmail: "test@example.com",
- ToEmail: "recipient@example.com",
- }
-
- // Create email service with mock config (won't actually send emails in tests)
- emailService := services.NewEmailService(emailConfig)
+ // Create mock email service that doesn't actually send
+ emailService := &mockEmailSender{}
// Create the contact handler
contactHandler := handlers.NewContactHandler(tmpl, emailService)
@@ -306,36 +308,43 @@ func TestInputValidation_EmailFormat(t *testing.T) {
tests := []struct {
name string
email string
+ ip string // Use unique IPs to avoid rate limiting
wantCode int
}{
{
name: "valid email",
email: "test@example.com",
+ ip: "10.1.1.1",
wantCode: http.StatusOK,
},
{
name: "valid email with subdomain",
email: "user@mail.example.com",
+ ip: "10.1.1.2",
wantCode: http.StatusOK,
},
{
name: "invalid - no @",
email: "notanemail",
+ ip: "10.1.1.3",
wantCode: http.StatusBadRequest,
},
{
name: "invalid - no domain",
email: "test@",
+ ip: "10.1.1.4",
wantCode: http.StatusBadRequest,
},
{
name: "invalid - no TLD",
email: "test@example",
+ ip: "10.1.1.5",
wantCode: http.StatusBadRequest,
},
{
name: "empty email",
email: "",
+ ip: "10.1.1.6",
wantCode: http.StatusBadRequest,
},
}
@@ -356,6 +365,7 @@ func TestInputValidation_EmailFormat(t *testing.T) {
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)")
req.Header.Set("Referer", "http://localhost:8080/")
req.Header.Set("HX-Request", "true")
+ req.Header.Set("X-Forwarded-For", tt.ip) // Use unique IP to avoid rate limiting
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
@@ -374,31 +384,37 @@ func TestInputValidation_MessageLength(t *testing.T) {
tests := []struct {
name string
message string
+ ip string // Use unique IPs to avoid rate limiting
wantCode int
}{
{
name: "valid message - minimum length",
message: "Short msg!",
+ ip: "10.2.1.1",
wantCode: http.StatusOK,
},
{
name: "valid message - normal length",
message: "This is a normal length message that should pass validation.",
+ ip: "10.2.1.2",
wantCode: http.StatusOK,
},
{
name: "valid message - maximum length",
message: strings.Repeat("a", 5000),
+ ip: "10.2.1.3",
wantCode: http.StatusOK,
},
{
name: "invalid - too long",
message: strings.Repeat("a", 5001),
+ ip: "10.2.1.4",
wantCode: http.StatusBadRequest,
},
{
name: "invalid - empty",
message: "",
+ ip: "10.2.1.5",
wantCode: http.StatusBadRequest,
},
}
@@ -419,6 +435,7 @@ func TestInputValidation_MessageLength(t *testing.T) {
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)")
req.Header.Set("Referer", "http://localhost:8080/")
req.Header.Set("HX-Request", "true")
+ req.Header.Set("X-Forwarded-For", tt.ip) // Use unique IP to avoid rate limiting
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
@@ -438,30 +455,35 @@ func TestInputValidation_RequiredFields(t *testing.T) {
name string
email string
message string
+ ip string // Use unique IPs to avoid rate limiting
wantCode int
}{
{
name: "all required fields present",
email: "test@example.com",
message: "Valid message",
+ ip: "10.3.1.1",
wantCode: http.StatusOK,
},
{
name: "missing email",
email: "",
message: "Valid message",
+ ip: "10.3.1.2",
wantCode: http.StatusBadRequest,
},
{
name: "missing message",
email: "test@example.com",
message: "",
+ ip: "10.3.1.3",
wantCode: http.StatusBadRequest,
},
{
name: "both missing",
email: "",
message: "",
+ ip: "10.3.1.4",
wantCode: http.StatusBadRequest,
},
}
@@ -481,6 +503,7 @@ func TestInputValidation_RequiredFields(t *testing.T) {
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)")
req.Header.Set("Referer", "http://localhost:8080/")
req.Header.Set("HX-Request", "true")
+ req.Header.Set("X-Forwarded-For", tt.ip) // Use unique IP to avoid rate limiting
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)