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

464 lines
14 KiB
Go

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