392 lines
13 KiB
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")
|
|
}
|