package tenantsettings import ( "context" "os" "testing" "github.com/jackc/pgx/v5/pgxpool" ) func setupTest(t *testing.T) (*Store, string, string, func()) { t.Helper() adminDSN := os.Getenv("TEST_ADMIN_DSN") if adminDSN == "" { t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen") } ctx := context.Background() pool, err := pgxpool.New(ctx, adminDSN) if err != nil { t.Fatalf("pool: %v", err) } if _, err := pool.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() ); 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() ); `); err != nil { t.Fatalf("schema: %v", err) } var tenantA, tenantB string if err := pool.QueryRow(ctx, `INSERT INTO tenants (slug, name, db_name, db_dsn) VALUES ('ts_a', 'A', 'tenant_ts_a', 'unused') RETURNING id`).Scan(&tenantA); err != nil { t.Fatalf("tenant a anlegen: %v", err) } if err := pool.QueryRow(ctx, `INSERT INTO tenants (slug, name, db_name, db_dsn) VALUES ('ts_b', 'B', 'tenant_ts_b', 'unused') RETURNING id`).Scan(&tenantB); err != nil { t.Fatalf("tenant b anlegen: %v", err) } cleanup := func() { _, _ = pool.Exec(ctx, `DELETE FROM tenant_settings_history WHERE tenant_id IN ($1, $2)`, tenantA, tenantB) _, _ = pool.Exec(ctx, `DELETE FROM tenant_settings WHERE tenant_id IN ($1, $2)`, tenantA, tenantB) _, _ = pool.Exec(ctx, `DELETE FROM tenants WHERE id IN ($1, $2)`, tenantA, tenantB) pool.Close() } return NewStore(pool), tenantA, tenantB, cleanup } func strp(s string) *string { return &s } // Akzeptanzkriterium 2 + Pruefung 2: fehlende Werte liefern Defaults, kein Fehler. func TestGet_UnsetTenantReturnsDefaults(t *testing.T) { store, tenantA, _, cleanup := setupTest(t) defer cleanup() ctx := context.Background() got, err := store.Get(ctx, tenantA) if err != nil { t.Fatalf("get: %v", err) } want := Defaults() if got != want { t.Fatalf("erwartet defaults %+v, habe %+v", want, got) } } // Akzeptanzkriterium 1 + 2: Teil-Update laesst nicht gesetzte Felder auf Default. func TestUpdate_PartialPatchKeepsOtherFieldsAtDefault(t *testing.T) { store, tenantA, _, cleanup := setupTest(t) defer cleanup() ctx := context.Background() got, err := store.Update(ctx, tenantA, Patch{DisplayName: strp("Acme GmbH")}) if err != nil { t.Fatalf("update: %v", err) } if got.DisplayName != "Acme GmbH" { t.Fatalf("erwartet 'Acme GmbH', habe %q", got.DisplayName) } d := Defaults() if got.ColorScheme != d.ColorScheme || got.Timezone != d.Timezone || got.Language != d.Language { t.Fatalf("nicht gesetzte felder haetten auf default bleiben sollen, habe %+v", got) } if got.Version != 1 { t.Fatalf("erwartet version 1, habe %d", got.Version) } } // Akzeptanzkriterium 1 + Pruefung 1: Aenderung eines Tenants wirkt sich // nicht auf einen anderen aus. func TestUpdate_IsolatedBetweenTenants(t *testing.T) { store, tenantA, tenantB, cleanup := setupTest(t) defer cleanup() ctx := context.Background() if _, err := store.Update(ctx, tenantA, Patch{DisplayName: strp("Tenant A")}); err != nil { t.Fatalf("update a: %v", err) } gotB, err := store.Get(ctx, tenantB) if err != nil { t.Fatalf("get b: %v", err) } if gotB != Defaults() { t.Fatalf("tenant b sollte unberuehrt bei defaults bleiben, habe %+v", gotB) } } // Akzeptanzkriterium 3: Aenderungen versioniert nachvollziehbar. func TestUpdate_HistoryTracksVersions(t *testing.T) { store, tenantA, _, cleanup := setupTest(t) defer cleanup() ctx := context.Background() if _, err := store.Update(ctx, tenantA, Patch{DisplayName: strp("v1")}); err != nil { t.Fatalf("update 1: %v", err) } if _, err := store.Update(ctx, tenantA, Patch{DisplayName: strp("v2")}); err != nil { t.Fatalf("update 2: %v", err) } if _, err := store.Update(ctx, tenantA, Patch{ColorScheme: strp("dark")}); err != nil { t.Fatalf("update 3: %v", err) } history, err := store.History(ctx, tenantA) if err != nil { t.Fatalf("history: %v", err) } if len(history) != 3 { t.Fatalf("erwartet 3 historieneintraege, habe %d", len(history)) } if history[0].DisplayName != "v1" || history[0].Version != 1 { t.Fatalf("history[0] unerwartet: %+v", history[0]) } if history[1].DisplayName != "v2" || history[1].Version != 2 { t.Fatalf("history[1] unerwartet: %+v", history[1]) } if history[2].ColorScheme != "dark" || history[2].DisplayName != "v2" || history[2].Version != 3 { t.Fatalf("history[2] unerwartet (display_name haette aus vorherigem update erhalten bleiben sollen): %+v", history[2]) } }