package tenantbackup import ( "context" "os" "os/exec" "testing" "time" ) // requireTestConfig braucht eine EIGENE Rolle mit CREATEDB-Recht // (TEST_TENANTBACKUP_PG_USER), NICHT BAK-01/BAK-03s TEST_BACKUP_PG_USER // (nexarch_backup) - jene Rolle hat bewusst nur REPLICATION, kein // CREATEDB (Prinzip geringster Rechte, siehe BAK-01). BAK-04 braucht // stattdessen CREATEDB, um isolierte Ziel-Datenbanken anzulegen - // bekommt daher eine eigene, separat scharf gestellte Rolle. func requireTestConfig(t *testing.T) Config { t.Helper() user := os.Getenv("TEST_TENANTBACKUP_PG_USER") if user == "" { t.Skip("TEST_TENANTBACKUP_PG_USER nicht gesetzt, Integrationstest uebersprungen (braucht echte Postgres-Rolle mit CREATEDB)") } if _, err := exec.LookPath("pg_dump"); err != nil { t.Skip("pg_dump nicht installiert, Integrationstest uebersprungen") } return Config{ Host: envOr("TEST_TENANTBACKUP_PG_HOST", "localhost"), Port: envOr("TEST_TENANTBACKUP_PG_PORT", "5432"), User: user, Password: os.Getenv("TEST_TENANTBACKUP_PG_PASSWORD"), BackupDir: t.TempDir(), } } func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } // createMarkerDB legt eine frische Testdatenbank mit genau EINER // Markierungszeile an (Kennzeichen für "das ist eindeutig Tenant X's // Datensatz") - real ueber psql, kein Mock. func createMarkerDB(t *testing.T, cfg Config, dbName, marker string) { t.Helper() ctx := context.Background() run := func(args ...string) { t.Helper() cmd := exec.CommandContext(ctx, cfg.psqlBinary(), args...) cmd.Env = append(os.Environ(), "PGPASSWORD="+cfg.Password) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("psql %v: %v (ausgabe: %s)", args, err, out) } } base := []string{"-h", cfg.Host, "-p", cfg.Port, "-U", cfg.User, "--no-password"} run(append(append([]string{}, base...), "-d", "postgres", "-c", "DROP DATABASE IF EXISTS "+quoteIdent(dbName))...) run(append(append([]string{}, base...), "-d", "postgres", "-c", "CREATE DATABASE "+quoteIdent(dbName))...) run(append(append([]string{}, base...), "-d", dbName, "-c", "CREATE TABLE marker (value TEXT); INSERT INTO marker VALUES ('"+marker+"')")...) t.Cleanup(func() { run(append(append([]string{}, base...), "-d", "postgres", "-c", "DROP DATABASE IF EXISTS "+quoteIdent(dbName))...) }) } func markerValue(t *testing.T, cfg Config, dbName string) string { t.Helper() cmd := exec.CommandContext(context.Background(), cfg.psqlBinary(), "-h", cfg.Host, "-p", cfg.Port, "-U", cfg.User, "-d", dbName, "--no-password", "-tAc", "SELECT value FROM marker") cmd.Env = append(os.Environ(), "PGPASSWORD="+cfg.Password) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("markerwert lesen: %v (ausgabe: %s)", err, out) } return trimNL(string(out)) } func trimNL(s string) string { for len(s) > 0 && (s[len(s)-1] == '\n' || s[len(s)-1] == '\r') { s = s[:len(s)-1] } return s } func databaseExists(t *testing.T, cfg Config, dbName string) bool { t.Helper() cmd := exec.CommandContext(context.Background(), cfg.psqlBinary(), "-h", cfg.Host, "-p", cfg.Port, "-U", cfg.User, "-d", "postgres", "--no-password", "-tAc", "SELECT 1 FROM pg_database WHERE datname = '"+dbName+"'") cmd.Env = append(os.Environ(), "PGPASSWORD="+cfg.Password) out, _ := cmd.CombinedOutput() return trimNL(string(out)) == "1" } // TestBackupRestore_RecoversExactTenantData ist Pruefung 1 (angepasst // auf Akzeptanzkriterium 1): eine Sicherung von Tenant A, wiederhergestellt // in eine isolierte Zieldatenbank, enthaelt real Tenant As Datensatz. func TestBackupRestore_RecoversExactTenantData(t *testing.T) { cfg := requireTestConfig(t) ctx := context.Background() tenantA := "tenantbackup_test_a" createMarkerDB(t, cfg, tenantA, "gehoert-zu-tenant-a") dumpPath, err := Backup(ctx, cfg, tenantA, time.Now().UTC().Format("20060102T150405Z")) if err != nil { t.Fatalf("backup: %v", err) } if err := Verify(ctx, cfg, dumpPath); err != nil { t.Fatalf("verify: %v", err) } target := "tenantbackup_test_a_restored" if databaseExists(t, cfg, target) { t.Fatalf("zieldatenbank %q existiert bereits vor dem test", target) } if err := CreateEmptyDatabase(ctx, cfg, target); err != nil { t.Fatalf("createemptydatabase: %v", err) } t.Cleanup(func() { cmd := exec.Command(cfg.psqlBinary(), "-h", cfg.Host, "-p", cfg.Port, "-U", cfg.User, "-d", "postgres", "--no-password", "-c", "DROP DATABASE IF EXISTS "+quoteIdent(target)) cmd.Env = append(os.Environ(), "PGPASSWORD="+cfg.Password) _ = cmd.Run() }) if err := Restore(ctx, cfg, dumpPath, target); err != nil { t.Fatalf("restore: %v", err) } got := markerValue(t, cfg, target) if got != "gehoert-zu-tenant-a" { t.Fatalf("markerwert in wiederhergestellter datenbank = %q, want %q", got, "gehoert-zu-tenant-a") } } // TestRestore_DoesNotAffectOtherTenant ist Pruefung 2: Wiederherstellung // von Tenant A veraendert Tenant B nicht - reales zweites Tenant-DB, // Markerwert vor UND nach dem Restore geprueft. func TestRestore_DoesNotAffectOtherTenant(t *testing.T) { cfg := requireTestConfig(t) ctx := context.Background() tenantA := "tenantbackup_test_iso_a" tenantB := "tenantbackup_test_iso_b" createMarkerDB(t, cfg, tenantA, "wert-a") createMarkerDB(t, cfg, tenantB, "wert-b-unveraendert") dumpPath, err := Backup(ctx, cfg, tenantA, time.Now().UTC().Format("20060102T150405Z")) if err != nil { t.Fatalf("backup: %v", err) } target := "tenantbackup_test_iso_a_restored" if err := CreateEmptyDatabase(ctx, cfg, target); err != nil { t.Fatalf("createemptydatabase: %v", err) } t.Cleanup(func() { cmd := exec.Command(cfg.psqlBinary(), "-h", cfg.Host, "-p", cfg.Port, "-U", cfg.User, "-d", "postgres", "--no-password", "-c", "DROP DATABASE IF EXISTS "+quoteIdent(target)) cmd.Env = append(os.Environ(), "PGPASSWORD="+cfg.Password) _ = cmd.Run() }) if err := Restore(ctx, cfg, dumpPath, target); err != nil { t.Fatalf("restore: %v", err) } // Tenant B, der nie an diesem Vorgang beteiligt war, muss unveraendert sein. gotB := markerValue(t, cfg, tenantB) if gotB != "wert-b-unveraendert" { t.Fatalf("tenant b wurde veraendert: %q, want %q", gotB, "wert-b-unveraendert") } // Und Tenant Bs Datenbank existiert weiterhin unter ihrem eigenen Namen // (kein versehentliches Ueberschreiben/Umbenennen). if !databaseExists(t, cfg, tenantB) { t.Fatal("tenant-b-datenbank fehlt nach restore von tenant a") } } // TestVerify_DetectsCorruptedDump ist Nachweis der Vollstaendigkeitspruefung. func TestVerify_DetectsCorruptedDump(t *testing.T) { cfg := requireTestConfig(t) ctx := context.Background() tenantA := "tenantbackup_test_corrupt" createMarkerDB(t, cfg, tenantA, "wert") dumpPath, err := Backup(ctx, cfg, tenantA, time.Now().UTC().Format("20060102T150405Z")) if err != nil { t.Fatalf("backup: %v", err) } if err := Verify(ctx, cfg, dumpPath); err != nil { t.Fatalf("verify (unbeschaedigt) haette erfolgreich sein muessen: %v", err) } if err := os.WriteFile(dumpPath, []byte("kein gueltiges pg_dump-custom-archiv"), 0o600); err != nil { t.Fatal(err) } if err := Verify(ctx, cfg, dumpPath); err == nil { t.Fatal("verify haette die beschaedigte sicherung erkennen muessen") } }