package e2e import ( "context" "strings" "testing" "github.com/jackc/pgx/v5/pgxpool" "gitea.perlbach24.de/scripte/nexarch/internal/auth" "gitea.perlbach24.de/scripte/nexarch/internal/tenant" "gitea.perlbach24.de/scripte/nexarch/internal/user" ) // TestE2E_TenantOnboardingLoginFlow ist der zentrale QA-02-Nachweis: der // vollstaendige, echte Weg ueber TEN-01..07/IAM-01..14 hinweg - // // Mandant provisionieren (internal/tenant.Provisioner, physisch isolierte DB) // -> Administrator anlegen (internal/user.TenantUserStore.Create + // SetPasswordHash, wie es internal/tenant.OnboardingService fuer TEN-02 // tut, hier explizit nachgebaut um zusaetzlich ein Passwort zu setzen) // -> Anmelden (internal/auth.LoginService.Login) // -> Assert: gueltiges Token, richtiger Tenant. func TestE2E_TenantOnboardingLoginFlow(t *testing.T) { adminDSN := requireAdminDSN(t) ctx := context.Background() root := repoRoot(t) adminPool, err := pgxpool.New(ctx, adminDSN) if err != nil { t.Fatalf("admin pool: %v", err) } // Ueber t.Cleanup statt defer geschlossen: t.Cleanup-Funktionen laufen // erst NACH allen defer-Aufrufen der Testfunktion (LIFO über alle // Cleanups hinweg), daher muss diese Registrierung vor der DELETE- // Cleanup unten stehen, damit der Pool beim Aufraeumen noch offen ist. t.Cleanup(func() { adminPool.Close() }) applyRegistrySchema(t, ctx, adminPool, root) registry := tenant.NewRegistry(adminPool) dsnTemplate := strings.Replace(adminDSN, "/postgres?", "/%s?", 1) provisioner := tenant.NewProvisioner(adminPool, registry, dsnTemplate) t.Cleanup(func() { _, _ = adminPool.Exec(ctx, `DELETE FROM tenants WHERE slug = 'e2e_onboard'`) dropTenantDBs(ctx, adminPool, "tenant_e2e_onboard") }) // Schritt 1: Mandant provisionieren (TEN-01/TEN-02) - eigene, physisch // getrennte Datenbank statt gemeinsamer tenant_id-Spalte. tn, err := provisioner.Provision(ctx, "e2e_onboard", "E2E Onboarding GmbH") if err != nil { t.Fatalf("provision: %v", err) } tenantPool, err := pgxpool.New(ctx, tn.DBDSN) if err != nil { t.Fatalf("connect tenant db: %v", err) } defer tenantPool.Close() // Schema in der frisch angelegten Tenant-Datenbank ausrollen (TEN-07- // Migrationslogik, hier direkt statt ueber den Orchestrator, um im Test // nicht von der Registry-Iteration abhaengig zu sein). applyTenantSchema(t, ctx, tenantPool, root) // Schritt 2: Benutzer anlegen (IAM-01/IAM-02) und Passwort setzen // (IAM-03, bcrypt ueber internal/auth.HashPassword). userStore := user.NewTenantUserStore(tenantPool) created, err := userStore.Create(ctx, "admin@e2e_onboard.example", "E2E Admin") if err != nil { t.Fatalf("benutzer anlegen: %v", err) } hash, err := auth.HashPassword("korrekt-pferd-batterie-heftklammer") if err != nil { t.Fatalf("passwort hashen: %v", err) } if err := userStore.SetPasswordHash(ctx, created.ID, hash); err != nil { t.Fatalf("passwort-hash setzen: %v", err) } // Schritt 3: Anmelden (IAM-04/IAM-05 - LoginService gegen genau diese // Tenant-DB, kein tenant-uebergreifendes Login moeglich). issuer := auth.NewTokenIssuer("test-signing-secret-fuer-qa-02-e2e") login := auth.NewLoginService(userStore, issuer, tn.Slug) token, err := login.Login(ctx, "admin@e2e_onboard.example", "korrekt-pferd-batterie-heftklammer") if err != nil { t.Fatalf("login: %v", err) } if token == "" { t.Fatal("login lieferte leeres token") } // Assert: Token ist gueltig UND traegt den richtigen Tenant (Verify statt // blindem String-Nichtleer-Check - sonst waere ein kaputtes Token auch // "gruen"). claims, err := issuer.Verify(token) if err != nil { t.Fatalf("token verifizieren: %v", err) } if claims.TenantSlug != "e2e_onboard" { t.Fatalf("token tenant = %q, want %q", claims.TenantSlug, "e2e_onboard") } if claims.UserID != created.ID { t.Fatalf("token user id = %q, want %q", claims.UserID, created.ID) } // Gegenprobe: falsches Passwort scheitert weiterhin (kein False-Positive // durch einen zu freizuegigen Testaufbau). if _, err := login.Login(ctx, "admin@e2e_onboard.example", "falsches-passwort"); err == nil { t.Fatal("login mit falschem passwort haette scheitern muessen") } }