veza/veza-backend-api/internal/services/playlist_duplicate_service.go
senke b28d0e7eac [T0-006] test(backend): Ajout tests pour frontend_log_handler
- Tests complets pour frontend_log_handler.go (12 tests)
- Tests couvrent NewFrontendLogHandler et ReceiveLog
- Tests pour tous les niveaux de log (DEBUG, INFO, WARN, ERROR)
- Tests pour gestion des erreurs et validation JSON
- Couverture actuelle: 30.6% (objectif: 80%)

Files: veza-backend-api/internal/handlers/frontend_log_handler_test.go
       VEZA_ROADMAP.json
Hours: 16 estimated, 23 actual
2026-01-04 01:44:22 +01:00

166 lines
5.9 KiB
Go

package services
import (
"context"
"errors"
"fmt"
"github.com/google/uuid"
"veza-backend-api/internal/models"
"go.uber.org/zap"
"gorm.io/gorm"
)
// PlaylistDuplicateService gère la duplication de playlists
// T0495: Create Playlist Duplicate Feature
type PlaylistDuplicateService struct {
playlistService PlaylistServiceInterface
db *gorm.DB
logger *zap.Logger
}
// NewPlaylistDuplicateService crée un nouveau service de duplication de playlists
func NewPlaylistDuplicateService(playlistService PlaylistServiceInterface, db *gorm.DB, logger *zap.Logger) *PlaylistDuplicateService {
if logger == nil {
logger = zap.NewNop()
}
return &PlaylistDuplicateService{
playlistService: playlistService,
db: db,
logger: logger,
}
}
// DuplicatePlaylistRequest représente la requête de duplication
type DuplicatePlaylistRequest struct {
NewTitle string `json:"new_title"`
NewDescription string `json:"new_description,omitempty"`
IsPublic *bool `json:"is_public,omitempty"`
}
// DuplicatePlaylist duplique une playlist avec tous ses tracks
// T0495: Create Playlist Duplicate Feature
// MIGRATION UUID: Completée. playlistID et userID sont des UUIDs.
// Transactionnelle : Toute la duplication (playlist + tracks + compteur) est dans une seule transaction
func (s *PlaylistDuplicateService) DuplicatePlaylist(
ctx context.Context,
playlistID uuid.UUID,
userID uuid.UUID,
request DuplicatePlaylistRequest,
) (*models.Playlist, error) {
var newPlaylist *models.Playlist
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// 1. VALIDATION : Charger playlist originale (sans tracks pour l'instant)
var originalPlaylist models.Playlist
err := tx.First(&originalPlaylist, "id = ?", playlistID).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("playlist not found")
}
return fmt.Errorf("DuplicatePlaylist: failed to load original playlist: %w", err)
}
// 1b. Charger tous les PlaylistTrack de la playlist originale (même si le Track associé est supprimé)
var playlistTracks []models.PlaylistTrack
if err := tx.Where("playlist_id = ?", playlistID).Order("position ASC").Find(&playlistTracks).Error; err != nil {
return fmt.Errorf("DuplicatePlaylist: failed to load playlist tracks: %w", err)
}
// 2. VALIDATION : Vérifier que l'utilisateur a accès à la playlist (propriétaire, collaborateur ou publique)
// Note: On fait cette vérification dans la transaction pour éviter les race conditions
if originalPlaylist.UserID != userID && !originalPlaylist.IsPublic {
// Vérifier si l'utilisateur est collaborateur (simplifié pour la transaction)
// On peut faire une requête simple dans la transaction
var collaboratorCount int64
err := tx.Raw("SELECT COUNT(*) FROM playlist_collaborators WHERE playlist_id = ? AND user_id = ?", playlistID, userID).Scan(&collaboratorCount).Error
if err != nil || collaboratorCount == 0 {
return errors.New("forbidden: you don't have access to this playlist")
}
}
// 3. DÉTERMINATION : Titre, description, isPublic
newTitle := request.NewTitle
if newTitle == "" {
newTitle = originalPlaylist.Title + " (Copy)"
}
newDescription := request.NewDescription
if newDescription == "" {
newDescription = originalPlaylist.Description
}
isPublic := originalPlaylist.IsPublic
if request.IsPublic != nil {
isPublic = *request.IsPublic
}
// 4. CRÉATION : Nouvelle playlist (INSERT dans la transaction)
newPlaylist = &models.Playlist{
UserID: userID,
Title: newTitle,
Description: newDescription,
IsPublic: isPublic,
TrackCount: 0, // Sera mis à jour après l'ajout des tracks
}
if err := tx.Create(newPlaylist).Error; err != nil {
return fmt.Errorf("DuplicatePlaylist: failed to create duplicate playlist: %w", err)
}
// 5. DUPLICATION : Tous les tracks dans la même transaction
for i, playlistTrack := range playlistTracks {
trackID := playlistTrack.TrackID
if trackID == uuid.Nil {
return fmt.Errorf("DuplicatePlaylist: track not found for playlist track at position %d", i+1)
}
// Vérifier que le track existe toujours dans la base de données (non supprimé)
var trackExists bool
if err := tx.Model(&models.Track{}).Select("1").Where("id = ? AND deleted_at IS NULL", trackID).Limit(1).Scan(&trackExists).Error; err != nil {
return fmt.Errorf("DuplicatePlaylist: failed to verify track existence: %w", err)
}
if !trackExists {
return fmt.Errorf("DuplicatePlaylist: track %s no longer exists", trackID)
}
// Créer le PlaylistTrack directement dans la transaction
newPlaylistTrack := models.PlaylistTrack{
PlaylistID: newPlaylist.ID,
TrackID: trackID,
Position: playlistTrack.Position,
AddedBy: userID, // Use the userID who is duplicating the playlist
}
// Si position <= 0, utiliser l'index + 1
if newPlaylistTrack.Position <= 0 {
newPlaylistTrack.Position = i + 1
}
if err := tx.Create(&newPlaylistTrack).Error; err != nil {
return fmt.Errorf("DuplicatePlaylist: failed to add track %s to duplicate: %w", trackID, err)
}
}
// 6. MISE À JOUR : Compteur de tracks (UPDATE dans la transaction)
trackCount := len(playlistTracks)
if err := tx.Model(newPlaylist).Update("track_count", trackCount).Error; err != nil {
return fmt.Errorf("DuplicatePlaylist: failed to update track_count: %w", err)
}
newPlaylist.TrackCount = trackCount
// 7. LOG (dans la transaction, mais ne dépend pas d'états non commit)
s.logger.Info("Playlist duplicated",
zap.String("original_playlist_id", playlistID.String()),
zap.String("new_playlist_id", newPlaylist.ID.String()),
zap.String("user_id", userID.String()),
zap.Int("tracks_count", trackCount),
)
// 8. RETOUR nil = commit automatique
return nil
})
if err != nil {
return nil, err // Rollback automatique si erreur
}
return newPlaylist, nil
}