//go:build integration // +build integration package middleware import ( "encoding/json" "net/http" "net/http/httptest" "testing" "time" "veza-backend-api/internal/models" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "gorm.io/driver/sqlite" "gorm.io/gorm" ) // setupOwnershipIntegrationTestRouter crée un router de test avec le middleware d'ownership func setupOwnershipIntegrationTestRouter(t *testing.T) (*gin.Engine, *gorm.DB, *AuthMiddleware, func()) { gin.SetMode(gin.TestMode) // Setup in-memory SQLite database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) // Enable foreign keys for SQLite db.Exec("PRAGMA foreign_keys = ON") // Auto-migrate all models err = db.AutoMigrate( &models.User{}, &models.Track{}, &models.Playlist{}, &models.Role{}, &models.Permission{}, &models.UserRole{}, &models.RolePermission{}, ) require.NoError(t, err) // Setup logger logger := zap.NewNop() // Setup PermissionService permissionService := services.NewPermissionService(db) // Setup AuthMiddleware authMiddleware := &AuthMiddleware{ permissionService: permissionService, logger: 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 == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) c.Abort() return } uid, err := uuid.Parse(userIDStr) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid user id"}) c.Abort() return } c.Set("user_id", uid) c.Next() }) cleanup := func() { // Database will be closed automatically } return router, db, authMiddleware, cleanup } // createTestUser crée un utilisateur de test func createTestUserForOwnership(t *testing.T, db *gorm.DB, userID uuid.UUID, username string, isAdmin bool) *models.User { user := &models.User{ ID: userID, Username: username, Slug: username, Email: username + "@example.com", PasswordHash: "hashed_password", IsActive: true, CreatedAt: time.Now(), } err := db.Create(user).Error require.NoError(t, err) if isAdmin { // Create admin role adminRole := &models.Role{ ID: uuid.New(), Name: "admin", IsActive: true, IsSystem: true, CreatedAt: time.Now(), } err = db.FirstOrCreate(adminRole, models.Role{Name: "admin"}).Error require.NoError(t, err) // Assign admin role to user userRole := &models.UserRole{ ID: uuid.New(), UserID: userID, RoleID: adminRole.ID, RoleName: "admin", IsActive: true, AssignedAt: time.Now(), } err = db.Create(userRole).Error require.NoError(t, err) } return user } // createTestTrack crée un track de test func createTestTrackForOwnership(t *testing.T, db *gorm.DB, trackID uuid.UUID, userID uuid.UUID) *models.Track { track := &models.Track{ ID: trackID, UserID: userID, Title: "Test Track", Artist: "Test Artist", FilePath: "/tmp/test.mp3", IsPublic: true, Status: models.TrackStatusCompleted, CreatedAt: time.Now(), } err := db.Create(track).Error require.NoError(t, err) return track } // createTestPlaylist crée une playlist de test func createTestPlaylistForOwnership(t *testing.T, db *gorm.DB, playlistID uuid.UUID, userID uuid.UUID) *models.Playlist { playlist := &models.Playlist{ ID: playlistID, UserID: userID, Title: "Test Playlist", Description: "Test Description", IsPublic: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } err := db.Create(playlist).Error require.NoError(t, err) return playlist } // TestOwnershipMiddleware_TrackOwnerAccess teste que le propriétaire d'un track peut y accéder func TestOwnershipMiddleware_TrackOwnerAccess(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, authMiddleware, cleanup := setupOwnershipIntegrationTestRouter(t) defer cleanup() // Create test user ownerID := uuid.New() createTestUserForOwnership(t, db, ownerID, "owner", false) // Create test track trackID := uuid.New() createTestTrackForOwnership(t, db, trackID, ownerID) // Setup track owner resolver trackOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { trackIDStr := c.Param("id") trackID, err := uuid.Parse(trackIDStr) if err != nil { return uuid.Nil, err } var track models.Track if err := db.First(&track, "id = ?", trackID).Error; err != nil { return uuid.Nil, err } return track.UserID, nil } // Setup route with ownership middleware router.PUT("/tracks/:id", authMiddleware.RequireOwnershipOrAdmin("track", trackOwnerResolver), func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "success"}) }) // Test: Owner tries to access their own track req := httptest.NewRequest("PUT", "/tracks/"+trackID.String(), nil) req.Header.Set("X-User-ID", ownerID.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, "success", response["message"]) } // TestOwnershipMiddleware_TrackNonOwnerAccess teste qu'un non-propriétaire ne peut pas accéder à un track func TestOwnershipMiddleware_TrackNonOwnerAccess(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, authMiddleware, cleanup := setupOwnershipIntegrationTestRouter(t) defer cleanup() // Create two users ownerID := uuid.New() otherUserID := uuid.New() createTestUserForOwnership(t, db, ownerID, "owner", false) createTestUserForOwnership(t, db, otherUserID, "otheruser", false) // Create test track owned by owner trackID := uuid.New() createTestTrackForOwnership(t, db, trackID, ownerID) // Setup track owner resolver trackOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { trackIDStr := c.Param("id") trackID, err := uuid.Parse(trackIDStr) if err != nil { return uuid.Nil, err } var track models.Track if err := db.First(&track, "id = ?", trackID).Error; err != nil { return uuid.Nil, err } return track.UserID, nil } // Setup route with ownership middleware router.PUT("/tracks/:id", authMiddleware.RequireOwnershipOrAdmin("track", trackOwnerResolver), func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "success"}) }) // Test: Other user tries to access owner's track req := httptest.NewRequest("PUT", "/tracks/"+trackID.String(), nil) req.Header.Set("X-User-ID", otherUserID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) } // TestOwnershipMiddleware_TrackAdminAccess teste qu'un admin peut accéder à n'importe quel track func TestOwnershipMiddleware_TrackAdminAccess(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, authMiddleware, cleanup := setupOwnershipIntegrationTestRouter(t) defer cleanup() // Create owner and admin users ownerID := uuid.New() adminID := uuid.New() createTestUserForOwnership(t, db, ownerID, "owner", false) createTestUserForOwnership(t, db, adminID, "admin", true) // Create test track owned by owner trackID := uuid.New() createTestTrackForOwnership(t, db, trackID, ownerID) // Setup track owner resolver trackOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { trackIDStr := c.Param("id") trackID, err := uuid.Parse(trackIDStr) if err != nil { return uuid.Nil, err } var track models.Track if err := db.First(&track, "id = ?", trackID).Error; err != nil { return uuid.Nil, err } return track.UserID, nil } // Setup route with ownership middleware router.PUT("/tracks/:id", authMiddleware.RequireOwnershipOrAdmin("track", trackOwnerResolver), func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "success"}) }) // Test: Admin tries to access owner's track req := httptest.NewRequest("PUT", "/tracks/"+trackID.String(), nil) req.Header.Set("X-User-ID", adminID.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, "success", response["message"]) } // TestOwnershipMiddleware_PlaylistOwnerAccess teste que le propriétaire d'une playlist peut y accéder func TestOwnershipMiddleware_PlaylistOwnerAccess(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, authMiddleware, cleanup := setupOwnershipIntegrationTestRouter(t) defer cleanup() // Create test user ownerID := uuid.New() createTestUserForOwnership(t, db, ownerID, "owner", false) // Create test playlist playlistID := uuid.New() createTestPlaylistForOwnership(t, db, playlistID, ownerID) // Setup playlist owner resolver playlistOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { playlistIDStr := c.Param("id") playlistID, err := uuid.Parse(playlistIDStr) if err != nil { return uuid.Nil, err } var playlist models.Playlist if err := db.First(&playlist, "id = ?", playlistID).Error; err != nil { return uuid.Nil, err } return playlist.UserID, nil } // Setup route with ownership middleware router.PUT("/playlists/:id", authMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "success"}) }) // Test: Owner tries to access their own playlist req := httptest.NewRequest("PUT", "/playlists/"+playlistID.String(), nil) req.Header.Set("X-User-ID", ownerID.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, "success", response["message"]) } // TestOwnershipMiddleware_PlaylistNonOwnerAccess teste qu'un non-propriétaire ne peut pas accéder à une playlist func TestOwnershipMiddleware_PlaylistNonOwnerAccess(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, authMiddleware, cleanup := setupOwnershipIntegrationTestRouter(t) defer cleanup() // Create two users ownerID := uuid.New() otherUserID := uuid.New() createTestUserForOwnership(t, db, ownerID, "owner", false) createTestUserForOwnership(t, db, otherUserID, "otheruser", false) // Create test playlist owned by owner playlistID := uuid.New() createTestPlaylistForOwnership(t, db, playlistID, ownerID) // Setup playlist owner resolver playlistOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { playlistIDStr := c.Param("id") playlistID, err := uuid.Parse(playlistIDStr) if err != nil { return uuid.Nil, err } var playlist models.Playlist if err := db.First(&playlist, "id = ?", playlistID).Error; err != nil { return uuid.Nil, err } return playlist.UserID, nil } // Setup route with ownership middleware router.PUT("/playlists/:id", authMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "success"}) }) // Test: Other user tries to access owner's playlist req := httptest.NewRequest("PUT", "/playlists/"+playlistID.String(), nil) req.Header.Set("X-User-ID", otherUserID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) } // TestOwnershipMiddleware_ResourceNotFound teste que le middleware retourne 404 si la ressource n'existe pas func TestOwnershipMiddleware_ResourceNotFound(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, authMiddleware, cleanup := setupOwnershipIntegrationTestRouter(t) defer cleanup() // Create test user userID := uuid.New() createTestUserForOwnership(t, db, userID, "user", false) // Setup track owner resolver trackOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { trackIDStr := c.Param("id") trackID, err := uuid.Parse(trackIDStr) if err != nil { return uuid.Nil, err } var track models.Track if err := db.First(&track, "id = ?", trackID).Error; err != nil { return uuid.Nil, err // Resource not found } return track.UserID, nil } // Setup route with ownership middleware router.PUT("/tracks/:id", authMiddleware.RequireOwnershipOrAdmin("track", trackOwnerResolver), func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "success"}) }) // Test: Try to access non-existent track nonExistentTrackID := uuid.New() req := httptest.NewRequest("PUT", "/tracks/"+nonExistentTrackID.String(), nil) req.Header.Set("X-User-ID", userID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) } // TestOwnershipMiddleware_UnauthenticatedAccess teste que le middleware rejette les requêtes non authentifiées func TestOwnershipMiddleware_UnauthenticatedAccess(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, authMiddleware, cleanup := setupOwnershipIntegrationTestRouter(t) defer cleanup() // Create test track ownerID := uuid.New() createTestUserForOwnership(t, db, ownerID, "owner", false) trackID := uuid.New() createTestTrackForOwnership(t, db, trackID, ownerID) // Setup track owner resolver trackOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { trackIDStr := c.Param("id") trackID, err := uuid.Parse(trackIDStr) if err != nil { return uuid.Nil, err } var track models.Track if err := db.First(&track, "id = ?", trackID).Error; err != nil { return uuid.Nil, err } return track.UserID, nil } // Setup route with ownership middleware router.PUT("/tracks/:id", authMiddleware.RequireOwnershipOrAdmin("track", trackOwnerResolver), func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "success"}) }) // Test: Try to access without authentication req := httptest.NewRequest("PUT", "/tracks/"+trackID.String(), nil) // No X-User-ID header w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // TestOwnershipMiddleware_CompleteFlow teste le flux complet d'ownership avec différentes ressources func TestOwnershipMiddleware_CompleteFlow(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, authMiddleware, cleanup := setupOwnershipIntegrationTestRouter(t) defer cleanup() // Create users: owner, other user, and admin ownerID := uuid.New() otherUserID := uuid.New() adminID := uuid.New() createTestUserForOwnership(t, db, ownerID, "owner", false) createTestUserForOwnership(t, db, otherUserID, "otheruser", false) createTestUserForOwnership(t, db, adminID, "admin", true) // Create resources trackID := uuid.New() playlistID := uuid.New() createTestTrackForOwnership(t, db, trackID, ownerID) createTestPlaylistForOwnership(t, db, playlistID, ownerID) // Setup resolvers trackOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { trackIDStr := c.Param("id") trackID, err := uuid.Parse(trackIDStr) if err != nil { return uuid.Nil, err } var track models.Track if err := db.First(&track, "id = ?", trackID).Error; err != nil { return uuid.Nil, err } return track.UserID, nil } playlistOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { playlistIDStr := c.Param("id") playlistID, err := uuid.Parse(playlistIDStr) if err != nil { return uuid.Nil, err } var playlist models.Playlist if err := db.First(&playlist, "id = ?", playlistID).Error; err != nil { return uuid.Nil, err } return playlist.UserID, nil } // Setup routes router.PUT("/tracks/:id", authMiddleware.RequireOwnershipOrAdmin("track", trackOwnerResolver), func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "track updated"}) }) router.PUT("/playlists/:id", authMiddleware.RequireOwnershipOrAdmin("playlist", playlistOwnerResolver), func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "playlist updated"}) }) // Test 1: Owner can access their own resources req := httptest.NewRequest("PUT", "/tracks/"+trackID.String(), nil) req.Header.Set("X-User-ID", ownerID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) req = httptest.NewRequest("PUT", "/playlists/"+playlistID.String(), nil) req.Header.Set("X-User-ID", ownerID.String()) w = httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) // Test 2: Other user cannot access owner's resources req = httptest.NewRequest("PUT", "/tracks/"+trackID.String(), nil) req.Header.Set("X-User-ID", otherUserID.String()) w = httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) req = httptest.NewRequest("PUT", "/playlists/"+playlistID.String(), nil) req.Header.Set("X-User-ID", otherUserID.String()) w = httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) // Test 3: Admin can access any resource req = httptest.NewRequest("PUT", "/tracks/"+trackID.String(), nil) req.Header.Set("X-User-ID", adminID.String()) w = httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) req = httptest.NewRequest("PUT", "/playlists/"+playlistID.String(), nil) req.Header.Set("X-User-ID", adminID.String()) w = httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) }