package backup import ( "io" "log" "os" "path/filepath" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/config" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" ) // f5Manager wires a Manager with a real settings store holding the given enrolled drives, plus a // sys drive, and a fake provider mapping each deployed app to its CURRENT HDD path. func f5Manager(t *testing.T, sysDrive string, enrolled []string, deployed map[string]string) *Manager { t.Helper() sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), log.New(io.Discard, "", 0)) if err != nil { t.Fatal(err) } for _, d := range enrolled { if err := sett.AddStoragePath(settings.StoragePath{Path: d, Label: filepath.Base(d), Schedulable: true}); err != nil { t.Fatal(err) } } var stacks []StackSummary for name := range deployed { stacks = append(stacks, StackSummary{Name: name}) } cfg := &config.Config{} cfg.Paths.SystemDataPath = sysDrive fake := &volDumpFakeProvider{stacks: stacks, hdd: deployed} return &Manager{cfg: cfg, settings: sett, logger: log.New(io.Discard, "", 0), systemDataPath: sysDrive, stackProvider: fake} } // seedPrimaryDir creates a backups/primary/ dir (with a marker file) under a drive's namespace. func seedPrimaryDir(t *testing.T, driveNsRoot, app string) string { t.Helper() dir := RecoveryUnitPath(driveNsRoot, app) if err := os.MkdirAll(filepath.Join(dir, "volume-dumps"), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "manifest.json"), []byte("{}"), 0644); err != nil { t.Fatal(err) } return dir } // F5: an app redeployed from drive A to drive B → its stale primary dir on A is removed, its live // dir on B is kept. Guard red-proof lives in the two "kept" assertions. func TestPruneStalePrimaryDirs_RemovesRedeployedResidue(t *testing.T) { driveA := t.TempDir() driveB := t.TempDir() sys := filepath.Join(t.TempDir(), "sys") // nextcloud currently on B; a stale unit remains on A. m := f5Manager(t, sys, []string{driveA, driveB}, map[string]string{"nextcloud": driveB}) nsA := NamespaceRoot(driveA, true) nsB := NamespaceRoot(driveB, true) staleA := seedPrimaryDir(t, nsA, "nextcloud") liveB := seedPrimaryDir(t, nsB, "nextcloud") m.pruneStalePrimaryDirs() if _, err := os.Stat(staleA); !os.IsNotExist(err) { t.Errorf("stale primary dir on the OLD drive was not removed (F5)") } if _, err := os.Stat(liveB); err != nil { t.Errorf("the LIVE primary dir on the current drive was wrongly removed: %v", err) } } // GUARD: an UNDEPLOYED app's primary dir is a restore point — it must be KEPT (companion: drop the // `!deployed { continue }` guard → this dir is deleted → fail). func TestPruneStalePrimaryDirs_KeepsUndeployedRestorePoint(t *testing.T) { driveA := t.TempDir() sys := filepath.Join(t.TempDir(), "sys") // Only "nextcloud" is deployed (on A); "removed-app" has a leftover unit but is NOT deployed. m := f5Manager(t, sys, []string{driveA}, map[string]string{"nextcloud": driveA}) nsA := NamespaceRoot(driveA, true) seedPrimaryDir(t, nsA, "nextcloud") orphan := seedPrimaryDir(t, nsA, "removed-app") m.pruneStalePrimaryDirs() if _, err := os.Stat(orphan); err != nil { t.Errorf("an UNDEPLOYED app's restore point was wrongly deleted (guard failure): %v", err) } } // GUARD: the dir on an app's CURRENT drive is never touched (already covered above, but assert it // directly with a single-drive app so no cross-drive move is involved). func TestPruneStalePrimaryDirs_KeepsCurrentDriveDir(t *testing.T) { driveA := t.TempDir() sys := filepath.Join(t.TempDir(), "sys") m := f5Manager(t, sys, []string{driveA}, map[string]string{"nextcloud": driveA}) nsA := NamespaceRoot(driveA, true) live := seedPrimaryDir(t, nsA, "nextcloud") m.pruneStalePrimaryDirs() if _, err := os.Stat(live); err != nil { t.Errorf("the current-drive primary dir was wrongly removed: %v", err) } }