233 lines
7.8 KiB
Go
233 lines
7.8 KiB
Go
package tenantadmin
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/tenant"
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/tenantsettings"
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/user"
|
|
)
|
|
|
|
type testEnv struct {
|
|
handler *Handler
|
|
superadminID string
|
|
slug string
|
|
adminPool *pgxpool.Pool
|
|
registry *tenant.Registry
|
|
}
|
|
|
|
func setupTest(t *testing.T) (testEnv, func()) {
|
|
t.Helper()
|
|
adminDSN := os.Getenv("TEST_ADMIN_DSN")
|
|
if adminDSN == "" {
|
|
t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen")
|
|
}
|
|
ctx := context.Background()
|
|
|
|
registryPool, err := pgxpool.New(ctx, adminDSN)
|
|
if err != nil {
|
|
t.Fatalf("registry pool: %v", err)
|
|
}
|
|
adminPool, err := pgxpool.New(ctx, adminDSN)
|
|
if err != nil {
|
|
t.Fatalf("admin 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(), previous_status TEXT, deletion_scheduled_at TIMESTAMPTZ
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tenant_settings (
|
|
tenant_id UUID PRIMARY KEY REFERENCES tenants(id), display_name TEXT, logo_url TEXT,
|
|
color_scheme TEXT, timezone TEXT, language TEXT, version INT NOT NULL DEFAULT 0,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tenant_settings_history (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, display_name TEXT,
|
|
logo_url TEXT, color_scheme TEXT, timezone TEXT, language TEXT, version INT NOT NULL,
|
|
changed_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE TABLE IF NOT EXISTS 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()
|
|
);
|
|
`); err != nil {
|
|
t.Fatalf("schema: %v", err)
|
|
}
|
|
|
|
registry := tenant.NewRegistry(registryPool)
|
|
dsnTemplate := strings.Replace(adminDSN, "/postgres?", "/%s?", 1)
|
|
provisioner := tenant.NewProvisioner(adminPool, registry, dsnTemplate)
|
|
lifecycle := tenant.NewLifecycle(registry, adminPool)
|
|
settingsStore := tenantsettings.NewStore(registryPool)
|
|
superadmins := user.NewSuperadminStore(registryPool)
|
|
|
|
slug := fmt.Sprintf("tadm_%d", time.Now().UnixNano()%1_000_000_000)
|
|
if _, err := provisioner.Provision(ctx, slug, "Test Mandant "+slug); err != nil {
|
|
t.Fatalf("provision: %v", err)
|
|
}
|
|
|
|
admin, err := superadmins.Create(ctx, fmt.Sprintf("admin-%d@example.com", time.Now().UnixNano()), "Test Superadmin")
|
|
if err != nil {
|
|
t.Fatalf("superadmin anlegen: %v", err)
|
|
}
|
|
|
|
handler := NewHandler(registry, lifecycle, settingsStore, superadmins)
|
|
|
|
cleanup := func() {
|
|
_, _ = adminPool.Exec(ctx, fmt.Sprintf(`DROP DATABASE IF EXISTS %q`, "tenant_"+slug))
|
|
_, _ = registryPool.Exec(ctx, `DELETE FROM tenant_settings_history WHERE tenant_id IN (SELECT id FROM tenants WHERE slug = $1)`, slug)
|
|
_, _ = registryPool.Exec(ctx, `DELETE FROM tenant_settings WHERE tenant_id IN (SELECT id FROM tenants WHERE slug = $1)`, slug)
|
|
_, _ = registryPool.Exec(ctx, `DELETE FROM tenants WHERE slug = $1`, slug)
|
|
_, _ = registryPool.Exec(ctx, `DELETE FROM superadmins WHERE id = $1`, admin.ID)
|
|
registryPool.Close()
|
|
adminPool.Close()
|
|
}
|
|
return testEnv{handler: handler, superadminID: admin.ID, slug: slug, adminPool: adminPool, registry: registry}, cleanup
|
|
}
|
|
|
|
// Akzeptanzkriterium 1 + Pruefung 1: nur berechtigte (aktive) Superadmins
|
|
// sehen die Mandantenliste ueberhaupt.
|
|
func TestListTenants_RejectsNonSuperadmin(t *testing.T) {
|
|
env, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
_, err := env.handler.ListTenants(ctx, "irgendeine-nicht-existierende-id", "", "")
|
|
if !errors.Is(err, ErrNotSuperadmin) {
|
|
t.Fatalf("erwartet ErrNotSuperadmin, habe: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListTenants_ShowsSearchAndStatusFilterResults(t *testing.T) {
|
|
env, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
all, err := env.handler.ListTenants(ctx, env.superadminID, "", "")
|
|
if err != nil {
|
|
t.Fatalf("list: %v", err)
|
|
}
|
|
found := false
|
|
for _, item := range all {
|
|
if item.Slug == env.slug {
|
|
found = true
|
|
if item.Status != "active" {
|
|
t.Fatalf("status = %q, want active", item.Status)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("erwartet test-tenant in ungefilterter liste")
|
|
}
|
|
|
|
bySearch, err := env.handler.ListTenants(ctx, env.superadminID, env.slug, "")
|
|
if err != nil {
|
|
t.Fatalf("list mit suche: %v", err)
|
|
}
|
|
if len(bySearch) != 1 || bySearch[0].Slug != env.slug {
|
|
t.Fatalf("suche nach slug lieferte unerwartetes ergebnis: %+v", bySearch)
|
|
}
|
|
|
|
byWrongStatus, err := env.handler.ListTenants(ctx, env.superadminID, env.slug, "suspended")
|
|
if err != nil {
|
|
t.Fatalf("list mit statusfilter: %v", err)
|
|
}
|
|
if len(byWrongStatus) != 0 {
|
|
t.Fatalf("statusfilter haette test-tenant (status=active) ausfiltern muessen, habe: %+v", byWrongStatus)
|
|
}
|
|
}
|
|
|
|
// Akzeptanzkriterium 2 + Pruefung 2: fehlender Pflichtwert (Anzeigename)
|
|
// wird serverseitig abgelehnt, bevor etwas gespeichert wird.
|
|
func TestUpdateSettings_RejectsMissingDisplayName(t *testing.T) {
|
|
env, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
_, err := env.handler.UpdateSettings(ctx, env.superadminID, env.slug, SettingsPatch{DisplayName: " "})
|
|
if !errors.Is(err, ErrMissingDisplayName) {
|
|
t.Fatalf("erwartet ErrMissingDisplayName, habe: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUpdateSettings_AppliesValidPatch(t *testing.T) {
|
|
env, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
updated, err := env.handler.UpdateSettings(ctx, env.superadminID, env.slug, SettingsPatch{DisplayName: "Neuer Name"})
|
|
if err != nil {
|
|
t.Fatalf("update: %v", err)
|
|
}
|
|
if updated.DisplayName != "Neuer Name" {
|
|
t.Fatalf("displayname = %q, want 'Neuer Name'", updated.DisplayName)
|
|
}
|
|
|
|
detail, err := env.handler.GetTenantDetail(ctx, env.superadminID, env.slug)
|
|
if err != nil {
|
|
t.Fatalf("detail: %v", err)
|
|
}
|
|
if detail.Settings.DisplayName != "Neuer Name" {
|
|
t.Fatalf("detail zeigt nicht den aktualisierten namen: %+v", detail.Settings)
|
|
}
|
|
}
|
|
|
|
// Akzeptanzkriterium 3: Lifecycle-Aktionen sind ausloesbar und wirken sich
|
|
// auf den tatsaechlichen Mandantenstatus aus.
|
|
func TestPerformLifecycleAction_SuspendAndReactivate(t *testing.T) {
|
|
env, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
suspended, err := env.handler.PerformLifecycleAction(ctx, env.superadminID, env.slug, ActionSuspend)
|
|
if err != nil {
|
|
t.Fatalf("suspend: %v", err)
|
|
}
|
|
if suspended.Status != tenant.StatusSuspended {
|
|
t.Fatalf("status = %q, want suspended", suspended.Status)
|
|
}
|
|
|
|
reactivated, err := env.handler.PerformLifecycleAction(ctx, env.superadminID, env.slug, ActionReactivate)
|
|
if err != nil {
|
|
t.Fatalf("reactivate: %v", err)
|
|
}
|
|
if reactivated.Status != tenant.StatusActive {
|
|
t.Fatalf("status = %q, want active", reactivated.Status)
|
|
}
|
|
}
|
|
|
|
func TestPerformLifecycleAction_RejectsInvalidTransition(t *testing.T) {
|
|
env, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
// Reaktivieren eines bereits aktiven Tenants ist kein gueltiger Uebergang.
|
|
_, err := env.handler.PerformLifecycleAction(ctx, env.superadminID, env.slug, ActionReactivate)
|
|
if !errors.Is(err, tenant.ErrInvalidTransition) {
|
|
t.Fatalf("erwartet ErrInvalidTransition, habe: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPerformLifecycleAction_RejectsNonSuperadmin(t *testing.T) {
|
|
env, cleanup := setupTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
_, err := env.handler.PerformLifecycleAction(ctx, "keine-berechtigung", env.slug, ActionSuspend)
|
|
if !errors.Is(err, ErrNotSuperadmin) {
|
|
t.Fatalf("erwartet ErrNotSuperadmin, habe: %v", err)
|
|
}
|
|
}
|