// Package e2e enthaelt QA-02 (Pruefgate Identitaet & Mandanten): echte // End-to-End-Tests gegen eine laufende Postgres-Instanz, die den vollen Weg // "Mandant anlegen -> Benutzer anlegen -> anmelden" ueber die tatsaechlichen // Produktionspakete (internal/tenant, internal/user, internal/auth) treiben, // statt nur die Einzelpakete isoliert zu testen. package e2e import ( "context" "errors" "os" "path/filepath" "sort" "strings" "testing" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" ) // requireAdminDSN liefert TEST_ADMIN_DSN oder ueberspringt den Test - wie in // allen bestehenden Integrationstests dieses Repos (siehe // internal/tenant/provisioner_test.go). func requireAdminDSN(t *testing.T) string { t.Helper() dsn := os.Getenv("TEST_ADMIN_DSN") if dsn == "" { t.Skip("TEST_ADMIN_DSN nicht gesetzt, E2E-Test uebersprungen") } return dsn } // repoRoot findet den Modul-Root ausgehend vom Testverzeichnis, damit die // SQL-Migrationsverzeichnisse unabhaengig vom Arbeitsverzeichnis (go test // wechselt ins Paketverzeichnis) gefunden werden. func repoRoot(t *testing.T) string { t.Helper() wd, err := os.Getwd() if err != nil { t.Fatalf("getwd: %v", err) } // internal/e2e -> zwei Ebenen hoch zum Modul-Root. return filepath.Join(wd, "..", "..") } // applyRegistrySchema legt die Control-Plane-Tabellen (tenants, superadmins, // lifecycle-Spalten) frisch an, indem alle migrations/*.up.sql in // Dateinamen-Reihenfolge angewendet werden - identisch zur echten // Rollout-Reihenfolge, nicht eine verkuerzte Testkopie des Schemas. func applyRegistrySchema(t *testing.T, ctx context.Context, pool *pgxpool.Pool, root string) { t.Helper() applyAllUpSQL(t, ctx, pool, filepath.Join(root, "migrations")) } // applyTenantSchema initialisiert eine frisch provisionierte Tenant-Datenbank // mit allen migrations/tenant/*.up.sql (nicht nur 0001_users, damit // password_hash, role_assignments etc. wie in Produktion vorhanden sind). func applyTenantSchema(t *testing.T, ctx context.Context, pool *pgxpool.Pool, root string) { t.Helper() applyAllUpSQL(t, ctx, pool, filepath.Join(root, "migrations", "tenant")) } func applyAllUpSQL(t *testing.T, ctx context.Context, pool *pgxpool.Pool, dir string) { t.Helper() entries, err := os.ReadDir(dir) if err != nil { t.Fatalf("migrationsverzeichnis %q lesen: %v", dir, err) } var names []string for _, e := range entries { if !e.IsDir() && strings.HasSuffix(e.Name(), ".up.sql") { names = append(names, e.Name()) } } sort.Strings(names) for _, name := range names { sqlBytes, err := os.ReadFile(filepath.Join(dir, name)) if err != nil { t.Fatalf("migration %q lesen: %v", name, err) } if _, err := pool.Exec(ctx, string(sqlBytes)); err != nil { // Migrationen sind Produktionsartefakte ohne eingebaute // Idempotenz (in Produktion durch eine Migrations-Tracking- // Tabelle abgesichert, die dieser Test bewusst nicht nachbaut). // Zwei E2E-Tests im selben Prozess teilen sich dieselbe // Registry-Datenbank, daher ist "schon vorhanden" hier kein // Fehler, sondern der Normalfall beim zweiten Testlauf. var pgErr *pgconn.PgError if errors.As(err, &pgErr) && (pgErr.Code == "42P07" || pgErr.Code == "42701" || pgErr.Code == "42710" || pgErr.Code == "42723") { continue } t.Fatalf("migration %q anwenden: %v", name, err) } } } // dropTenantDBs entfernt Test-Datenbanken bedingungslos, fuer Cleanup nach // dem Test unabhaengig vom Testausgang. func dropTenantDBs(ctx context.Context, adminPool *pgxpool.Pool, names ...string) { for _, n := range names { _, _ = adminPool.Exec(ctx, `DROP DATABASE IF EXISTS `+quoteIdent(n)) } } func quoteIdent(name string) string { return `"` + strings.ReplaceAll(name, `"`, `""`) + `"` }