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, "
") { 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") } }