internal/retention: object_type/object_reference als reine Textfelder (Adapter-Muster, keine Fremdschluessel auf DMS-/Mail-Tabellen). Aufbewahrungsklassen-Zuordnung historisiert (jede Zuordnung eigene, unveraenderliche Zeile). Migration real vorwaerts+rueckwaerts gegen die tatsaechlichen .sql-Dateien getestet, Mandantentrennung gegen echtes zweites Tenant-DB bewiesen. Grundlage fuer RET-05 (Adapter- Interface) und RET-02.
79 lines
2.5 KiB
Go
79 lines
2.5 KiB
Go
package retention
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// TestMigration_ForwardAndBackward ist Pruefung 3 fuer Akzeptanzkriterium
|
|
// 3: Migration laeuft gegen leere Datenbank durch UND ist rueckrollbar -
|
|
// real gegen die TATSAECHLICHEN Migrationsdateien, kein Nachbau.
|
|
func TestMigration_ForwardAndBackward(t *testing.T) {
|
|
dsn := os.Getenv("TEST_TENANT_DSN")
|
|
if dsn == "" {
|
|
t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest uebersprungen")
|
|
}
|
|
if _, err := exec.LookPath("psql"); err != nil {
|
|
t.Skip("psql nicht installiert, Integrationstest uebersprungen")
|
|
}
|
|
|
|
_, thisFile, _, _ := runtime.Caller(0)
|
|
migrationsDir := filepath.Join(filepath.Dir(thisFile), "..", "..", "migrations")
|
|
upSQL := filepath.Join(migrationsDir, "0002_retention.up.sql")
|
|
downSQL := filepath.Join(migrationsDir, "0002_retention.down.sql")
|
|
|
|
ctx := context.Background()
|
|
pool, err := pgxpool.New(ctx, dsn)
|
|
if err != nil {
|
|
t.Fatalf("pool: %v", err)
|
|
}
|
|
defer pool.Close()
|
|
// sauberer Ausgangszustand, falls von einem frueheren Testlauf uebrig.
|
|
_, _ = pool.Exec(ctx, `DROP TABLE IF EXISTS retention_class_assignments, retention_objects CASCADE`)
|
|
|
|
runPsql := func(sqlFile string) []byte {
|
|
t.Helper()
|
|
cmd := exec.CommandContext(ctx, "psql", dsn, "-v", "ON_ERROR_STOP=1", "-f", sqlFile)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("psql -f %s: %v (ausgabe: %s)", sqlFile, err, out)
|
|
}
|
|
return out
|
|
}
|
|
|
|
tableExists := func(name string) bool {
|
|
var exists bool
|
|
if err := pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1)`, name).Scan(&exists); err != nil {
|
|
t.Fatalf("tabellenexistenz pruefen: %v", err)
|
|
}
|
|
return exists
|
|
}
|
|
|
|
// vorwaerts
|
|
runPsql(upSQL)
|
|
if !tableExists("retention_objects") || !tableExists("retention_class_assignments") {
|
|
t.Fatal("migration vorwaerts: erwartete tabellen fehlen")
|
|
}
|
|
|
|
// rueckwaerts
|
|
runPsql(downSQL)
|
|
if tableExists("retention_objects") || tableExists("retention_class_assignments") {
|
|
t.Fatal("migration rueckwaerts: tabellen haetten entfernt sein muessen")
|
|
}
|
|
|
|
// erneut vorwaerts (beweist: rueckwaerts hat wirklich sauber
|
|
// aufgeraeumt, kein Rest, der einen zweiten Vorwaertslauf bloeckieren wuerde)
|
|
runPsql(upSQL)
|
|
if !tableExists("retention_objects") {
|
|
t.Fatal("zweiter vorwaertslauf nach rollback fehlgeschlagen")
|
|
}
|
|
// aufraeumen
|
|
_, _ = pool.Exec(ctx, `DROP TABLE IF EXISTS retention_class_assignments, retention_objects CASCADE`)
|
|
}
|