// Package testdbisolation demonstriert und prüft die in docs/TESTSTRATEGIE-CORE.md Abschnitt 4 // beschriebene Testdatenbank-Isolation: jeder Test, der eine Tenant-Datenbank braucht, // provisioniert seine eigene, eindeutig benannte DB und räumt sie über Cleanup wieder ab — // keine geteilten Fixture-Datenbanken zwischen parallelen Testläufen. package testdbisolation import ( "context" "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) // ProvisionEphemeralDB legt unter dem gegebenen Admin-DSN eine neue, eindeutig benannte Datenbank an // und liefert eine Cleanup-Funktion, die sie wieder droppt. adminDSN muss auf eine Wartungs-DB // (z. B. "postgres") zeigen, name muss ein gültiger, bereits eindeutiger Bezeichner sein. func ProvisionEphemeralDB(ctx context.Context, adminDSN, name string) (cleanup func(context.Context) error, err error) { conn, err := pgx.Connect(ctx, adminDSN) if err != nil { return nil, fmt.Errorf("verbindung zur wartungs-db fehlgeschlagen: %w", err) } defer conn.Close(ctx) if _, err := conn.Exec(ctx, fmt.Sprintf("CREATE DATABASE %s", pgx.Identifier{name}.Sanitize())); err != nil { return nil, fmt.Errorf("datenbank %q konnte nicht angelegt werden: %w", name, err) } cleanup = func(cleanupCtx context.Context) error { adminConn, err := pgx.Connect(cleanupCtx, adminDSN) if err != nil { return fmt.Errorf("cleanup: verbindung fehlgeschlagen: %w", err) } defer adminConn.Close(cleanupCtx) _, err = adminConn.Exec(cleanupCtx, fmt.Sprintf("DROP DATABASE IF EXISTS %s", pgx.Identifier{name}.Sanitize())) return err } return cleanup, nil } // WriteMarker schreibt einen eindeutigen Beleg-Datensatz in die angegebene Datenbank (verbindet // per dsnForDB), um im Test nachzuweisen, dass zwei parallele Provisionierungen sich nicht sehen. func WriteMarker(ctx context.Context, dsn, table, value string) error { pool, err := pgxpool.New(ctx, dsn) if err != nil { return err } defer pool.Close() if _, err := pool.Exec(ctx, fmt.Sprintf( "CREATE TABLE IF NOT EXISTS %s (value text primary key)", pgx.Identifier{table}.Sanitize(), )); err != nil { return err } _, err = pool.Exec(ctx, fmt.Sprintf("INSERT INTO %s (value) VALUES ($1)", pgx.Identifier{table}.Sanitize()), value) return err } // CountMarkers zählt die Belegzeilen in der angegebenen Datenbank/Tabelle. func CountMarkers(ctx context.Context, dsn, table string) (int, error) { pool, err := pgxpool.New(ctx, dsn) if err != nil { return 0, err } defer pool.Close() if _, err := pool.Exec(ctx, fmt.Sprintf( "CREATE TABLE IF NOT EXISTS %s (value text primary key)", pgx.Identifier{table}.Sanitize(), )); err != nil { return 0, err } var n int err = pool.QueryRow(ctx, fmt.Sprintf("SELECT count(*) FROM %s", pgx.Identifier{table}.Sanitize())).Scan(&n) return n, err }