package tenant import ( "context" "errors" "os" "strings" "sync" "testing" "github.com/jackc/pgx/v5/pgxpool" "gitea.perlbach24.de/scripte/nexarch/internal/user" ) // schemaPathForTest liefert den Pfad zur echten Migration, damit Tests exakt // dasselbe Schema anwenden wie die Produktion (keine Zweitkopie des SQL). const schemaPathForTest = "../../migrations/tenant/0001_users.up.sql" func TestOnboarding_ValidationErrors(t *testing.T) { // Reine Eingabevalidierung, braucht keine Datenbank. svc := NewOnboardingService(nil, schemaPathForTest) ctx := context.Background() cases := []struct { name string slug string tenantName string email string adminName string wantErr error }{ {"leerer name", "acme", "", "a@b.de", "Admin", ErrMissingField}, {"leerer admin-name", "acme", "Acme", "a@b.de", "", ErrMissingField}, {"ungueltiger slug", "Acme!", "Acme", "a@b.de", "Admin", ErrInvalidSlug}, {"ungueltige email", "acme", "Acme", "keine-email", "Admin", user.ErrInvalidEmail}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { _, err := svc.Onboard(ctx, c.slug, c.tenantName, c.email, c.adminName) if !errors.Is(err, c.wantErr) { t.Fatalf("erwartet %v, habe %v", c.wantErr, err) } }) } } func newTestOnboardingService(t *testing.T) (*OnboardingService, func()) { 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) } registryPool, err := pgxpool.New(ctx, adminDSN) if err != nil { t.Fatalf("registry pool: %v", err) } 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) } registry := NewRegistry(registryPool) dsnTemplate := strings.Replace(adminDSN, "/postgres?", "/%s?", 1) provisioner := NewProvisioner(adminPool, registry, dsnTemplate) svc := NewOnboardingService(provisioner, schemaPathForTest) cleanup := func() { registryPool.Close() adminPool.Close() } return svc, cleanup } // Akzeptanzkriterien 1+2: Mandant + Erst-Admin in einem Vorgang. func TestOnboarding_CreatesTenantAndAdmin(t *testing.T) { svc, cleanup := newTestOnboardingService(t) defer cleanup() ctx := context.Background() t.Cleanup(func() { adminDSN := os.Getenv("TEST_ADMIN_DSN") p, _ := pgxpool.New(ctx, adminDSN) defer p.Close() _, _ = p.Exec(ctx, `DELETE FROM tenants WHERE slug = 'onboardtest'`) _, _ = p.Exec(ctx, `DROP DATABASE IF EXISTS tenant_onboardtest`) }) result, err := svc.Onboard(ctx, "onboardtest", "Onboard Test GmbH", "admin@onboardtest.example", "Erste Admin") if err != nil { t.Fatalf("onboard: %v", err) } if result.Tenant.Slug != "onboardtest" { t.Fatalf("tenant slug = %q", result.Tenant.Slug) } if result.Admin.Email != "admin@onboardtest.example" { t.Fatalf("admin email = %q", result.Admin.Email) } pool, err := pgxpool.New(ctx, result.Tenant.DBDSN) if err != nil { t.Fatalf("connect tenant db: %v", err) } defer pool.Close() var count int if err := pool.QueryRow(ctx, `SELECT count(*) FROM users WHERE email = $1`, "admin@onboardtest.example").Scan(&count); err != nil { t.Fatalf("admin in tenant-db pruefen: %v", err) } if count != 1 { t.Fatalf("erwartet genau einen admin in der tenant-db, habe %d", count) } } // Akzeptanzkriterium 3 + Pruefung 1: doppelte Registrierung wird abgewiesen, // auch bei zwei parallelen Anfragen entsteht kein Doppel-Tenant. func TestOnboarding_RejectsDuplicateSlugConcurrently(t *testing.T) { svc, cleanup := newTestOnboardingService(t) defer cleanup() ctx := context.Background() t.Cleanup(func() { adminDSN := os.Getenv("TEST_ADMIN_DSN") p, _ := pgxpool.New(ctx, adminDSN) defer p.Close() _, _ = p.Exec(ctx, `DELETE FROM tenants WHERE slug = 'dupetest'`) _, _ = p.Exec(ctx, `DROP DATABASE IF EXISTS tenant_dupetest`) }) var wg sync.WaitGroup results := make([]error, 2) for i := 0; i < 2; i++ { wg.Add(1) go func(i int) { defer wg.Done() _, err := svc.Onboard(ctx, "dupetest", "Dupe Test", "admin@dupetest.example", "Admin") results[i] = err }(i) } wg.Wait() successCount := 0 for _, err := range results { if err == nil { successCount++ } else if !errors.Is(err, ErrTenantExists) { t.Fatalf("unerwarteter fehler: %v", err) } } if successCount != 1 { t.Fatalf("erwartet genau einen erfolgreichen onboarding-vorgang, habe %d", successCount) } }