veza/veza-backend-api/internal/middleware/timeout_goroutine_test.go

184 lines
5.6 KiB
Go
Raw Normal View History

2025-12-13 02:34:34 +00:00
//go:build !integration
// +build !integration
package middleware_test
import (
"net/http"
"net/http/httptest"
"runtime"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"veza-backend-api/internal/middleware"
)
// TestTimeoutMiddleware_NoGoroutineLeak vérifie qu'il n'y a pas de fuite de goroutines après des timeouts
func TestTimeoutMiddleware_NoGoroutineLeak(t *testing.T) {
gin.SetMode(gin.TestMode)
// Capturer le nombre initial de goroutines
initialGoroutines := runtime.NumGoroutine()
// Créer un router avec timeout middleware
router := gin.New()
router.Use(middleware.Timeout(10 * time.Millisecond))
// Handler qui dépasse le timeout
router.GET("/slow", func(c *gin.Context) {
// Simuler un travail qui dépasse le timeout
time.Sleep(100 * time.Millisecond)
// Vérifier que le contexte est annulé
select {
case <-c.Request.Context().Done():
// Contexte annulé, ne pas écrire de réponse
return
case <-time.After(10 * time.Millisecond):
// Si on arrive ici, le contexte n'a pas été annulé (ne devrait pas arriver)
c.JSON(http.StatusOK, gin.H{"status": "should not reach here"})
}
})
// Exécuter plusieurs requêtes qui vont timeout
const numRequests = 10
for i := 0; i < numRequests; i++ {
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/slow", nil)
router.ServeHTTP(w, req)
// Vérifier que le timeout est respecté
assert.Equal(t, http.StatusGatewayTimeout, w.Code)
}
// Attendre un peu pour que les goroutines se nettoient
time.Sleep(200 * time.Millisecond)
// Forcer un GC pour nettoyer les ressources
runtime.GC()
time.Sleep(50 * time.Millisecond)
// Vérifier que le nombre de goroutines n'a pas augmenté de manière significative
// On accepte une marge de 2 goroutines pour les goroutines système
finalGoroutines := runtime.NumGoroutine()
goroutineIncrease := finalGoroutines - initialGoroutines
// Le nombre de goroutines ne devrait pas augmenter de plus de 2 (marge pour les goroutines système)
assert.LessOrEqual(t, goroutineIncrease, 2,
"Goroutine leak detected: initial=%d, final=%d, increase=%d",
initialGoroutines, finalGoroutines, goroutineIncrease)
}
// TestTimeoutMiddleware_HandlerRespectsContext vérifie que le handler respecte l'annulation du contexte
func TestTimeoutMiddleware_HandlerRespectsContext(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(middleware.Timeout(10 * time.Millisecond))
handlerExecuted := false
router.GET("/test", func(c *gin.Context) {
handlerExecuted = true
// Attendre que le contexte soit annulé
select {
case <-c.Request.Context().Done():
// Contexte annulé, ne pas écrire de réponse
return
case <-time.After(100 * time.Millisecond):
// Timeout du test lui-même
}
})
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/test", nil)
router.ServeHTTP(w, req)
// Vérifier que le handler a été exécuté
assert.True(t, handlerExecuted, "Handler should have been executed")
// Vérifier que le contexte a été annulé
// Note: Le handler peut ne pas avoir eu le temps de détecter l'annulation
// mais le middleware doit avoir retourné 504
assert.Equal(t, http.StatusGatewayTimeout, w.Code)
// Attendre un peu pour que le handler détecte l'annulation
time.Sleep(50 * time.Millisecond)
// Le contexte devrait être annulé (mais le handler peut ne pas avoir eu le temps de le détecter)
// L'important est que le middleware retourne 504 et que les ressources soient nettoyées
}
// TestTimeoutMiddleware_MultipleConcurrentRequests vérifie qu'il n'y a pas de fuite avec plusieurs requêtes concurrentes
func TestTimeoutMiddleware_MultipleConcurrentRequests(t *testing.T) {
gin.SetMode(gin.TestMode)
initialGoroutines := runtime.NumGoroutine()
router := gin.New()
router.Use(middleware.Timeout(10 * time.Millisecond))
router.GET("/slow", func(c *gin.Context) {
time.Sleep(100 * time.Millisecond)
select {
case <-c.Request.Context().Done():
return
default:
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
})
// Lancer plusieurs requêtes concurrentes
const numConcurrent = 5
done := make(chan bool, numConcurrent)
for i := 0; i < numConcurrent; i++ {
go func() {
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/slow", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusGatewayTimeout, w.Code)
done <- true
}()
}
// Attendre que toutes les requêtes soient terminées
for i := 0; i < numConcurrent; i++ {
<-done
}
// Attendre que les goroutines se nettoient
time.Sleep(200 * time.Millisecond)
runtime.GC()
time.Sleep(50 * time.Millisecond)
finalGoroutines := runtime.NumGoroutine()
goroutineIncrease := finalGoroutines - initialGoroutines
// Le nombre de goroutines ne devrait pas augmenter de plus de 2
assert.LessOrEqual(t, goroutineIncrease, 2,
"Goroutine leak detected with concurrent requests: initial=%d, final=%d, increase=%d",
initialGoroutines, finalGoroutines, goroutineIncrease)
}
// TestTimeoutMiddleware_FastHandler_NoTimeout vérifie qu'un handler rapide ne déclenche pas de timeout
func TestTimeoutMiddleware_FastHandler_NoTimeout(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(middleware.Timeout(100 * time.Millisecond))
router.GET("/fast", func(c *gin.Context) {
time.Sleep(10 * time.Millisecond)
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/fast", nil)
router.ServeHTTP(w, req)
// Vérifier que le handler a réussi (pas de timeout)
assert.Equal(t, http.StatusOK, w.Code)
}