package backup import ( "os" "path/filepath" "testing" "time" ) func genID(t time.Time) string { return NewGenerationID(t) } // TestPruneTiered_KeepsCorrectStaggering ist Pruefung 1: Prune-Test mit // simulierten Snapshots ueber mehrere Altersstufen ergibt korrekte // Staffelung. func TestPruneTiered_KeepsCorrectStaggering(t *testing.T) { now := time.Date(2026, 8, 30, 12, 0, 0, 0, time.UTC) var generations []string // 20 taegliche generationen der letzten 20 Tage. for i := 0; i < 20; i++ { generations = append(generations, genID(now.Add(-time.Duration(i)*24*time.Hour))) } // zusaetzlich aeltere, wochenweise verteilte generationen (Wochen 4-15 // zurueck), damit KeepWeekly etwas zu greifen hat, das KeepDaily nicht // schon abdeckt. for w := 4; w <= 15; w++ { generations = append(generations, genID(now.Add(-time.Duration(w*7)*24*time.Hour))) } // und einige monatealte generationen. for m := 3; m <= 10; m++ { generations = append(generations, genID(now.AddDate(0, -m, 0))) } policy := RetentionPolicy{KeepDaily: 7, KeepWeekly: 4, KeepMonthly: 6} keep, remove := PruneTiered(generations, policy) if len(keep)+len(remove) != len(generations) { t.Fatalf("keep+remove = %d, want %d (gesamtzahl)", len(keep)+len(remove), len(generations)) } // Genau die 7 juengsten taeglichen generationen muessen behalten sein. keepSet := make(map[string]bool, len(keep)) for _, k := range keep { keepSet[k] = true } for i := 0; i < 7; i++ { id := genID(now.Add(-time.Duration(i) * 24 * time.Hour)) if !keepSet[id] { t.Fatalf("generation %q (tag %d) haette per KeepDaily behalten werden muessen", id, i) } } // Zu weit zurueckliegende taegliche generationen (ausserhalb aller // Stufen) duerfen NICHT behalten sein. tooOld := genID(now.Add(-19 * 24 * time.Hour)) if keepSet[tooOld] { t.Fatalf("generation %q haette NICHT behalten werden duerfen (zu alt fuer jede stufe)", tooOld) } if len(keep) == 0 { t.Fatal("erwartet mindestens eine behaltene generation") } } // TestPruneTiered_AlwaysKeepsNewest ist das Sicherheitsnetz fuer // Akzeptanzkriterium 3 (mindestens ein aktueller Snapshot bleibt immer // erhalten) - selbst bei Policy 0/0/0 (versehentlich deaktiviert). func TestPruneTiered_AlwaysKeepsNewest(t *testing.T) { now := time.Date(2026, 8, 30, 12, 0, 0, 0, time.UTC) generations := []string{genID(now), genID(now.Add(-48 * time.Hour))} keep, remove := PruneTiered(generations, RetentionPolicy{}) if len(keep) != 1 || keep[0] != genID(now) { t.Fatalf("erwartet genau die neueste generation behalten, habe keep=%v", keep) } if len(remove) != 1 { t.Fatalf("erwartet 1 entfernte generation, habe %d", len(remove)) } } // TestPruneTiered_DeterministicForIdenticalInput - zwei Laeufe mit // identischer Eingabe liefern dasselbe Ergebnis. func TestPruneTiered_DeterministicForIdenticalInput(t *testing.T) { now := time.Date(2026, 8, 30, 12, 0, 0, 0, time.UTC) var generations []string for i := 0; i < 10; i++ { generations = append(generations, genID(now.Add(-time.Duration(i)*24*time.Hour))) } policy := RetentionPolicy{KeepDaily: 3} keep1, remove1 := PruneTiered(generations, policy) keep2, remove2 := PruneTiered(generations, policy) if len(keep1) != len(keep2) || len(remove1) != len(remove2) { t.Fatal("unterschiedliches ergebnis zwischen zwei laeufen mit identischer eingabe") } } // TestPruneRotate_DeletesOnlyExpiredGenerations ist Pruefung 2: // automatisierte Loeschung abgelaufener Snapshots im Test nachgewiesen - // real gegen echte Verzeichnisse. func TestPruneRotate_DeletesOnlyExpiredGenerations(t *testing.T) { backupDir := t.TempDir() now := time.Date(2026, 8, 30, 12, 0, 0, 0, time.UTC) var recent, expired string for i := 0; i < 3; i++ { id := genID(now.Add(-time.Duration(i) * 24 * time.Hour)) if err := os.MkdirAll(filepath.Join(backupDir, id, FullBackupDirName), 0o750); err != nil { t.Fatal(err) } recent = id } expired = genID(now.Add(-60 * 24 * time.Hour)) if err := os.MkdirAll(filepath.Join(backupDir, expired, FullBackupDirName), 0o750); err != nil { t.Fatal(err) } removed, err := PruneRotate(backupDir, RetentionPolicy{KeepDaily: 3}) if err != nil { t.Fatalf("prunerotate: %v", err) } if len(removed) != 1 || removed[0] != expired { t.Fatalf("erwartet genau die abgelaufene generation %q entfernt, habe %v", expired, removed) } if _, err := os.Stat(filepath.Join(backupDir, recent)); err != nil { t.Fatalf("juengste generation faelschlich entfernt: %v", err) } if _, err := os.Stat(filepath.Join(backupDir, expired)); !os.IsNotExist(err) { t.Fatal("abgelaufene generation wurde nicht tatsaechlich vom dateisystem entfernt") } }