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

392 lines
13 KiB
Go

//go:build integration
// +build integration
package middleware
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// setupRateLimitingIntegrationTestRouter crée un router de test avec rate limiting
func setupRateLimitingIntegrationTestRouter(t *testing.T) (*gin.Engine, func()) {
gin.SetMode(gin.TestMode)
// Setup SimpleRateLimiter with low limits for testing
// Note: We use SimpleRateLimiter for integration tests as it doesn't require Redis
simpleLimiter := NewSimpleRateLimiter(5, 1*time.Minute) // 5 requests per minute
// Setup EndpointLimiter (without Redis for tests)
endpointLimiterConfig := &EndpointLimiterConfig{
RedisClient: nil, // No Redis for integration tests
KeyPrefix: "test:endpoint_limit",
}
endpointLimits := &EndpointLimits{
LoginAttempts: 3, // 3 login attempts per window
LoginWindow: 1 * time.Minute,
RegisterAttempts: 2, // 2 register attempts per window
RegisterWindow: 1 * time.Minute,
}
endpointLimiter := NewEndpointLimiter(endpointLimiterConfig, endpointLimits)
// Create router
router := gin.New()
// Apply global rate limiting
router.Use(simpleLimiter.Middleware())
// Mock authentication middleware - set user_id from header
router.Use(func(c *gin.Context) {
userIDStr := c.GetHeader("X-User-ID")
if userIDStr != "" {
uid, err := uuid.Parse(userIDStr)
if err == nil {
c.Set("user_id", uid)
}
}
c.Next()
})
// Setup test endpoints
v1 := router.Group("/api/v1")
{
// Public endpoints with endpoint-specific rate limiting
auth := v1.Group("/auth")
{
auth.POST("/login", endpointLimiter.LoginRateLimit(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "login successful"})
})
auth.POST("/register", endpointLimiter.RegisterRateLimit(), func(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{"message": "registration successful"})
})
}
// Protected endpoints (require authentication)
protected := v1.Group("/protected")
protected.Use(func(c *gin.Context) {
if _, exists := c.Get("user_id"); !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
c.Abort()
return
}
c.Next()
})
{
protected.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "success"})
})
}
}
cleanup := func() {
// No specific cleanup needed
}
return router, cleanup
}
// TestRateLimitingIntegration_GlobalRateLimit teste le rate limiting global
func TestRateLimitingIntegration_GlobalRateLimit(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
router, cleanup := setupRateLimitingIntegrationTestRouter(t)
defer cleanup()
// Make requests within limit (5 requests)
for i := 0; i < 5; i++ {
req := httptest.NewRequest("GET", "/api/v1/protected/test", nil)
req.Header.Set("X-User-ID", uuid.New().String())
req.RemoteAddr = "127.0.0.1:12345"
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, "Request %d should succeed", i+1)
assert.Equal(t, "5", w.Header().Get("X-RateLimit-Limit"))
remaining := w.Header().Get("X-RateLimit-Remaining")
assert.NotEmpty(t, remaining)
}
// 6th request should be rate limited
req := httptest.NewRequest("GET", "/api/v1/protected/test", nil)
req.Header.Set("X-User-ID", uuid.New().String())
req.RemoteAddr = "127.0.0.1:12345"
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusTooManyRequests, w.Code)
assert.Equal(t, "5", w.Header().Get("X-RateLimit-Limit"))
assert.Equal(t, "0", w.Header().Get("X-RateLimit-Remaining"))
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response, "error")
}
// TestRateLimitingIntegration_EndpointSpecificRateLimit teste le rate limiting spécifique par endpoint
func TestRateLimitingIntegration_EndpointSpecificRateLimit(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
router, cleanup := setupRateLimitingIntegrationTestRouter(t)
defer cleanup()
// Make login requests within limit (3 requests)
loginReq := map[string]interface{}{
"email": "test@example.com",
"password": "password123",
}
loginBody, _ := json.Marshal(loginReq)
for i := 0; i < 3; i++ {
req := httptest.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer(loginBody))
req.Header.Set("Content-Type", "application/json")
req.RemoteAddr = "127.0.0.1:12345"
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, "Login request %d should succeed", i+1)
}
// 4th login request should be rate limited
req := httptest.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer(loginBody))
req.Header.Set("Content-Type", "application/json")
req.RemoteAddr = "127.0.0.1:12345"
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusTooManyRequests, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response, "error")
assert.Contains(t, response["error"].(string), "Too many login attempts")
}
// TestRateLimitingIntegration_RegisterRateLimit teste le rate limiting pour l'inscription
func TestRateLimitingIntegration_RegisterRateLimit(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
router, cleanup := setupRateLimitingIntegrationTestRouter(t)
defer cleanup()
// Make register requests within limit (2 requests)
registerReq := map[string]interface{}{
"email": "test@example.com",
"username": "testuser",
"password": "SecurePassword123!",
"password_confirm": "SecurePassword123!",
}
registerBody, _ := json.Marshal(registerReq)
for i := 0; i < 2; i++ {
req := httptest.NewRequest("POST", "/api/v1/auth/register", bytes.NewBuffer(registerBody))
req.Header.Set("Content-Type", "application/json")
req.RemoteAddr = "127.0.0.1:12345"
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code, "Register request %d should succeed", i+1)
}
// 3rd register request should be rate limited
req := httptest.NewRequest("POST", "/api/v1/auth/register", bytes.NewBuffer(registerBody))
req.Header.Set("Content-Type", "application/json")
req.RemoteAddr = "127.0.0.1:12345"
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusTooManyRequests, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response, "error")
assert.Contains(t, response["error"].(string), "Too many registration attempts")
}
// TestRateLimitingIntegration_DifferentIPs teste que différentes IPs ont des limites séparées
func TestRateLimitingIntegration_DifferentIPs(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
router, cleanup := setupRateLimitingIntegrationTestRouter(t)
defer cleanup()
// IP 1: Make 5 requests (limit reached)
for i := 0; i < 5; i++ {
req := httptest.NewRequest("GET", "/api/v1/protected/test", nil)
req.Header.Set("X-User-ID", uuid.New().String())
req.RemoteAddr = "127.0.0.1:12345"
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
// IP 1: 6th request should be rate limited
req1 := httptest.NewRequest("GET", "/api/v1/protected/test", nil)
req1.Header.Set("X-User-ID", uuid.New().String())
req1.RemoteAddr = "127.0.0.1:12345"
w1 := httptest.NewRecorder()
router.ServeHTTP(w1, req1)
assert.Equal(t, http.StatusTooManyRequests, w1.Code)
// IP 2: Should be able to make 5 requests (different IP)
for i := 0; i < 5; i++ {
req := httptest.NewRequest("GET", "/api/v1/protected/test", nil)
req.Header.Set("X-User-ID", uuid.New().String())
req.RemoteAddr = "192.168.1.1:12345"
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, "IP 2 request %d should succeed", i+1)
}
}
// TestRateLimitingIntegration_RateLimitHeaders teste que les headers de rate limiting sont présents
func TestRateLimitingIntegration_RateLimitHeaders(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
router, cleanup := setupRateLimitingIntegrationTestRouter(t)
defer cleanup()
req := httptest.NewRequest("GET", "/api/v1/protected/test", nil)
req.Header.Set("X-User-ID", uuid.New().String())
req.RemoteAddr = "127.0.0.1:12345"
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Check rate limit headers
assert.Equal(t, "5", w.Header().Get("X-RateLimit-Limit"))
assert.NotEmpty(t, w.Header().Get("X-RateLimit-Remaining"))
assert.NotEmpty(t, w.Header().Get("X-RateLimit-Reset"))
}
// TestRateLimitingIntegration_EndpointSpecificHeaders teste les headers spécifiques par endpoint
func TestRateLimitingIntegration_EndpointSpecificHeaders(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
router, cleanup := setupRateLimitingIntegrationTestRouter(t)
defer cleanup()
loginReq := map[string]interface{}{
"email": "test@example.com",
"password": "password123",
}
loginBody, _ := json.Marshal(loginReq)
req := httptest.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer(loginBody))
req.Header.Set("Content-Type", "application/json")
req.RemoteAddr = "127.0.0.1:12345"
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Check endpoint-specific rate limit headers
assert.Equal(t, "3", w.Header().Get("X-LoginLimit-Limit"))
assert.NotEmpty(t, w.Header().Get("X-LoginLimit-Remaining"))
assert.NotEmpty(t, w.Header().Get("X-LoginLimit-Reset"))
}
// TestRateLimitingIntegration_UnauthenticatedRateLimit teste le rate limiting pour les utilisateurs non authentifiés
func TestRateLimitingIntegration_UnauthenticatedRateLimit(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
router, cleanup := setupRateLimitingIntegrationTestRouter(t)
defer cleanup()
// Make requests without authentication (should still be rate limited by IP)
for i := 0; i < 5; i++ {
req := httptest.NewRequest("GET", "/api/v1/protected/test", nil)
// No X-User-ID header
req.RemoteAddr = "127.0.0.1:12345"
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Should return 401 (unauthorized) but not 429 (rate limited) yet
assert.Equal(t, http.StatusUnauthorized, w.Code, "Request %d should be unauthorized", i+1)
}
// 6th request should still be rate limited even if unauthorized
req := httptest.NewRequest("GET", "/api/v1/protected/test", nil)
req.RemoteAddr = "127.0.0.1:12345"
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// The rate limiter runs before auth check, so it should return 429
assert.Equal(t, http.StatusTooManyRequests, w.Code)
}
// TestRateLimitingIntegration_MultipleEndpoints teste le rate limiting sur plusieurs endpoints
func TestRateLimitingIntegration_MultipleEndpoints(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
router, cleanup := setupRateLimitingIntegrationTestRouter(t)
defer cleanup()
// Exhaust login rate limit
loginReq := map[string]interface{}{
"email": "test@example.com",
"password": "password123",
}
loginBody, _ := json.Marshal(loginReq)
for i := 0; i < 3; i++ {
req := httptest.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer(loginBody))
req.Header.Set("Content-Type", "application/json")
req.RemoteAddr = "127.0.0.1:12345"
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
// Login should now be rate limited
req := httptest.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer(loginBody))
req.Header.Set("Content-Type", "application/json")
req.RemoteAddr = "127.0.0.1:12345"
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusTooManyRequests, w.Code)
// But register should still work (different endpoint, different limit)
registerReq := map[string]interface{}{
"email": "test2@example.com",
"username": "testuser2",
"password": "SecurePassword123!",
"password_confirm": "SecurePassword123!",
}
registerBody, _ := json.Marshal(registerReq)
req2 := httptest.NewRequest("POST", "/api/v1/auth/register", bytes.NewBuffer(registerBody))
req2.Header.Set("Content-Type", "application/json")
req2.RemoteAddr = "127.0.0.1:12345"
w2 := httptest.NewRecorder()
router.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusCreated, w2.Code, "Register should still work as it has separate rate limit")
}