342 lines
9.5 KiB
Go
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")
|
||
|
|
}
|
||
|
|
}
|