package services import "testing" // Regression tests for directory-boundary folder matching. // // getRelativePath (and the folder-membership checks across the scanner) // once used bare string-prefix matching, so a registered folder whose path // was a string prefix of a sibling's ("/app/audiobooks" vs // "/app/audiobookssd") claimed the sibling's files: relative paths were // cut mid-component into "sd/..."-style garbage, stored that way, and // resolved against the wrong root — covers 404ing while the bytes sat one // directory away. folderContains pins the rule: a folder only ever claims // itself or paths beneath it at a real separator boundary, regardless of // names, registration order, or trailing slashes. func TestFolderContains(t *testing.T) { tests := []struct { name string folder string path string want bool }{ {"exact root", "/app/books", "/app/books", true}, {"direct child", "/app/books", "/app/books/x.epub", true}, {"deep child", "/app/books", "/app/books/a/b/c.epub", true}, {"sibling extension", "/app/books", "/app/books2", false}, {"sibling extension child", "/app/books", "/app/books2/x.epub", false}, {"mid-word sibling", "/data/lib", "/data/library/x.epub", false}, {"suffix without separator", "/app/books", "/app/books-backup/x.epub", false}, {"unrelated", "/app/books", "/opt/other/x.epub", false}, {"prefix inside path segment", "/app/books", "/app/series/books/x.epub", false}, {"trailing slash in folder", "/app/books/", "/app/books/x.epub", true}, {"trailing slash exact", "/app/books/", "/app/books", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := folderContains(tt.folder, tt.path); got != tt.want { t.Errorf("folderContains(%q, %q) = %v, want %v", tt.folder, tt.path, got, tt.want) } }) } } func TestGetRelativePathFolderBoundary(t *testing.T) { // The prod trap: /app/audiobooks registered before /app/audiobookssd. // String-prefix cutting produced "sd/Brian Jacques/..." for every file // in the second folder; the boundary rule must make that impossible. newScanner := func(folders []string) *MediaScanner { s := NewMediaScanner(nil) s.folders = folders // SetFolders requires a live DB; tests only need path relativization return s } siblingFolders := []string{"/app/audiobooks", "/app/audiobookssd"} tests := []struct { name string folders []string path string want string }{ { name: "sibling-prefix folder keeps its full name in relative path", folders: siblingFolders, path: "/app/audiobookssd/Brian Jacques/Redwall/Book 01 - Redwall/file.m4b", want: "Brian Jacques/Redwall/Book 01 - Redwall/file.m4b", }, { name: "first registered folder unaffected", folders: siblingFolders, path: "/app/audiobooks/Author/Series/file.mp3", want: "Author/Series/file.mp3", }, { name: "exact root yields empty path", folders: siblingFolders, path: "/app/audiobookssd", want: "", }, { name: "order independence: sibling registered first", folders: []string{"/app/audiobookssd", "/app/audiobooks"}, path: "/app/audiobookssd/Brian Jacques/Redwall/Book 01 - Redwall/file.m4b", want: "Brian Jacques/Redwall/Book 01 - Redwall/file.m4b", }, { name: "order independence: first folder still correct when second registered first", folders: []string{"/app/audiobookssd", "/app/audiobooks"}, path: "/app/audiobooks/Author/Series/file.mp3", want: "Author/Series/file.mp3", }, { name: "trailing slash on registered folders", folders: []string{"/app/audiobooks/", "/app/audiobookssd/"}, path: "/app/audiobookssd/Brian Jacques/file.m4b", want: "Brian Jacques/file.m4b", }, { name: "mid-word sibling folders", folders: []string{"/data/lib", "/data/library"}, path: "/data/library/author/book.epub", want: "author/book.epub", }, { name: "author/series/title layout preserved under true root", folders: siblingFolders, path: "/app/audiobookssd/Brian Jacques/Redwall/Book 14 - The Taggerung/Taggerung.mp3", want: "Brian Jacques/Redwall/Book 14 - The Taggerung/Taggerung.mp3", }, { name: "path outside every folder returned as-is", folders: siblingFolders, path: "/app/audiobooksBackup/x.mp3", want: "/app/audiobooksBackup/x.mp3", }, { name: "unrelated path returned as-is", folders: siblingFolders, path: "/elsewhere/x.mp3", want: "/elsewhere/x.mp3", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := newScanner(tt.folders) if got := s.getRelativePath(tt.path); got != tt.want { t.Errorf("getRelativePath(%q) with folders %v = %q, want %q", tt.path, tt.folders, got, tt.want) } }) } }