veza/veza-backend-api/internal/middleware/upload_rate_limit_test.go
2025-12-03 20:29:37 +01:00

220 lines
6.3 KiB
Go

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