feat: Implement error handling and response structure for API
- Added standardized error response structure in `errors.go` for consistent error handling across the API. - Implemented specific response functions for various HTTP status codes (400, 401, 403, 404, 500) to enhance error reporting. - Introduced validation error handling to provide detailed feedback on input validation issues. test: Add comprehensive tests for share handler functionality - Created a suite of tests for share handler endpoints, covering scenarios for creating, listing, deleting shares, and managing share links. - Included tests for permission checks, validation errors, and edge cases such as unauthorized access and invalid document IDs. chore: Set up test utilities and database for integration tests - Established a base handler suite for common setup tasks in tests, including database initialization and teardown. - Implemented test data seeding to facilitate consistent testing across different scenarios. migration: Add public sharing support in the database schema - Modified the `documents` table to include `share_token` and `is_public` columns for managing public document sharing. - Added constraints to ensure data integrity, preventing public documents from lacking a share token.
This commit is contained in:
95
backend/internal/handlers/errors.go
Normal file
95
backend/internal/handlers/errors.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// ErrorResponse represents a standardized error response
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// respondWithError sends a standardized error response
|
||||
func respondWithError(c *gin.Context, statusCode int, errorType string, message string) {
|
||||
c.JSON(statusCode, ErrorResponse{
|
||||
Error: errorType,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// respondWithValidationError parses validation errors and sends detailed response
|
||||
func respondWithValidationError(c *gin.Context, err error) {
|
||||
details := make(map[string]interface{})
|
||||
|
||||
// Try to parse validator errors
|
||||
if validationErrors, ok := err.(validator.ValidationErrors); ok {
|
||||
for _, fieldError := range validationErrors {
|
||||
fieldName := fieldError.Field()
|
||||
switch fieldError.Tag() {
|
||||
case "required":
|
||||
details[fieldName] = "This field is required"
|
||||
case "oneof":
|
||||
details[fieldName] = fmt.Sprintf("Must be one of: %s", fieldError.Param())
|
||||
case "email":
|
||||
details[fieldName] = "Must be a valid email address"
|
||||
case "uuid":
|
||||
details[fieldName] = "Must be a valid UUID"
|
||||
default:
|
||||
details[fieldName] = fmt.Sprintf("Validation failed on '%s'", fieldError.Tag())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Generic validation error
|
||||
details["message"] = err.Error()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||
Error: "validation_error",
|
||||
Message: "Invalid input",
|
||||
Details: details,
|
||||
})
|
||||
}
|
||||
|
||||
// respondBadRequest sends a 400 Bad Request error
|
||||
func respondBadRequest(c *gin.Context, message string) {
|
||||
respondWithError(c, http.StatusBadRequest, "bad_request", message)
|
||||
}
|
||||
|
||||
// respondUnauthorized sends a 401 Unauthorized error
|
||||
func respondUnauthorized(c *gin.Context, message string) {
|
||||
respondWithError(c, http.StatusUnauthorized, "unauthorized", message)
|
||||
}
|
||||
|
||||
// respondForbidden sends a 403 Forbidden error
|
||||
func respondForbidden(c *gin.Context, message string) {
|
||||
respondWithError(c, http.StatusForbidden, "forbidden", message)
|
||||
}
|
||||
|
||||
// respondNotFound sends a 404 Not Found error
|
||||
func respondNotFound(c *gin.Context, resource string) {
|
||||
message := fmt.Sprintf("%s not found", resource)
|
||||
if resource == "" {
|
||||
message = "Resource not found"
|
||||
}
|
||||
respondWithError(c, http.StatusNotFound, "not_found", message)
|
||||
}
|
||||
|
||||
// respondInternalError sends a 500 Internal Server Error
|
||||
// In production, you may want to log the actual error but not expose it to clients
|
||||
func respondInternalError(c *gin.Context, message string, err error) {
|
||||
// Log the actual error for debugging (you can replace with proper logging)
|
||||
if err != nil {
|
||||
fmt.Printf("Internal error: %v\n", err)
|
||||
}
|
||||
|
||||
respondWithError(c, http.StatusInternalServerError, "internal_error", message)
|
||||
}
|
||||
func respondInvalidID(c *gin.Context, message string) {
|
||||
respondWithError(c, http.StatusBadRequest, "invalid_id", message)
|
||||
}
|
||||
Reference in New Issue
Block a user