package web import ( "io" "log" "net/http/httptest" "os" "path/filepath" "strings" "syscall" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/backup" "gitea.dooplex.hu/admin/felhom-controller/internal/config" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" ) // D25 (operator ruling 2026-10-21): the restore page lists kept second-drive copies with „Eltávolítás" — and shows no // section at all when there are none. Render test per branch of the {{if .KeptCopies}} gate (the seam-wiring rule). func TestD25_RestorePageListsKeptCopiesWithRemove(t *testing.T) { base := func() map[string]interface{} { d := i18nLayoutData("backups-restore", "Visszaállítás a mentésből") d["Backup"] = i18nBackupStatus(false, false) d["OffboxConfigured"] = true d["OffsiteStoreState"] = "known" return d } with := base() with["KeptCopies"] = []keptCopyRow{{ID: "0123456789abcdef", 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"}} html := renderBackupPage(t, "backups_restore", with) for _, want := range []string{"Megőrzött másolatok", "csaladi_fotok", "a(z) hdd_3 meghajtóról megőrzött másolat", `action="/backup/kept/remove"`, `name="id" value="0123456789abcdef"`, `name="confirm" value="1"`, "Eltávolítás", "confirmRemoveKeptCopy(this)"} { if !strings.Contains(html, want) { t.Errorf("restore page with a kept copy lacks %q", want) } } if html := renderBackupPage(t, "backups_restore", base()); strings.Contains(html, "Megőrzött másolatok") || strings.Contains(html, "/backup/kept/remove") { t.Error("restore page with NO kept copy shows the kept-copies section") } } // „Eltávolítás" through the handler: no confirm → refused, nothing removed; an unknown id → its own refusal; the // recorded copy → removed, and ONLY it. Real settings + manager + disk (the record is the only selector). func TestD25_RemoveHandlerRemovesOnlyTheRecordedCopy(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) } destBase := filepath.Join(tmp, "hdd_1", "backups", "secondary", "_shares") kept := filepath.Join(destBase, "hdd_3", "csaladi_fotok") other := filepath.Join(destBase, "hdd_2", "b_share") for _, d := range []string{kept, other} { if err := os.MkdirAll(d, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(d, "f.txt"), []byte("x"), 0o644); err != nil { t.Fatal(err) } } st, _ := os.Stat(kept) sys := st.Sys().(*syscall.Stat_t) if err := sett.UpsertKeptCopy(settings.KeptCopy{Kind: "share", Source: "csaladi_fotok", Drive: "hdd_3", Reason: "drive_away", DestBase: destBase, Path: kept, Dev: uint64(sys.Dev), Ino: sys.Ino}); err != nil { t.Fatal(err) } id := settings.KeptCopyID(kept) 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/remove", strings.NewReader(form)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") s.keptCopyRemoveHandler(rr, req) return rr.Header().Get("Location") } if loc := post("id=" + id); !strings.Contains(loc, "flash.kept.needs_confirmation") { t.Fatalf("no confirm → %q", loc) } if _, err := os.Stat(kept); err != nil { t.Fatal("removed without a confirm") } if loc := post("id=ffffffffffffffff&confirm=1"); !strings.Contains(loc, "flash.kept.not_found") { t.Fatalf("unknown id → %q", loc) } if loc := post("id=" + id + "&confirm=1"); !strings.Contains(loc, "flash.kept.removed") { t.Fatalf("recorded copy → %q", loc) } if _, err := os.Stat(kept); !os.IsNotExist(err) { t.Fatalf("the kept copy is still there: %v", err) } if _, err := os.Stat(filepath.Join(other, "f.txt")); err != nil { t.Fatalf("another copy was touched: %v", err) } }