package backup import ( "context" "testing" "gitea.dooplex.hu/admin/felhom-agent/internal/config" "gitea.dooplex.hu/admin/felhom-agent/internal/proxmox" ) // localTarget is a non-PBS dir storage; pbsTarget is a PBS storage — for the scope gate. var ( localTargetStores = []proxmox.Storage{{Storage: "local", Type: "dir", Content: "backup"}} pbsTargetStores = []proxmox.Storage{{Storage: "felhom-pbs", Type: "pbs", Content: "backup"}} ) func okAPI(stores []proxmox.Storage) *fakeBackupAPI { return &fakeBackupAPI{vzdumpUPID: "UPID:vzdump:1", storages: stores} } // TestPrune_LocalCarriesKeepLast: a LOCAL-target backup with retention carries `--prune-backups // keep-last=3` on the vzdump. Companion: the SAME runner built with no retention ("") emits NO prune // option — proving the flag only rides when retention is set (a no-prune build accumulates). func TestPrune_LocalCarriesKeepLast(t *testing.T) { api := okAPI(localTargetStores) r := NewBackupRunner(api, "local", proxmox.ModeStop, "", "keep-last=3", quiet()) _, _ = r.Backup(context.Background(), 9201) // archive-resolution may fail in the fake; we assert the captured vzdump opts if len(api.vzdumps) != 1 || api.vzdumps[0].PruneBackups != "keep-last=3" { t.Fatalf("local backup must carry prune-backups keep-last=3, got %q", api.vzdumps[0].PruneBackups) } // COMPANION: no retention → no prune option (dumps would accumulate). api2 := okAPI(localTargetStores) r2 := NewBackupRunner(api2, "local", proxmox.ModeStop, "", "", quiet()) _, _ = r2.Backup(context.Background(), 9201) if api2.vzdumps[0].PruneBackups != "" { t.Fatalf("a no-retention runner must NOT prune, got %q", api2.vzdumps[0].PruneBackups) } } // TestPrune_NeverPrunesPBS is the scope rule (§9): retention is NOT applied when the target is a PBS // storage (offsite retention is a separate lifecycle). Companion: the identical retention on a LOCAL // target IS applied — proving the gate keys on storage type, not luck. func TestPrune_NeverPrunesPBS(t *testing.T) { api := okAPI(pbsTargetStores) r := NewBackupRunner(api, "felhom-pbs", proxmox.ModeStop, "", "keep-last=3", quiet()) _, _ = r.Backup(context.Background(), 9201) if api.vzdumps[0].PruneBackups != "" { t.Fatalf("a PBS target must NEVER be pruned by the per-run flag, got %q", api.vzdumps[0].PruneBackups) } // COMPANION: same retention, local target → applied. api2 := okAPI(localTargetStores) r2 := NewBackupRunner(api2, "local", proxmox.ModeStop, "", "keep-last=3", quiet()) _, _ = r2.Backup(context.Background(), 9201) if api2.vzdumps[0].PruneBackups != "keep-last=3" { t.Fatalf("control: a local target with the same retention MUST be pruned, got %q", api2.vzdumps[0].PruneBackups) } } // TestPrune_FailSafeOnUnknownTarget: if the target's type can't be confirmed (lookup error / not in the // list), the run SKIPS pruning rather than risk pruning a PBS/unknown storage. func TestPrune_FailSafeOnUnknownTarget(t *testing.T) { // target not present in the list → skip. api := &fakeBackupAPI{vzdumpUPID: "UPID:vzdump:1", storages: localTargetStores} r := NewBackupRunner(api, "some-other-store", proxmox.ModeStop, "", "keep-last=3", quiet()) _, _ = r.Backup(context.Background(), 9201) if api.vzdumps[0].PruneBackups != "" { t.Fatalf("an unknown target must skip pruning (fail-safe), got %q", api.vzdumps[0].PruneBackups) } } // TestPrune_KeepLastClamp (§7-B): a 0/negative/unset LocalBackupRetention clamps to ≥1 (default 3) so the // vzdump NEVER prunes the archive it just made. Companion: a no-clamp impl that returns 0 would emit // keep-last=0 → PVE prunes everything → FAILS the "≥1" assertion. func TestPrune_KeepLastClamp(t *testing.T) { cases := []struct { set int want int }{ {0, 3}, // unset → default {-5, 3}, // negative → default {1, 1}, // honored {3, 3}, // honored {10, 10}, // honored } for _, c := range cases { b := config.BackupConfig{LocalBackupRetention: c.set} if got := b.KeepLast(); got != c.want { t.Errorf("KeepLast(%d) = %d, want %d", c.set, got, c.want) } if b.KeepLast() < 1 { t.Fatalf("keep-last must NEVER be < 1 (would prune the fresh backup), got %d for %d", b.KeepLast(), c.set) } } if spec := (config.BackupConfig{}).PruneBackupsSpec(); spec != "keep-last=3" { t.Fatalf("default PruneBackupsSpec = %q, want keep-last=3", spec) } }