package backup import ( "context" "os" "path/filepath" "testing" "time" ) func requireTestConfig(t *testing.T) Config { t.Helper() user := os.Getenv("TEST_BACKUP_PG_USER") if user == "" { t.Skip("TEST_BACKUP_PG_USER nicht gesetzt, Integrationstest uebersprungen (braucht echten Postgres mit REPLICATION-Rolle)") } return Config{ Host: envOr("TEST_BACKUP_PG_HOST", "localhost"), Port: envOr("TEST_BACKUP_PG_PORT", "5432"), User: user, Password: os.Getenv("TEST_BACKUP_PG_PASSWORD"), BackupDir: t.TempDir(), } } func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } // TestFullBackup_CreatesVerifiedBackup ist Pruefung 1: Sicherung gegen // Testdatenbank erfolgreich erstellt und verifiziert. func TestFullBackup_CreatesVerifiedBackup(t *testing.T) { cfg := requireTestConfig(t) ctx := context.Background() genID := NewGenerationID(time.Now()) manifest, err := FullBackup(ctx, cfg, genID) if err != nil { t.Fatalf("fullbackup: %v", err) } if _, err := os.Stat(manifest); err != nil { t.Fatalf("backup_manifest fehlt: %v", err) } dir := filepath.Dir(manifest) if _, err := os.Stat(filepath.Join(dir, BaseTarGzFile)); err != nil { t.Fatalf("%s fehlt: %v", BaseTarGzFile, err) } if err := Verify(dir); err != nil { t.Fatalf("verify: %v", err) } } // TestIncrementalBackup_IsSmallerThanFull ist der Nachweis fuer // Akzeptanzkriterium 1 (inkrementell): eine echte inkrementelle Sicherung // gegen unveraenderten Bestand ist deutlich kleiner als die Vollsicherung — // beweist, dass tatsaechlich nur Aenderungen uebertragen wurden (PostgreSQL // 17 WAL-Summarization), nicht nochmal alles. func TestIncrementalBackup_IsSmallerThanFull(t *testing.T) { cfg := requireTestConfig(t) ctx := context.Background() genID := NewGenerationID(time.Now()) fullManifest, err := FullBackup(ctx, cfg, genID) if err != nil { t.Fatalf("fullbackup: %v", err) } fullDir := filepath.Dir(fullManifest) fullSize := fileSize(t, filepath.Join(fullDir, BaseTarGzFile)) incID := NewGenerationID(time.Now().Add(time.Second)) incManifest, err := IncrementalBackup(ctx, cfg, genID, incID, fullManifest) if err != nil { t.Fatalf("incrementalbackup: %v", err) } incDir := filepath.Dir(incManifest) if err := Verify(incDir); err != nil { t.Fatalf("verify (inkrementell): %v", err) } incSize := fileSize(t, filepath.Join(incDir, BaseTarGzFile)) if incSize >= fullSize { t.Fatalf("inkrementelle sicherung (%d bytes) ist nicht kleiner als die vollsicherung (%d bytes) - keine echte inkrementelle Uebertragung", incSize, fullSize) } } func fileSize(t *testing.T, path string) int64 { t.Helper() info, err := os.Stat(path) if err != nil { t.Fatalf("dateigroesse von %q ermitteln: %v", path, err) } return info.Size() } // TestVerify_DetectsCorruptedFile ist Pruefung 2: Verifikation erkennt eine // absichtlich beschaedigte Sicherungsdatei. func TestVerify_DetectsCorruptedFile(t *testing.T) { cfg := requireTestConfig(t) ctx := context.Background() genID := NewGenerationID(time.Now()) manifest, err := FullBackup(ctx, cfg, genID) if err != nil { t.Fatalf("fullbackup: %v", err) } dir := filepath.Dir(manifest) if err := Verify(dir); err != nil { t.Fatalf("verify (unbeschaedigt) haette erfolgreich sein muessen: %v", err) } // Absichtliche Beschaedigung: mehrere Bytes in der Mitte der Datei kippen. path := filepath.Join(dir, BaseTarGzFile) data, err := os.ReadFile(path) if err != nil { t.Fatalf("sicherungsdatei lesen: %v", err) } mid := len(data) / 2 for i := mid; i < mid+64 && i < len(data); i++ { data[i] ^= 0xFF } if err := os.WriteFile(path, data, 0o600); err != nil { t.Fatalf("beschaedigte sicherungsdatei schreiben: %v", err) } if err := Verify(dir); err == nil { t.Fatal("verify haette die beschaedigte sicherungsdatei erkennen muessen") } } // TestRotate_RemovesOnlyOldestGenerations ist Pruefung 3. func TestRotate_RemovesOnlyOldestGenerations(t *testing.T) { backupDir := t.TempDir() generationIDs := []string{ "20260101T000000Z", "20260102T000000Z", "20260103T000000Z", "20260104T000000Z", "20260105T000000Z", } for _, id := range generationIDs { if err := os.MkdirAll(filepath.Join(backupDir, id, FullBackupDirName), 0o750); err != nil { t.Fatalf("generation %q anlegen: %v", id, err) } } removed, err := Rotate(backupDir, 2) if err != nil { t.Fatalf("rotate: %v", err) } wantRemoved := []string{"20260101T000000Z", "20260102T000000Z", "20260103T000000Z"} if len(removed) != len(wantRemoved) { t.Fatalf("entfernte generationen = %v, want %v", removed, wantRemoved) } for i, w := range wantRemoved { if removed[i] != w { t.Fatalf("entfernte generationen = %v, want %v", removed, wantRemoved) } } remaining, err := ListGenerations(backupDir) if err != nil { t.Fatalf("listgenerations: %v", err) } wantRemaining := []string{"20260104T000000Z", "20260105T000000Z"} if len(remaining) != len(wantRemaining) { t.Fatalf("verbleibende generationen = %v, want %v", remaining, wantRemaining) } for i, w := range wantRemaining { if remaining[i] != w { t.Fatalf("verbleibende generationen = %v, want %v", remaining, wantRemaining) } } // Die NEUESTEN duerfen NICHT entfernt sein (Pruefung 3: nur die // aeltesten Generationen). for _, w := range wantRemaining { if _, err := os.Stat(filepath.Join(backupDir, w)); err != nil { t.Fatalf("neueste generation %q wurde faelschlich entfernt: %v", w, err) } } }