package objectbackup import ( "context" "encoding/json" "os" "path/filepath" "testing" "time" ) // backupAt erstellt einen Snapshot mit ERZWUNGENEM Zeitstempel // (restics `backup --time`) - so lassen sich mehrere Altersstufen real // simulieren, ohne tatsaechlich tagelang zu warten. func backupAt(t *testing.T, cfg Config, sourceDir string, at time.Time) { t.Helper() // restics --time erwartet SEIN EIGENES Format ("2006-01-02 15:04:05"), // nicht RFC3339 - real erst hier festgestellt (restic verweigert // RFC3339 mit einem Parse-Fehler). output, err := run(context.Background(), cfg, "backup", sourceDir, "--time", at.Format("2006-01-02 15:04:05"), "--json") if err != nil { t.Fatalf("backup (zeitstempel %s): %v (ausgabe: %s)", at, err, output) } } // TestPruneTiered_KeepsCorrectStaggering_RealSnapshots ist Pruefung 1 // fuer den Objekt-Storage-Teil: reale, gestaffelt datierte Snapshots, // restics eigene Staffelung liefert die erwartete Anzahl verbleibender // Snapshots. func TestPruneTiered_KeepsCorrectStaggering_RealSnapshots(t *testing.T) { cfg := setupTest(t) sourceDir := t.TempDir() writeFile(t, sourceDir, "datei.txt", "inhalt") now := time.Now().UTC() for i := 0; i < 10; i++ { backupAt(t, cfg, sourceDir, now.Add(-time.Duration(i)*24*time.Hour)) } if got, err := SnapshotCount(context.Background(), cfg); err != nil || got != 10 { t.Fatalf("erwartet 10 snapshots vor dem prune, habe %d (err=%v)", got, err) } if err := PruneTiered(context.Background(), cfg, RetentionPolicy{KeepDaily: 3}); err != nil { t.Fatalf("prunetiered: %v", err) } got, err := SnapshotCount(context.Background(), cfg) if err != nil { t.Fatalf("snapshotcount: %v", err) } if got != 3 { t.Fatalf("erwartet 3 verbleibende snapshots nach KeepDaily=3, habe %d", got) } } // TestPruneTiered_KeptSnapshotStillFullyRestorable ist der reale Beweis // fuer Akzeptanzkriterium 3 (Pruefung 3, Legal-Hold-Analog): Pruning // aelterer Snapshots darf niemals Daten beschaedigen, die ein // VERBLEIBENDER (aktueller) Snapshot noch braucht - genau die // Eigenschaft, die verhindert, dass Pruning mit einer laufenden // Dokumenten-Aufbewahrungssperre kollidiert: solange das Dokument in // einem behaltenen Snapshot steckt, bleibt es nach dem Pruning // vollstaendig wiederherstellbar. func TestPruneTiered_KeptSnapshotStillFullyRestorable(t *testing.T) { cfg := setupTest(t) sourceDir := t.TempDir() content := "unveraendertes dokument, muss nach dem pruning vollstaendig lesbar bleiben" writeFile(t, sourceDir, "gehaltenes-dokument.txt", content) now := time.Now().UTC() // mehrere aeltere snapshots desselben unveraenderten inhalts (dedupliziert) for i := 5; i >= 1; i-- { backupAt(t, cfg, sourceDir, now.Add(-time.Duration(i)*24*time.Hour)) } // EIN aktueller, zu behaltender snapshot backupAt(t, cfg, sourceDir, now) // erwarte 5 aeltere zu entfernen, den juengsten zu behalten if err := PruneTiered(context.Background(), cfg, RetentionPolicy{KeepDaily: 1}); err != nil { t.Fatalf("prunetiered: %v", err) } remaining, err := SnapshotCount(context.Background(), cfg) if err != nil { t.Fatalf("snapshotcount: %v", err) } if remaining != 1 { t.Fatalf("erwartet 1 verbleibenden snapshot, habe %d", remaining) } latestID := latestSnapshotShortID(t, cfg) targetDir := t.TempDir() if err := Restore(context.Background(), cfg, latestID, targetDir); err != nil { t.Fatalf("restore nach pruning: %v", err) } got, err := os.ReadFile(filepath.Join(targetDir, sourceDir, "gehaltenes-dokument.txt")) if err != nil { t.Fatalf("wiederhergestelltes dokument lesen: %v", err) } if string(got) != content { t.Fatalf("dokumentinhalt nach pruning beschaedigt: %q, want %q", got, content) } // Zusaetzlich: restics eigene Integritaetspruefung bestaetigt, dass // das Pruning kein von behaltenen Snapshots benoetigtes Datenblock // entfernt hat. if err := Check(context.Background(), cfg, true); err != nil { t.Fatalf("check nach pruning: %v", err) } } func latestSnapshotShortID(t *testing.T, cfg Config) string { t.Helper() output, err := run(context.Background(), cfg, "snapshots", "--json") if err != nil { t.Fatalf("snapshots: %v", err) } var snapshots []snapshotEntry if err := json.Unmarshal(output, &snapshots); err != nil { t.Fatalf("snapshot-liste dekodieren: %v", err) } if len(snapshots) == 0 { t.Fatal("keine snapshots vorhanden") } return snapshots[len(snapshots)-1].ShortID }