379 lines
12 KiB
Go
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")
|
|
}
|
|
}
|
|
}
|
|
}
|