From 62a8d85772391a776cd75ca35e0148b774b4c515 Mon Sep 17 00:00:00 2001 From: Deluan Date: Wed, 12 Nov 2025 14:35:14 -0500 Subject: [PATCH] test(scanner): refactor tests Signed-off-by: Deluan --- scanner/scanner_selective_test.go | 348 +++++++++++++++++------------- scanner/scanner_test.go | 67 +----- 2 files changed, 209 insertions(+), 206 deletions(-) diff --git a/scanner/scanner_selective_test.go b/scanner/scanner_selective_test.go index 13d0f02cb..bf289b203 100644 --- a/scanner/scanner_selective_test.go +++ b/scanner/scanner_selective_test.go @@ -20,11 +20,12 @@ import ( "github.com/navidrome/navidrome/scanner" "github.com/navidrome/navidrome/server/events" "github.com/navidrome/navidrome/tests" + "github.com/navidrome/navidrome/utils/slice" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) -var _ = Describe("Selective Scan - Deleted Child Folders", Ordered, func() { +var _ = Describe("ScanFolders", Ordered, func() { var ctx context.Context var lib model.Library var ds model.DataStore @@ -72,169 +73,224 @@ var _ = Describe("Selective Scan - Deleted Child Folders", Ordered, func() { storagetest.Register("fake", &fsys) }) - Context("when a child folder is deleted", func() { - var ( - revolver, help func(...map[string]any) *fstest.MapFile - artistFolderID string - album1FolderID string - album2FolderID string - album1TrackIDs []string - album2TrackIDs []string - ) - - BeforeEach(func() { - // Setup template functions for creating test files - revolver = storagetest.Template(_t{"albumartist": "The Beatles", "album": "Revolver", "year": 1966}) - help = storagetest.Template(_t{"albumartist": "The Beatles", "album": "Help!", "year": 1965}) - - // Initial filesystem with nested folders - fsys.SetFiles(fstest.MapFS{ - "The Beatles/Revolver/01 - Taxman.mp3": revolver(storagetest.Track(1, "Taxman")), - "The Beatles/Revolver/02 - Eleanor Rigby.mp3": revolver(storagetest.Track(2, "Eleanor Rigby")), - "The Beatles/Help!/01 - Help!.mp3": help(storagetest.Track(1, "Help!")), - "The Beatles/Help!/02 - The Night Before.mp3": help(storagetest.Track(2, "The Night Before")), + Describe("Adding tracks to the library", func() { + It("scans specified folders recursively including all subdirectories", func() { + rock := template(_t{"albumartist": "Rock Artist", "album": "Rock Album"}) + jazz := template(_t{"albumartist": "Jazz Artist", "album": "Jazz Album"}) + pop := template(_t{"albumartist": "Pop Artist", "album": "Pop Album"}) + createFS(fstest.MapFS{ + "rock/track1.mp3": rock(track(1, "Rock Track 1")), + "rock/track2.mp3": rock(track(2, "Rock Track 2")), + "rock/subdir/track3.mp3": rock(track(3, "Rock Track 3")), + "jazz/track4.mp3": jazz(track(1, "Jazz Track 1")), + "jazz/subdir/track5.mp3": jazz(track(2, "Jazz Track 2")), + "pop/track6.mp3": pop(track(1, "Pop Track 1")), }) - // First scan - import everything - _, err := s.ScanAll(ctx, true) - Expect(err).ToNot(HaveOccurred()) + // Use the existing library from BeforeEach + // (lib is already created with the path "fake:///music") - // Verify initial state - all folders exist - folders, err := ds.Folder(ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"library_id": lib.ID}}) - Expect(err).ToNot(HaveOccurred()) - Expect(folders).To(HaveLen(4)) // root, Artist, Album1, Album2 - - // Store folder IDs for later verification - for _, f := range folders { - switch f.Name { - case "The Beatles": - artistFolderID = f.ID - case "Revolver": - album1FolderID = f.ID - case "Help!": - album2FolderID = f.ID - } + // Scan only the "rock" and "jazz" folders (including their subdirectories) + targets := []model.ScanTarget{ + {LibraryID: lib.ID, FolderPath: "rock"}, + {LibraryID: lib.ID, FolderPath: "jazz"}, } - // Verify all tracks exist - allTracks, err := ds.MediaFile(ctx).GetAll() + warnings, err := s.ScanFolders(ctx, false, targets) Expect(err).ToNot(HaveOccurred()) - Expect(allTracks).To(HaveLen(4)) + Expect(warnings).To(BeEmpty()) - // Store track IDs for later verification - for _, t := range allTracks { - if t.Album == "Revolver" { - album1TrackIDs = append(album1TrackIDs, t.ID) - } else if t.Album == "Help!" { - album2TrackIDs = append(album2TrackIDs, t.ID) - } - } + // Verify all tracks in rock and jazz folders (including subdirectories) were imported + allFiles, err := ds.MediaFile(ctx).GetAll() + Expect(err).ToNot(HaveOccurred()) - // Verify no tracks are missing initially - for _, t := range allTracks { - Expect(t.Missing).To(BeFalse()) - } + // Should have 5 tracks (all rock and jazz tracks including subdirectories) + Expect(allFiles).To(HaveLen(5)) + + // Get the file paths + paths := slice.Map(allFiles, func(mf model.MediaFile) string { + return filepath.ToSlash(mf.Path) + }) + + // Verify the correct files were scanned (including subdirectories) + Expect(paths).To(ContainElements( + "rock/track1.mp3", + "rock/track2.mp3", + "rock/subdir/track3.mp3", + "jazz/track4.mp3", + "jazz/subdir/track5.mp3", + )) + + // Verify files in the pop folder were NOT scanned + Expect(paths).ToNot(ContainElement("pop/track6.mp3")) }) + }) - It("should mark child folder and its tracks as missing when parent is scanned", func() { - // Delete the child folder (Help!) from the filesystem - fsys.SetFiles(fstest.MapFS{ - "The Beatles/Revolver/01 - Taxman.mp3": revolver(storagetest.Track(1, "Taxman")), - "The Beatles/Revolver/02 - Eleanor Rigby.mp3": revolver(storagetest.Track(2, "Eleanor Rigby")), - // "The Beatles/Help!" folder and its contents are DELETED - }) + Describe("Deleting folders", func() { + Context("when a child folder is deleted", func() { + var ( + revolver, help func(...map[string]any) *fstest.MapFile + artistFolderID string + album1FolderID string + album2FolderID string + album1TrackIDs []string + album2TrackIDs []string + ) - // Run selective scan on the parent folder (Artist) - // This simulates what the watcher does when a child folder is deleted - _, err := s.ScanFolders(ctx, false, []model.ScanTarget{ - {LibraryID: lib.ID, FolderPath: "The Beatles"}, - }) - Expect(err).ToNot(HaveOccurred()) + BeforeEach(func() { + // Setup template functions for creating test files + revolver = storagetest.Template(_t{"albumartist": "The Beatles", "album": "Revolver", "year": 1966}) + help = storagetest.Template(_t{"albumartist": "The Beatles", "album": "Help!", "year": 1965}) - // Verify the deleted child folder is now marked as missing - deletedFolder, err := ds.Folder(ctx).Get(album2FolderID) - Expect(err).ToNot(HaveOccurred()) - Expect(deletedFolder.Missing).To(BeTrue(), "Deleted child folder should be marked as missing") + // Initial filesystem with nested folders + fsys.SetFiles(fstest.MapFS{ + "The Beatles/Revolver/01 - Taxman.mp3": revolver(storagetest.Track(1, "Taxman")), + "The Beatles/Revolver/02 - Eleanor Rigby.mp3": revolver(storagetest.Track(2, "Eleanor Rigby")), + "The Beatles/Help!/01 - Help!.mp3": help(storagetest.Track(1, "Help!")), + "The Beatles/Help!/02 - The Night Before.mp3": help(storagetest.Track(2, "The Night Before")), + }) - // Verify the deleted folder's tracks are marked as missing - for _, trackID := range album2TrackIDs { - track, err := ds.MediaFile(ctx).Get(trackID) + // First scan - import everything + _, err := s.ScanAll(ctx, true) Expect(err).ToNot(HaveOccurred()) - Expect(track.Missing).To(BeTrue(), "Track in deleted folder should be marked as missing") - } - // Verify the parent folder is still present and not marked as missing - parentFolder, err := ds.Folder(ctx).Get(artistFolderID) - Expect(err).ToNot(HaveOccurred()) - Expect(parentFolder.Missing).To(BeFalse(), "Parent folder should not be marked as missing") - - // Verify the sibling folder and its tracks are still present and not missing - siblingFolder, err := ds.Folder(ctx).Get(album1FolderID) - Expect(err).ToNot(HaveOccurred()) - Expect(siblingFolder.Missing).To(BeFalse(), "Sibling folder should not be marked as missing") - - for _, trackID := range album1TrackIDs { - track, err := ds.MediaFile(ctx).Get(trackID) + // Verify initial state - all folders exist + folders, err := ds.Folder(ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"library_id": lib.ID}}) Expect(err).ToNot(HaveOccurred()) - Expect(track.Missing).To(BeFalse(), "Track in sibling folder should not be marked as missing") - } - }) + Expect(folders).To(HaveLen(4)) // root, Artist, Album1, Album2 - It("should mark deeply nested child folders as missing", func() { - // Add a deeply nested folder structure - fsys.SetFiles(fstest.MapFS{ - "The Beatles/Revolver/01 - Taxman.mp3": revolver(storagetest.Track(1, "Taxman")), - "The Beatles/Revolver/02 - Eleanor Rigby.mp3": revolver(storagetest.Track(2, "Eleanor Rigby")), - "The Beatles/Help!/01 - Help!.mp3": help(storagetest.Track(1, "Help!")), - "The Beatles/Help!/02 - The Night Before.mp3": help(storagetest.Track(2, "The Night Before")), - "The Beatles/Help!/Bonus/01 - Bonus Track.mp3": help(storagetest.Track(99, "Bonus Track")), - "The Beatles/Help!/Bonus/Nested/01 - Deep Track.mp3": help(storagetest.Track(100, "Deep Track")), - }) - - // Rescan to import the new nested structure - _, err := s.ScanAll(ctx, true) - Expect(err).ToNot(HaveOccurred()) - - // Verify nested folders were created - allFolders, err := ds.Folder(ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"library_id": lib.ID}}) - Expect(err).ToNot(HaveOccurred()) - Expect(len(allFolders)).To(BeNumerically(">", 4), "Should have more folders with nested structure") - - // Now delete the entire Help! folder including nested children - fsys.SetFiles(fstest.MapFS{ - "The Beatles/Revolver/01 - Taxman.mp3": revolver(storagetest.Track(1, "Taxman")), - "The Beatles/Revolver/02 - Eleanor Rigby.mp3": revolver(storagetest.Track(2, "Eleanor Rigby")), - // All Help! subfolders are deleted - }) - - // Run selective scan on parent - _, err = s.ScanFolders(ctx, false, []model.ScanTarget{ - {LibraryID: lib.ID, FolderPath: "The Beatles"}, - }) - Expect(err).ToNot(HaveOccurred()) - - // Verify all Help! folders (including nested ones) are marked as missing - missingFolders, err := ds.Folder(ctx).GetAll(model.QueryOptions{ - Filters: squirrel.And{ - squirrel.Eq{"library_id": lib.ID}, - squirrel.Eq{"missing": true}, - }, - }) - Expect(err).ToNot(HaveOccurred()) - Expect(len(missingFolders)).To(BeNumerically(">", 0), "At least one folder should be marked as missing") - - // Verify all tracks in deleted folders are marked as missing - allTracks, err := ds.MediaFile(ctx).GetAll() - Expect(err).ToNot(HaveOccurred()) - Expect(allTracks).To(HaveLen(6)) - - for _, track := range allTracks { - if track.Album == "Help!" { - Expect(track.Missing).To(BeTrue(), "All tracks in deleted Help! folder should be marked as missing") - } else if track.Album == "Revolver" { - Expect(track.Missing).To(BeFalse(), "Tracks in Revolver folder should not be marked as missing") + // Store folder IDs for later verification + for _, f := range folders { + switch f.Name { + case "The Beatles": + artistFolderID = f.ID + case "Revolver": + album1FolderID = f.ID + case "Help!": + album2FolderID = f.ID + } } - } + + // Verify all tracks exist + allTracks, err := ds.MediaFile(ctx).GetAll() + Expect(err).ToNot(HaveOccurred()) + Expect(allTracks).To(HaveLen(4)) + + // Store track IDs for later verification + for _, t := range allTracks { + if t.Album == "Revolver" { + album1TrackIDs = append(album1TrackIDs, t.ID) + } else if t.Album == "Help!" { + album2TrackIDs = append(album2TrackIDs, t.ID) + } + } + + // Verify no tracks are missing initially + for _, t := range allTracks { + Expect(t.Missing).To(BeFalse()) + } + }) + + It("should mark child folder and its tracks as missing when parent is scanned", func() { + // Delete the child folder (Help!) from the filesystem + fsys.SetFiles(fstest.MapFS{ + "The Beatles/Revolver/01 - Taxman.mp3": revolver(storagetest.Track(1, "Taxman")), + "The Beatles/Revolver/02 - Eleanor Rigby.mp3": revolver(storagetest.Track(2, "Eleanor Rigby")), + // "The Beatles/Help!" folder and its contents are DELETED + }) + + // Run selective scan on the parent folder (Artist) + // This simulates what the watcher does when a child folder is deleted + _, err := s.ScanFolders(ctx, false, []model.ScanTarget{ + {LibraryID: lib.ID, FolderPath: "The Beatles"}, + }) + Expect(err).ToNot(HaveOccurred()) + + // Verify the deleted child folder is now marked as missing + deletedFolder, err := ds.Folder(ctx).Get(album2FolderID) + Expect(err).ToNot(HaveOccurred()) + Expect(deletedFolder.Missing).To(BeTrue(), "Deleted child folder should be marked as missing") + + // Verify the deleted folder's tracks are marked as missing + for _, trackID := range album2TrackIDs { + track, err := ds.MediaFile(ctx).Get(trackID) + Expect(err).ToNot(HaveOccurred()) + Expect(track.Missing).To(BeTrue(), "Track in deleted folder should be marked as missing") + } + + // Verify the parent folder is still present and not marked as missing + parentFolder, err := ds.Folder(ctx).Get(artistFolderID) + Expect(err).ToNot(HaveOccurred()) + Expect(parentFolder.Missing).To(BeFalse(), "Parent folder should not be marked as missing") + + // Verify the sibling folder and its tracks are still present and not missing + siblingFolder, err := ds.Folder(ctx).Get(album1FolderID) + Expect(err).ToNot(HaveOccurred()) + Expect(siblingFolder.Missing).To(BeFalse(), "Sibling folder should not be marked as missing") + + for _, trackID := range album1TrackIDs { + track, err := ds.MediaFile(ctx).Get(trackID) + Expect(err).ToNot(HaveOccurred()) + Expect(track.Missing).To(BeFalse(), "Track in sibling folder should not be marked as missing") + } + }) + + It("should mark deeply nested child folders as missing", func() { + // Add a deeply nested folder structure + fsys.SetFiles(fstest.MapFS{ + "The Beatles/Revolver/01 - Taxman.mp3": revolver(storagetest.Track(1, "Taxman")), + "The Beatles/Revolver/02 - Eleanor Rigby.mp3": revolver(storagetest.Track(2, "Eleanor Rigby")), + "The Beatles/Help!/01 - Help!.mp3": help(storagetest.Track(1, "Help!")), + "The Beatles/Help!/02 - The Night Before.mp3": help(storagetest.Track(2, "The Night Before")), + "The Beatles/Help!/Bonus/01 - Bonus Track.mp3": help(storagetest.Track(99, "Bonus Track")), + "The Beatles/Help!/Bonus/Nested/01 - Deep Track.mp3": help(storagetest.Track(100, "Deep Track")), + }) + + // Rescan to import the new nested structure + _, err := s.ScanAll(ctx, true) + Expect(err).ToNot(HaveOccurred()) + + // Verify nested folders were created + allFolders, err := ds.Folder(ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"library_id": lib.ID}}) + Expect(err).ToNot(HaveOccurred()) + Expect(len(allFolders)).To(BeNumerically(">", 4), "Should have more folders with nested structure") + + // Now delete the entire Help! folder including nested children + fsys.SetFiles(fstest.MapFS{ + "The Beatles/Revolver/01 - Taxman.mp3": revolver(storagetest.Track(1, "Taxman")), + "The Beatles/Revolver/02 - Eleanor Rigby.mp3": revolver(storagetest.Track(2, "Eleanor Rigby")), + // All Help! subfolders are deleted + }) + + // Run selective scan on parent + _, err = s.ScanFolders(ctx, false, []model.ScanTarget{ + {LibraryID: lib.ID, FolderPath: "The Beatles"}, + }) + Expect(err).ToNot(HaveOccurred()) + + // Verify all Help! folders (including nested ones) are marked as missing + missingFolders, err := ds.Folder(ctx).GetAll(model.QueryOptions{ + Filters: squirrel.And{ + squirrel.Eq{"library_id": lib.ID}, + squirrel.Eq{"missing": true}, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(len(missingFolders)).To(BeNumerically(">", 0), "At least one folder should be marked as missing") + + // Verify all tracks in deleted folders are marked as missing + allTracks, err := ds.MediaFile(ctx).GetAll() + Expect(err).ToNot(HaveOccurred()) + Expect(allTracks).To(HaveLen(6)) + + for _, track := range allTracks { + if track.Album == "Help!" { + Expect(track.Missing).To(BeTrue(), "All tracks in deleted Help! folder should be marked as missing") + } else if track.Album == "Revolver" { + Expect(track.Missing).To(BeFalse(), "Tracks in Revolver folder should not be marked as missing") + } + } + }) }) }) }) diff --git a/scanner/scanner_test.go b/scanner/scanner_test.go index 604561058..4dabcf893 100644 --- a/scanner/scanner_test.go +++ b/scanner/scanner_test.go @@ -34,6 +34,13 @@ type _t = map[string]any var template = storagetest.Template var track = storagetest.Track +func createFS(files fstest.MapFS) storagetest.FakeFS { + fs := storagetest.FakeFS{} + fs.SetFiles(files) + storagetest.Register("fake", &fs) + return fs +} + var _ = Describe("Scanner", Ordered, func() { var ctx context.Context var lib model.Library @@ -41,13 +48,6 @@ var _ = Describe("Scanner", Ordered, func() { var mfRepo *mockMediaFileRepo var s model.Scanner - createFS := func(files fstest.MapFS) storagetest.FakeFS { - fs := storagetest.FakeFS{} - fs.SetFiles(files) - storagetest.Register("fake", &fs) - return fs - } - BeforeAll(func() { ctx = request.WithUser(GinkgoT().Context(), model.User{ID: "123", IsAdmin: true}) tmpDir := GinkgoT().TempDir() @@ -717,59 +717,6 @@ var _ = Describe("Scanner", Ordered, func() { Expect(albumArtistStats.SongCount).To(Equal(3)) // 3 songs }) }) - - Describe("ScanFolders", func() { - It("scans specified folders recursively including all subdirectories", func() { - rock := template(_t{"albumartist": "Rock Artist", "album": "Rock Album"}) - jazz := template(_t{"albumartist": "Jazz Artist", "album": "Jazz Album"}) - pop := template(_t{"albumartist": "Pop Artist", "album": "Pop Album"}) - createFS(fstest.MapFS{ - "rock/track1.mp3": rock(track(1, "Rock Track 1")), - "rock/track2.mp3": rock(track(2, "Rock Track 2")), - "rock/subdir/track3.mp3": rock(track(3, "Rock Track 3")), - "jazz/track4.mp3": jazz(track(1, "Jazz Track 1")), - "jazz/subdir/track5.mp3": jazz(track(2, "Jazz Track 2")), - "pop/track6.mp3": pop(track(1, "Pop Track 1")), - }) - - // Use the existing library from BeforeEach - // (lib is already created with the path "fake:///music") - - // Scan only the "rock" and "jazz" folders (including their subdirectories) - targets := []model.ScanTarget{ - {LibraryID: lib.ID, FolderPath: "rock"}, - {LibraryID: lib.ID, FolderPath: "jazz"}, - } - - warnings, err := s.ScanFolders(ctx, false, targets) - Expect(err).ToNot(HaveOccurred()) - Expect(warnings).To(BeEmpty()) - - // Verify all tracks in rock and jazz folders (including subdirectories) were imported - allFiles, err := ds.MediaFile(ctx).GetAll() - Expect(err).ToNot(HaveOccurred()) - - // Should have 5 tracks (all rock and jazz tracks including subdirectories) - Expect(allFiles).To(HaveLen(5)) - - // Get the file paths - paths := slice.Map(allFiles, func(mf model.MediaFile) string { - return filepath.ToSlash(mf.Path) - }) - - // Verify the correct files were scanned (including subdirectories) - Expect(paths).To(ContainElements( - "rock/track1.mp3", - "rock/track2.mp3", - "rock/subdir/track3.mp3", - "jazz/track4.mp3", - "jazz/subdir/track5.mp3", - )) - - // Verify files in the pop folder were NOT scanned - Expect(paths).ToNot(ContainElement("pop/track6.mp3")) - }) - }) }) func createFindByPath(ctx context.Context, ds model.DataStore) func(string) (*model.MediaFile, error) {