220 lines
6.3 KiB
Go
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")
|
|
}
|
|
}
|