- 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
166 lines
5.9 KiB
Go
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
|
|
}
|