veza/veza-backend-api/internal/handlers/playlist_handler_integration_test.go
2025-12-16 11:23:49 -05:00

705 lines
21 KiB
Go

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"])
}
}