package restorespace import ( "context" "errors" "testing" "gitea.dooplex.hu/admin/felhom-agent/internal/proxmox" ) // R-672 (v0.133.0). The provider behind the restore-test's space preflight. No test reaches a real // Proxmox or a real file: the API and ReadFile are fakes. type fakeAPI struct { cfg []proxmox.Storage live []proxmox.Storage content map[string][]proxmox.StorageContent perms map[string]map[string]int } func (f fakeAPI) ListStorage(context.Context) ([]proxmox.Storage, error) { return f.cfg, nil } func (f fakeAPI) NodeStorage(context.Context) ([]proxmox.Storage, error) { return f.live, nil } func (f fakeAPI) StorageContent(_ context.Context, s string) ([]proxmox.StorageContent, error) { return f.content[s], nil } func (f fakeAPI) Permissions(_ context.Context, p string) (map[string]int, error) { if m, ok := f.perms[p]; ok { return m, nil } return map[string]int{}, nil } const archive = "local:backup/vzdump-lxc-9201-2026_09_23-06_55_25.tar.zst" // The real log's tail (demo-hp, 2026-09-23): 22.6 GB written, a 6.91 GB archive file. const vzdumpLog = "2026-09-23 07:02:47 INFO: Total bytes written: 22607360000 (22GiB, 49MiB/s)\n2026-09-23 07:02:47 INFO: archive file size: 6.91GB\n" // demoHP is demo-hp's storage layout: `local` (dir, backups), `local-lvm` (thin), `nvme-scratch` (dir, // rootdir — but the agent holds NO grant there), a pbs. func demoHP() fakeAPI { return fakeAPI{ cfg: []proxmox.Storage{ {Storage: "local", Type: "dir", Path: "/var/lib/vz"}, {Storage: "local-lvm", Type: "lvmthin", VGName: "pve", ThinPool: "data"}, {Storage: "nvme-scratch", Type: "dir", Path: "/mnt/hdd_1"}, {Storage: "felhom-pbs", Type: "pbs"}, }, live: []proxmox.Storage{ {Storage: "local", Type: "dir", Content: "vztmpl,backup,iso,import", Active: 1, Avail: 4 << 30, Used: 34 << 30}, {Storage: "local-lvm", Type: "lvmthin", Content: "images,rootdir", Active: 1, Avail: 23210892 * 1024, Used: 33277043 * 1024}, {Storage: "nvme-scratch", Type: "dir", Content: "images,rootdir", Active: 1, Avail: 800 << 30}, {Storage: "felhom-pbs", Type: "pbs", Content: "backup", Active: 0}, }, content: map[string][]proxmox.StorageContent{ "local": {{VolID: archive, Size: 7417540996}}, "felhom-pbs": {{VolID: "felhom-pbs:backup/ct/9201/2026-09-23T02:00:00Z", Size: 21 << 30}}, }, perms: map[string]map[string]int{ "/storage/local": {"Datastore.Audit": 1, "Datastore.AllocateSpace": 1}, "/storage/local-lvm": {"Datastore.Audit": 1, "Datastore.AllocateSpace": 1}, // nvme-scratch: only the inherited box-wide Datastore.Audit — the trap proxmox.Permissions names. "/storage/nvme-scratch": {"Datastore.Audit": 1}, }, } } // TestRestoredBytes_ReadsTheUncompressedSize — the vzdump log's "Total bytes written", never the archive // FILE size (6.9 GB for a 22.6 GB restore). // // COMPANION RED-PROOF (REPORT): return the storage content's Size for a dir storage → 7417540996, and // this test fails at "the compressed file size was used". func TestRestoredBytes_ReadsTheUncompressedSize(t *testing.T) { var asked string p := &Provider{API: demoHP(), ReadFile: func(n string) ([]byte, error) { asked = n; return []byte(vzdumpLog), nil }} n, src, err := p.RestoredBytes(context.Background(), archive) if err != nil { t.Fatal(err) } if n == 7417540996 { t.Fatal("the compressed file size was used — the restore writes 3× that") } if n != 22607360000 || asked != "/var/lib/vz/dump/vzdump-lxc-9201-2026_09_23-06_55_25.log" { t.Fatalf("n=%d from %q (log %q)", n, src, asked) } } func TestRestoredBytes_UnknownIsAnError(t *testing.T) { cases := map[string]*Provider{ "no log": {API: demoHP(), ReadFile: func(string) ([]byte, error) { return nil, errors.New("ENOENT") }}, "log without size": {API: demoHP(), ReadFile: func(string) ([]byte, error) { return []byte("ERROR: failed\n"), nil }}, } for name, p := range cases { if n, _, err := p.RestoredBytes(context.Background(), archive); err == nil { t.Fatalf("%s: got %d, want an error (the preflight then refuses)", name, n) } } if _, _, err := (&Provider{API: demoHP()}).RestoredBytes(context.Background(), "local:backup/weird.vma"); err == nil { t.Fatal("an unknown archive suffix must be an error") } } func TestRestoredBytes_PBS(t *testing.T) { p := &Provider{API: demoHP()} n, _, err := p.RestoredBytes(context.Background(), "felhom-pbs:backup/ct/9201/2026-09-23T02:00:00Z") if err != nil || n != 21<<30 { t.Fatalf("n=%d err=%v", n, err) } } // TestEligible_NeedsTheSpecificGrant — nvme-scratch takes rootdir but the agent holds only the inherited // Datastore.Audit there, so it is NOT eligible; `local` holds no rootdir. func TestEligible_NeedsTheSpecificGrant(t *testing.T) { got, err := (&Provider{API: demoHP()}).Eligible(context.Background()) if err != nil || len(got) != 1 || got[0] != "local-lvm" { t.Fatalf("eligible = %v (%v) — want only local-lvm on demo-hp", got, err) } } func TestFree_ThinCarriesMetadata(t *testing.T) { p := &Provider{API: demoHP(), ThinMeta: func(_ context.Context, vg, pool string) (float64, bool) { if vg != "pve" || pool != "data" { t.Fatalf("metadata read for %s/%s", vg, pool) } return 0.0265, true }} fr, err := p.Free(context.Background(), "local-lvm") if err != nil || !fr.Thin || !fr.MetaKnown || fr.MetaUsedFraction != 0.0265 || fr.AvailBytes != 23210892*1024 { t.Fatalf("free = %+v err=%v", fr, err) } if _, err := p.Free(context.Background(), "felhom-pbs"); err == nil { t.Fatal("an inactive storage must be an error") } }