//go:build integration || error_handling // +build integration error_handling package error_handling import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "gorm.io/driver/sqlite" "gorm.io/gorm" apperrors "veza-backend-api/internal/errors" "veza-backend-api/internal/middleware" "veza-backend-api/internal/models" "veza-backend-api/internal/response" ) // setupErrorHandlingTestRouter crée un router de test pour les tests de gestion d'erreurs func setupErrorHandlingTestRouter(t *testing.T) (*gin.Engine, *gorm.DB, func()) { gin.SetMode(gin.TestMode) logger := zaptest.NewLogger(t) // Setup in-memory SQLite database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) db.Exec("PRAGMA foreign_keys = ON") // Auto-migrate models err = db.AutoMigrate( &models.User{}, &models.Track{}, ) require.NoError(t, err) // Create router with error handler router := gin.New() router.Use(middleware.ErrorHandler(logger, nil, false)) // Test endpoints that return different error types router.GET("/test/validation-error", func(c *gin.Context) { c.Error(apperrors.NewValidationError("validation failed", apperrors.ErrorDetail{ Field: "email", Message: "invalid email format", })) c.Abort() }) router.GET("/test/not-found", func(c *gin.Context) { c.Error(apperrors.NewNotFoundError("resource")) c.Abort() }) router.GET("/test/unauthorized", func(c *gin.Context) { c.Error(apperrors.NewUnauthorizedError("unauthorized")) c.Abort() }) router.GET("/test/forbidden", func(c *gin.Context) { c.Error(apperrors.NewForbiddenError("forbidden")) c.Abort() }) router.GET("/test/internal-error", func(c *gin.Context) { c.Error(apperrors.New(apperrors.ErrCodeInternal, "internal server error")) c.Abort() }) router.GET("/test/database-error", func(c *gin.Context) { c.Error(apperrors.New(apperrors.ErrCodeDatabase, "database error")) c.Abort() }) router.GET("/test/conflict", func(c *gin.Context) { c.Error(apperrors.New(apperrors.ErrCodeConflict, "resource conflict")) c.Abort() }) router.GET("/test/rate-limit", func(c *gin.Context) { c.Error(apperrors.New(apperrors.ErrCodeRateLimitExceeded, "rate limit exceeded")) c.Abort() }) router.GET("/test/quota-exceeded", func(c *gin.Context) { c.Error(apperrors.New(apperrors.ErrCodeQuotaExceeded, "quota exceeded")) c.Abort() }) // Endpoint that recovers from error router.GET("/test/recovery", func(c *gin.Context) { // Simulate error if c.Query("error") == "true" { c.Error(apperrors.New(apperrors.ErrCodeInternal, "simulated error")) c.Abort() return } // Success path response.Success(c, gin.H{"message": "recovered"}, "success") }) // Endpoint with error details router.POST("/test/validation-details", func(c *gin.Context) { var req struct { Email string `json:"email"` Password string `json:"password"` } if err := c.ShouldBindJSON(&req); err != nil { details := []apperrors.ErrorDetail{ {Field: "email", Message: "email is required"}, {Field: "password", Message: "password is required"}, } c.Error(apperrors.NewValidationError("validation failed", details...)) c.Abort() return } response.Success(c, gin.H{"email": req.Email}, "valid") }) cleanup := func() { // Cleanup handled by in-memory DB } return router, db, cleanup } // TestErrorResponse_Format teste que les réponses d'erreur sont au format standardisé func TestErrorResponse_Format(t *testing.T) { router, _, cleanup := setupErrorHandlingTestRouter(t) defer cleanup() tests := []struct { name string endpoint string expectedStatus int expectedCode apperrors.ErrorCode validateError func(t *testing.T, body []byte) }{ { name: "Validation Error", endpoint: "/test/validation-error", expectedStatus: http.StatusBadRequest, expectedCode: apperrors.ErrCodeValidation, validateError: func(t *testing.T, body []byte) { var resp map[string]interface{} err := json.Unmarshal(body, &resp) require.NoError(t, err) errorData, ok := resp["error"].(map[string]interface{}) require.True(t, ok, "Error should be a map") assert.Equal(t, float64(apperrors.ErrCodeValidation), errorData["code"]) assert.Contains(t, errorData, "message") assert.Contains(t, errorData, "details") }, }, { name: "Not Found Error", endpoint: "/test/not-found", expectedStatus: http.StatusNotFound, expectedCode: apperrors.ErrCodeNotFound, validateError: func(t *testing.T, body []byte) { var resp map[string]interface{} err := json.Unmarshal(body, &resp) require.NoError(t, err) errorData, ok := resp["error"].(map[string]interface{}) require.True(t, ok) assert.Equal(t, float64(apperrors.ErrCodeNotFound), errorData["code"]) }, }, { name: "Unauthorized Error", endpoint: "/test/unauthorized", expectedStatus: http.StatusUnauthorized, expectedCode: apperrors.ErrCodeUnauthorized, validateError: func(t *testing.T, body []byte) { var resp map[string]interface{} err := json.Unmarshal(body, &resp) require.NoError(t, err) errorData, ok := resp["error"].(map[string]interface{}) require.True(t, ok) assert.Equal(t, float64(apperrors.ErrCodeUnauthorized), errorData["code"]) }, }, { name: "Forbidden Error", endpoint: "/test/forbidden", expectedStatus: http.StatusForbidden, expectedCode: apperrors.ErrCodeForbidden, validateError: func(t *testing.T, body []byte) { var resp map[string]interface{} err := json.Unmarshal(body, &resp) require.NoError(t, err) errorData, ok := resp["error"].(map[string]interface{}) require.True(t, ok) assert.Equal(t, float64(apperrors.ErrCodeForbidden), errorData["code"]) }, }, { name: "Internal Server Error", endpoint: "/test/internal-error", expectedStatus: http.StatusInternalServerError, expectedCode: apperrors.ErrCodeInternal, validateError: func(t *testing.T, body []byte) { var resp map[string]interface{} err := json.Unmarshal(body, &resp) require.NoError(t, err) errorData, ok := resp["error"].(map[string]interface{}) require.True(t, ok) assert.Equal(t, float64(apperrors.ErrCodeInternal), errorData["code"]) }, }, { name: "Database Error", endpoint: "/test/database-error", expectedStatus: http.StatusInternalServerError, expectedCode: apperrors.ErrCodeDatabase, validateError: func(t *testing.T, body []byte) { var resp map[string]interface{} err := json.Unmarshal(body, &resp) require.NoError(t, err) errorData, ok := resp["error"].(map[string]interface{}) require.True(t, ok) assert.Equal(t, float64(apperrors.ErrCodeDatabase), errorData["code"]) }, }, { name: "Conflict Error", endpoint: "/test/conflict", expectedStatus: http.StatusConflict, expectedCode: apperrors.ErrCodeConflict, validateError: func(t *testing.T, body []byte) { var resp map[string]interface{} err := json.Unmarshal(body, &resp) require.NoError(t, err) errorData, ok := resp["error"].(map[string]interface{}) require.True(t, ok) assert.Equal(t, float64(apperrors.ErrCodeConflict), errorData["code"]) }, }, { name: "Rate Limit Error", endpoint: "/test/rate-limit", expectedStatus: http.StatusTooManyRequests, expectedCode: apperrors.ErrCodeRateLimitExceeded, validateError: func(t *testing.T, body []byte) { var resp map[string]interface{} err := json.Unmarshal(body, &resp) require.NoError(t, err) errorData, ok := resp["error"].(map[string]interface{}) require.True(t, ok) assert.Equal(t, float64(apperrors.ErrCodeRateLimitExceeded), errorData["code"]) }, }, { name: "Quota Exceeded Error", endpoint: "/test/quota-exceeded", expectedStatus: http.StatusForbidden, expectedCode: apperrors.ErrCodeQuotaExceeded, validateError: func(t *testing.T, body []byte) { var resp map[string]interface{} err := json.Unmarshal(body, &resp) require.NoError(t, err) errorData, ok := resp["error"].(map[string]interface{}) require.True(t, ok) assert.Equal(t, float64(apperrors.ErrCodeQuotaExceeded), errorData["code"]) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, tt.endpoint, nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tt.expectedStatus, w.Code, fmt.Sprintf("Expected status %d, got %d", tt.expectedStatus, w.Code)) if tt.validateError != nil { tt.validateError(t, w.Body.Bytes()) } }) } } // TestErrorResponse_Details teste que les détails d'erreur sont présents quand nécessaire func TestErrorResponse_Details(t *testing.T) { router, _, cleanup := setupErrorHandlingTestRouter(t) defer cleanup() // Test validation error with details req := httptest.NewRequest(http.MethodGet, "/test/validation-error", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) errorData, ok := resp["error"].(map[string]interface{}) require.True(t, ok) details, ok := errorData["details"].([]interface{}) require.True(t, ok, "Details should be present") assert.NotEmpty(t, details, "Details should not be empty") // Verify detail structure detail, ok := details[0].(map[string]interface{}) require.True(t, ok) assert.Contains(t, detail, "field") assert.Contains(t, detail, "message") } // TestErrorRecovery teste la récupération après erreur func TestErrorRecovery(t *testing.T) { router, _, cleanup := setupErrorHandlingTestRouter(t) defer cleanup() // Test error case req := httptest.NewRequest(http.MethodGet, "/test/recovery?error=true", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusInternalServerError, w.Code) var errorResp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &errorResp) require.NoError(t, err) assert.Contains(t, errorResp, "error") // Test recovery (success case) req2 := httptest.NewRequest(http.MethodGet, "/test/recovery", nil) w2 := httptest.NewRecorder() router.ServeHTTP(w2, req2) assert.Equal(t, http.StatusOK, w2.Code) var successResp map[string]interface{} err = json.Unmarshal(w2.Body.Bytes(), &successResp) require.NoError(t, err) assert.True(t, successResp["success"].(bool)) assert.Contains(t, successResp, "data") } // TestErrorRecovery_ValidationDetails teste la récupération avec détails de validation func TestErrorRecovery_ValidationDetails(t *testing.T) { router, _, cleanup := setupErrorHandlingTestRouter(t) defer cleanup() // Test with invalid input (should return validation error) invalidPayload := map[string]interface{}{} body, _ := json.Marshal(invalidPayload) req := httptest.NewRequest(http.MethodPost, "/test/validation-details", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var errorResp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &errorResp) require.NoError(t, err) errorData, ok := errorResp["error"].(map[string]interface{}) require.True(t, ok) details, ok := errorData["details"].([]interface{}) require.True(t, ok) assert.Len(t, details, 2, "Should have 2 validation errors") // Test with valid input (should succeed) validPayload := map[string]interface{}{ "email": "test@example.com", "password": "password123", } body2, _ := json.Marshal(validPayload) req2 := httptest.NewRequest(http.MethodPost, "/test/validation-details", bytes.NewBuffer(body2)) req2.Header.Set("Content-Type", "application/json") w2 := httptest.NewRecorder() router.ServeHTTP(w2, req2) assert.Equal(t, http.StatusOK, w2.Code) var successResp map[string]interface{} err = json.Unmarshal(w2.Body.Bytes(), &successResp) require.NoError(t, err) assert.True(t, successResp["success"].(bool)) } // TestErrorResponse_Timestamp teste que le timestamp est présent dans les erreurs func TestErrorResponse_Timestamp(t *testing.T) { router, _, cleanup := setupErrorHandlingTestRouter(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/test/validation-error", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) errorData, ok := resp["error"].(map[string]interface{}) require.True(t, ok) // Note: The error handler might not include timestamp in the current format // This test verifies the error structure is valid assert.Contains(t, errorData, "code") assert.Contains(t, errorData, "message") } // TestErrorResponse_Context teste que le contexte est présent dans les erreurs func TestErrorResponse_Context(t *testing.T) { router, _, cleanup := setupErrorHandlingTestRouter(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/test/validation-error", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) errorData, ok := resp["error"].(map[string]interface{}) require.True(t, ok) // Context might be present if set by middleware // This test verifies the error structure is valid assert.Contains(t, errorData, "code") assert.Contains(t, errorData, "message") } // TestErrorResponse_Consistency teste que toutes les erreurs suivent le même format func TestErrorResponse_Consistency(t *testing.T) { router, _, cleanup := setupErrorHandlingTestRouter(t) defer cleanup() endpoints := []string{ "/test/validation-error", "/test/not-found", "/test/unauthorized", "/test/forbidden", "/test/internal-error", } for _, endpoint := range endpoints { t.Run(fmt.Sprintf("Consistency check: %s", endpoint), func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, endpoint, nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) // All errors should have the same structure assert.Contains(t, resp, "error") errorData, ok := resp["error"].(map[string]interface{}) require.True(t, ok, "Error should be a map") // Required fields assert.Contains(t, errorData, "code") assert.Contains(t, errorData, "message") // Code should be a number code, ok := errorData["code"].(float64) assert.True(t, ok, "Code should be a number") assert.Greater(t, code, float64(0), "Code should be positive") }) } } // TestErrorRecovery_RetryLogic teste la logique de retry après erreur func TestErrorRecovery_RetryLogic(t *testing.T) { router, _, cleanup := setupErrorHandlingTestRouter(t) defer cleanup() // Simulate retry after error // First request: error req1 := httptest.NewRequest(http.MethodGet, "/test/recovery?error=true", nil) w1 := httptest.NewRecorder() router.ServeHTTP(w1, req1) assert.Equal(t, http.StatusInternalServerError, w1.Code) // Second request: success (recovery) req2 := httptest.NewRequest(http.MethodGet, "/test/recovery", nil) w2 := httptest.NewRecorder() router.ServeHTTP(w2, req2) assert.Equal(t, http.StatusOK, w2.Code) var successResp map[string]interface{} err := json.Unmarshal(w2.Body.Bytes(), &successResp) require.NoError(t, err) assert.True(t, successResp["success"].(bool)) } // TestErrorHandling_AppErrorWrapper teste que les AppError sont correctement wrappées func TestErrorHandling_AppErrorWrapper(t *testing.T) { router, _, cleanup := setupErrorHandlingTestRouter(t) defer cleanup() req := httptest.NewRequest(http.MethodGet, "/test/validation-error", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) errorData, ok := resp["error"].(map[string]interface{}) require.True(t, ok) // Verify AppError structure assert.Equal(t, float64(apperrors.ErrCodeValidation), errorData["code"]) assert.NotEmpty(t, errorData["message"]) } // TestErrorHandling_HTTPStatusMapping teste que les codes d'erreur sont correctement mappés vers les codes HTTP func TestErrorHandling_HTTPStatusMapping(t *testing.T) { router, _, cleanup := setupErrorHandlingTestRouter(t) defer cleanup() tests := []struct { endpoint string expectedStatus int expectedCode apperrors.ErrorCode }{ {"/test/validation-error", http.StatusBadRequest, apperrors.ErrCodeValidation}, {"/test/not-found", http.StatusNotFound, apperrors.ErrCodeNotFound}, {"/test/unauthorized", http.StatusUnauthorized, apperrors.ErrCodeUnauthorized}, {"/test/forbidden", http.StatusForbidden, apperrors.ErrCodeForbidden}, {"/test/internal-error", http.StatusInternalServerError, apperrors.ErrCodeInternal}, {"/test/database-error", http.StatusInternalServerError, apperrors.ErrCodeDatabase}, {"/test/conflict", http.StatusConflict, apperrors.ErrCodeConflict}, {"/test/rate-limit", http.StatusTooManyRequests, apperrors.ErrCodeRateLimitExceeded}, {"/test/quota-exceeded", http.StatusForbidden, apperrors.ErrCodeQuotaExceeded}, } for _, tt := range tests { t.Run(tt.endpoint, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, tt.endpoint, nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tt.expectedStatus, w.Code, fmt.Sprintf("Expected HTTP status %d for error code %d", tt.expectedStatus, tt.expectedCode)) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) errorData, ok := resp["error"].(map[string]interface{}) require.True(t, ok) assert.Equal(t, float64(tt.expectedCode), errorData["code"]) }) } }