Files
cv-site/internal/handlers/errors_test.go
T
juanatsap 69012bb1ae test: add comprehensive Go test suite with ~75% coverage
New test files:
- config/config_test.go (100% coverage)
- constants/constants_test.go (100% coverage)
- httputil/response_test.go (100% coverage)
- validation/rules_test.go (91.9% coverage)
- middleware/logger_test.go, security_test.go, security_logger_test.go
- handlers/errors_test.go

Updated documentation:
- doc/27-GO-TESTING.md: Complete testing guide
- doc/00-GO-DOCUMENTATION-INDEX.md: Added testing section
- doc/01-ARCHITECTURE.md: Updated package structure
- doc/DECISIONS.md: Added ADR-004 caching decision
- PROJECT-MEMORY.md: Added Go testing section
2025-12-06 17:51:20 +00:00

342 lines
9.5 KiB
Go

package handlers
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
c "github.com/juanatsap/cv-site/internal/constants"
)
func TestAppError_Error(t *testing.T) {
t.Run("With underlying error", func(t *testing.T) {
err := &AppError{
Err: errors.New("underlying error"),
Message: "app message",
}
if err.Error() != "underlying error" {
t.Errorf("Error() = %q, want %q", err.Error(), "underlying error")
}
})
t.Run("Without underlying error", func(t *testing.T) {
err := &AppError{
Message: "app message",
}
if err.Error() != "app message" {
t.Errorf("Error() = %q, want %q", err.Error(), "app message")
}
})
}
func TestNewAppError(t *testing.T) {
underlying := errors.New("underlying")
err := NewAppError(underlying, "message", http.StatusBadRequest, false)
if err.Err != underlying {
t.Error("Err should be set")
}
if err.Message != "message" {
t.Errorf("Message = %q, want %q", err.Message, "message")
}
if err.StatusCode != http.StatusBadRequest {
t.Errorf("StatusCode = %d, want %d", err.StatusCode, http.StatusBadRequest)
}
if err.Internal {
t.Error("Internal should be false")
}
}
func TestHandleError_JSON(t *testing.T) {
appErr := NewAppError(nil, "Bad request", http.StatusBadRequest, false)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(c.HeaderAccept, c.ContentTypeJSON)
rec := httptest.NewRecorder()
HandleError(rec, req, appErr)
if rec.Code != http.StatusBadRequest {
t.Errorf("Status = %d, want %d", rec.Code, http.StatusBadRequest)
}
contentType := rec.Header().Get(c.HeaderContentType)
if contentType != c.ContentTypeJSON {
t.Errorf("Content-Type = %q, want %q", contentType, c.ContentTypeJSON)
}
var response ErrorResponse
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
t.Fatalf("Failed to parse JSON response: %v", err)
}
if response.Code != http.StatusBadRequest {
t.Errorf("Response Code = %d, want %d", response.Code, http.StatusBadRequest)
}
if response.Message != "Bad request" {
t.Errorf("Response Message = %q, want %q", response.Message, "Bad request")
}
}
func TestHandleError_JSON_Internal(t *testing.T) {
appErr := NewAppError(errors.New("secret error"), "Internal error", http.StatusInternalServerError, true)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(c.HeaderAccept, c.ContentTypeJSON)
rec := httptest.NewRecorder()
HandleError(rec, req, appErr)
var response ErrorResponse
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
t.Fatalf("Failed to parse JSON response: %v", err)
}
// Internal errors should not expose message
if response.Message != "" {
t.Errorf("Internal error should not expose message, got %q", response.Message)
}
}
func TestHandleError_HTMX(t *testing.T) {
appErr := NewAppError(nil, "Something went wrong", http.StatusBadRequest, false)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(c.HeaderHXRequest, "true")
rec := httptest.NewRecorder()
HandleError(rec, req, appErr)
if rec.Code != http.StatusBadRequest {
t.Errorf("Status = %d, want %d", rec.Code, http.StatusBadRequest)
}
body := rec.Body.String()
if !strings.Contains(body, "Something went wrong") {
t.Error("HTMX response should contain error message")
}
if !strings.Contains(body, "<div class='error'>") {
t.Error("HTMX response should contain error div")
}
}
func TestHandleError_HTMX_Internal(t *testing.T) {
appErr := NewAppError(errors.New("secret"), "Secret error", http.StatusInternalServerError, true)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set(c.HeaderHXRequest, "true")
rec := httptest.NewRecorder()
HandleError(rec, req, appErr)
body := rec.Body.String()
if strings.Contains(body, "secret") {
t.Error("Internal error should not expose secret")
}
if !strings.Contains(body, "An error occurred") {
t.Error("Internal error should show generic message")
}
}
func TestHandleError_Standard(t *testing.T) {
appErr := NewAppError(nil, "Not found", http.StatusNotFound, false)
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
HandleError(rec, req, appErr)
if rec.Code != http.StatusNotFound {
t.Errorf("Status = %d, want %d", rec.Code, http.StatusNotFound)
}
body := rec.Body.String()
if !strings.Contains(body, "Not found") {
t.Error("Standard response should contain error message")
}
}
func TestHandleError_Standard_Internal(t *testing.T) {
appErr := NewAppError(errors.New("secret"), "Secret", http.StatusInternalServerError, true)
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
HandleError(rec, req, appErr)
body := rec.Body.String()
if strings.Contains(body, "secret") {
t.Error("Internal error should not expose secret")
}
if !strings.Contains(body, "Internal Server Error") {
t.Error("Internal error should show generic message")
}
}
func TestHandleError_NonAppError(t *testing.T) {
// Regular error should be treated as internal error
regularErr := errors.New("some error")
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
HandleError(rec, req, regularErr)
if rec.Code != http.StatusInternalServerError {
t.Errorf("Status = %d, want %d", rec.Code, http.StatusInternalServerError)
}
}
func TestErrorConstructors(t *testing.T) {
t.Run("NotFoundError", func(t *testing.T) {
err := NotFoundError("resource not found")
if err.StatusCode != http.StatusNotFound {
t.Errorf("StatusCode = %d, want %d", err.StatusCode, http.StatusNotFound)
}
if err.Message != "resource not found" {
t.Errorf("Message = %q, want %q", err.Message, "resource not found")
}
if err.Internal {
t.Error("NotFoundError should not be internal")
}
})
t.Run("BadRequestError", func(t *testing.T) {
err := BadRequestError("invalid input")
if err.StatusCode != http.StatusBadRequest {
t.Errorf("StatusCode = %d, want %d", err.StatusCode, http.StatusBadRequest)
}
if err.Internal {
t.Error("BadRequestError should not be internal")
}
})
t.Run("InternalError", func(t *testing.T) {
underlying := errors.New("db error")
err := InternalError(underlying)
if err.StatusCode != http.StatusInternalServerError {
t.Errorf("StatusCode = %d, want %d", err.StatusCode, http.StatusInternalServerError)
}
if !err.Internal {
t.Error("InternalError should be internal")
}
if err.Err != underlying {
t.Error("Err should be set")
}
})
t.Run("TemplateError", func(t *testing.T) {
underlying := errors.New("template error")
err := TemplateError(underlying, "home.html")
if err.StatusCode != http.StatusInternalServerError {
t.Errorf("StatusCode = %d, want %d", err.StatusCode, http.StatusInternalServerError)
}
if !err.Internal {
t.Error("TemplateError should be internal")
}
if !strings.Contains(err.Message, "home.html") {
t.Error("Message should contain template name")
}
})
t.Run("DataLoadError", func(t *testing.T) {
underlying := errors.New("json error")
err := DataLoadError(underlying, "CV")
if err.StatusCode != http.StatusInternalServerError {
t.Errorf("StatusCode = %d, want %d", err.StatusCode, http.StatusInternalServerError)
}
if !err.Internal {
t.Error("DataLoadError should be internal")
}
if !strings.Contains(err.Message, "CV") {
t.Error("Message should contain data type")
}
})
}
func TestDomainError(t *testing.T) {
t.Run("Error with underlying", func(t *testing.T) {
underlying := errors.New("underlying")
err := &DomainError{
Code: ErrCodeInvalidLanguage,
Message: "invalid language",
Err: underlying,
StatusCode: http.StatusBadRequest,
}
errStr := err.Error()
if !strings.Contains(errStr, string(ErrCodeInvalidLanguage)) {
t.Error("Error() should contain code")
}
if !strings.Contains(errStr, "underlying") {
t.Error("Error() should contain underlying error")
}
})
t.Run("Error without underlying", func(t *testing.T) {
err := &DomainError{
Code: ErrCodeInvalidTheme,
Message: "invalid theme",
StatusCode: http.StatusBadRequest,
}
errStr := err.Error()
if !strings.Contains(errStr, string(ErrCodeInvalidTheme)) {
t.Error("Error() should contain code")
}
if !strings.Contains(errStr, "invalid theme") {
t.Error("Error() should contain message")
}
})
t.Run("Unwrap", func(t *testing.T) {
underlying := errors.New("underlying")
err := &DomainError{
Code: ErrCodeDataLoad,
Err: underlying,
}
if err.Unwrap() != underlying {
t.Error("Unwrap() should return underlying error")
}
})
}
func TestNewDomainError(t *testing.T) {
err := NewDomainError(ErrCodePDFGeneration, "PDF failed", http.StatusInternalServerError)
if err.Code != ErrCodePDFGeneration {
t.Errorf("Code = %q, want %q", err.Code, ErrCodePDFGeneration)
}
if err.Message != "PDF failed" {
t.Errorf("Message = %q, want %q", err.Message, "PDF failed")
}
if err.StatusCode != http.StatusInternalServerError {
t.Errorf("StatusCode = %d, want %d", err.StatusCode, http.StatusInternalServerError)
}
}
func TestDomainError_WithError(t *testing.T) {
underlying := errors.New("root cause")
err := NewDomainError(ErrCodeDataLoad, "load failed", http.StatusInternalServerError).
WithError(underlying)
if err.Err != underlying {
t.Error("WithError should set underlying error")
}
}
func TestDomainError_WithField(t *testing.T) {
err := NewDomainError(ErrCodeInvalidLength, "invalid", http.StatusBadRequest).
WithField("cv_length")
if err.Field != "cv_length" {
t.Errorf("Field = %q, want %q", err.Field, "cv_length")
}
}