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