package tenant import ( "context" "os" "strings" "testing" "github.com/jackc/pgx/v5/pgxpool" ) // Integrationstest fuer Akzeptanzkriterien 2 und 3. Benoetigt eine echte // Postgres-Instanz und wird ohne TEST_ADMIN_DSN uebersprungen, nicht als // fehlgeschlagen gewertet — siehe Pruefungen-Ergebnis im PR. // // TEST_ADMIN_DSN muss auf die Wartungsdatenbank zeigen, z. B.: // // postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable func TestProvision_CreatesIsolatedDatabases(t *testing.T) { adminDSN := os.Getenv("TEST_ADMIN_DSN") if adminDSN == "" { t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen") } ctx := context.Background() adminPool, err := pgxpool.New(ctx, adminDSN) if err != nil { t.Fatalf("admin pool: %v", err) } defer adminPool.Close() registryPool, err := pgxpool.New(ctx, adminDSN) if err != nil { t.Fatalf("registry pool: %v", err) } defer registryPool.Close() if _, err := registryPool.Exec(ctx, ` CREATE TABLE IF NOT EXISTS tenants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), slug TEXT NOT NULL UNIQUE, name TEXT NOT NULL, db_name TEXT NOT NULL UNIQUE, db_dsn TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT now() )`); err != nil { t.Fatalf("registry-schema: %v", err) } dsnTemplate := strings.Replace(adminDSN, "/postgres?", "/%s?", 1) registry := NewRegistry(registryPool) provisioner := NewProvisioner(adminPool, registry, dsnTemplate) t.Cleanup(func() { _, _ = registryPool.Exec(ctx, `DELETE FROM tenants WHERE slug IN ('acme','globex')`) _, _ = adminPool.Exec(ctx, `DROP DATABASE IF EXISTS tenant_acme`) _, _ = adminPool.Exec(ctx, `DROP DATABASE IF EXISTS tenant_globex`) }) tenantA, err := provisioner.Provision(ctx, "acme", "Acme GmbH") if err != nil { t.Fatalf("provision acme: %v", err) } tenantB, err := provisioner.Provision(ctx, "globex", "Globex AG") if err != nil { t.Fatalf("provision globex: %v", err) } if tenantA.DBName == tenantB.DBName { t.Fatalf("erwartet unterschiedliche db_name, beide sind %q", tenantA.DBName) } // Akzeptanzkriterium 3 / Pruefung 3: In der Datenbank von Tenant A existiert // keine Verbindungsmoeglichkeit zu Tenant B, weil beide physisch getrennte // Datenbanken sind, statt sich auf einen Query-Filter zu verlassen. poolA, err := pgxpool.New(ctx, tenantA.DBDSN) if err != nil { t.Fatalf("connect tenant a: %v", err) } defer poolA.Close() var globexVisible bool err = poolA.QueryRow(ctx, ` SELECT EXISTS ( SELECT 1 FROM pg_catalog.pg_database WHERE datname = $1 ) `, tenantB.DBName).Scan(&globexVisible) if err != nil { t.Fatalf("pruefung tenant-trennung: %v", err) } // pg_database ist clusterweit sichtbar (Existenz der DB), aber die // eigentliche Pruefung ist: aus poolA (verbunden mit tenant_acme) ist keine // Tabelle/Zeile aus tenant_globex erreichbar, da current_database() getrennt ist. var currentDB string if err := poolA.QueryRow(ctx, `SELECT current_database()`).Scan(¤tDB); err != nil { t.Fatalf("current_database: %v", err) } if currentDB != tenantA.DBName { t.Fatalf("current_database() = %q, want %q — keine physische Trennung", currentDB, tenantA.DBName) } if currentDB == tenantB.DBName { t.Fatalf("tenant a verbindung zeigt auf tenant b datenbank") } }