package stacks import ( "os" "path/filepath" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/appbackup" ) const beltCompose = `services: app: image: x volumes: - app_config:/config - ${USERDATA_PATH}/media/movies:/media/movies - ${USERDATA_PATH}/downloads:/downloads - ${HDD_PATH}/appdata/app:/data - /etc/passwd:/host:ro volumes: app_config: ` // TestParseComposeUserdataMounts: only ${USERDATA_PATH}/... bind sources are returned, resolved; HDD // appdata mounts, named volumes, and unrelated host paths are ignored. func TestParseComposeUserdataMounts(t *testing.T) { dir := t.TempDir() cp := filepath.Join(dir, "docker-compose.yml") if err := os.WriteFile(cp, []byte(beltCompose), 0o644); err != nil { t.Fatal(err) } ud := filepath.Clean("/mnt/felhom-usb/userdata") got := map[string]bool{} for _, m := range ParseComposeUserdataMounts(cp, ud) { got[m] = true } for _, want := range []string{ filepath.Join(ud, "media", "movies"), filepath.Join(ud, "downloads"), } { if !got[want] { t.Errorf("missing userdata mount %q (got %v)", want, got) } } if len(got) != 2 { t.Errorf("expected exactly 2 userdata mounts, got %d: %v", len(got), got) } } // TestEnsureUserdataMounts_CreatesBeltDirs: the deploy belt pre-creates every declared ${USERDATA_PATH} // bind source before compose-up (so Docker never auto-creates one as root). Uses a real temp userdata // root via env injection. func TestEnsureUserdataMounts_CreatesBeltDirs(t *testing.T) { m := newMigManager(t, "") // minimal Manager (cfg+logger+settings) stackDir := t.TempDir() if err := os.WriteFile(filepath.Join(stackDir, "docker-compose.yml"), []byte(beltCompose), 0o644); err != nil { t.Fatal(err) } ud := filepath.Join(t.TempDir(), "userdata") env := []string{"USERDATA_PATH=" + ud} // movies dir absent before if _, err := os.Stat(filepath.Join(ud, "media", "movies")); err == nil { t.Fatal("precondition: movies dir should not exist yet") } m.ensureUserdataMounts(stackDir, env) for _, p := range []string{filepath.Join(ud, "media", "movies"), filepath.Join(ud, "downloads")} { if fi, err := os.Stat(p); err != nil || !fi.IsDir() { t.Errorf("belt did not create %s (%v)", p, err) } } _ = appbackup.SharedContentGID // keep import referenced cross-platform } // TestWithUserdataPath: the shared injector adds USERDATA_PATH=/userdata when HDD_PATH is set, and // adds nothing when it's empty. Regression for the initial-deploy bug where ${USERDATA_PATH} resolved // to "" and bound a bogus root-owned dir at the container root. func TestWithUserdataPath(t *testing.T) { got := withUserdataPath([]string{"DOMAIN=x"}, "/mnt/felhom-usb") want := "USERDATA_PATH=" + appbackup.UserdataDir("/mnt/felhom-usb") found := false for _, e := range got { if e == want { found = true } } if !found { t.Errorf("USERDATA_PATH not injected: got %v, want %q", got, want) } // companion: empty HDD_PATH → no USERDATA_PATH at all for _, e := range withUserdataPath([]string{"DOMAIN=x"}, "") { if len(e) >= 13 && e[:13] == "USERDATA_PATH" { t.Errorf("USERDATA_PATH must NOT be set when HDD_PATH is empty: %q", e) } } }