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