Migrate from Gin to Echo framework and add comprehensive integration tests

Major changes:
- Migrate all handlers from Gin to Echo framework
- Add new apperrors, echohelpers, and validator packages
- Update middleware for Echo compatibility
- Add ArchivedHandler to task categorization chain (archived tasks go to cancelled_tasks column)
- Add 6 new integration tests:
  - RecurringTaskLifecycle: NextDueDate advancement for weekly/monthly tasks
  - MultiUserSharing: Complex sharing with user removal
  - TaskStateTransitions: All state transitions and kanban column changes
  - DateBoundaryEdgeCases: Threshold boundary testing
  - CascadeOperations: Residence deletion cascade effects
  - MultiUserOperations: Shared residence collaboration
- Add single-purpose repository functions for kanban columns (GetOverdueTasks, GetDueSoonTasks, etc.)
- Fix RemoveUser route param mismatch (userId -> user_id)
- Fix determineExpectedColumn helper to correctly prioritize in_progress over overdue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-16 13:52:08 -06:00
parent c51f1ce34a
commit 6dac34e373
98 changed files with 8209 additions and 4425 deletions

View File

@@ -0,0 +1,97 @@
package apperrors
import (
"fmt"
"net/http"
)
// AppError represents an application error with HTTP status and i18n key
type AppError struct {
Code int // HTTP status code
Message string // Default message (fallback if i18n key not found)
MessageKey string // i18n key for localization
Err error // Wrapped error (for internal errors)
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
if e.Message != "" {
return e.Message
}
return e.MessageKey
}
func (e *AppError) Unwrap() error {
return e.Err
}
// NotFound creates a 404 Not Found error
func NotFound(messageKey string) *AppError {
return &AppError{
Code: http.StatusNotFound,
MessageKey: messageKey,
}
}
// Forbidden creates a 403 Forbidden error
func Forbidden(messageKey string) *AppError {
return &AppError{
Code: http.StatusForbidden,
MessageKey: messageKey,
}
}
// BadRequest creates a 400 Bad Request error
func BadRequest(messageKey string) *AppError {
return &AppError{
Code: http.StatusBadRequest,
MessageKey: messageKey,
}
}
// Unauthorized creates a 401 Unauthorized error
func Unauthorized(messageKey string) *AppError {
return &AppError{
Code: http.StatusUnauthorized,
MessageKey: messageKey,
}
}
// Conflict creates a 409 Conflict error
func Conflict(messageKey string) *AppError {
return &AppError{
Code: http.StatusConflict,
MessageKey: messageKey,
}
}
// TooManyRequests creates a 429 Too Many Requests error
func TooManyRequests(messageKey string) *AppError {
return &AppError{
Code: http.StatusTooManyRequests,
MessageKey: messageKey,
}
}
// Internal creates a 500 Internal Server Error, wrapping the original error
func Internal(err error) *AppError {
return &AppError{
Code: http.StatusInternalServerError,
MessageKey: "error.internal",
Err: err,
}
}
// WithMessage adds a default message to the error (used when i18n key not found)
func (e *AppError) WithMessage(msg string) *AppError {
e.Message = msg
return e
}
// Wrap wraps an underlying error
func (e *AppError) Wrap(err error) *AppError {
e.Err = err
return e
}

View File

@@ -0,0 +1,66 @@
package apperrors
import (
"errors"
"fmt"
"net/http"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
"github.com/treytartt/casera-api/internal/dto/responses"
"github.com/treytartt/casera-api/internal/i18n"
customvalidator "github.com/treytartt/casera-api/internal/validator"
)
// HTTPErrorHandler handles all errors returned from handlers in a consistent way.
// It converts AppErrors, validation errors, and Echo HTTPErrors to JSON responses.
// This is the base handler - additional service-level error handling can be added in router.go.
func HTTPErrorHandler(err error, c echo.Context) {
// Already committed? Skip
if c.Response().Committed {
return
}
// Handle AppError (our custom application errors)
var appErr *AppError
if errors.As(err, &appErr) {
message := i18n.LocalizedMessage(c, appErr.MessageKey)
// If i18n key not found (returns the key itself), use fallback message
if message == appErr.MessageKey && appErr.Message != "" {
message = appErr.Message
} else if message == appErr.MessageKey {
message = appErr.MessageKey // Use the key as last resort
}
// Log internal errors
if appErr.Err != nil {
log.Error().Err(appErr.Err).Str("message_key", appErr.MessageKey).Msg("Application error")
}
c.JSON(appErr.Code, responses.ErrorResponse{Error: message})
return
}
// Handle validation errors from go-playground/validator
var validationErrs validator.ValidationErrors
if errors.As(err, &validationErrs) {
c.JSON(http.StatusBadRequest, customvalidator.FormatValidationErrors(err))
return
}
// Handle Echo's built-in HTTPError
var httpErr *echo.HTTPError
if errors.As(err, &httpErr) {
msg := fmt.Sprintf("%v", httpErr.Message)
c.JSON(httpErr.Code, responses.ErrorResponse{Error: msg})
return
}
// Default: Internal server error (don't expose error details to client)
log.Error().Err(err).Msg("Unhandled error")
c.JSON(http.StatusInternalServerError, responses.ErrorResponse{
Error: i18n.LocalizedMessage(c, "error.internal"),
})
}