package services import ( "context" "fmt" "testing" "time" "github.com/google/uuid" "veza-backend-api/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func setupTestTrackSearchService(t *testing.T) (*TrackSearchService, *gorm.DB, uuid.UUID, 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.Track{}, &models.User{}) require.NoError(t, err) // Create test user userID := uuid.New() user := &models.User{ ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true, } err = db.Create(user).Error require.NoError(t, err) // Setup service service := NewTrackSearchService(db) // Cleanup function cleanup := func() { // Database will be closed automatically } return service, db, userID, cleanup } func TestTrackSearchService_SearchTracks_FullTextSearch(t *testing.T) { service, db, userID, cleanup := setupTestTrackSearchService(t) defer cleanup() ctx := context.Background() // Create test tracks track1 := &models.Track{ UserID: userID, Title: "Test Track 1", Artist: "Artist One", FilePath: "/test/track1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, Genre: "Rock", IsPublic: true, Status: models.TrackStatusCompleted, } err := db.Create(track1).Error require.NoError(t, err) track2 := &models.Track{ UserID: userID, Title: "Another Track", Artist: "Artist Two", FilePath: "/test/track2.mp3", FileSize: 6 * 1024 * 1024, Format: "FLAC", Duration: 200, Genre: "Pop", IsPublic: true, Status: models.TrackStatusCompleted, } err = db.Create(track2).Error require.NoError(t, err) // Test full-text search results, total, err := service.SearchTracks(ctx, TrackSearchParams{ Query: "Test", Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(1), total) assert.Len(t, results, 1) assert.Equal(t, "Test Track 1", results[0].Title) } func TestTrackSearchService_SearchTracks_GenreFilter(t *testing.T) { service, db, userID, cleanup := setupTestTrackSearchService(t) defer cleanup() ctx := context.Background() // Create test tracks track1 := &models.Track{ UserID: userID, Title: "Rock Track", Artist: "Rock Artist", FilePath: "/test/track1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, Genre: "Rock", IsPublic: true, Status: models.TrackStatusCompleted, } err := db.Create(track1).Error require.NoError(t, err) track2 := &models.Track{ UserID: userID, Title: "Pop Track", Artist: "Pop Artist", FilePath: "/test/track2.mp3", FileSize: 6 * 1024 * 1024, Format: "FLAC", Duration: 200, Genre: "Pop", IsPublic: true, Status: models.TrackStatusCompleted, } err = db.Create(track2).Error require.NoError(t, err) // Test genre filter genre := "Rock" results, total, err := service.SearchTracks(ctx, TrackSearchParams{ Genre: &genre, Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(1), total) assert.Len(t, results, 1) assert.Equal(t, "Rock Track", results[0].Title) } func TestTrackSearchService_SearchTracks_DurationFilter(t *testing.T) { service, db, userID, cleanup := setupTestTrackSearchService(t) defer cleanup() ctx := context.Background() // Create test tracks track1 := &models.Track{ UserID: userID, Title: "Short Track", Artist: "Artist One", FilePath: "/test/track1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 120, // 2 minutes Genre: "Rock", IsPublic: true, Status: models.TrackStatusCompleted, } err := db.Create(track1).Error require.NoError(t, err) track2 := &models.Track{ UserID: userID, Title: "Long Track", Artist: "Artist Two", FilePath: "/test/track2.mp3", FileSize: 6 * 1024 * 1024, Format: "FLAC", Duration: 300, // 5 minutes Genre: "Pop", IsPublic: true, Status: models.TrackStatusCompleted, } err = db.Create(track2).Error require.NoError(t, err) // Test min duration filter minDuration := 200 results, total, err := service.SearchTracks(ctx, TrackSearchParams{ MinDuration: &minDuration, Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(1), total) assert.Len(t, results, 1) assert.Equal(t, "Long Track", results[0].Title) // Test max duration filter maxDuration := 150 results, total, err = service.SearchTracks(ctx, TrackSearchParams{ MaxDuration: &maxDuration, Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(1), total) assert.Len(t, results, 1) assert.Equal(t, "Short Track", results[0].Title) } func TestTrackSearchService_SearchTracks_FormatFilter(t *testing.T) { service, db, userID, cleanup := setupTestTrackSearchService(t) defer cleanup() ctx := context.Background() // Create test tracks track1 := &models.Track{ UserID: userID, Title: "MP3 Track", Artist: "Artist One", FilePath: "/test/track1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, Genre: "Rock", IsPublic: true, Status: models.TrackStatusCompleted, } err := db.Create(track1).Error require.NoError(t, err) track2 := &models.Track{ UserID: userID, Title: "FLAC Track", Artist: "Artist Two", FilePath: "/test/track2.flac", FileSize: 6 * 1024 * 1024, Format: "FLAC", Duration: 200, Genre: "Pop", IsPublic: true, Status: models.TrackStatusCompleted, } err = db.Create(track2).Error require.NoError(t, err) // Test format filter format := "MP3" results, total, err := service.SearchTracks(ctx, TrackSearchParams{ Format: &format, Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(1), total) assert.Len(t, results, 1) assert.Equal(t, "MP3 Track", results[0].Title) } func TestTrackSearchService_SearchTracks_DateRangeFilter(t *testing.T) { service, db, userID, cleanup := setupTestTrackSearchService(t) defer cleanup() ctx := context.Background() // Create test tracks with different dates now := time.Now() oldDate := now.AddDate(0, -2, 0) // 2 months ago recentDate := now.AddDate(0, 0, -5) // 5 days ago track1 := &models.Track{ UserID: userID, Title: "Old Track", Artist: "Artist One", FilePath: "/test/track1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, Genre: "Rock", IsPublic: true, Status: models.TrackStatusCompleted, CreatedAt: oldDate, } err := db.Create(track1).Error require.NoError(t, err) track2 := &models.Track{ UserID: userID, Title: "Recent Track", Artist: "Artist Two", FilePath: "/test/track2.mp3", FileSize: 6 * 1024 * 1024, Format: "FLAC", Duration: 200, Genre: "Pop", IsPublic: true, Status: models.TrackStatusCompleted, CreatedAt: recentDate, } err = db.Create(track2).Error require.NoError(t, err) // Test min date filter minDate := now.AddDate(0, -1, 0).Format(time.RFC3339) // 1 month ago results, total, err := service.SearchTracks(ctx, TrackSearchParams{ MinDate: &minDate, Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(1), total) assert.Len(t, results, 1) assert.Equal(t, "Recent Track", results[0].Title) } func TestTrackSearchService_SearchTracks_Pagination(t *testing.T) { service, db, userID, cleanup := setupTestTrackSearchService(t) defer cleanup() ctx := context.Background() // Create multiple test tracks for i := 0; i < 25; i++ { track := &models.Track{ UserID: userID, Title: "Track " + fmt.Sprintf("%d", i+1), Artist: "Artist", FilePath: fmt.Sprintf("/test/track%d.mp3", i+1), FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, Genre: "Rock", IsPublic: true, Status: models.TrackStatusCompleted, } err := db.Create(track).Error require.NoError(t, err) } // Test pagination - first page results, total, err := service.SearchTracks(ctx, TrackSearchParams{ Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(25), total) assert.Len(t, results, 10) // Test pagination - second page results, total, err = service.SearchTracks(ctx, TrackSearchParams{ Page: 2, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(25), total) assert.Len(t, results, 10) // Test pagination - third page results, total, err = service.SearchTracks(ctx, TrackSearchParams{ Page: 3, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(25), total) assert.Len(t, results, 5) // Only 5 remaining } func TestTrackSearchService_SearchTracks_Sorting(t *testing.T) { service, db, userID, cleanup := setupTestTrackSearchService(t) defer cleanup() ctx := context.Background() // Create test tracks track1 := &models.Track{ UserID: userID, Title: "A Track", Artist: "Artist One", FilePath: "/test/track1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, Genre: "Rock", IsPublic: true, Status: models.TrackStatusCompleted, } err := db.Create(track1).Error require.NoError(t, err) track2 := &models.Track{ UserID: userID, Title: "Z Track", Artist: "Artist Two", FilePath: "/test/track2.mp3", FileSize: 6 * 1024 * 1024, Format: "FLAC", Duration: 200, Genre: "Pop", IsPublic: true, Status: models.TrackStatusCompleted, } err = db.Create(track2).Error require.NoError(t, err) // Test sorting by title ascending results, total, err := service.SearchTracks(ctx, TrackSearchParams{ SortBy: "title", SortOrder: "asc", Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(2), total) assert.Len(t, results, 2) assert.Equal(t, "A Track", results[0].Title) assert.Equal(t, "Z Track", results[1].Title) } func TestTrackSearchService_SearchTracks_OnlyPublic(t *testing.T) { service, db, userID, cleanup := setupTestTrackSearchService(t) defer cleanup() ctx := context.Background() // Create public track track1 := &models.Track{ UserID: userID, Title: "Public Track", Artist: "Artist One", FilePath: "/test/track1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, Genre: "Rock", IsPublic: true, Status: models.TrackStatusCompleted, } err := db.Create(track1).Error require.NoError(t, err) // Create private track track2 := &models.Track{ UserID: userID, Title: "Private Track", Artist: "Artist Two", FilePath: "/test/track2.mp3", FileSize: 6 * 1024 * 1024, Format: "FLAC", Duration: 200, Genre: "Pop", IsPublic: false, Status: models.TrackStatusCompleted, } err = db.Create(track2).Error require.NoError(t, err) // Force IsPublic to false (GORM might use default value true) err = db.Model(track2).Update("is_public", false).Error require.NoError(t, err) // Test that only public tracks are returned results, total, err := service.SearchTracks(ctx, TrackSearchParams{ Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(1), total) assert.Len(t, results, 1) assert.Equal(t, "Public Track", results[0].Title) } func TestTrackSearchService_SearchTracks_CombinedFilters(t *testing.T) { service, db, userID, cleanup := setupTestTrackSearchService(t) defer cleanup() ctx := context.Background() // Create test tracks with different attributes track1 := &models.Track{ UserID: userID, Title: "Rock MP3 Track", Artist: "Rock Artist", FilePath: "/test/track1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, Genre: "Rock", IsPublic: true, Status: models.TrackStatusCompleted, } err := db.Create(track1).Error require.NoError(t, err) track2 := &models.Track{ UserID: userID, Title: "Pop FLAC Track", Artist: "Pop Artist", FilePath: "/test/track2.flac", FileSize: 6 * 1024 * 1024, Format: "FLAC", Duration: 200, Genre: "Pop", IsPublic: true, Status: models.TrackStatusCompleted, } err = db.Create(track2).Error require.NoError(t, err) track3 := &models.Track{ UserID: userID, Title: "Rock FLAC Track", Artist: "Rock Artist 2", FilePath: "/test/track3.flac", FileSize: 7 * 1024 * 1024, Format: "FLAC", Duration: 250, Genre: "Rock", IsPublic: true, Status: models.TrackStatusCompleted, } err = db.Create(track3).Error require.NoError(t, err) // Test combined filters: genre + format genre := "Rock" format := "MP3" results, total, err := service.SearchTracks(ctx, TrackSearchParams{ Genre: &genre, Format: &format, Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(1), total) assert.Len(t, results, 1) assert.Equal(t, "Rock MP3 Track", results[0].Title) // Test combined filters: genre + duration range minDuration := 200 maxDuration := 300 results, total, err = service.SearchTracks(ctx, TrackSearchParams{ Genre: &genre, MinDuration: &minDuration, MaxDuration: &maxDuration, Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(1), total) assert.Len(t, results, 1) assert.Equal(t, "Rock FLAC Track", results[0].Title) } func TestTrackSearchService_SearchTracks_SortByPopularity(t *testing.T) { service, db, userID, cleanup := setupTestTrackSearchService(t) defer cleanup() ctx := context.Background() // Create test tracks with different like counts track1 := &models.Track{ UserID: userID, Title: "Low Likes Track", Artist: "Artist One", FilePath: "/test/track1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, Genre: "Rock", IsPublic: true, Status: models.TrackStatusCompleted, LikeCount: 5, } err := db.Create(track1).Error require.NoError(t, err) track2 := &models.Track{ UserID: userID, Title: "High Likes Track", Artist: "Artist Two", FilePath: "/test/track2.mp3", FileSize: 6 * 1024 * 1024, Format: "FLAC", Duration: 200, Genre: "Pop", IsPublic: true, Status: models.TrackStatusCompleted, LikeCount: 50, } err = db.Create(track2).Error require.NoError(t, err) // Test sorting by popularity (descending) results, total, err := service.SearchTracks(ctx, TrackSearchParams{ SortBy: "popularity", SortOrder: "desc", Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(2), total) assert.Len(t, results, 2) assert.Equal(t, "High Likes Track", results[0].Title) // Highest likes first assert.Equal(t, "Low Likes Track", results[1].Title) } func TestTrackSearchService_SearchTracks_SortByPlayCount(t *testing.T) { service, db, userID, cleanup := setupTestTrackSearchService(t) defer cleanup() ctx := context.Background() // Create test tracks with different play counts track1 := &models.Track{ UserID: userID, Title: "Low Plays Track", Artist: "Artist One", FilePath: "/test/track1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, Genre: "Rock", IsPublic: true, Status: models.TrackStatusCompleted, PlayCount: 10, } err := db.Create(track1).Error require.NoError(t, err) track2 := &models.Track{ UserID: userID, Title: "High Plays Track", Artist: "Artist Two", FilePath: "/test/track2.mp3", FileSize: 6 * 1024 * 1024, Format: "FLAC", Duration: 200, Genre: "Pop", IsPublic: true, Status: models.TrackStatusCompleted, PlayCount: 100, } err = db.Create(track2).Error require.NoError(t, err) // Test sorting by play_count (descending) results, total, err := service.SearchTracks(ctx, TrackSearchParams{ SortBy: "play_count", SortOrder: "desc", Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(2), total) assert.Len(t, results, 2) assert.Equal(t, "High Plays Track", results[0].Title) // Highest plays first assert.Equal(t, "Low Plays Track", results[1].Title) } func TestTrackSearchService_SearchTracks_SortByTitle(t *testing.T) { service, db, userID, cleanup := setupTestTrackSearchService(t) defer cleanup() ctx := context.Background() // Create test tracks with different titles track1 := &models.Track{ UserID: userID, Title: "Zebra Track", Artist: "Artist One", FilePath: "/test/track1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, Genre: "Rock", IsPublic: true, Status: models.TrackStatusCompleted, } err := db.Create(track1).Error require.NoError(t, err) track2 := &models.Track{ UserID: userID, Title: "Alpha Track", Artist: "Artist Two", FilePath: "/test/track2.mp3", FileSize: 6 * 1024 * 1024, Format: "FLAC", Duration: 200, Genre: "Pop", IsPublic: true, Status: models.TrackStatusCompleted, } err = db.Create(track2).Error require.NoError(t, err) // Test sorting by title (ascending) results, total, err := service.SearchTracks(ctx, TrackSearchParams{ SortBy: "title", SortOrder: "asc", Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(2), total) assert.Len(t, results, 2) assert.Equal(t, "Alpha Track", results[0].Title) // Alphabetically first assert.Equal(t, "Zebra Track", results[1].Title) } func TestTrackSearchService_SearchTracks_SortByCommentCount(t *testing.T) { service, db, userID, cleanup := setupTestTrackSearchService(t) defer cleanup() ctx := context.Background() // Create test tracks track1 := &models.Track{ UserID: userID, Title: "Track With Comments", Artist: "Artist One", FilePath: "/test/track1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, Genre: "Rock", IsPublic: true, Status: models.TrackStatusCompleted, } err := db.Create(track1).Error require.NoError(t, err) track2 := &models.Track{ UserID: userID, Title: "Track Without Comments", Artist: "Artist Two", FilePath: "/test/track2.mp3", FileSize: 6 * 1024 * 1024, Format: "FLAC", Duration: 200, Genre: "Pop", IsPublic: true, Status: models.TrackStatusCompleted, } err = db.Create(track2).Error require.NoError(t, err) // Create comments for track1 err = db.AutoMigrate(&models.TrackComment{}) require.NoError(t, err) comment1 := &models.TrackComment{ TrackID: track1.ID, UserID: userID, Content: "Great track!", } err = db.Create(comment1).Error require.NoError(t, err) comment2 := &models.TrackComment{ TrackID: track1.ID, UserID: userID, Content: "Love it!", } err = db.Create(comment2).Error require.NoError(t, err) // Test sorting by comment_count (descending) results, total, err := service.SearchTracks(ctx, TrackSearchParams{ SortBy: "comment_count", SortOrder: "desc", Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(2), total) assert.Len(t, results, 2) assert.Equal(t, "Track With Comments", results[0].Title) // Most comments first assert.Equal(t, "Track Without Comments", results[1].Title) } func TestTrackSearchService_SearchTracks_InvalidSortBy_FallbackToCreatedAt(t *testing.T) { // v0.903: SQL injection prevention - invalid sortBy falls back to created_at DESC service, db, userID, cleanup := setupTestTrackSearchService(t) defer cleanup() ctx := context.Background() // Create test tracks track1 := &models.Track{ UserID: userID, Title: "First Track", Artist: "Artist", FilePath: "/test/track1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, Genre: "Rock", IsPublic: true, Status: models.TrackStatusCompleted, } err := db.Create(track1).Error require.NoError(t, err) track2 := &models.Track{ UserID: userID, Title: "Second Track", Artist: "Artist", FilePath: "/test/track2.mp3", FileSize: 6 * 1024 * 1024, Format: "FLAC", Duration: 200, Genre: "Pop", IsPublic: true, Status: models.TrackStatusCompleted, } err = db.Create(track2).Error require.NoError(t, err) // Malicious sortBy - should fallback to created_at DESC (no error, no SQL injection) maliciousSortBy := "invalid'; DROP TABLE tracks;--" results, total, err := service.SearchTracks(ctx, TrackSearchParams{ SortBy: maliciousSortBy, Page: 1, Limit: 10, }) assert.NoError(t, err) assert.Equal(t, int64(2), total) assert.Len(t, results, 2) // Should return results (fallback to created_at DESC applied, no injection) assert.Contains(t, []string{results[0].Title, results[1].Title}, "First Track") assert.Contains(t, []string{results[0].Title, results[1].Title}, "Second Track") }