package middleware import ( "context" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // requireRedis vérifie que Redis est disponible et skip le test sinon // ÉTAPE 1.3: Skip conditionnel pour les tests dépendant de Redis func requireRedis(t *testing.T, client *redis.Client) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if _, err := client.Ping(ctx).Result(); err != nil { t.Skipf("Redis not available (connection refused); skipping rate limit tests: %v", err) } } func setupTestRedis() (*redis.Client, func()) { // Utiliser un client Redis de test (en mémoire via Miniredis ou un conteneur) // Pour simplifier, on va utiliser un client Redis réel ou mock // Dans un vrai test, on utiliserait un conteneur Docker ou Miniredis client := redis.NewClient(&redis.Options{ Addr: "localhost:6379", DB: 15, // Utiliser une DB de test }) // Nettoyer la DB de test (si Redis est disponible) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if err := client.FlushDB(ctx).Err(); err == nil { // Redis est disponible, on peut nettoyer } cleanup := func() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() client.FlushDB(ctx) client.Close() } return client, cleanup } func TestUploadRateLimit_Allowed(t *testing.T) { redisClient, cleanup := setupTestRedis() defer cleanup() requireRedis(t, redisClient) // ÉTAPE 1.3: Skip si Redis indisponible // Mettre en place Gin en mode test gin.SetMode(gin.TestMode) router := gin.New() // Middleware de rate limiting router.Use(func(c *gin.Context) { c.Set("user_id", int64(123)) }) router.Use(UploadRateLimit(redisClient)) // Route de test router.POST("/upload", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "upload successful"}) }) // Première requête - devrait être autorisée req, _ := http.NewRequest("POST", "/upload", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, "10", w.Header().Get("X-RateLimit-Limit")) assert.Equal(t, "9", w.Header().Get("X-RateLimit-Remaining")) } func TestUploadRateLimit_Exceeded(t *testing.T) { redisClient, cleanup := setupTestRedis() defer cleanup() requireRedis(t, redisClient) // ÉTAPE 1.3: Skip si Redis indisponible // Mettre en place Gin en mode test gin.SetMode(gin.TestMode) router := gin.New() // Middleware de rate limiting router.Use(func(c *gin.Context) { c.Set("user_id", int64(123)) }) router.Use(UploadRateLimit(redisClient)) // Route de test router.POST("/upload", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "upload successful"}) }) // Effectuer 11 requêtes (limite est 10) for i := 0; i < 10; i++ { req, _ := http.NewRequest("POST", "/upload", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code, "Request %d should be allowed", i+1) } // La 11ème requête devrait être bloquée req, _ := http.NewRequest("POST", "/upload", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusTooManyRequests, w.Code) assert.Equal(t, "10", w.Header().Get("X-RateLimit-Limit")) assert.Equal(t, "0", w.Header().Get("X-RateLimit-Remaining")) } func TestUploadRateLimit_NoUserID(t *testing.T) { redisClient, cleanup := setupTestRedis() defer cleanup() requireRedis(t, redisClient) // ÉTAPE 1.3: Skip si Redis indisponible // Mettre en place Gin en mode test gin.SetMode(gin.TestMode) router := gin.New() // Pas de user_id dans le contexte router.Use(UploadRateLimit(redisClient)) // Route de test router.POST("/upload", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "upload successful"}) }) // Requête sans user_id - devrait passer sans rate limiting req, _ := http.NewRequest("POST", "/upload", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) // Pas de headers de rate limit car pas d'utilisateur authentifié } func TestUploadRateLimit_RedisError(t *testing.T) { // Créer un client Redis invalide pour simuler une erreur invalidClient := redis.NewClient(&redis.Options{ Addr: "localhost:9999", // Port invalide }) // Mettre en place Gin en mode test gin.SetMode(gin.TestMode) router := gin.New() // Middleware de rate limiting avec Redis invalide router.Use(func(c *gin.Context) { c.Set("user_id", int64(123)) }) router.Use(UploadRateLimit(invalidClient)) // Route de test router.POST("/upload", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "upload successful"}) }) // Requête - devrait passer en cas d'erreur Redis (fail-open) req, _ := http.NewRequest("POST", "/upload", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Devrait être autorisé en cas d'erreur Redis (fail-open) assert.Equal(t, http.StatusOK, w.Code) invalidClient.Close() } func TestUploadRateLimit_Headers(t *testing.T) { redisClient, cleanup := setupTestRedis() defer cleanup() requireRedis(t, redisClient) // ÉTAPE 1.3: Skip si Redis indisponible // Mettre en place Gin en mode test gin.SetMode(gin.TestMode) router := gin.New() // Middleware de rate limiting router.Use(func(c *gin.Context) { c.Set("user_id", int64(123)) }) router.Use(UploadRateLimit(redisClient)) // Route de test router.POST("/upload", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "upload successful"}) }) req, _ := http.NewRequest("POST", "/upload", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Vérifier les headers assert.Equal(t, "10", w.Header().Get("X-RateLimit-Limit")) assert.NotEmpty(t, w.Header().Get("X-RateLimit-Remaining")) assert.NotEmpty(t, w.Header().Get("X-RateLimit-Reset")) // Vérifier que le reset timestamp est dans le futur resetTime, err := time.Parse(time.RFC3339, w.Header().Get("X-RateLimit-Reset")) if err != nil { // Si ce n'est pas un timestamp RFC3339, essayer un timestamp Unix resetTimestamp := w.Header().Get("X-RateLimit-Reset") require.NotEmpty(t, resetTimestamp, "X-RateLimit-Reset header should be present") } if err == nil { assert.True(t, resetTime.After(time.Now()), "Reset time should be in the future") } }