package handlers import ( "bytes" "encoding/json" "fmt" "github.com/google/uuid" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "gorm.io/driver/sqlite" "gorm.io/gorm" "veza-backend-api/internal/models" "veza-backend-api/internal/services" ) // setupPlaylistIntegrationTestRouter crée un router de test avec les handlers de playlists // T0456: Create Playlist Integration Tests func setupPlaylistIntegrationTestRouter(t *testing.T) (*gin.Engine, *gorm.DB, 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 err = db.AutoMigrate(&models.User{}, &models.Track{}, &models.Playlist{}, &models.PlaylistTrack{}) require.NoError(t, err) // Setup logger logger := zap.NewNop() // Setup service playlistService := services.NewPlaylistServiceWithDB(db, logger) playlistHandler := NewPlaylistHandler(playlistService, db, logger) // Create router router := gin.New() v1 := router.Group("/api/v1") { // Public routes v1.GET("/playlists", playlistHandler.GetPlaylists) v1.GET("/playlists/:id", playlistHandler.GetPlaylist) // Protected routes (simplified - no real auth middleware for integration tests) protected := v1.Group("/") protected.Use(func(c *gin.Context) { // Mock auth middleware - set user_id from query param or header if userIDStr := c.Query("user_id"); userIDStr != "" { uid, err := uuid.Parse(userIDStr) if err == nil { c.Set("user_id", uid) } } else if userIDStr := c.GetHeader("X-User-ID"); userIDStr != "" { uid, err := uuid.Parse(userIDStr) if err == nil { c.Set("user_id", uid) } } c.Next() }) { protected.POST("/playlists", playlistHandler.CreatePlaylist) protected.PUT("/playlists/:id", playlistHandler.UpdatePlaylist) protected.DELETE("/playlists/:id", playlistHandler.DeletePlaylist) } } cleanup := func() { // Database will be closed automatically } return router, db, cleanup } // createTestUser crée un utilisateur de test func createTestUserForPlaylist(t *testing.T, db *gorm.DB, userID uuid.UUID, username string) *models.User { timestamp := time.Now().UnixNano() uniqueUsername := fmt.Sprintf("%s_%d", username, timestamp) user := &models.User{ ID: userID, Username: uniqueUsername, Slug: uniqueUsername, Email: fmt.Sprintf("%s@example.com", uniqueUsername), PasswordHash: "hashed_password", IsActive: true, CreatedAt: time.Now(), } err := db.Create(user).Error require.NoError(t, err) return user } // TestCreatePlaylist_Success teste la création réussie d'une playlist // T0456: Create Playlist Integration Tests func TestCreatePlaylist_Success(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, cleanup := setupPlaylistIntegrationTestRouter(t) defer cleanup() // Créer un utilisateur de test userID := uuid.New() createTestUserForPlaylist(t, db, userID, "testuser") // Créer une playlist reqBody := map[string]interface{}{ "title": "My Awesome Playlist", "description": "A test playlist with great songs", "is_public": true, } body, err := json.Marshal(reqBody) require.NoError(t, err) req := httptest.NewRequest("POST", fmt.Sprintf("/api/v1/playlists?user_id=%s", userID), bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code) var response map[string]interface{} err = json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.Contains(t, response, "playlist") playlist := response["playlist"].(map[string]interface{}) assert.Equal(t, "My Awesome Playlist", playlist["title"]) assert.Equal(t, "A test playlist with great songs", playlist["description"]) assert.Equal(t, true, playlist["is_public"]) assert.Equal(t, userID.String(), playlist["user_id"]) } // TestCreatePlaylist_ValidationErrors teste les erreurs de validation // T0456: Create Playlist Integration Tests func TestCreatePlaylist_ValidationErrors(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, cleanup := setupPlaylistIntegrationTestRouter(t) defer cleanup() userID := uuid.New() createTestUserForPlaylist(t, db, userID, "testuser") tests := []struct { name string reqBody map[string]interface{} expectedCode int errorContains string }{ { name: "empty title", reqBody: map[string]interface{}{ "title": "", "is_public": true, }, expectedCode: http.StatusBadRequest, errorContains: "required", }, { name: "title too long", reqBody: map[string]interface{}{ "title": string(make([]byte, 201)), // 201 characters "is_public": true, }, expectedCode: http.StatusBadRequest, errorContains: "200", }, { name: "missing title", reqBody: map[string]interface{}{ "description": "Some description", "is_public": true, }, expectedCode: http.StatusBadRequest, errorContains: "required", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { body, err := json.Marshal(tt.reqBody) require.NoError(t, err) req := httptest.NewRequest("POST", fmt.Sprintf("/api/v1/playlists?user_id=%s", userID), bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, tt.expectedCode, w.Code) var response map[string]interface{} json.Unmarshal(w.Body.Bytes(), &response) if tt.errorContains != "" { assert.Contains(t, response["error"].(string), tt.errorContains) } }) } } // TestCreatePlaylist_Unauthorized teste la création sans authentification // T0456: Create Playlist Integration Tests func TestCreatePlaylist_Unauthorized(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, _, cleanup := setupPlaylistIntegrationTestRouter(t) defer cleanup() reqBody := map[string]interface{}{ "title": "My Playlist", "is_public": true, } body, _ := json.Marshal(reqBody) req := httptest.NewRequest("POST", "/api/v1/playlists", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Le handler vérifie user_id, donc si pas d'auth, ça devrait échouer // Mais notre mock middleware ne set pas user_id si pas de query param assert.Equal(t, http.StatusUnauthorized, w.Code) } // TestGetPlaylist_Public teste la récupération d'une playlist publique // T0456: Create Playlist Integration Tests func TestGetPlaylist_Public(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, cleanup := setupPlaylistIntegrationTestRouter(t) defer cleanup() // Créer un utilisateur et une playlist publique userID := uuid.New() createTestUserForPlaylist(t, db, userID, "testuser") playlist := &models.Playlist{ UserID: userID, Title: "Public Playlist", IsPublic: true, } err := db.Create(playlist).Error require.NoError(t, err) // Récupérer la playlist sans authentification req := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/playlists/%d", playlist.ID), nil) 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, "playlist") playlistData := response["playlist"].(map[string]interface{}) assert.Equal(t, "Public Playlist", playlistData["title"]) assert.Equal(t, true, playlistData["is_public"]) } // TestGetPlaylist_Private_Unauthorized teste l'accès à une playlist privée sans auth // T0456: Create Playlist Integration Tests func TestGetPlaylist_Private_Unauthorized(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, cleanup := setupPlaylistIntegrationTestRouter(t) defer cleanup() // Créer un utilisateur et une playlist privée userID := uuid.New() createTestUserForPlaylist(t, db, userID, "testuser") playlist := &models.Playlist{ UserID: userID, Title: "Private Playlist", IsPublic: false, } err := db.Create(playlist).Error require.NoError(t, err) // Essayer de récupérer la playlist sans authentification req := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/playlists/%d", playlist.ID), nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Devrait retourner 404 (playlist not found) car privée assert.Equal(t, http.StatusNotFound, w.Code) } // TestGetPlaylist_Private_AsOwner teste l'accès à une playlist privée en tant que propriétaire // T0456: Create Playlist Integration Tests func TestGetPlaylist_Private_AsOwner(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, cleanup := setupPlaylistIntegrationTestRouter(t) defer cleanup() // Créer un utilisateur et une playlist privée userID := uuid.New() createTestUserForPlaylist(t, db, userID, "testuser") playlist := &models.Playlist{ UserID: userID, Title: "Private Playlist", IsPublic: false, } err := db.Create(playlist).Error require.NoError(t, err) // Récupérer la playlist en tant que propriétaire req := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/playlists/%d?user_id=%s", playlist.ID, userID), nil) 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, "playlist") playlistData := response["playlist"].(map[string]interface{}) assert.Equal(t, "Private Playlist", playlistData["title"]) } // TestUpdatePlaylist_AsOwner teste la mise à jour d'une playlist en tant que propriétaire // T0456: Create Playlist Integration Tests func TestUpdatePlaylist_AsOwner(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, cleanup := setupPlaylistIntegrationTestRouter(t) defer cleanup() // Créer un utilisateur et une playlist userID := uuid.New() createTestUserForPlaylist(t, db, userID, "testuser") playlist := &models.Playlist{ UserID: userID, Title: "Original Title", Description: "Original description", IsPublic: true, } err := db.Create(playlist).Error require.NoError(t, err) // Mettre à jour la playlist newTitle := "Updated Title" newDescription := "Updated description" newIsPublic := false reqBody := map[string]interface{}{ "title": newTitle, "description": newDescription, "is_public": newIsPublic, } body, err := json.Marshal(reqBody) require.NoError(t, err) req := httptest.NewRequest("PUT", fmt.Sprintf("/api/v1/playlists/%d?user_id=%s", playlist.ID, userID), bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") 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, "playlist") playlistData := response["playlist"].(map[string]interface{}) assert.Equal(t, newTitle, playlistData["title"]) assert.Equal(t, newDescription, playlistData["description"]) assert.Equal(t, newIsPublic, playlistData["is_public"]) } // TestUpdatePlaylist_NotOwner teste la mise à jour d'une playlist par un non-propriétaire // T0456: Create Playlist Integration Tests func TestUpdatePlaylist_NotOwner(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, cleanup := setupPlaylistIntegrationTestRouter(t) defer cleanup() // Créer deux utilisateurs user1ID := uuid.New() user2ID := uuid.New() createTestUserForPlaylist(t, db, user1ID, "user1") createTestUserForPlaylist(t, db, user2ID, "user2") // Créer une playlist pour user1 playlist := &models.Playlist{ UserID: user1ID, Title: "User1's Playlist", IsPublic: true, } err := db.Create(playlist).Error require.NoError(t, err) // Essayer de mettre à jour en tant que user2 reqBody := map[string]interface{}{ "title": "Hacked Title", } body, err := json.Marshal(reqBody) require.NoError(t, err) req := httptest.NewRequest("PUT", fmt.Sprintf("/api/v1/playlists/%d?user_id=%s", playlist.ID, user2ID), bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Devrait retourner 403 Forbidden assert.Equal(t, http.StatusForbidden, w.Code) } // TestDeletePlaylist_AsOwner teste la suppression d'une playlist en tant que propriétaire // T0456: Create Playlist Integration Tests func TestDeletePlaylist_AsOwner(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, cleanup := setupPlaylistIntegrationTestRouter(t) defer cleanup() // Créer un utilisateur et une playlist userID := uuid.New() createTestUserForPlaylist(t, db, userID, "testuser") playlist := &models.Playlist{ UserID: userID, Title: "Playlist to Delete", IsPublic: true, } err := db.Create(playlist).Error require.NoError(t, err) // Supprimer la playlist req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/v1/playlists/%d?user_id=%s", playlist.ID, userID), nil) 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, "message") assert.Equal(t, "playlist deleted", response["message"]) // Vérifier que la playlist est bien supprimée var count int64 db.Model(&models.Playlist{}).Where("id = ?", playlist.ID).Count(&count) assert.Equal(t, int64(0), count) } // TestDeletePlaylist_NotOwner teste la suppression d'une playlist par un non-propriétaire // T0456: Create Playlist Integration Tests func TestDeletePlaylist_NotOwner(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, cleanup := setupPlaylistIntegrationTestRouter(t) defer cleanup() // Créer deux utilisateurs user1ID := uuid.New() user2ID := uuid.New() createTestUserForPlaylist(t, db, user1ID, "user1") createTestUserForPlaylist(t, db, user2ID, "user2") // Créer une playlist pour user1 playlist := &models.Playlist{ UserID: user1ID, Title: "User1's Playlist", IsPublic: true, } err := db.Create(playlist).Error require.NoError(t, err) // Essayer de supprimer en tant que user2 req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/v1/playlists/%d?user_id=%s", playlist.ID, user2ID), nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Devrait retourner 403 Forbidden assert.Equal(t, http.StatusForbidden, w.Code) } // TestListPlaylists_Pagination teste la pagination des playlists // T0456: Create Playlist Integration Tests func TestListPlaylists_Pagination(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, cleanup := setupPlaylistIntegrationTestRouter(t) defer cleanup() // Créer un utilisateur userID := uuid.New() createTestUserForPlaylist(t, db, userID, "testuser") // Créer plusieurs playlists for i := 0; i < 5; i++ { playlist := &models.Playlist{ UserID: userID, Title: fmt.Sprintf("Playlist %d", i+1), IsPublic: true, } err := db.Create(playlist).Error require.NoError(t, err) } // Récupérer la première page (limit=2) req := httptest.NewRequest("GET", "/api/v1/playlists?page=1&limit=2", nil) 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, "playlists") assert.Contains(t, response, "total") assert.Contains(t, response, "page") assert.Contains(t, response, "limit") playlists := response["playlists"].([]interface{}) assert.LessOrEqual(t, len(playlists), 2) assert.Equal(t, float64(5), response["total"]) assert.Equal(t, float64(1), response["page"]) assert.Equal(t, float64(2), response["limit"]) } // TestListPlaylists_FilterByUser teste le filtrage par utilisateur // T0456: Create Playlist Integration Tests func TestListPlaylists_FilterByUser(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") } router, db, cleanup := setupPlaylistIntegrationTestRouter(t) defer cleanup() // Créer deux utilisateurs user1ID := uuid.New() user2ID := uuid.New() createTestUserForPlaylist(t, db, user1ID, "user1") createTestUserForPlaylist(t, db, user2ID, "user2") // Créer des playlists pour chaque utilisateur for i := 0; i < 3; i++ { playlist := &models.Playlist{ UserID: user1ID, Title: fmt.Sprintf("User1 Playlist %d", i+1), IsPublic: true, } err := db.Create(playlist).Error require.NoError(t, err) } for i := 0; i < 2; i++ { playlist := &models.Playlist{ UserID: user2ID, Title: fmt.Sprintf("User2 Playlist %d", i+1), IsPublic: true, } err := db.Create(playlist).Error require.NoError(t, err) } // Filtrer par user1 req := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/playlists?user_id=%s", user1ID), nil) 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) playlists := response["playlists"].([]interface{}) assert.Equal(t, 3, len(playlists)) assert.Equal(t, float64(3), response["total"]) // Vérifier que toutes les playlists appartiennent à user1 for _, p := range playlists { playlistData := p.(map[string]interface{}) assert.Equal(t, user1ID.String(), playlistData["user_id"]) } }