package middleware import ( "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "go.uber.org/zap" "gorm.io/gorm" "veza-backend-api/internal/errors" "veza-backend-api/internal/metrics" ) func TestErrorHandler_AppError(t *testing.T) { gin.SetMode(gin.TestMode) logger := zap.NewNop() errorMetrics := metrics.NewErrorMetrics() router := gin.New() router.Use(ErrorHandler(logger, errorMetrics)) router.GET("/test", func(c *gin.Context) { c.Error(errors.NewNotFoundError("User")) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) errorObj := response["error"].(map[string]interface{}) assert.Equal(t, float64(errors.ErrCodeNotFound), errorObj["code"]) assert.Contains(t, errorObj["message"].(string), "not found") } func TestErrorHandler_GORMError(t *testing.T) { gin.SetMode(gin.TestMode) logger := zap.NewNop() router := gin.New() errorMetrics := metrics.NewErrorMetrics() router.Use(ErrorHandler(logger, errorMetrics)) router.GET("/test", func(c *gin.Context) { c.Error(gorm.ErrRecordNotFound) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) errorObj := response["error"].(map[string]interface{}) assert.Equal(t, float64(errors.ErrCodeNotFound), errorObj["code"]) assert.Equal(t, "Resource not found", errorObj["message"]) } func TestErrorHandler_GenericError(t *testing.T) { gin.SetMode(gin.TestMode) logger := zap.NewNop() router := gin.New() errorMetrics := metrics.NewErrorMetrics() router.Use(ErrorHandler(logger, errorMetrics)) router.GET("/test", func(c *gin.Context) { c.Error(assert.AnError) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusInternalServerError, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) errorObj := response["error"].(map[string]interface{}) assert.Equal(t, float64(errors.ErrCodeInternal), errorObj["code"]) assert.Equal(t, "Internal server error", errorObj["message"]) } func TestErrorHandler_ValidationError(t *testing.T) { gin.SetMode(gin.TestMode) logger := zap.NewNop() router := gin.New() errorMetrics := metrics.NewErrorMetrics() router.Use(ErrorHandler(logger, errorMetrics)) router.GET("/test", func(c *gin.Context) { validationErr := errors.NewValidationError("Validation failed", errors.ErrorDetail{Field: "email", Message: "Invalid email format"}, ) c.Error(validationErr) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) errorObj := response["error"].(map[string]interface{}) assert.Equal(t, float64(errors.ErrCodeValidation), errorObj["code"]) assert.Equal(t, "Validation failed", errorObj["message"]) assert.NotNil(t, errorObj["details"]) } func TestErrorHandler_UnauthorizedError(t *testing.T) { gin.SetMode(gin.TestMode) logger := zap.NewNop() router := gin.New() errorMetrics := metrics.NewErrorMetrics() router.Use(ErrorHandler(logger, errorMetrics)) router.GET("/test", func(c *gin.Context) { c.Error(errors.NewUnauthorizedError("Invalid credentials")) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) errorObj := response["error"].(map[string]interface{}) assert.Equal(t, float64(errors.ErrCodeUnauthorized), errorObj["code"]) } func TestMapErrorCodeToHTTPStatus(t *testing.T) { tests := []struct { name string code errors.ErrorCode expected int }{ {"Unauthorized", errors.ErrCodeUnauthorized, http.StatusUnauthorized}, {"Forbidden", errors.ErrCodeForbidden, http.StatusForbidden}, {"Validation", errors.ErrCodeValidation, http.StatusBadRequest}, {"NotFound", errors.ErrCodeNotFound, http.StatusNotFound}, {"AlreadyExists", errors.ErrCodeAlreadyExists, http.StatusConflict}, {"RateLimit", errors.ErrCodeRateLimitExceeded, http.StatusTooManyRequests}, {"Internal", errors.ErrCodeInternal, http.StatusInternalServerError}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := mapErrorCodeToHTTPStatus(tt.code) assert.Equal(t, tt.expected, result) }) } } func TestErrorHandler_NoErrors(t *testing.T) { gin.SetMode(gin.TestMode) logger := zap.NewNop() router := gin.New() errorMetrics := metrics.NewErrorMetrics() router.Use(ErrorHandler(logger, errorMetrics)) router.GET("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true}) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "success") } func TestErrorHandler_MultipleErrors(t *testing.T) { gin.SetMode(gin.TestMode) logger := zap.NewNop() router := gin.New() errorMetrics := metrics.NewErrorMetrics() router.Use(ErrorHandler(logger, errorMetrics)) router.GET("/test", func(c *gin.Context) { c.Error(errors.NewValidationError("First error")) c.Error(errors.NewNotFoundError("Second error")) // Seule la dernière erreur doit être traitée }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) errorObj := response["error"].(map[string]interface{}) assert.Equal(t, float64(errors.ErrCodeNotFound), errorObj["code"]) } func TestErrorHandler_ContextPropagation_RequestID(t *testing.T) { gin.SetMode(gin.TestMode) logger := zap.NewNop() router := gin.New() router.Use(RequestID()) // On est déjà dans le package middleware errorMetrics := metrics.NewErrorMetrics() router.Use(ErrorHandler(logger, errorMetrics)) router.GET("/test", func(c *gin.Context) { c.Error(errors.NewNotFoundError("User")) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) errorObj := response["error"].(map[string]interface{}) assert.NotNil(t, errorObj["context"]) context := errorObj["context"].(map[string]interface{}) assert.NotEmpty(t, context["request_id"]) // Vérifier que le request_id dans la réponse correspond au header assert.Equal(t, w.Header().Get("X-Request-ID"), context["request_id"]) } func TestErrorHandler_ContextPropagation_UserID(t *testing.T) { gin.SetMode(gin.TestMode) logger := zap.NewNop() router := gin.New() router.Use(RequestID()) // On est déjà dans le package middleware errorMetrics := metrics.NewErrorMetrics() router.Use(ErrorHandler(logger, errorMetrics)) router.GET("/test", func(c *gin.Context) { // Simuler un user_id dans le contexte c.Set("user_id", int64(42)) c.Error(errors.NewValidationError("Validation failed")) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) errorObj := response["error"].(map[string]interface{}) assert.NotNil(t, errorObj["context"]) context := errorObj["context"].(map[string]interface{}) assert.NotEmpty(t, context["request_id"]) assert.Equal(t, float64(42), context["user_id"]) } func TestErrorHandler_ContextPropagation_BothIDs(t *testing.T) { gin.SetMode(gin.TestMode) logger := zap.NewNop() router := gin.New() router.Use(RequestID()) // On est déjà dans le package middleware errorMetrics := metrics.NewErrorMetrics() router.Use(ErrorHandler(logger, errorMetrics)) router.GET("/test", func(c *gin.Context) { c.Set("user_id", "user-123") c.Error(errors.NewNotFoundError("Resource")) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) assert.NoError(t, err) errorObj := response["error"].(map[string]interface{}) context := errorObj["context"].(map[string]interface{}) assert.NotEmpty(t, context["request_id"]) assert.Equal(t, "user-123", context["user_id"]) } func TestEnrichErrorWithContext_NoContext(t *testing.T) { gin.SetMode(gin.TestMode) c, _ := gin.CreateTestContext(httptest.NewRecorder()) c.Request = httptest.NewRequest("GET", "/test", nil) appErr := errors.New(errors.ErrCodeValidation, "Test error") enrichErrorWithContext(c, appErr) assert.NotNil(t, appErr.Context) // Sans RequestID middleware, request_id ne sera pas présent assert.NotContains(t, appErr.Context, "request_id") } func TestEnrichErrorWithContext_ExistingContext(t *testing.T) { gin.SetMode(gin.TestMode) c, _ := gin.CreateTestContext(httptest.NewRecorder()) c.Request = httptest.NewRequest("GET", "/test", nil) c.Set("request_id", "existing-request-id") c.Set("user_id", int64(99)) appErr := errors.New(errors.ErrCodeValidation, "Test error") appErr.Context = map[string]interface{}{ "existing_field": "value", } enrichErrorWithContext(c, appErr) assert.Equal(t, "existing-request-id", appErr.Context["request_id"]) assert.Equal(t, int64(99), appErr.Context["user_id"]) assert.Equal(t, "value", appErr.Context["existing_field"]) }