package middleware import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" "veza-backend-api/internal/errors" "veza-backend-api/internal/metrics" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zapcore" "gorm.io/gorm" ) func TestStructuredErrorLogging_AppError_AllFields(t *testing.T) { gin.SetMode(gin.TestMode) // Créer un logger avec un buffer pour capturer les logs buffer := &strings.Builder{} writer := zapcore.AddSync(buffer) encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) core := zapcore.NewCore(encoder, writer, zap.DebugLevel) logger := zap.New(core) errorMetrics := metrics.NewErrorMetrics() router := gin.New() router.Use(RequestID()) router.Use(Tracing()) router.Use(ErrorHandler(logger, errorMetrics, true)) router.GET("/test", func(c *gin.Context) { c.Set("user_id", int64(123)) appErr := errors.New(errors.ErrCodeValidation, "Test validation error") appErr.Details = []errors.ErrorDetail{ {Field: "email", Message: "Invalid email format"}, } c.Error(appErr) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) // Vérifier la réponse JSON var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) // Vérifier les logs structurés logOutput := buffer.String() require.NotEmpty(t, logOutput) // Parser les logs JSON var logEntry map[string]interface{} lines := strings.Split(strings.TrimSpace(logOutput), "\n") found := false for _, line := range lines { if strings.Contains(line, "Application error") { err := json.Unmarshal([]byte(line), &logEntry) require.NoError(t, err) found = true break } } require.True(t, found, "Log entry not found") // Vérifier tous les champs requis dans les logs structurés assert.Equal(t, "Application error", logEntry["msg"]) assert.Equal(t, "error", logEntry["level"]) assert.Contains(t, logEntry, "code") assert.Contains(t, logEntry, "message") assert.Contains(t, logEntry, "http_status") assert.Contains(t, logEntry, "request_id") assert.Contains(t, logEntry, "trace_id") assert.Contains(t, logEntry, "span_id") assert.Contains(t, logEntry, "user_id") assert.Contains(t, logEntry, "details") } func TestStructuredErrorLogging_InternalError_StackTrace(t *testing.T) { gin.SetMode(gin.TestMode) // Créer un logger avec un buffer pour capturer les logs buffer := &strings.Builder{} writer := zapcore.AddSync(buffer) encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) core := zapcore.NewCore(encoder, writer, zap.DebugLevel) logger := zap.New(core) errorMetrics := metrics.NewErrorMetrics() router := gin.New() router.Use(RequestID()) router.Use(Tracing()) router.Use(ErrorHandler(logger, errorMetrics, true)) router.GET("/test", func(c *gin.Context) { c.Set("user_id", int64(456)) // Utiliser une erreur générique (non-AppError) pour déclencher le chemin "erreur générique" c.Error(fmt.Errorf("generic error: something went wrong")) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusInternalServerError, w.Code) // Vérifier les logs structurés logOutput := buffer.String() require.NotEmpty(t, logOutput) // Parser les logs JSON var logEntry map[string]interface{} lines := strings.Split(strings.TrimSpace(logOutput), "\n") found := false for _, line := range lines { if strings.Contains(line, "Internal server error") && strings.Contains(line, "stack_trace") { err := json.Unmarshal([]byte(line), &logEntry) require.NoError(t, err) found = true break } } require.True(t, found, "Log entry with stack_trace not found. Log output: %s", logOutput) // Vérifier tous les champs requis dans les logs structurés assert.Equal(t, "Internal server error", logEntry["msg"]) assert.Equal(t, "error", logEntry["level"]) assert.Contains(t, logEntry, "code") assert.Contains(t, logEntry, "message") assert.Contains(t, logEntry, "http_status") assert.Contains(t, logEntry, "request_id") assert.Contains(t, logEntry, "trace_id") assert.Contains(t, logEntry, "span_id") assert.Contains(t, logEntry, "user_id") assert.Contains(t, logEntry, "stack_trace") // Vérifier que stack_trace contient des données stackTrace, ok := logEntry["stack_trace"].(string) require.True(t, ok, "stack_trace should be a string") assert.NotEmpty(t, stackTrace) assert.Contains(t, stackTrace, "runtime") } func TestStructuredErrorLogging_AppError_MinimalContext(t *testing.T) { gin.SetMode(gin.TestMode) // Créer un logger avec un buffer pour capturer les logs buffer := &strings.Builder{} writer := zapcore.AddSync(buffer) encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) core := zapcore.NewCore(encoder, writer, zap.DebugLevel) logger := zap.New(core) errorMetrics := metrics.NewErrorMetrics() router := gin.New() router.Use(RequestID()) router.Use(Tracing()) router.Use(ErrorHandler(logger, errorMetrics, true)) router.GET("/test", func(c *gin.Context) { // Pas de user_id - test avec contexte minimal appErr := errors.New(errors.ErrCodeNotFound, "Resource not found") c.Error(appErr) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) // Vérifier les logs structurés logOutput := buffer.String() require.NotEmpty(t, logOutput) // Parser les logs JSON var logEntry map[string]interface{} lines := strings.Split(strings.TrimSpace(logOutput), "\n") found := false for _, line := range lines { if strings.Contains(line, "Application error") { err := json.Unmarshal([]byte(line), &logEntry) require.NoError(t, err) found = true break } } require.True(t, found, "Log entry not found") // Vérifier les champs de base (sans user_id) assert.Equal(t, "Application error", logEntry["msg"]) assert.Contains(t, logEntry, "request_id") assert.Contains(t, logEntry, "trace_id") assert.Contains(t, logEntry, "span_id") // user_id ne devrait pas être présent assert.NotContains(t, logEntry, "user_id") } func TestStructuredErrorLogging_GORMError_WithContext(t *testing.T) { gin.SetMode(gin.TestMode) // Créer un logger avec un buffer pour capturer les logs buffer := &strings.Builder{} writer := zapcore.AddSync(buffer) encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) core := zapcore.NewCore(encoder, writer, zap.DebugLevel) logger := zap.New(core) errorMetrics := metrics.NewErrorMetrics() router := gin.New() router.Use(RequestID()) router.Use(Tracing()) router.Use(ErrorHandler(logger, errorMetrics, true)) router.GET("/test", func(c *gin.Context) { c.Set("user_id", "user-789") c.Error(gorm.ErrRecordNotFound) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) // Vérifier les logs structurés logOutput := buffer.String() require.NotEmpty(t, logOutput) // Parser les logs JSON var logEntry map[string]interface{} lines := strings.Split(strings.TrimSpace(logOutput), "\n") found := false for _, line := range lines { if strings.Contains(line, "Record not found") { err := json.Unmarshal([]byte(line), &logEntry) require.NoError(t, err) found = true break } } require.True(t, found, "Log entry not found") // Vérifier les champs dans les logs structurés assert.Equal(t, "Record not found", logEntry["msg"]) assert.Equal(t, "warn", logEntry["level"]) assert.Contains(t, logEntry, "code") assert.Contains(t, logEntry, "message") assert.Contains(t, logEntry, "http_status") assert.Contains(t, logEntry, "request_id") assert.Contains(t, logEntry, "trace_id") assert.Contains(t, logEntry, "span_id") } func TestStructuredErrorLogging_JSONFormat(t *testing.T) { gin.SetMode(gin.TestMode) // Créer un logger avec un buffer pour capturer les logs buffer := &strings.Builder{} writer := zapcore.AddSync(buffer) encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) core := zapcore.NewCore(encoder, writer, zap.DebugLevel) logger := zap.New(core) errorMetrics := metrics.NewErrorMetrics() router := gin.New() router.Use(RequestID()) router.Use(ErrorHandler(logger, errorMetrics, true)) router.GET("/test", func(c *gin.Context) { appErr := errors.New(errors.ErrCodeValidation, "Validation failed") c.Error(appErr) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) // Vérifier que les logs sont au format JSON logOutput := buffer.String() require.NotEmpty(t, logOutput) lines := strings.Split(strings.TrimSpace(logOutput), "\n") for _, line := range lines { if strings.Contains(line, "Application error") { var logEntry map[string]interface{} err := json.Unmarshal([]byte(line), &logEntry) assert.NoError(t, err, "Log should be valid JSON") assert.NotEmpty(t, logEntry, "Log entry should not be empty") } } } func TestStructuredErrorLogging_NoSensitiveData(t *testing.T) { gin.SetMode(gin.TestMode) // Créer un logger avec un buffer pour capturer les logs buffer := &strings.Builder{} writer := zapcore.AddSync(buffer) encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) core := zapcore.NewCore(encoder, writer, zap.DebugLevel) logger := zap.New(core) errorMetrics := metrics.NewErrorMetrics() router := gin.New() router.Use(RequestID()) router.Use(ErrorHandler(logger, errorMetrics, true)) router.GET("/test", func(c *gin.Context) { // Simuler une erreur qui pourrait contenir des données sensibles appErr := errors.New(errors.ErrCodeUnauthorized, "Authentication failed") c.Error(appErr) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) // Vérifier que les logs ne contiennent pas de données sensibles logOutput := buffer.String() // Vérifier qu'il n'y a pas de mots-clés sensibles dans les logs sensitiveKeywords := []string{"password", "token", "secret", "key", "credential"} for _, keyword := range sensitiveKeywords { assert.NotContains(t, strings.ToLower(logOutput), keyword, "Logs should not contain sensitive data: %s", keyword) } } // Test helper: vérifier que le format JSON est valide func TestStructuredErrorLogging_ValidJSON(t *testing.T) { gin.SetMode(gin.TestMode) buffer := &strings.Builder{} writer := zapcore.AddSync(buffer) encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) core := zapcore.NewCore(encoder, writer, zap.DebugLevel) logger := zap.New(core) errorMetrics := metrics.NewErrorMetrics() router := gin.New() router.Use(RequestID()) router.Use(Tracing()) router.Use(ErrorHandler(logger, errorMetrics, true)) router.GET("/test", func(c *gin.Context) { c.Set("user_id", int64(999)) appErr := errors.New(errors.ErrCodeInternal, "Internal error") appErr.Err = errors.New(errors.ErrCodeValidation, "wrapped error") c.Error(appErr) }) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/test", nil) router.ServeHTTP(w, req) logOutput := buffer.String() lines := strings.Split(strings.TrimSpace(logOutput), "\n") for _, line := range lines { if strings.Contains(line, "Application error") || strings.Contains(line, "Internal server error") { var logEntry map[string]interface{} err := json.Unmarshal([]byte(line), &logEntry) assert.NoError(t, err, "Each log line should be valid JSON") // Vérifier la structure des champs if code, ok := logEntry["code"].(float64); ok { assert.Greater(t, code, float64(0), "Error code should be positive") } if httpStatus, ok := logEntry["http_status"].(float64); ok { assert.GreaterOrEqual(t, httpStatus, float64(400), "HTTP status should be 4xx or 5xx") assert.LessOrEqual(t, httpStatus, float64(599), "HTTP status should be 4xx or 5xx") } } } }