package handlers import ( "encoding/json" "net/http" "net/http/httptest" "testing" apperrors "veza-backend-api/internal/errors" responsePkg "veza-backend-api/internal/response" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestErrorContract vérifie que les endpoints critiques retournent des erreurs au format standardisé // Format attendu: {"success": false, "error": {"code": int, "message": string, "timestamp": string, ...}} func TestErrorContract(t *testing.T) { gin.SetMode(gin.TestMode) tests := []struct { name string endpoint string method string handler gin.HandlerFunc expectedStatus int validateError func(t *testing.T, body []byte) }{ { name: "BitrateHandler - Invalid track ID", endpoint: "/api/v1/tracks/invalid-id/bitrate/adapt", method: "POST", handler: func(c *gin.Context) { // Simuler erreur validation track ID RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid track id")) }, expectedStatus: http.StatusBadRequest, validateError: func(t *testing.T, body []byte) { var resp APIResponse err := json.Unmarshal(body, &resp) require.NoError(t, err) assert.False(t, resp.Success) assert.NotNil(t, resp.Error) // Vérifier structure error errorMap, ok := resp.Error.(map[string]interface{}) require.True(t, ok, "Error should be a map") assert.Contains(t, errorMap, "code") assert.Contains(t, errorMap, "message") assert.Contains(t, errorMap, "timestamp") assert.Equal(t, float64(apperrors.ErrCodeValidation), errorMap["code"]) }, }, { name: "BitrateHandler - Unauthorized", endpoint: "/api/v1/tracks/123/bitrate/adapt", method: "POST", handler: func(c *gin.Context) { RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized")) }, expectedStatus: http.StatusUnauthorized, // ErrCodeUnauthorized (1004) maps to 401 validateError: func(t *testing.T, body []byte) { var resp APIResponse err := json.Unmarshal(body, &resp) require.NoError(t, err) assert.False(t, resp.Success) errorMap, ok := resp.Error.(map[string]interface{}) require.True(t, ok) assert.Equal(t, float64(apperrors.ErrCodeUnauthorized), errorMap["code"]) }, }, { name: "PlaybackAnalyticsHandler - Not Found", endpoint: "/api/v1/playback/analytics/tracks/123", method: "GET", handler: func(c *gin.Context) { RespondWithAppError(c, apperrors.NewNotFoundError("track")) }, expectedStatus: http.StatusNotFound, validateError: func(t *testing.T, body []byte) { var resp APIResponse err := json.Unmarshal(body, &resp) require.NoError(t, err) assert.False(t, resp.Success) errorMap, ok := resp.Error.(map[string]interface{}) require.True(t, ok) assert.Equal(t, float64(apperrors.ErrCodeNotFound), errorMap["code"]) }, }, { name: "Validation Error with Details", endpoint: "/api/v1/test/validation", method: "POST", handler: func(c *gin.Context) { details := []apperrors.ErrorDetail{ {Field: "email", Message: "invalid email format"}, {Field: "password", Message: "password too short"}, } RespondWithAppError(c, apperrors.NewValidationError("Validation failed", details...)) }, expectedStatus: http.StatusBadRequest, validateError: func(t *testing.T, body []byte) { var resp APIResponse err := json.Unmarshal(body, &resp) require.NoError(t, err) assert.False(t, resp.Success) errorMap, ok := resp.Error.(map[string]interface{}) require.True(t, ok) assert.Contains(t, errorMap, "details") details, ok := errorMap["details"].([]interface{}) require.True(t, ok) assert.Len(t, details, 2) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { router := gin.New() router.Handle(tt.method, tt.endpoint, tt.handler) req := httptest.NewRequest(tt.method, tt.endpoint, nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tt.expectedStatus, w.Code, "Status code should match") tt.validateError(t, w.Body.Bytes()) }) } } // TestErrorContractFormat vérifie le format exact des erreurs selon ORIGIN_API_SPECIFICATION func TestErrorContractFormat(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.POST("/test", func(c *gin.Context) { RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "test error message")) }) req := httptest.NewRequest("POST", "/test", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var resp APIResponse err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) // Vérifier structure globale assert.False(t, resp.Success) assert.Nil(t, resp.Data) assert.NotNil(t, resp.Error) // Vérifier structure error détaillée errorMap, ok := resp.Error.(map[string]interface{}) require.True(t, ok) // Champs obligatoires assert.Contains(t, errorMap, "code") assert.Contains(t, errorMap, "message") assert.Contains(t, errorMap, "timestamp") // Types attendus code, ok := errorMap["code"].(float64) require.True(t, ok) assert.Greater(t, code, float64(0)) message, ok := errorMap["message"].(string) require.True(t, ok) assert.NotEmpty(t, message) timestamp, ok := errorMap["timestamp"].(string) require.True(t, ok) assert.NotEmpty(t, timestamp) // Vérifier format RFC3339 (approximatif) assert.Contains(t, timestamp, "T") assert.Contains(t, timestamp, "Z") } // TestErrorContractAuthEndpoints teste les endpoints auth (register/login) avec format standardisé // P0: Vérifie que response.Error() utilise maintenant le format AppError func TestErrorContractAuthEndpoints(t *testing.T) { gin.SetMode(gin.TestMode) tests := []struct { name string endpoint string method string handler gin.HandlerFunc expectedStatus int validateError func(t *testing.T, body []byte) }{ { name: "Auth Register - Validation Error", endpoint: "/api/v1/auth/register", method: "POST", handler: func(c *gin.Context) { // Simuler erreur validation (email manquant) // Utilise response.Error() qui maintenant utilise AppError responsePkg.Error(c, http.StatusBadRequest, "Format d'email invalide") }, expectedStatus: http.StatusBadRequest, validateError: func(t *testing.T, body []byte) { var resp APIResponse err := json.Unmarshal(body, &resp) require.NoError(t, err) assert.False(t, resp.Success) assert.NotNil(t, resp.Error) errorMap, ok := resp.Error.(map[string]interface{}) require.True(t, ok, "Error should be a map") assert.Contains(t, errorMap, "code") assert.Contains(t, errorMap, "message") assert.Contains(t, errorMap, "timestamp") // response.Error() avec 400 mappe vers ErrCodeValidation assert.Equal(t, float64(apperrors.ErrCodeValidation), errorMap["code"]) }, }, { name: "Auth Login - Invalid Credentials", endpoint: "/api/v1/auth/login", method: "POST", handler: func(c *gin.Context) { // Simuler erreur credentials invalides responsePkg.Error(c, http.StatusUnauthorized, "Invalid credentials") }, expectedStatus: http.StatusUnauthorized, validateError: func(t *testing.T, body []byte) { var resp APIResponse err := json.Unmarshal(body, &resp) require.NoError(t, err) assert.False(t, resp.Success) errorMap, ok := resp.Error.(map[string]interface{}) require.True(t, ok) // response.Error() avec 401 mappe vers ErrCodeUnauthorized (1004) assert.Equal(t, float64(apperrors.ErrCodeUnauthorized), errorMap["code"]) }, }, { name: "Auth Middleware - Missing Authorization Header", endpoint: "/api/v1/protected", method: "GET", handler: func(c *gin.Context) { // Simuler middleware auth qui retourne erreur responsePkg.Unauthorized(c, "Authorization header required") }, expectedStatus: http.StatusUnauthorized, validateError: func(t *testing.T, body []byte) { var resp APIResponse err := json.Unmarshal(body, &resp) require.NoError(t, err) assert.False(t, resp.Success) errorMap, ok := resp.Error.(map[string]interface{}) require.True(t, ok) assert.Equal(t, float64(apperrors.ErrCodeUnauthorized), errorMap["code"]) assert.Equal(t, "Authorization header required", errorMap["message"]) }, }, { name: "Auth Middleware - Invalid Token", endpoint: "/api/v1/protected", method: "GET", handler: func(c *gin.Context) { // Simuler middleware auth avec token invalide responsePkg.Unauthorized(c, "Invalid token") }, expectedStatus: http.StatusUnauthorized, validateError: func(t *testing.T, body []byte) { var resp APIResponse err := json.Unmarshal(body, &resp) require.NoError(t, err) assert.False(t, resp.Success) errorMap, ok := resp.Error.(map[string]interface{}) require.True(t, ok) assert.Equal(t, float64(apperrors.ErrCodeUnauthorized), errorMap["code"]) }, }, { name: "Auth Middleware - Forbidden", endpoint: "/api/v1/admin", method: "GET", handler: func(c *gin.Context) { // Simuler middleware RBAC qui retourne forbidden responsePkg.Forbidden(c, "Insufficient permissions") }, expectedStatus: http.StatusForbidden, validateError: func(t *testing.T, body []byte) { var resp APIResponse err := json.Unmarshal(body, &resp) require.NoError(t, err) assert.False(t, resp.Success) errorMap, ok := resp.Error.(map[string]interface{}) require.True(t, ok) // response.Error() avec 403 mappe vers ErrCodeForbidden assert.Equal(t, float64(apperrors.ErrCodeForbidden), errorMap["code"]) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { router := gin.New() router.Handle(tt.method, tt.endpoint, tt.handler) req := httptest.NewRequest(tt.method, tt.endpoint, nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tt.expectedStatus, w.Code, "Status code should match") tt.validateError(t, w.Body.Bytes()) }) } } // TestErrorContractEndpoints répertorie les endpoints critiques et vérifie leur format d'erreur // Cette fonction peut être étendue pour tester les vrais endpoints avec mocks func TestErrorContractEndpoints(t *testing.T) { // Liste des endpoints critiques à vérifier criticalEndpoints := []struct { name string endpoint string method string }{ {"Bitrate Adaptation", "/api/v1/tracks/:id/bitrate/adapt", "POST"}, {"Playback Analytics", "/api/v1/playback/analytics/tracks/:id", "GET"}, {"Health Check", "/health", "GET"}, {"Readiness Check", "/readyz", "GET"}, {"Auth Register", "/api/v1/auth/register", "POST"}, {"Auth Login", "/api/v1/auth/login", "POST"}, } for _, ep := range criticalEndpoints { t.Run(ep.name, func(t *testing.T) { // Ce test peut être étendu pour tester les vrais endpoints // Pour l'instant, on vérifie juste que la liste est complète assert.NotEmpty(t, ep.endpoint) assert.NotEmpty(t, ep.method) }) } }