//go:build integration // +build integration package middleware import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "os" "testing" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) // setupTestRedisClient crée un client Redis de test func setupTestRedisClient(t *testing.T) *redis.Client { redisAddr := os.Getenv("REDIS_ADDR") if redisAddr == "" { redisAddr = "localhost:6379" } client := redis.NewClient(&redis.Options{ Addr: redisAddr, DB: 15, // Use DB 15 for tests }) // Test connection ctx := context.Background() _, err := client.Ping(ctx).Result() if err != nil { t.Skipf("Skipping integration test: Redis not available at %s: %v", redisAddr, err) return nil } // Cleanup: Flush DB after tests t.Cleanup(func() { client.FlushDB(ctx) client.Close() }) // Flush DB before tests client.FlushDB(ctx) return client } // setupCSRFIntegrationTestRouter crée un router de test avec protection CSRF func setupCSRFIntegrationTestRouter(t *testing.T) (*gin.Engine, *CSRFMiddleware, *redis.Client, func()) { gin.SetMode(gin.TestMode) logger := zaptest.NewLogger(t) // Setup Redis redisClient := setupTestRedisClient(t) if redisClient == nil { t.Skip("Redis not available") } // Setup CSRF middleware csrfMiddleware := NewCSRFMiddleware(redisClient, logger) // Create router router := gin.New() // 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() }) // Apply CSRF middleware router.Use(csrfMiddleware.Middleware()) // Setup test endpoints v1 := router.Group("/api/v1") { // Public endpoint (no auth required) v1.GET("/public", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "public endpoint"}) }) // Protected endpoints (require auth and CSRF for state-changing methods) protected := v1.Group("/protected") { protected.GET("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "get successful"}) }) protected.POST("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "post successful"}) }) protected.PUT("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "put successful"}) }) protected.DELETE("/test", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "delete successful"}) }) protected.HEAD("/test", func(c *gin.Context) { c.Status(http.StatusOK) }) protected.OPTIONS("/test", func(c *gin.Context) { c.Status(http.StatusOK) }) } // CSRF token endpoint v1.GET("/csrf-token", func(c *gin.Context) { userIDInterface, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } userID := userIDInterface.(uuid.UUID) token, err := csrfMiddleware.GetToken(c.Request.Context(), userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"}) return } c.JSON(http.StatusOK, gin.H{"csrf_token": token}) }) } cleanup := func() { // Cleanup handled by t.Cleanup in setupTestRedisClient } return router, csrfMiddleware, redisClient, cleanup } // TestCSRFProtectionIntegration_GETPassesWithoutToken teste que GET passe sans token CSRF func TestCSRFProtectionIntegration_GETPassesWithoutToken(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, _, _, cleanup := setupCSRFIntegrationTestRouter(t) defer cleanup() userID := uuid.New() req := httptest.NewRequest("GET", "/api/v1/protected/test", nil) req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.Equal(t, "get successful", response["message"]) } // TestCSRFProtectionIntegration_HEADPassesWithoutToken teste que HEAD passe sans token CSRF func TestCSRFProtectionIntegration_HEADPassesWithoutToken(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, _, _, cleanup := setupCSRFIntegrationTestRouter(t) defer cleanup() userID := uuid.New() req := httptest.NewRequest("HEAD", "/api/v1/protected/test", nil) req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // TestCSRFProtectionIntegration_OPTIONSPassesWithoutToken teste que OPTIONS passe sans token CSRF func TestCSRFProtectionIntegration_OPTIONSPassesWithoutToken(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, _, _, cleanup := setupCSRFIntegrationTestRouter(t) defer cleanup() userID := uuid.New() req := httptest.NewRequest("OPTIONS", "/api/v1/protected/test", nil) req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // TestCSRFProtectionIntegration_POSTRequiresToken teste que POST nécessite un token CSRF func TestCSRFProtectionIntegration_POSTRequiresToken(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, _, _, cleanup := setupCSRFIntegrationTestRouter(t) defer cleanup() userID := uuid.New() req := httptest.NewRequest("POST", "/api/v1/protected/test", bytes.NewBuffer([]byte(`{}`))) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) // No X-CSRF-Token header w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.False(t, response["success"].(bool)) errorObj := response["error"].(map[string]interface{}) assert.Equal(t, "CSRF token required", errorObj["message"]) } // TestCSRFProtectionIntegration_PUTRequiresToken teste que PUT nécessite un token CSRF func TestCSRFProtectionIntegration_PUTRequiresToken(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, _, _, cleanup := setupCSRFIntegrationTestRouter(t) defer cleanup() userID := uuid.New() req := httptest.NewRequest("PUT", "/api/v1/protected/test", bytes.NewBuffer([]byte(`{}`))) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) // No X-CSRF-Token header w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) } // TestCSRFProtectionIntegration_DELETERequiresToken teste que DELETE nécessite un token CSRF func TestCSRFProtectionIntegration_DELETERequiresToken(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, _, _, cleanup := setupCSRFIntegrationTestRouter(t) defer cleanup() userID := uuid.New() req := httptest.NewRequest("DELETE", "/api/v1/protected/test", nil) req.Header.Set("X-User-ID", userID.String()) // No X-CSRF-Token header w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) } // TestCSRFProtectionIntegration_InvalidTokenRejected teste qu'un token invalide est rejeté func TestCSRFProtectionIntegration_InvalidTokenRejected(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, _, _, cleanup := setupCSRFIntegrationTestRouter(t) defer cleanup() userID := uuid.New() req := httptest.NewRequest("POST", "/api/v1/protected/test", bytes.NewBuffer([]byte(`{}`))) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) req.Header.Set("X-CSRF-Token", "invalid_token_12345") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.False(t, response["success"].(bool)) errorObj := response["error"].(map[string]interface{}) assert.Contains(t, errorObj["message"].(string), "Invalid") } // TestCSRFProtectionIntegration_ValidTokenPasses teste qu'un token valide permet la requête func TestCSRFProtectionIntegration_ValidTokenPasses(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, csrfMiddleware, _, cleanup := setupCSRFIntegrationTestRouter(t) defer cleanup() userID := uuid.New() // Generate a valid CSRF token ctx := context.Background() token, err := csrfMiddleware.GenerateToken(ctx, userID) require.NoError(t, err) require.NotEmpty(t, token) // Make POST request with valid token req := httptest.NewRequest("POST", "/api/v1/protected/test", bytes.NewBuffer([]byte(`{}`))) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) req.Header.Set("X-CSRF-Token", token) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.Equal(t, "post successful", response["message"]) } // TestCSRFProtectionIntegration_GetTokenEndpoint teste l'endpoint de génération de token func TestCSRFProtectionIntegration_GetTokenEndpoint(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, _, _, cleanup := setupCSRFIntegrationTestRouter(t) defer cleanup() userID := uuid.New() // Request CSRF token req := httptest.NewRequest("GET", "/api/v1/csrf-token", nil) req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.Contains(t, response, "csrf_token") token := response["csrf_token"].(string) assert.NotEmpty(t, token) assert.Len(t, token, 64) // 32 bytes = 64 hex characters } // TestCSRFProtectionIntegration_UnauthenticatedNotBlocked teste que les utilisateurs non authentifiés ne sont pas bloqués func TestCSRFProtectionIntegration_UnauthenticatedNotBlocked(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, _, _, cleanup := setupCSRFIntegrationTestRouter(t) defer cleanup() // Make POST request without authentication (no X-User-ID header) req := httptest.NewRequest("POST", "/api/v1/protected/test", bytes.NewBuffer([]byte(`{}`))) req.Header.Set("Content-Type", "application/json") // No X-User-ID header, so no CSRF check w := httptest.NewRecorder() router.ServeHTTP(w, req) // Should pass CSRF check (no user_id), but fail auth check in handler // Actually, the protected endpoint might require auth, but CSRF middleware won't block it // Let's check what happens - the endpoint handler might return 401 // But CSRF middleware should not block it assert.NotEqual(t, http.StatusForbidden, w.Code, "CSRF should not block unauthenticated requests") } // TestCSRFProtectionIntegration_PublicEndpointNotBlocked teste que les endpoints publics ne sont pas bloqués func TestCSRFProtectionIntegration_PublicEndpointNotBlocked(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, _, _, cleanup := setupCSRFIntegrationTestRouter(t) defer cleanup() // Make POST request to public endpoint req := httptest.NewRequest("POST", "/api/v1/public", bytes.NewBuffer([]byte(`{}`))) req.Header.Set("Content-Type", "application/json") // No X-User-ID header w := httptest.NewRecorder() router.ServeHTTP(w, req) // Public endpoint should work (though it's GET in our setup, let's test GET) req2 := httptest.NewRequest("GET", "/api/v1/public", nil) w2 := httptest.NewRecorder() router.ServeHTTP(w2, req2) assert.Equal(t, http.StatusOK, w2.Code) } // TestCSRFProtectionIntegration_TokenPerUser teste que chaque utilisateur a son propre token func TestCSRFProtectionIntegration_TokenPerUser(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, csrfMiddleware, _, cleanup := setupCSRFIntegrationTestRouter(t) defer cleanup() userID1 := uuid.New() userID2 := uuid.New() // Generate tokens for both users ctx := context.Background() token1, err := csrfMiddleware.GenerateToken(ctx, userID1) require.NoError(t, err) token2, err := csrfMiddleware.GenerateToken(ctx, userID2) require.NoError(t, err) // User 1's token should not work for User 2 req := httptest.NewRequest("POST", "/api/v1/protected/test", bytes.NewBuffer([]byte(`{}`))) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID2.String()) req.Header.Set("X-CSRF-Token", token1) // User 1's token w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code, "User 1's token should not work for User 2") // User 2's token should work for User 2 req2 := httptest.NewRequest("POST", "/api/v1/protected/test", bytes.NewBuffer([]byte(`{}`))) req2.Header.Set("Content-Type", "application/json") req2.Header.Set("X-User-ID", userID2.String()) req2.Header.Set("X-CSRF-Token", token2) // User 2's token w2 := httptest.NewRecorder() router.ServeHTTP(w2, req2) assert.Equal(t, http.StatusOK, w2.Code, "User 2's token should work for User 2") } // TestCSRFProtectionIntegration_MultipleRequestsSameToken teste que le même token peut être utilisé plusieurs fois func TestCSRFProtectionIntegration_MultipleRequestsSameToken(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, csrfMiddleware, _, cleanup := setupCSRFIntegrationTestRouter(t) defer cleanup() userID := uuid.New() // Generate a token ctx := context.Background() token, err := csrfMiddleware.GenerateToken(ctx, userID) require.NoError(t, err) // Make multiple requests with the same token for i := 0; i < 3; i++ { req := httptest.NewRequest("POST", "/api/v1/protected/test", bytes.NewBuffer([]byte(`{}`))) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", userID.String()) req.Header.Set("X-CSRF-Token", token) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code, "Request %d should succeed with same token", i+1) } }