package appbackup import ( "path/filepath" "reflect" "testing" ) // fp joins the elements under an HDD path with OS separators — mounts in the ParseComposeHDDMounts // shape are already filepath.Clean'd, so tests build them the same way. func fp(elems ...string) string { return filepath.Join(elems...) } // TestAppDataDirNames is the pure derivation table (Group A). Every case asserts the RESOLVED name // list, never mere absence of error. Companion RP-1: a resolver that ignores mounts and returns // []string{stackName} fails the paperless, two-name, and dedupe cases. func TestAppDataDirNames(t *testing.T) { const hdd = "/mnt/felhom-usb" cases := []struct { name string stack string mounts []string want []string }{ { // paperless shape: stack "paperless-ngx", dir "paperless" (F-S2/F-S3 core). name: "paperless mismatch", stack: "paperless-ngx", mounts: []string{ fp(hdd, "appdata", "paperless", "media"), fp(hdd, "appdata", "paperless", "export"), }, want: []string{"paperless"}, // media+export dedupe to one name }, { // match shape: dir name == stack name (immich/nextcloud/romm). name: "matching name", stack: "nextcloud", mounts: []string{fp(hdd, "appdata", "nextcloud")}, want: []string{"nextcloud"}, }, { // two DISTINCT names → both, sorted (no catalog app does this today). name: "two distinct names sorted", stack: "weird", mounts: []string{ fp(hdd, "appdata", "zebra", "x"), fp(hdd, "appdata", "alpha", "y"), }, want: []string{"alpha", "zebra"}, }, { // non-appdata HDD binds + a foreign-drive mount are filtered → fallback. name: "non-appdata and foreign filtered", stack: "romm", mounts: []string{ fp(hdd, "roms"), // under HDD but not appdata/ fp("/mnt/other-drive", "appdata", "ghost"), // foreign drive — wrong prefix }, want: []string{"romm"}, }, { // whole-appdata-root bind (no name derivable) → ignored → fallback. name: "whole appdata root bind", stack: "root-binder", mounts: []string{fp(hdd, "appdata")}, want: []string{"root-binder"}, }, { name: "empty mounts fallback", stack: "vaultwarden", mounts: nil, want: []string{"vaultwarden"}, }, { // unclean paths still resolve (Clean applied both sides). name: "unclean path", stack: "paperless-ngx", mounts: []string{hdd + "/appdata/paperless/../paperless/media"}, want: []string{"paperless"}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := AppDataDirNames(hdd, tc.stack, tc.mounts) if !reflect.DeepEqual(got, tc.want) { t.Errorf("AppDataDirNames(%q, %q, %v) = %v, want %v", hdd, tc.stack, tc.mounts, got, tc.want) } }) } } // TestAppDataBindsPresent pins the WARN predicate: true only when a mount sits under appdata/. func TestAppDataBindsPresent(t *testing.T) { const hdd = "/mnt/felhom-usb" if !AppDataBindsPresent(hdd, []string{fp(hdd, "appdata", "paperless", "media")}) { t.Error("declared appdata bind should report present") } if AppDataBindsPresent(hdd, []string{fp(hdd, "roms")}) { t.Error("non-appdata bind should NOT report present") } if AppDataBindsPresent(hdd, nil) { t.Error("no mounts should NOT report present") } }