package backup import ( "log" "os" "path/filepath" "testing" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/config" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" ) // r690Provider answers GetStackComposePath the way PRODUCTION does (main.go stackAdapter): ok for // every stack the box knows — deployed or not, because every catalog template is a stack — while // ListDeployedStacks names only the deployed ones. The R-487 fake answered it for deployed apps // only, which is why its test passed while the box restored nextcloud with no env (R-690). type r690Provider struct{ floorProvider } func (p *r690Provider) GetStackComposePath(name string) (string, bool) { return filepath.Join(p.dir, "stacks", name, "docker-compose.yml"), true } func r690Manager(t *testing.T) (*Manager, *settings.Settings, string) { t.Helper() sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), log.New(os.Stderr, "", 0)) if err != nil { t.Fatal(err) } sys := t.TempDir() cfg := &config.Config{} cfg.Paths.SystemDataPath = sys m := NewManager(cfg, sett, log.New(os.Stderr, "", 0)) m.SetStackProvider(&r690Provider{floorProvider{stacks: []string{"still-here"}, dir: t.TempDir()}}) drive := t.TempDir() if err := sett.AddStoragePath(settings.StoragePath{Path: drive, Label: "HDD", Schedulable: true}); err != nil { t.Fatal(err) } return m, sett, drive } // The consequence, not the mechanism: the unit the restore OPENS and the copy the picker OFFERS are // the removed app's unit on the data drive, while its catalog stack still exists. // COMPANION RED-PROOF: put GetStackComposePath back as the "removed?" test in primaryUnitDirFor → // this fails naming the system-drive path; in ListRestorePoints → the picker offers 0 copies. func TestR690_RemovedUnitFoundWhenTheStackStillExists(t *testing.T) { m, _, drive := r690Manager(t) want := writeR487Unit(t, m.namespaceRoot(drive), "nextcloud", "Nextcloud", time.Now()) if got := m.primaryUnitDirFor("nextcloud"); got != want { t.Fatalf("the restore opens %s, want the kept unit %s on the data drive", got, want) } pts, found := m.ListRestorePoints("nextcloud") if !found || len(pts) != 1 || pts[0].DriveLabel != "HDD" { t.Fatalf("the picker offers %+v (found=%v), want the one kept copy on HDD", pts, found) } // Negative control: a DEPLOYED app is never redirected to a removed-app unit. writeR487Unit(t, m.namespaceRoot(drive), "still-here", "Still", time.Now()) if got := m.primaryUnitDirFor("still-here"); got == RecoveryUnitPath(m.namespaceRoot(drive), "still-here") { t.Fatalf("a deployed app was sent to the removed-app unit %s", got) } }