package mediawire import ( "encoding/json" "io" "log" "net/http" "net/http/httptest" "strings" "sync" "testing" ) // newLayoutBox: Sonarr runs the NEW template — no /media bind, the drive's media at the same path. type newLayoutBox struct{ fakeBox } func (b *newLayoutBox) Mounts(string) map[string]string { return map[string]string{"/mnt/felhom-drives/hdd_1/userdata/media": "/mnt/felhom-drives/hdd_1/userdata/media"} } // TestRepairOldRoot_K7 pins R-1047 with the bench's measured sequence (K7): an install from before bc54c01 stored its // library at /media/tv; after the update the repair adds the same-path root, moves the series' root with // moveFiles=false, rescans, and deletes the old root — and a second run does nothing. // // COMPANION RED-PROOF (2026-10-15): send moveFiles=true → this test fails (a file move is never allowed). Restored. func TestRepairOldRoot_K7(t *testing.T) { var mu sync.Mutex roots := []map[string]interface{}{{"id": 1, "path": "/media/tv"}} series := []map[string]interface{}{{"id": 7, "path": "/media/tv/Problem Child"}} var calls []string srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { mu.Lock() defer mu.Unlock() j := func(v interface{}) { _ = json.NewEncoder(rw).Encode(v) } switch p := r.URL.Path; { case p == "/api/v3/series": j(series) case p == "/api/v3/rootfolder" && r.Method == "GET": j(roots) case p == "/api/v3/rootfolder" && r.Method == "POST": var in map[string]interface{} _ = json.NewDecoder(r.Body).Decode(&in) in["id"] = 2 roots = append(roots, in) calls = append(calls, "add root "+in["path"].(string)) case p == "/api/v3/series/editor": var in map[string]interface{} _ = json.NewDecoder(r.Body).Decode(&in) if in["moveFiles"] != false { t.Errorf("the repair asked the app to MOVE FILES: %v", in) } for _, s := range series { s["path"] = strings.Replace(s["path"].(string), "/media/tv", in["rootFolderPath"].(string), 1) } calls = append(calls, "editor "+in["rootFolderPath"].(string)) case p == "/api/v3/command": calls = append(calls, "rescan") case p == "/api/v3/rootfolder/1" && r.Method == "DELETE": roots = roots[1:] calls = append(calls, "delete old root") default: rw.WriteHeader(404) } })) defer srv.Close() w := &Wirer{Box: &newLayoutBox{}, Logger: log.New(io.Discard, "", 0), HTTP: srv.Client(), URL: func(string, int) string { return srv.URL }} var r Result if err := w.repairOldRoot("sonarr", srv.URL+"/api/v3", nil, "tv", &r); err != nil { t.Fatal(err) } want := "add root /mnt/felhom-drives/hdd_1/userdata/media/tv|editor /mnt/felhom-drives/hdd_1/userdata/media/tv|rescan|delete old root" if got := strings.Join(calls, "|"); got != want { t.Fatalf("repair calls:\n got %s\n want %s", got, want) } if series[0]["path"] != "/mnt/felhom-drives/hdd_1/userdata/media/tv/Problem Child" { t.Fatalf("the series' stored path did not move: %v", series[0]["path"]) } n := len(calls) if err := w.repairOldRoot("sonarr", srv.URL+"/api/v3", nil, "tv", &r); err != nil { t.Fatal(err) } if len(calls) != n { t.Fatalf("a second repair run wrote again: %v", calls[n:]) } } // The OLD template still running (/media bound): nothing is touched. func TestRepairOldRoot_OldTemplateUntouched(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { t.Errorf("no call may reach an app still on the old template: %s %s", r.Method, r.URL.Path) })) defer srv.Close() w := &Wirer{Box: &fakeBox{}, Logger: log.New(io.Discard, "", 0), HTTP: srv.Client(), URL: func(string, int) string { return srv.URL }} var r Result if err := w.repairOldRoot("sonarr", srv.URL+"/api/v3", nil, "tv", &r); err != nil { t.Fatal(err) } }