D47 / R-1269 (P2): a new install never writes into an earlier install's backup — every install has an identity (install_id), the unit records it, every unit writer asks first; another install's unit is set aside whole (backups/set-aside/<app>/<stamp>, a D25 kept copy, „Visszaállítás” + „Eltávolítás” on the restore page); a move that fails refuses the write. Red-proved. Commit only.
gates / gates (push) Successful in 1m33s

This commit is contained in:
2026-10-25 09:27:48 +01:00
parent e1613aea8d
commit 02daebe9c5
30 changed files with 1990 additions and 17 deletions
@@ -95,3 +95,71 @@ func TestD25_RemoveHandlerRemovesOnlyTheRecordedCopy(t *testing.T) {
t.Fatalf("another copy was touched: %v", err)
}
}
// D47 (2026-10-25): a set-aside unit of an earlier install is a kept copy with „Visszaállítás" beside „Eltávolítás";
// a second-drive copy is not restorable from this list (render test per branch of {{if .Restorable}}).
func TestD47_RestorePageOffersRestoreOnlyForASetAsideUnit(t *testing.T) {
d := i18nLayoutData("backups-restore", "Visszaállítás a mentésből")
d["Backup"] = i18nBackupStatus(false, false)
d["OffboxConfigured"] = true
d["OffsiteStoreState"] = "known"
d["KeptCopies"] = []keptCopyRow{
{ID: "aaaaaaaaaaaaaaaa", Source: "tandoor", Line: "a(z) belso meghajtóról megőrzött másolat · egy korábbi telepítés mentése (újratelepítés előtt készült)", SizeHuman: "150 MB", Since: "2026-10-25T08:00:00Z", Restorable: true},
{ID: "bbbbbbbbbbbbbbbb", Source: "csaladi_fotok", Line: "a(z) hdd_3 meghajtóról megőrzött másolat · a meghajtó nincs csatlakoztatva", SizeHuman: "1.2 GB", Since: "2026-10-21T12:00:00Z"},
}
d["KeptUnitRestorable"] = true
html := renderBackupPage(t, "backups_restore", d)
if n := strings.Count(html, `action="/backup/kept/restore"`); n != 1 {
t.Fatalf("exactly the set-aside unit's row offers the restore, found %d", n)
}
i := strings.Index(html, `action="/backup/kept/restore"`)
if !strings.Contains(html[i:i+300], `value="aaaaaaaaaaaaaaaa"`) || !strings.Contains(html, "confirmRestoreKeptUnit(this)") {
t.Fatal("the restore form must name the set-aside unit's record and ask first")
}
if strings.Count(html, `action="/backup/kept/remove"`) != 2 {
t.Fatal("both rows keep the remove button")
}
}
// The restore handler names a set-aside unit ONLY by its record: no confirm → refused; an unknown id → not found; a
// second-drive copy's id is not a unit → not found (nothing starts).
func TestD47_KeptUnitRestoreHandlerRefusals(t *testing.T) {
tmp := t.TempDir()
lg := log.New(io.Discard, "", 0)
sett, err := settings.Load(filepath.Join(tmp, "settings.json"), lg)
if err != nil {
t.Fatal(err)
}
share := filepath.Join(tmp, "hdd_1", "backups", "secondary", "_shares", "hdd_3", "csaladi_fotok")
if err := os.MkdirAll(share, 0o755); err != nil {
t.Fatal(err)
}
st, _ := os.Stat(share)
sys := st.Sys().(*syscall.Stat_t)
if err := sett.UpsertKeptCopy(settings.KeptCopy{Kind: "share", Source: "csaladi_fotok", Drive: "hdd_3", Reason: "drive_away",
DestBase: filepath.Dir(filepath.Dir(share)), Path: share, Dev: uint64(sys.Dev), Ino: sys.Ino}); err != nil {
t.Fatal(err)
}
cfg := &config.Config{}
cfg.Paths.DataDir = tmp
s := &Server{cfg: cfg, backupMgr: backup.NewManager(cfg, sett, lg), logger: lg}
post := func(form string) string {
rr := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/backup/kept/restore", strings.NewReader(form))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
s.keptUnitRestoreHandler(rr, req)
return rr.Header().Get("Location")
}
if loc := post("id=x"); !strings.Contains(loc, "flash.kept.restore_needs_confirmation") {
t.Fatalf("no confirm → %q", loc)
}
if loc := post("id=ffffffffffffffff&confirm=1"); !strings.Contains(loc, "flash.kept.not_found") {
t.Fatalf("unknown id → %q", loc)
}
if loc := post("id=" + settings.KeptCopyID(share) + "&confirm=1"); !strings.Contains(loc, "flash.kept.not_found") {
t.Fatalf("a second-drive copy is not a restorable unit → %q", loc)
}
if s.backupMgr.RestoreStatus().Running {
t.Fatal("a refusal must start nothing")
}
}