package backup import ( "context" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" "time" ) // TestRestore_CombinesIntoStartablePostgresInstance ist Pruefung 1 fuer // BAK-03 (Datenbank-Teil): Restore auf leerem System vollstaendig und // erfolgreich durchgefuehrt — real bewiesen, indem das wiederhergestellte // Verzeichnis tatsaechlich als eigenstaendige Postgres-Instanz gestartet // und per echter Verbindung abgefragt wird, nicht nur auf Dateiexistenz // geprueft. func TestRestore_CombinesIntoStartablePostgresInstance(t *testing.T) { cfg := requireTestConfig(t) ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() if _, err := exec.LookPath("pg_combinebackup"); err != nil { t.Skip("pg_combinebackup nicht installiert, restore-integrationstest uebersprungen") } pgCtl, err := exec.LookPath("pg_ctl") if err != nil { t.Skip("pg_ctl nicht installiert, restore-integrationstest uebersprungen") } genID := NewGenerationID(time.Now()) if _, err := FullBackup(ctx, cfg, genID); err != nil { t.Fatalf("fullbackup: %v", err) } restoreOut := filepath.Join(t.TempDir(), "restored-pgdata") if err := os.MkdirAll(restoreOut, 0o700); err != nil { t.Fatal(err) } if err := Restore(ctx, cfg, genID, restoreOut); err != nil { t.Fatalf("restore: %v", err) } if _, err := os.Stat(filepath.Join(restoreOut, "PG_VERSION")); err != nil { t.Fatalf("wiederhergestelltes verzeichnis ist kein gueltiges PGDATA (PG_VERSION fehlt): %v", err) } // Auf diesem Debian-Postgres liegt postgresql.conf NICHT in PGDATA // (sondern in /etc/postgresql/17/main/) - pg_basebackup sichert daher // nur PGDATA-Inhalte, die Konfigurationsdatei fehlt im Restore // GENAUSO wie im echten Betriebs-Restore-Verfahren. Fuer den // End-zu-End-Nachweis (echte Daten wiederherstellbar) hier eine // minimale, ausschliesslich fuer den Testlauf gueltige Konfiguration // nachgereicht - dokumentiert als operativer Hinweis fuer ein echtes // Restore-Runbook, nicht Teil des Produktcodes. minimalConf := "listen_addresses = ''\n" if err := os.WriteFile(filepath.Join(restoreOut, "postgresql.conf"), []byte(minimalConf), 0o600); err != nil { t.Fatal(err) } // dito pg_hba.conf - liegt auf Debian ebenfalls in /etc/postgresql, // nicht in PGDATA. Nur Unix-Socket-Verbindungen des lokalen Testlaufs // erlaubt (kein TCP, dieselbe Isolation wie listen_addresses=''). hba := "local all all trust\n" if err := os.WriteFile(filepath.Join(restoreOut, "pg_hba.conf"), []byte(hba), 0o600); err != nil { t.Fatal(err) } // Konfigurationsdateien aus der Quellinstanz brachte pg_basebackup mit // (sie enthalten ggf. den alten Port/Unix-Socket) - fuer diesen Test // bewusst neuer Port und eigenes Socket-Verzeichnis, damit die // wiederhergestellte Instanz nicht mit der laufenden Test-Instanz // kollidiert. socketDir := t.TempDir() testPort := "55432" // WICHTIG: pg_ctl start foerdert postgres als Hintergrundprozess, der // die geerbten stdout/stderr-Pipes NICHT schliesst - CombinedOutput() // (das auf ein Pipe-EOF wartet) haengt sich daher auf, obwohl pg_ctl // selbst laengst zurueckgekehrt ist. Deshalb echte Logdatei statt Pipe // (Standard-pg_ctl-Muster), kein exec.Cmd.Stdout/-Stderr-Pipe-Zugriff. logFile := filepath.Join(t.TempDir(), "postgres.log") startCmd := exec.CommandContext(ctx, pgCtl, "start", "-D", restoreOut, "-w", "-t", "30", "-l", logFile, "-o", fmt.Sprintf("-p %s -k %s -c listen_addresses=''", testPort, socketDir)) if err := startCmd.Run(); err != nil { logContent, _ := os.ReadFile(logFile) t.Fatalf("pg_ctl start (wiederhergestellte instanz): %v (log: %s)", err, string(logContent)) } defer func() { stopCmd := exec.Command(pgCtl, "stop", "-D", restoreOut, "-m", "fast") _ = stopCmd.Run() }() psqlOut, err := exec.CommandContext(ctx, "psql", "-h", socketDir, "-p", testPort, "-U", cfg.User, "-d", "postgres", "-tAc", "SELECT 1").CombinedOutput() if err != nil { t.Fatalf("echte verbindung zur wiederhergestellten instanz fehlgeschlagen: %v (ausgabe: %s)", err, string(psqlOut)) } if strings.TrimSpace(string(psqlOut)) != "1" { t.Fatalf("unerwartete antwort von der wiederhergestellten instanz: %q", string(psqlOut)) } }