package user import ( "context" "errors" "fmt" "os" "strings" "testing" "github.com/jackc/pgx/v5/pgxpool" ) // setupTestDB legt eine frische, isolierte Testdatenbank an, wendet die // uebergebene Migration an und liefert einen verbundenen Pool. Wird ohne // TEST_ADMIN_DSN uebersprungen — siehe internal/tenant/provisioner_test.go // fuer dasselbe Muster. func setupTestDB(t *testing.T, dbName, schemaSQL string) *pgxpool.Pool { t.Helper() 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) } _, _ = adminPool.Exec(ctx, fmt.Sprintf(`DROP DATABASE IF EXISTS %q`, dbName)) if _, err := adminPool.Exec(ctx, fmt.Sprintf(`CREATE DATABASE %q`, dbName)); err != nil { t.Fatalf("testdatenbank anlegen: %v", err) } dsn := strings.Replace(adminDSN, "/postgres?", "/"+dbName+"?", 1) pool, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatalf("connect testdatenbank: %v", err) } if _, err := pool.Exec(ctx, schemaSQL); err != nil { t.Fatalf("schema anwenden: %v", err) } t.Cleanup(func() { pool.Close() _, _ = adminPool.Exec(ctx, fmt.Sprintf(`DROP DATABASE IF EXISTS %q`, dbName)) adminPool.Close() }) return pool } const usersSchema = ` CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() );` const superadminsSchema = ` CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE superadmins ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() );` // Akzeptanzkriterium 1 + 3, Pruefung 1 (inkl. Negativfall doppelte E-Mail). func TestTenantUserStore_CRUD(t *testing.T) { pool := setupTestDB(t, "test_iam01_users", usersSchema) store := NewTenantUserStore(pool) ctx := context.Background() created, err := store.Create(ctx, "alice@example.com", "Alice") if err != nil { t.Fatalf("create: %v", err) } if created.Status != StatusActive { t.Fatalf("erwartet status active, hat %q", created.Status) } got, err := store.Get(ctx, created.ID) if err != nil { t.Fatalf("get: %v", err) } if got.Email != "alice@example.com" { t.Fatalf("get email = %q", got.Email) } updated, err := store.Update(ctx, created.ID, "", "Alice A.") if err != nil { t.Fatalf("update: %v", err) } if updated.Name != "Alice A." || updated.Email != "alice@example.com" { t.Fatalf("update ergebnis unerwartet: %+v", updated) } list, err := store.List(ctx) if err != nil { t.Fatalf("list: %v", err) } if len(list) != 1 { t.Fatalf("erwartet 1 benutzer, habe %d", len(list)) } deactivated, err := store.Deactivate(ctx, created.ID) if err != nil { t.Fatalf("deactivate: %v", err) } if deactivated.Status != StatusInactive { t.Fatalf("erwartet status inactive, hat %q", deactivated.Status) } // Negativfall: doppelte E-Mail-Adresse. if _, err := store.Create(ctx, "second@example.com", "Bob"); err != nil { t.Fatalf("create second: %v", err) } if _, err := store.Create(ctx, "second@example.com", "Bob Zwei"); !errors.Is(err, ErrEmailTaken) { t.Fatalf("erwartet ErrEmailTaken, habe %v", err) } // Negativfall: fehlender Benutzer. if _, err := store.Get(ctx, created.ID+"-nicht-vorhanden"); err == nil { t.Fatalf("erwartet fehler bei unbekannter/ungueltiger id") } } // Akzeptanzkriterium 2 + Pruefung 2: Superadmin-Anlage ohne Tenant-Kontext. // SuperadminStore.Create hat keinen Tenant-Parameter — es gibt syntaktisch // keine Moeglichkeit, hier versehentlich einen Tenant-Sonderfall zu vergessen. func TestSuperadminStore_CreateWithoutTenantContext(t *testing.T) { pool := setupTestDB(t, "test_iam01_superadmins", superadminsSchema) store := NewSuperadminStore(pool) ctx := context.Background() created, err := store.Create(ctx, "root@nexarch.internal", "Root") if err != nil { t.Fatalf("create superadmin: %v", err) } if created.Status != StatusActive { t.Fatalf("erwartet status active, hat %q", created.Status) } got, err := store.Get(ctx, created.ID) if err != nil { t.Fatalf("get: %v", err) } if got.Email != "root@nexarch.internal" { t.Fatalf("get email = %q", got.Email) } if _, err := store.Create(ctx, "root@nexarch.internal", "Root Zwei"); !errors.Is(err, ErrEmailTaken) { t.Fatalf("erwartet ErrEmailTaken (globale eindeutigkeit), habe %v", err) } deactivated, err := store.Deactivate(ctx, created.ID) if err != nil { t.Fatalf("deactivate: %v", err) } if deactivated.Status != StatusInactive { t.Fatalf("erwartet status inactive, hat %q", deactivated.Status) } }