package handlers import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/google/uuid" "veza-backend-api/internal/models" "veza-backend-api/internal/services" "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" ) // 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") { // Optional auth middleware for GET routes - sets user_id if present, but doesn't block optionalAuth := func(c *gin.Context) { if userIDStr := c.Query("user_id"); userIDStr != "" { if uid, err := uuid.Parse(userIDStr); err == nil { c.Set("user_id", uid) } } else if userIDStr := c.GetHeader("X-User-ID"); userIDStr != "" { if uid, err := uuid.Parse(userIDStr); err == nil { c.Set("user_id", uid) } } c.Next() } // Public routes - GET endpoints handle authorization internally // (they check if playlist is public or user is owner) v1.GET("/playlists", optionalAuth, playlistHandler.GetPlaylists) v1.GET("/playlists/:id", optionalAuth, 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 no user_id provided, return 401 Unauthorized userIDSet := false if userIDStr := c.Query("user_id"); userIDStr != "" { uid, err := uuid.Parse(userIDStr) if err == nil { c.Set("user_id", uid) userIDSet = true } } else if userIDStr := c.GetHeader("X-User-ID"); userIDStr != "" { uid, err := uuid.Parse(userIDStr) if err == nil { c.Set("user_id", uid) userIDSet = true } } // If user_id not set, return 401 Unauthorized if !userIDSet { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) c.Abort() return } 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) // Vérifier le format de réponse standardisé {success: true, data: {playlist: {...}}} assert.Contains(t, response, "data") data, ok := response["data"].(map[string]interface{}) require.True(t, ok, "response should contain 'data' key with map value") assert.Contains(t, data, "playlist") playlistData, ok := data["playlist"].(map[string]interface{}) require.True(t, ok, "data should contain 'playlist' key with map value") playlist := playlistData 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, w.Body.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/%s", 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) // MOD-P0-002: Accéder au format de réponse standardisé {success: true, data: {playlist: {...}}} assert.Contains(t, response, "data") data, ok := response["data"].(map[string]interface{}) require.True(t, ok, "response should contain 'data' key with map value") assert.Contains(t, data, "playlist") playlistData, ok := data["playlist"].(map[string]interface{}) require.True(t, ok, "data should contain 'playlist' key with map value") 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) // Force IsPublic to false (GORM might use default value true) err = db.Model(playlist).Update("is_public", false).Error require.NoError(t, err) // Vérifier que la playlist est bien privée var createdPlaylist models.Playlist err = db.First(&createdPlaylist, playlist.ID).Error require.NoError(t, err) require.False(t, createdPlaylist.IsPublic, "Playlist should be private") // Essayer de récupérer la playlist sans authentification req := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/playlists/%s", playlist.ID), nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Devrait retourner 404 (Not Found) car le service retourne ErrPlaylistNotFound pour les playlists privées // sans authentification (sécurité : ne pas révéler l'existence de playlists privées) 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/%s?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) // MOD-P0-002: Accéder au format de réponse standardisé {success: true, data: {playlist: {...}}} assert.Contains(t, response, "data") data, ok := response["data"].(map[string]interface{}) require.True(t, ok, "response should contain 'data' key with map value") assert.Contains(t, data, "playlist") playlistData, ok := data["playlist"].(map[string]interface{}) require.True(t, ok, "data should contain 'playlist' key with map value") 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/%s?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) // MOD-P0-002: Accéder au format de réponse standardisé {success: true, data: {playlist: {...}}} assert.Contains(t, response, "data") data, ok := response["data"].(map[string]interface{}) require.True(t, ok, "response should contain 'data' key with map value") assert.Contains(t, data, "playlist") playlistData, ok := data["playlist"].(map[string]interface{}) require.True(t, ok, "data should contain 'playlist' key with map value") 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/%s?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/%s?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) // MOD-P0-002: Accéder au format de réponse standardisé {success: true, data: {message: "..."}} assert.Contains(t, response, "data") data, ok := response["data"].(map[string]interface{}) require.True(t, ok, "response should contain 'data' key with map value") assert.Contains(t, data, "message") assert.Equal(t, "playlist deleted", data["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/%s?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) // MOD-P0-002: Accéder au format de réponse standardisé {success: true, data: {playlists: [...], total: ..., page: ...}} assert.Contains(t, response, "data") data, ok := response["data"].(map[string]interface{}) require.True(t, ok, "response should contain 'data' key with map value") assert.Contains(t, data, "playlists") assert.Contains(t, data, "total") assert.Contains(t, data, "page") assert.Contains(t, data, "limit") playlists := data["playlists"].([]interface{}) assert.LessOrEqual(t, len(playlists), 2) assert.Equal(t, float64(5), data["total"]) assert.Equal(t, float64(1), data["page"]) assert.Equal(t, float64(2), data["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) // MOD-P0-002: Accéder au format de réponse standardisé {success: true, data: {playlists: [...], total: ...}} assert.Contains(t, response, "data") data2, ok := response["data"].(map[string]interface{}) require.True(t, ok, "response should contain 'data' key with map value") playlists := data2["playlists"].([]interface{}) assert.Equal(t, 3, len(playlists)) assert.Equal(t, float64(3), data2["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"]) } }