From a902850471b5003e137f720df79c0a6dc0e2f116 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Mon, 16 Mar 2026 01:08:48 -0400 Subject: [PATCH] test(quick-19): add multi-root path resolution tests - Add TestResolveM3UPath with 7 cases: abs, relative, second root, no match, empty roots, nil knownPaths - Add TestToRelativePathMultiRoot with 5 cases: first root, second root, no root, empty roots, empty string - Add TestRemoveM3UEntriesMultiRoot: entries from different roots removed correctly - Add TestFindM3UEntryMultiRoot: entry relative to second root found - Add TestReplaceM3UEntryPathsMultiRoot: replacement under second root applied --- backend/playlist/m3u_test.go | 272 +++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) diff --git a/backend/playlist/m3u_test.go b/backend/playlist/m3u_test.go index 585e155..7222866 100644 --- a/backend/playlist/m3u_test.go +++ b/backend/playlist/m3u_test.go @@ -866,3 +866,275 @@ func TestFindM3UEntry(t *testing.T) { t.Errorf("expected index -1, got %d", idx) } } + +func TestResolveM3UPath(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + relativePath string + libraryRoots []string + knownPaths map[string]struct{} + expected string + }{ + { + name: "absolute path returned as-is", + relativePath: "/music/song.flac", + libraryRoots: []string{"/other"}, + knownPaths: nil, + expected: "/music/song.flac", + }, + { + name: "relative path resolves against first root", + relativePath: "Artist/song.flac", + libraryRoots: []string{"/music", "/archive"}, + knownPaths: map[string]struct{}{ + "/music/Artist/song.flac": {}, + }, + expected: "/music/Artist/song.flac", + }, + { + name: "relative path resolves against second root", + relativePath: "Artist/song.flac", + libraryRoots: []string{"/music", "/archive"}, + knownPaths: map[string]struct{}{ + "/archive/Artist/song.flac": {}, + }, + expected: "/archive/Artist/song.flac", + }, + { + name: "no match falls back to first root", + relativePath: "Artist/song.flac", + libraryRoots: []string{"/music", "/archive"}, + knownPaths: map[string]struct{}{}, + expected: "/music/Artist/song.flac", + }, + { + name: "empty roots returns path unchanged", + relativePath: "Artist/song.flac", + libraryRoots: nil, + knownPaths: nil, + expected: "Artist/song.flac", + }, + { + name: "nil knownPaths falls back to first root", + relativePath: "Artist/song.flac", + libraryRoots: []string{"/music"}, + knownPaths: nil, + expected: "/music/Artist/song.flac", + }, + { + name: "absolute path in knownPaths returned", + relativePath: "/music/Artist/song.flac", + libraryRoots: []string{"/other"}, + knownPaths: map[string]struct{}{ + "/music/Artist/song.flac": {}, + }, + expected: "/music/Artist/song.flac", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := resolveM3UPath( + tt.relativePath, + tt.libraryRoots, + tt.knownPaths, + ) + if result != tt.expected { + t.Errorf( + "resolveM3UPath(%q, %v, ...) = %q, want %q", + tt.relativePath, + tt.libraryRoots, + result, tt.expected, + ) + } + }) + } +} + +func TestToRelativePathMultiRoot(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + absolutePath string + libraryRoots []string + expected string + }{ + { + name: "path under first root", + absolutePath: "/music/Artist/song.flac", + libraryRoots: []string{"/music", "/archive"}, + expected: "Artist/song.flac", + }, + { + name: "path under second root", + absolutePath: "/archive/Artist/song.flac", + libraryRoots: []string{"/music", "/archive"}, + expected: "Artist/song.flac", + }, + { + name: "path under no root returns absolute", + absolutePath: "/other/song.flac", + libraryRoots: []string{"/music", "/archive"}, + expected: "/other/song.flac", + }, + { + name: "empty roots returns absolute", + absolutePath: "/music/song.flac", + libraryRoots: nil, + expected: "/music/song.flac", + }, + { + name: "empty root string skipped", + absolutePath: "/music/song.flac", + libraryRoots: []string{"", "/music"}, + expected: "song.flac", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := toRelativePathMultiRoot( + tt.absolutePath, tt.libraryRoots, + ) + if result != tt.expected { + t.Errorf( + "toRelativePathMultiRoot(%q, %v) = %q, want %q", + tt.absolutePath, + tt.libraryRoots, + result, tt.expected, + ) + } + }) + } +} + +func TestRemoveM3UEntriesMultiRoot(t *testing.T) { + t.Parallel() + + entries := []m3uEntry{ + {RelativePath: "Artist/Song1.flac"}, + {RelativePath: "Band/Song2.flac"}, + {RelativePath: "Artist/Song3.flac"}, + } + + // Song1 is under /music, Song2 is under /archive. + // Both should be found and removed. + targets := map[string]struct{}{ + "/music/Artist/Song1.flac": {}, + "/archive/Band/Song2.flac": {}, + } + + result := removeM3UEntries( + entries, targets, []string{"/music", "/archive"}, + ) + + if len(result) != 1 { + t.Fatalf("expected 1 entry, got %d", len(result)) + } + + if result[0].RelativePath != "Artist/Song3.flac" { + t.Errorf( + "entry[0] = %q, want %q", + result[0].RelativePath, + "Artist/Song3.flac", + ) + } +} + +func TestFindM3UEntryMultiRoot(t *testing.T) { + t.Parallel() + + entries := []m3uEntry{ + {RelativePath: "Artist/Song1.flac"}, + {RelativePath: "Band/Song2.flac"}, + } + + // Song2 is under the second root /archive. + entry, idx := findM3UEntry( + entries, "/archive/Band/Song2.flac", + []string{"/music", "/archive"}, + ) + + if idx != 1 { + t.Errorf("expected index 1, got %d", idx) + } + + if entry.RelativePath != "Band/Song2.flac" { + t.Errorf( + "entry.RelativePath = %q, want %q", + entry.RelativePath, + "Band/Song2.flac", + ) + } + + // Not found in any root. + _, idx = findM3UEntry( + entries, "/other/Missing.flac", + []string{"/music", "/archive"}, + ) + + if idx != -1 { + t.Errorf("expected index -1, got %d", idx) + } +} + +func TestReplaceM3UEntryPathsMultiRoot(t *testing.T) { + t.Parallel() + + entries := []m3uEntry{ + { + RelativePath: "old/path/song.flac", + DurationSec: 180, + DisplayTitle: "Song", + }, + { + RelativePath: "other/track.mp3", + DurationSec: 240, + DisplayTitle: "Track", + }, + } + + // The old path resolves under /archive, not /music. + replacements := map[string]string{ + "/archive/old/path/song.flac": "new/path/song.flac", + } + + result := replaceM3UEntryPaths( + entries, replacements, + []string{"/music", "/archive"}, + ) + + if len(result) != 2 { + t.Fatalf("expected 2 entries, got %d", len(result)) + } + + if result[0].RelativePath != "new/path/song.flac" { + t.Errorf( + "entry[0].RelativePath = %q, want %q", + result[0].RelativePath, + "new/path/song.flac", + ) + } + + if result[0].DurationSec != 180 { + t.Errorf( + "entry[0].DurationSec = %d, want 180", + result[0].DurationSec, + ) + } + + if result[1].RelativePath != "other/track.mp3" { + t.Errorf( + "entry[1].RelativePath = %q, want %q", + result[1].RelativePath, + "other/track.mp3", + ) + } +}