Files
nexarch/internal/e2e/tenant_onboarding_test.go
T
sysopsandClaude Sonnet 5 9f641fec76 QA-04: sicherheits-penetrationspruefung-core
Voller Merge von RBAC-02/IAM-06/IAM-07/API-03/API-10/API-08/API-09/IAM-10/
IAM-11/IAM-13 plus echte Angriffstests (internal/pentest) gegen SSO/OIDC
(alg=none, Fremdschluessel, Claims-Manipulation, Nonce-Replay), Rate-
Limiting/Lockout im simulierten Mehrinstanz-Betrieb, zentrale Policy-
Durchsetzung (Rechteausweitung, struktureller Guard-Bypass) und Master-
Key-/Tenant-KEK-Rotation. 29/29 Pakete gruen auf 192.168.1.131.

Vier real gefundene Testinfrastruktur-Fehler behoben: reset-test-env.sh
liess tenant_keks (und weitere neuere Registry-Tabellen) beim Reset stehen
(FK-CASCADE loescht nur die Constraint, keine Zeilen); zwei E2E-Tests und
kek_test.go schlossen ihren adminPool per defer VOR ihrer t.Cleanup-
Bereinigung (t.Cleanup laeuft immer nach allen defers); migrate_test.go
hatte ein Testschema ohne die TEN-04-Lifecycle-Spalten. Alle vier Fixes
betreffen ausschliesslich Testcode, kein Produktionscode geaendert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
2026-08-29 09:57:36 +02:00

117 lines
4.1 KiB
Go

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")
}
}