package stacks import ( "os" "path/filepath" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/appbackup" ) func writeClassCompose(t *testing.T, body string) string { t.Helper() dir := t.TempDir() p := filepath.Join(dir, "docker-compose.yml") if err := os.WriteFile(p, []byte(body), 0o644); err != nil { t.Fatal(err) } return p } func findBind(binds []appbackup.ComposeBind, root appbackup.BindRoot, rel string) (appbackup.ComposeBind, bool) { for _, b := range binds { if b.Root == root && b.RelPath == rel { return b, true } } return appbackup.ComposeBind{}, false } // TestParseClassifiableBinds_Variants (Group C): short-syntax variants, quotes, mode tokens // (ro/rw/ro,z/z), dedupe, bare root, both roots, and non-volume sections ignored. func TestParseClassifiableBinds_Variants(t *testing.T) { compose := ` services: app: image: x:1 environment: - HDD_PATH=${HDD_PATH} # NOT a volume — must be ignored volumes: - ${HDD_PATH}/appdata/immich:/upload - "${USERDATA_PATH}/media/photos:/external:ro" - ${USERDATA_PATH}/media/music:/music:ro,z - ${USERDATA_PATH}/downloads:/dl:z - ${USERDATA_PATH}/downloads:/dl2 # duplicate (root,relpath) — deduped, first wins - ${HDD_PATH}:/hddroot # bare root - ./local-only:/x # not a classifiable var — ignored - named_vol:/data # named volume — ignored ports: - 8080:80 # ports section — not volumes ` binds := ParseComposeClassifiableBinds(writeClassCompose(t, compose)) // Expect exactly: hdd/appdata/immich, userdata/media/photos(ro), userdata/media/music(ro), // userdata/downloads(writable, first occ), hdd/"" (bare root). if len(binds) != 5 { t.Fatalf("got %d binds, want 5: %+v", len(binds), binds) } if b, ok := findBind(binds, appbackup.RootHDD, "appdata/immich"); !ok || b.ReadOnly { t.Errorf("appdata/immich = %+v, want writable", b) } if b, ok := findBind(binds, appbackup.RootUserdata, "media/photos"); !ok || !b.ReadOnly { t.Errorf("media/photos = %+v, want ro", b) } if b, ok := findBind(binds, appbackup.RootUserdata, "media/music"); !ok || !b.ReadOnly { t.Errorf("media/music (ro,z) = %+v, want ro", b) } // downloads: first occurrence was `:z` (writable) — dedupe keeps the first, so writable wins. if b, ok := findBind(binds, appbackup.RootUserdata, "downloads"); !ok || b.ReadOnly { t.Errorf("downloads = %+v, want single writable entry (first-wins dedupe)", b) } if b, ok := findBind(binds, appbackup.RootHDD, ""); !ok || b.ReadOnly { t.Errorf("bare ${HDD_PATH} = %+v, want RelPath '' writable", b) } } // TestParseClassifiableBinds_CleanAndSubpaths: nested paths clean correctly; a false-prefix var // (${HDD_PATH_X}) does NOT match. func TestParseClassifiableBinds_CleanAndSubpaths(t *testing.T) { compose := ` services: app: volumes: - ${HDD_PATH}/appdata/paperless/media:/m - ${HDD_PATH_EXTRA}/nope:/n # false prefix — must NOT match ${HDD_PATH} - ${USERDATA_PATH}/import/calibre:/i:rw ` binds := ParseComposeClassifiableBinds(writeClassCompose(t, compose)) if len(binds) != 2 { t.Fatalf("got %d binds, want 2 (false-prefix excluded): %+v", len(binds), binds) } if _, ok := findBind(binds, appbackup.RootHDD, "appdata/paperless/media"); !ok { t.Errorf("nested hdd path missing: %+v", binds) } if b, ok := findBind(binds, appbackup.RootUserdata, "import/calibre"); !ok || b.ReadOnly { t.Errorf("import/calibre (rw) = %+v, want writable", b) } } // TestParseClassifiableBinds_Missing: an unreadable compose yields nil (no panic). func TestParseClassifiableBinds_Missing(t *testing.T) { if b := ParseComposeClassifiableBinds(filepath.Join(t.TempDir(), "nope.yml")); b != nil { t.Errorf("missing compose should yield nil, got %+v", b) } }