fix: security tests with mock email sender and rate limit isolation
- Add EmailSender interface to allow mocking in tests - Add IsInitialized() method to template.Manager for nil-safe checks - Update contact handler to use interface and safe initialization checks - Add mockEmailSender in security tests to avoid SMTP connection attempts - Use unique IPs per test case to avoid rate limiting interference
This commit is contained in:
@@ -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 := `<div class="alert alert-success">
|
||||
<h3>Message Sent!</h3>
|
||||
<p>Thank you for your message. I'll get back to you soon.</p>
|
||||
</div>`
|
||||
|
||||
// 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(`<div class="alert alert-success">
|
||||
<h3>Message Sent!</h3>
|
||||
<p>Thank you for your message. I'll get back to you soon.</p>
|
||||
</div>`))
|
||||
_, _ = w.Write([]byte(fallbackHTML))
|
||||
return
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(w, nil); err != nil {
|
||||
log.Printf("ERROR rendering success template: %v", err)
|
||||
_, _ = w.Write([]byte(`<div class="alert alert-success">
|
||||
<h3>Message Sent!</h3>
|
||||
<p>Thank you for your message. I'll get back to you soon.</p>
|
||||
</div>`))
|
||||
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 := `<div class="alert alert-error"><h3>Error</h3><p>` + message + `</p></div>`
|
||||
|
||||
// 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(`<div class="alert alert-error">
|
||||
<h3>Error</h3>
|
||||
<p>` + message + `</p>
|
||||
</div>`))
|
||||
_, _ = w.Write([]byte(fallbackHTML))
|
||||
return
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
log.Printf("ERROR rendering error template: %v", err)
|
||||
_, _ = w.Write([]byte(`<div class="alert alert-error">
|
||||
<h3>Error</h3>
|
||||
<p>` + message + `</p>
|
||||
</div>`))
|
||||
_, _ = w.Write([]byte(fallbackHTML))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user