package services import ( "context" "github.com/google/uuid" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "gorm.io/driver/sqlite" "gorm.io/gorm" "veza-backend-api/internal/models" ) func setupTestBatchUpdateDB(t *testing.T) (*TrackService, *gorm.DB, func()) { // Setup in-memory SQLite database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) // Auto-migrate err = db.AutoMigrate(&models.User{}, &models.Track{}) require.NoError(t, err) // Create test user user := &models.User{ ID: 1, Username: "testuser", Email: "test@example.com", IsActive: true, } err = db.Create(user).Error require.NoError(t, err) // Create another user user2 := &models.User{ ID: 2, Username: "otheruser", Email: "other@example.com", IsActive: true, } err = db.Create(user2).Error require.NoError(t, err) // Create logger logger := zap.NewNop() // Create service service := NewTrackService(db, logger, "test_uploads") // Cleanup function cleanup := func() { // SQLite in-memory database doesn't need explicit cleanup } return service, db, cleanup } func TestTrackService_BatchUpdateTracks_Success(t *testing.T) { service, db, cleanup := setupTestBatchUpdateDB(t) defer cleanup() ctx := context.Background() // Create tracks track1 := &models.Track{ UserID: 1, Title: "Track 1", FilePath: "/path/to/track1.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: false, Status: models.TrackStatusCompleted, } track2 := &models.Track{ UserID: 1, Title: "Track 2", FilePath: "/path/to/track2.mp3", FileSize: 2048, Format: "MP3", Duration: 240, IsPublic: false, Status: models.TrackStatusCompleted, } err := db.Create(track1).Error require.NoError(t, err) err = db.Create(track2).Error require.NoError(t, err) // Batch update updates := map[string]interface{}{ "is_public": true, "genre": "Electronic", } result, err := service.BatchUpdateTracks(ctx, []int64{track1.ID, track2.ID}, 1, updates) require.NoError(t, err) require.NotNil(t, result) // Verify results assert.Equal(t, 2, len(result.Updated)) assert.Contains(t, result.Updated, track1.ID) assert.Contains(t, result.Updated, track2.ID) assert.Equal(t, 0, len(result.Failed)) // Verify tracks are updated in database var updatedTrack1, updatedTrack2 models.Track db.First(&updatedTrack1, track1.ID) db.First(&updatedTrack2, track2.ID) assert.True(t, updatedTrack1.IsPublic) assert.True(t, updatedTrack2.IsPublic) assert.Equal(t, "Electronic", updatedTrack1.Genre) assert.Equal(t, "Electronic", updatedTrack2.Genre) } func TestTrackService_BatchUpdateTracks_EmptyList(t *testing.T) { service, _, cleanup := setupTestBatchUpdateDB(t) defer cleanup() ctx := context.Background() updates := map[string]interface{}{ "is_public": true, } result, err := service.BatchUpdateTracks(ctx, []int64{}, 1, updates) require.NoError(t, err) require.NotNil(t, result) assert.Equal(t, 0, len(result.Updated)) assert.Equal(t, 0, len(result.Failed)) } func TestTrackService_BatchUpdateTracks_ExceedsMaxBatchSize(t *testing.T) { service, _, cleanup := setupTestBatchUpdateDB(t) defer cleanup() ctx := context.Background() // Create a list with more than 100 tracks trackIDs := make([]int64, 101) for i := range trackIDs { trackIDs[i] = int64(i + 1) } updates := map[string]interface{}{ "is_public": true, } result, err := service.BatchUpdateTracks(ctx, trackIDs, 1, updates) assert.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "batch size exceeds maximum") } func TestTrackService_BatchUpdateTracks_EmptyUpdates(t *testing.T) { service, _, cleanup := setupTestBatchUpdateDB(t) defer cleanup() ctx := context.Background() result, err := service.BatchUpdateTracks(ctx, []int64{1, 2}, 1, map[string]interface{}{}) assert.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "updates cannot be empty") } func TestTrackService_BatchUpdateTracks_NotFound(t *testing.T) { service, _, cleanup := setupTestBatchUpdateDB(t) defer cleanup() ctx := context.Background() updates := map[string]interface{}{ "is_public": true, } result, err := service.BatchUpdateTracks(ctx, []int64{999, 1000}, 1, updates) require.NoError(t, err) require.NotNil(t, result) assert.Equal(t, 0, len(result.Updated)) assert.Equal(t, 2, len(result.Failed)) assert.Equal(t, int64(999), result.Failed[0].TrackID) assert.Equal(t, "track not found", result.Failed[0].Error) } func TestTrackService_BatchUpdateTracks_Forbidden(t *testing.T) { service, db, cleanup := setupTestBatchUpdateDB(t) defer cleanup() ctx := context.Background() // Create track owned by user 1 track1 := &models.Track{ UserID: 1, Title: "Track 1", FilePath: "/path/to/track1.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: false, Status: models.TrackStatusCompleted, } err := db.Create(track1).Error require.NoError(t, err) // Try to update as user 2 updates := map[string]interface{}{ "is_public": true, } result, err := service.BatchUpdateTracks(ctx, []int64{track1.ID}, 2, updates) require.NoError(t, err) require.NotNil(t, result) assert.Equal(t, 0, len(result.Updated)) assert.Equal(t, 1, len(result.Failed)) assert.Equal(t, track1.ID, result.Failed[0].TrackID) assert.Contains(t, result.Failed[0].Error, "forbidden") // Verify track is not updated var updatedTrack models.Track db.First(&updatedTrack, track1.ID) assert.False(t, updatedTrack.IsPublic) } func TestTrackService_BatchUpdateTracks_PartialSuccess(t *testing.T) { service, db, cleanup := setupTestBatchUpdateDB(t) defer cleanup() ctx := context.Background() // Create tracks track1 := &models.Track{ UserID: 1, Title: "Track 1", FilePath: "/path/to/track1.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: false, Status: models.TrackStatusCompleted, } track2 := &models.Track{ UserID: 2, // Owned by different user Title: "Track 2", FilePath: "/path/to/track2.mp3", FileSize: 2048, Format: "MP3", Duration: 240, IsPublic: false, Status: models.TrackStatusCompleted, } err := db.Create(track1).Error require.NoError(t, err) err = db.Create(track2).Error require.NoError(t, err) // Try to update both as user 1 updates := map[string]interface{}{ "is_public": true, } result, err := service.BatchUpdateTracks(ctx, []int64{track1.ID, track2.ID}, 1, updates) require.NoError(t, err) require.NotNil(t, result) // Track1 should be updated, track2 should fail assert.Equal(t, 1, len(result.Updated)) assert.Contains(t, result.Updated, track1.ID) assert.Equal(t, 1, len(result.Failed)) assert.Equal(t, track2.ID, result.Failed[0].TrackID) assert.Contains(t, result.Failed[0].Error, "forbidden") // Verify track1 is updated, track2 is not var updatedTrack1, updatedTrack2 models.Track db.First(&updatedTrack1, track1.ID) db.First(&updatedTrack2, track2.ID) assert.True(t, updatedTrack1.IsPublic) assert.False(t, updatedTrack2.IsPublic) } func TestTrackService_BatchUpdateTracks_InvalidTitle(t *testing.T) { service, _, cleanup := setupTestBatchUpdateDB(t) defer cleanup() ctx := context.Background() updates := map[string]interface{}{ "title": "", // Empty title } result, err := service.BatchUpdateTracks(ctx, []int64{1}, 1, updates) assert.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "title cannot be empty") } func TestTrackService_BatchUpdateTracks_InvalidYear(t *testing.T) { service, _, cleanup := setupTestBatchUpdateDB(t) defer cleanup() ctx := context.Background() updates := map[string]interface{}{ "year": 1800, // Year too old } result, err := service.BatchUpdateTracks(ctx, []int64{1}, 1, updates) assert.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "year must be between") } func TestTrackService_BatchUpdateTracks_InvalidIsPublic(t *testing.T) { service, _, cleanup := setupTestBatchUpdateDB(t) defer cleanup() ctx := context.Background() updates := map[string]interface{}{ "is_public": "not a boolean", // Invalid type } result, err := service.BatchUpdateTracks(ctx, []int64{1}, 1, updates) assert.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "invalid value for is_public") } func TestTrackService_BatchUpdateTracks_UnauthorizedField(t *testing.T) { service, db, cleanup := setupTestBatchUpdateDB(t) defer cleanup() ctx := context.Background() // Create track track := &models.Track{ UserID: 1, Title: "Track 1", FilePath: "/path/to/track1.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: false, Status: models.TrackStatusCompleted, } err := db.Create(track).Error require.NoError(t, err) // Try to update with unauthorized field (user_id should not be updatable) updates := map[string]interface{}{ "is_public": true, "user_id": 999, // This should be ignored } result, err := service.BatchUpdateTracks(ctx, []int64{track.ID}, 1, updates) require.NoError(t, err) require.NotNil(t, result) // Should succeed with only is_public updated assert.Equal(t, 1, len(result.Updated)) // Verify user_id is not changed var updatedTrack models.Track db.First(&updatedTrack, track.ID) assert.Equal(t, int64(1), updatedTrack.UserID) assert.True(t, updatedTrack.IsPublic) }