package track import ( "bytes" "context" "mime/multipart" // Removed "net/http" since it is not used in the existing imports "os" // Added "path" import "path/filepath" "testing" "time" "veza-backend-api/internal/models" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func setupTestTrackService(t *testing.T) (*TrackService, *gorm.DB, func()) { logger := zaptest.NewLogger(t) // Create temp upload dir uploadDir, err := os.MkdirTemp("", "track_service_test") require.NoError(t, err) // Setup SQLite database file dbPath := filepath.Join(uploadDir, "test.db") db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) require.NoError(t, err) // Enable foreign keys db.Exec("PRAGMA foreign_keys = ON") // Auto-migrate models err = db.AutoMigrate( &models.User{}, &models.Track{}, &models.TrackLike{}, // Added TrackLike model to migration ) require.NoError(t, err) service := NewTrackService(db, logger, uploadDir) cleanup := func() { os.RemoveAll(uploadDir) } return service, db, cleanup } func createMultipartFileHeader(t *testing.T, filename string, content []byte, contentType string) *multipart.FileHeader { body := &bytes.Buffer{} writer := multipart.NewWriter(body) part, err := writer.CreateFormFile("file", filename) require.NoError(t, err) _, err = part.Write(content) require.NoError(t, err) err = writer.Close() require.NoError(t, err) reader := multipart.NewReader(body, writer.Boundary()) form, err := reader.ReadForm(1024 * 1024) require.NoError(t, err) headers := form.File["file"] require.NotEmpty(t, headers) headers[0].Header.Set("Content-Type", contentType) return headers[0] } func TestTrackService_ValidateTrackFile(t *testing.T) { service, _, cleanup := setupTestTrackService(t) defer cleanup() // Test case: valid MP3 (mock content with ID3 header) mp3Content := append([]byte("ID3"), make([]byte, 100)...) header := createMultipartFileHeader(t, "test.mp3", mp3Content, "audio/mpeg") err := service.ValidateTrackFile(header) assert.NoError(t, err) // Test case: valid WAV (mock content with RIFF/WAVE header) wavContent := append([]byte("RIFF"), make([]byte, 4)...) wavContent = append(wavContent, []byte("WAVE")...) wavContent = append(wavContent, make([]byte, 100)...) headerWav := createMultipartFileHeader(t, "test.wav", wavContent, "audio/wav") err = service.ValidateTrackFile(headerWav) assert.NoError(t, err) // Test case: invalid extension headerInvalid := createMultipartFileHeader(t, "test.txt", []byte("some text"), "text/plain") err = service.ValidateTrackFile(headerInvalid) assert.ErrorIs(t, err, ErrInvalidTrackFormat) // Test case: file too large (manually set size to mock large file without large content) headerTooLarge := createMultipartFileHeader(t, "large.mp3", mp3Content, "audio/mpeg") headerTooLarge.Size = 500 * 1024 * 1024 // 500MB err = service.ValidateTrackFile(headerTooLarge) assert.ErrorIs(t, err, ErrTrackTooLarge) } func TestTrackService_CheckUserQuota(t *testing.T) { service, db, cleanup := setupTestTrackService(t) defer cleanup() // Create a user userID := uuid.New() user := &models.User{ ID: userID, Username: "quotatest", Email: "quota@example.com", } db.Create(user) ctx := context.Background() // Test: Empty user checks OK err := service.CheckUserQuota(ctx, userID, 1024*1024) assert.NoError(t, err) // Create a track consuming storage track := &models.Track{ ID: uuid.New(), UserID: userID, Title: "Big Track", FileSize: MaxStoragePerUser - 100, // Almost full Status: models.TrackStatusCompleted, } db.Create(track) // Now try to upload something bigger than remaining err = service.CheckUserQuota(ctx, userID, 200) assert.ErrorIs(t, err, ErrStorageQuotaExceeded) } func TestTrackService_GetUserQuota(t *testing.T) { service, db, cleanup := setupTestTrackService(t) defer cleanup() userID := uuid.New() user := &models.User{ID: userID, Username: "quotauser", Email: "qu@example.com"} db.Create(user) // Add 2 tracks db.Create(&models.Track{ID: uuid.New(), UserID: userID, FileSize: 1000, Status: models.TrackStatusCompleted}) db.Create(&models.Track{ID: uuid.New(), UserID: userID, FileSize: 2000, Status: models.TrackStatusCompleted}) ctx := context.Background() quota, err := service.GetUserQuota(ctx, userID) assert.NoError(t, err) assert.NotNil(t, quota) assert.Equal(t, int64(2), quota.TracksCount) assert.Equal(t, int64(3000), quota.StorageUsed) assert.Equal(t, int64(MaxTracksPerUser), quota.TracksLimit) } func TestTrackService_ListTracks(t *testing.T) { service, db, cleanup := setupTestTrackService(t) defer cleanup() userID := uuid.New() user := &models.User{ID: userID, Username: "listuser", Email: "list@example.com"} db.Create(user) // Create tracks for i := 0; i < 5; i++ { db.Create(&models.Track{ ID: uuid.New(), UserID: userID, Title: "Track " + string(rune('A'+i)), Format: "mp3", IsPublic: true, Status: models.TrackStatusCompleted, CreatedAt: time.Now().Add(time.Duration(i) * time.Minute), }) } // Private track db.Create(&models.Track{ ID: uuid.New(), UserID: userID, Title: "Private Track", Format: "wav", IsPublic: false, Status: models.TrackStatusCompleted, }) ctx := context.Background() // Test: List all params := TrackListParams{ UserID: &userID, Page: 1, Limit: 10, } tracks, total, err := service.ListTracks(ctx, params) assert.NoError(t, err) assert.Equal(t, int64(6), total) assert.Len(t, tracks, 6) // Test: Filter by format fmtMp3 := "mp3" params.Format = &fmtMp3 tracks, total, err = service.ListTracks(ctx, params) assert.NoError(t, err) assert.Equal(t, int64(5), total) } func TestTrackService_CreateTrackFromPath_Success(t *testing.T) { service, db, cleanup := setupTestTrackService(t) defer cleanup() userID := uuid.New() user := &models.User{ID: userID, Username: "pathuser", Email: "path@example.com"} db.Create(user) ctx := context.Background() filePath := "/tmp/some/file.mp3" filename := "file.mp3" fileSize := int64(12345) format := "mp3" track, err := service.CreateTrackFromPath(ctx, userID, filePath, filename, fileSize, format) assert.NoError(t, err) assert.NotNil(t, track) assert.Equal(t, models.TrackStatusUploading, track.Status) assert.Equal(t, filePath, track.FilePath) // Verify in DB var dbTrack models.Track err = db.First(&dbTrack, "id = ?", track.ID).Error assert.NoError(t, err) assert.Equal(t, fileSize, dbTrack.FileSize) } func TestTrackService_UpdateStreamStatus(t *testing.T) { service, db, cleanup := setupTestTrackService(t) defer cleanup() userID := uuid.New() db.Create(&models.User{ID: userID}) trackID := uuid.New() db.Create(&models.Track{ID: trackID, UserID: userID, Status: models.TrackStatusProcessing}) ctx := context.Background() err := service.UpdateStreamStatus(ctx, trackID, "ready", "http://manifest.url") assert.NoError(t, err) var track models.Track db.First(&track, "id = ?", trackID) assert.Equal(t, models.TrackStatusCompleted, track.Status) assert.Equal(t, "ready", track.StreamStatus) assert.Equal(t, "http://manifest.url", track.StreamManifestURL) // Test error status err = service.UpdateStreamStatus(ctx, trackID, "error", "") assert.NoError(t, err) db.First(&track, "id = ?", trackID) assert.Equal(t, models.TrackStatusFailed, track.Status) assert.Equal(t, "error", track.StreamStatus) } func TestTrackService_BatchOperations(t *testing.T) { service, db, cleanup := setupTestTrackService(t) defer cleanup() userID := uuid.New() db.Create(&models.User{ID: userID}) ids := []uuid.UUID{uuid.New(), uuid.New(), uuid.New()} for _, id := range ids { db.Create(&models.Track{ID: id, UserID: userID, Title: "Original", Status: models.TrackStatusCompleted}) } ctx := context.Background() // Batch Update updates := map[string]interface{}{ "title": "Batch Updated", } result, err := service.BatchUpdateTracks(ctx, ids, userID, updates) assert.NoError(t, err) assert.Equal(t, 3, len(result.Updated)) var tracks []models.Track db.Find(&tracks, "id IN ?", ids) for _, tr := range tracks { assert.Equal(t, "Batch Updated", tr.Title) } // Batch Delete deleteResult, err := service.BatchDeleteTracks(ctx, ids, userID) assert.NoError(t, err) assert.Equal(t, 3, len(deleteResult.Deleted)) var count int64 db.Model(&models.Track{}).Where("id IN ?", ids).Count(&count) assert.Equal(t, int64(0), count) } func TestTrackService_GetTrackByID(t *testing.T) { service, db, cleanup := setupTestTrackService(t) defer cleanup() userID := uuid.New() db.Create(&models.User{ID: userID, Username: "getuser", Email: "get@example.com"}) trackID := uuid.New() // Pre-create track track := &models.Track{ ID: trackID, UserID: userID, Title: "Test Track", Status: models.TrackStatusCompleted, IsPublic: true, } db.Create(track) ctx := context.Background() // Test: Success found, err := service.GetTrackByID(ctx, trackID) assert.NoError(t, err) assert.Equal(t, trackID, found.ID) assert.Equal(t, "Test Track", found.Title) // Test: NotFound _, err = service.GetTrackByID(ctx, uuid.New()) assert.ErrorIs(t, err, ErrTrackNotFound) } func TestTrackService_UpdateTrack(t *testing.T) { service, db, cleanup := setupTestTrackService(t) defer cleanup() ownerID := uuid.New() otherID := uuid.New() db.Create(&models.User{ID: ownerID, Username: "owner", Email: "owner@example.com"}) db.Create(&models.User{ID: otherID, Username: "other", Email: "other@example.com"}) trackID := uuid.New() db.Create(&models.Track{ ID: trackID, UserID: ownerID, Title: "Original Title", Genre: "Pop", IsPublic: true, }) ctx := context.Background() // Test: Update Success (Owner) newTitle := "Updated Title" newGenre := "Rock" isPublic := false params := UpdateTrackParams{ Title: &newTitle, Genre: &newGenre, IsPublic: &isPublic, } updated, err := service.UpdateTrack(ctx, trackID, ownerID, params) assert.NoError(t, err) assert.Equal(t, "Updated Title", updated.Title) assert.Equal(t, "Rock", updated.Genre) assert.False(t, updated.IsPublic) // Test: Forbidden (Other User) params2 := UpdateTrackParams{Title: &newTitle} _, err = service.UpdateTrack(ctx, trackID, otherID, params2) assert.ErrorIs(t, err, ErrForbidden) // Test: Admin Override // (Assuming context key "is_admin" works as implemented in service) adminCtx := context.WithValue(ctx, "is_admin", true) adminTitle := "Admin Title" params3 := UpdateTrackParams{Title: &adminTitle} updatedAdmin, err := service.UpdateTrack(adminCtx, trackID, otherID, params3) assert.NoError(t, err) assert.Equal(t, "Admin Title", updatedAdmin.Title) } func TestTrackService_DeleteTrack(t *testing.T) { service, db, cleanup := setupTestTrackService(t) defer cleanup() ownerID := uuid.New() otherID := uuid.New() db.Create(&models.User{ID: ownerID}) db.Create(&models.User{ID: otherID}) // Create file for deletion test tmpFile, err := os.CreateTemp(service.uploadDir, "track_*.mp3") require.NoError(t, err) tmpFile.Close() filePath := tmpFile.Name() trackID := uuid.New() db.Create(&models.Track{ ID: trackID, UserID: ownerID, FilePath: filePath, }) ctx := context.Background() // Test: Forbidden err = service.DeleteTrack(ctx, trackID, otherID) assert.ErrorIs(t, err, ErrForbidden) // Check file still exists _, err = os.Stat(filePath) assert.NoError(t, err) // Test: Success err = service.DeleteTrack(ctx, trackID, ownerID) assert.NoError(t, err) // Verify DB deletion var count int64 db.Model(&models.Track{}).Where("id = ?", trackID).Count(&count) assert.Equal(t, int64(0), count) // Verify File deletion _, err = os.Stat(filePath) assert.True(t, os.IsNotExist(err)) } func TestTrackService_UploadTrack_Basic(t *testing.T) { service, db, cleanup := setupTestTrackService(t) defer cleanup() userID := uuid.New() db.Create(&models.User{ID: userID}) ctx := context.Background() // Mock file header content := []byte{0xFF, 0xFB, 0x00, 0x00} // Fake MP3 frame header header := createMultipartFileHeader(t, "upload.mp3", content, "audio/mpeg") metadata := TrackMetadata{ Title: "Uploaded Track", IsPublic: true, } // Test Upload track, err := service.UploadTrack(ctx, userID, header, metadata) assert.NoError(t, err) assert.NotNil(t, track) assert.Equal(t, "Uploaded Track", track.Title) assert.Equal(t, models.TrackStatusUploading, track.Status) assert.NotEmpty(t, track.FilePath) // Verify DB var dbTrack models.Track db.First(&dbTrack, "id = ?", track.ID) assert.Equal(t, "Uploaded Track", dbTrack.Title) // Wait for async processing to finish to avoid "Log in goroutine after Test has completed" assert.Eventually(t, func() bool { db.First(&dbTrack, "id = ?", track.ID) return dbTrack.Status != models.TrackStatusUploading }, 2*time.Second, 100*time.Millisecond) }