Review follow-up: normalizePath strips a trailing slash, but the slash is
semantic for legacy-scope changes — filepath.Dir("/x/Show/") is the
directory itself while filepath.Dir("/x/Show") is its parent, so dropping
it widened targeted directory notifications into parent/library scans.
applyRewrites now records whether the incoming path ended with a separator
(either form) and restores it on the returned path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
152 lines
6.5 KiB
Go
152 lines
6.5 KiB
Go
package autoscan
|
|
|
|
import "testing"
|
|
|
|
func TestApplyRewrites(t *testing.T) {
|
|
rw := []PathRewrite{{From: "/data/media", To: "/mnt/media"}}
|
|
cases := []struct{ in, want string }{
|
|
{"/data/media/Movies/Dune/Dune.mkv", "/mnt/media/Movies/Dune/Dune.mkv"},
|
|
{"/other/path/file.mkv", "/other/path/file.mkv"}, // no-match pass-through
|
|
}
|
|
for _, tc := range cases {
|
|
if got := applyRewrites(tc.in, rw); got != tc.want {
|
|
t.Fatalf("applyRewrites(%q) = %q, want %q", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
|
|
// Exact match (no trailing segment) rewrites the whole path.
|
|
if got := applyRewrites("/data/media", rw); got != "/mnt/media" {
|
|
t.Fatalf("exact match: got %q want %q", got, "/mnt/media")
|
|
}
|
|
|
|
// nil/empty rewrites pass through unchanged.
|
|
if got := applyRewrites("/data/media/x", nil); got != "/data/media/x" {
|
|
t.Fatalf("nil rewrites: got %q", got)
|
|
}
|
|
|
|
// Segment-boundary matching: a sibling dir sharing the prefix must NOT match.
|
|
boundary := []PathRewrite{{From: "/data/media", To: "/mnt"}}
|
|
if got := applyRewrites("/data/media2/x", boundary); got != "/data/media2/x" {
|
|
t.Fatalf("boundary: /data/media2/x must not rewrite, got %q", got)
|
|
}
|
|
}
|
|
|
|
// TestApplyRewritesNormalizesStoredFrom verifies that a Windows-style / dup-slash
|
|
// stored From is normalized the same way coveredBy/normalizePath does, so a
|
|
// rewrite the suggester reports as "covered" actually matches at poll time.
|
|
func TestApplyRewritesNormalizesStoredFrom(t *testing.T) {
|
|
// Backslash From: a Windows-hosted arr root stored verbatim.
|
|
winFrom := []PathRewrite{{From: `D:\data\tv`, To: "/mnt/media/tv"}}
|
|
if got := applyRewrites("D:/data/tv/Show/S01/E01.mkv", winFrom); got != "/mnt/media/tv/Show/S01/E01.mkv" {
|
|
t.Fatalf("backslash From should match normalized path, got %q", got)
|
|
}
|
|
// coveredBy must agree with applyRewrites: the normalized root is covered.
|
|
if !coveredBy("D:/data/tv", winFrom) {
|
|
t.Fatalf("coveredBy should report the backslash From as covering the root")
|
|
}
|
|
|
|
// Dup-slash From collapses too.
|
|
dupFrom := []PathRewrite{{From: "/data//tv/", To: "/mnt/media/tv"}}
|
|
if got := applyRewrites("/data/tv/Show/E.mkv", dupFrom); got != "/mnt/media/tv/Show/E.mkv" {
|
|
t.Fatalf("dup-slash From should match collapsed path, got %q", got)
|
|
}
|
|
}
|
|
|
|
// TestApplyRewritesUNCPath verifies a Windows UNC root (\\NAS\Media\TV) from a
|
|
// Windows-hosted arr matches its rewrite rule. Separator swapping alone turns
|
|
// the incoming path into //NAS/... while the From normalizes to /NAS/..., so
|
|
// the prefix never matched — both sides must go through normalizePath.
|
|
func TestApplyRewritesUNCPath(t *testing.T) {
|
|
incoming := `\\NAS\Media\TV\Show\S01\E01.mkv`
|
|
for _, from := range []string{`\\NAS\Media\TV`, "//NAS/Media/TV", "/NAS/Media/TV"} {
|
|
rw := []PathRewrite{{From: from, To: "/mnt/media/tv"}}
|
|
if got := applyRewrites(incoming, rw); got != "/mnt/media/tv/Show/S01/E01.mkv" {
|
|
t.Fatalf("UNC path with From=%q: got %q", from, got)
|
|
}
|
|
}
|
|
|
|
// An unmatched UNC path still comes back normalized (collapsed slashes),
|
|
// consistent with what the resolver sees for matched paths.
|
|
if got := applyRewrites(incoming, nil); got != "/NAS/Media/TV/Show/S01/E01.mkv" {
|
|
t.Fatalf("unmatched UNC path: got %q", got)
|
|
}
|
|
|
|
// A trailing-slash To must not produce a doubled separator at the join.
|
|
slashTo := []PathRewrite{{From: `\\NAS\Media\TV`, To: "/mnt/media/tv/"}}
|
|
if got := applyRewrites(incoming, slashTo); got != "/mnt/media/tv/Show/S01/E01.mkv" {
|
|
t.Fatalf("trailing-slash To: got %q", got)
|
|
}
|
|
}
|
|
|
|
// TestApplyRewritesPreservesTrailingSlash verifies a trailing separator on the
|
|
// incoming path survives normalization and rewriting. It is semantic for
|
|
// legacy-scope changes: filepath.Dir("/x/Show/") is the directory itself while
|
|
// filepath.Dir("/x/Show") is its parent, so dropping it would widen a targeted
|
|
// directory notification into a parent/library scan.
|
|
func TestApplyRewritesPreservesTrailingSlash(t *testing.T) {
|
|
rw := []PathRewrite{{From: "/data/tv", To: "/mnt/media/tv"}}
|
|
if got := applyRewrites("/data/tv/Show/", rw); got != "/mnt/media/tv/Show/" {
|
|
t.Fatalf("rewritten dir: got %q", got)
|
|
}
|
|
// Unmatched paths keep it too.
|
|
if got := applyRewrites("/other/Show/", rw); got != "/other/Show/" {
|
|
t.Fatalf("unmatched dir: got %q", got)
|
|
}
|
|
// Windows separator form: trailing backslash counts as a trailing separator.
|
|
unc := []PathRewrite{{From: `\\NAS\Media\TV`, To: "/mnt/media/tv"}}
|
|
if got := applyRewrites(`\\NAS\Media\TV\Show\`, unc); got != "/mnt/media/tv/Show/" {
|
|
t.Fatalf("UNC dir: got %q", got)
|
|
}
|
|
// Files without a trailing separator stay without one.
|
|
if got := applyRewrites("/data/tv/Show/E01.mkv", rw); got != "/mnt/media/tv/Show/E01.mkv" {
|
|
t.Fatalf("file: got %q", got)
|
|
}
|
|
// Bare root never doubles.
|
|
if got := applyRewrites("/", nil); got != "/" {
|
|
t.Fatalf("root: got %q", got)
|
|
}
|
|
}
|
|
|
|
// TestApplyRewritesMostSpecificWins verifies the longest matching From wins
|
|
// regardless of slice ordering: a broad rule must not shadow a nested one.
|
|
func TestApplyRewritesMostSpecificWins(t *testing.T) {
|
|
// Broad rule listed FIRST: a first-match strategy would (wrongly) pick /data
|
|
// and yield "/A/media/x". Most-specific must pick /data/media -> "/B/x".
|
|
broadFirst := []PathRewrite{
|
|
{From: "/data", To: "/A"},
|
|
{From: "/data/media", To: "/B"},
|
|
}
|
|
if got := applyRewrites("/data/media/x", broadFirst); got != "/B/x" {
|
|
t.Fatalf("broad-first: got %q want %q", got, "/B/x")
|
|
}
|
|
|
|
// Same rules, specific listed first: result must be identical (order-independent).
|
|
specificFirst := []PathRewrite{
|
|
{From: "/data/media", To: "/B"},
|
|
{From: "/data", To: "/A"},
|
|
}
|
|
if got := applyRewrites("/data/media/x", specificFirst); got != "/B/x" {
|
|
t.Fatalf("specific-first: got %q want %q", got, "/B/x")
|
|
}
|
|
|
|
// A path under the broad rule but NOT the specific one still uses the broad rule.
|
|
if got := applyRewrites("/data/other/x", broadFirst); got != "/A/other/x" {
|
|
t.Fatalf("broad fallthrough: got %q want %q", got, "/A/other/x")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeSeparators(t *testing.T) {
|
|
if got := normalizeSeparators(`C:\Media\Movies\Dune\Dune.mkv`); got != "C:/Media/Movies/Dune/Dune.mkv" {
|
|
t.Fatalf("normalizeSeparators(windows) = %q", got)
|
|
}
|
|
// POSIX paths are unchanged.
|
|
if got := normalizeSeparators("/mnt/media/x.mkv"); got != "/mnt/media/x.mkv" {
|
|
t.Fatalf("normalizeSeparators(posix) = %q", got)
|
|
}
|
|
// A normalized Windows path then rewrites on the Linux host.
|
|
rw := []PathRewrite{{From: "C:/Media", To: "/mnt/media"}}
|
|
if got := applyRewrites(normalizeSeparators(`C:\Media\ShowA\S01\E01.mkv`), rw); got != "/mnt/media/ShowA/S01/E01.mkv" {
|
|
t.Fatalf("windows rewrite = %q", got)
|
|
}
|
|
}
|