veza/veza-backend-api/internal/middleware/error_handler_structured_test.go
2025-12-12 21:34:34 -05:00

379 lines
12 KiB
Go

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")
}
}
}
}