package engine import ( "os" "path/filepath" "strings" "testing" "github.com/cdnutter/playlink/internal/store" ) func (ev *env) noMusic(t *testing.T, side string) *NoMusicScan { t.Helper() if _, err := ev.e.StartNoMusicScan(ev.pair.ID, side); err != nil { t.Fatal(err) } ev.wait() s := ev.e.NoMusicResult(ev.pair.ID) if s == nil { t.Fatal("no scan result") } return s } func rels(s *NoMusicScan) []string { var out []string for _, f := range s.Folders { out = append(out, f.Rel) } return out } func TestFindFoldersWithoutMusic(t *testing.T) { ev := setup(t, false) s := ev.slave ev.write(s, "A/B/song.flac", "music") ev.write(s, "A/B/extra/cover.jpg", "art") // inside an album that has music os.MkdirAll(filepath.Join(s, "A/Empty"), 0o755) ev.write(s, "A/C/cover.jpg", "art") ev.write(s, "D/E/F/notes.txt", "text") // only D is listed, not D/E or D/E/F ev.write(s, "G/._song.flac", "mac metadata") ev.write(s, ".hidden/stuff.txt", "x") ev.write(s, "Loose.txt", "x") // a file at the root is not a folder scan := ev.noMusic(t, SideSlave) got := strings.Join(rels(scan), ",") if got != "A/B/extra,A/C,A/Empty,D,G" { t.Fatalf("got %s", got) } for _, f := range scan.Folders { if f.Rel == "D" && (f.Files != 1 || f.Folders != 2 || f.Sample[0] != "E/F/notes.txt") { t.Fatalf("D: %+v", f) } } // The quarantine never shows up, even when it isn't hidden. if strings.Contains(got, "trash") { t.Fatal("quarantine listed") } } func TestRemoveFoldersWithoutMusic(t *testing.T) { ev := setup(t, false) s := ev.slave ev.write(s, "A/B/song.flac", "music") os.MkdirAll(filepath.Join(s, "A/Empty"), 0o755) ev.write(s, "D/E/notes.txt", "text") ev.write(s, "H/cover.jpg", "art") ev.noMusic(t, SideSlave) if _, err := ev.e.StartRemoveFolders(ev.pair.ID, []string{"A/B"}); err == nil { t.Fatal("a folder that wasn't listed must be refused") } // Music added after the scan: that folder is skipped. ev.write(s, "H/new.flac", "music") if _, err := ev.e.StartRemoveFolders(ev.pair.ID, []string{"A/Empty", "D", "H"}); err != nil { t.Fatal(err) } j := ev.wait() if j.Results["removed"] != 2 || len(j.Skipped) != 1 || !strings.Contains(j.Skipped[0], "contains music") { t.Fatalf("results %+v skipped %v", j.Results, j.Skipped) } for _, p := range []string{"A/Empty", "D"} { if exists(filepath.Join(s, p)) { t.Errorf("%s should be gone", p) } } if !exists(filepath.Join(s, "A/B/song.flac")) || !exists(filepath.Join(s, "H/new.flac")) { t.Fatal("music must never be touched") } ops, _, _ := ev.st.Ops(ctx, store.OpFilter{PairID: ev.pair.ID}) var dOp store.Operation for _, o := range ops { if o.Action == store.ActRemoveFolder && o.RelSlave == "D" { dOp = o } } if dOp.ID == 0 || !exists(filepath.Join(dOp.QuarantinePath, "E/notes.txt")) { t.Fatalf("D should be in the quarantine: %+v", dOp) } // Undo brings it back with its contents; the scan result is stale now. if err := ev.e.Undo(ctx, dOp.ID); err != nil { t.Fatal(err) } if b, _ := os.ReadFile(filepath.Join(s, "D/E/notes.txt")); string(b) != "text" { t.Fatal("undo should restore the folder") } if _, err := ev.e.StartRemoveFolders(ev.pair.ID, []string{"D"}); err == nil { t.Fatal("removing needs a fresh scan") } } func TestRemoveFoldersDryRun(t *testing.T) { ev := setup(t, true) os.MkdirAll(filepath.Join(ev.master, "Empty"), 0o755) ev.noMusic(t, SideMaster) ev.e.StartRemoveFolders(ev.pair.ID, []string{"Empty"}) if j := ev.wait(); j.Results["planned"] != 1 || !exists(filepath.Join(ev.master, "Empty")) { t.Fatalf("dry run must only record: %+v", j.Results) } }