package rbac import ( "context" "errors" "os" "testing" "github.com/jackc/pgx/v5/pgxpool" "gitea.perlbach24.de/scripte/nexarch/internal/user" ) func setupStoreTest(t *testing.T) (*Store, 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 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() ); CREATE TABLE IF NOT EXISTS role_assignments ( user_id UUID PRIMARY KEY REFERENCES users(id), role TEXT NOT NULL CHECK (role IN ('user', 'tenant_admin')), granted_by TEXT NOT NULL, granted_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS role_assignment_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, role TEXT NOT NULL, granted_by TEXT NOT NULL, granted_at TIMESTAMPTZ NOT NULL DEFAULT now() ); `); err != nil { t.Fatalf("schema: %v", err) } userStore := user.NewTenantUserStore(pool) u, err := userStore.Create(ctx, "rbac_test@example.com", "RBAC Test") if err != nil { t.Fatalf("testuser anlegen: %v", err) } cleanup := func() { _, _ = pool.Exec(ctx, `DELETE FROM role_assignment_history WHERE user_id = $1`, u.ID) _, _ = pool.Exec(ctx, `DELETE FROM role_assignments WHERE user_id = $1`, u.ID) _, _ = pool.Exec(ctx, `DELETE FROM users WHERE id = $1`, u.ID) pool.Close() } return NewStore(pool), u.ID, cleanup } // Akzeptanzkriterium 1: Grundrollen sind einem Benutzer je Tenant zuweisbar. func TestStore_AssignAndGet(t *testing.T) { store, userID, cleanup := setupStoreTest(t) defer cleanup() ctx := context.Background() assigned, err := store.Assign(ctx, userID, RoleTenantAdmin, "admin@acme.example") if err != nil { t.Fatalf("assign: %v", err) } if assigned.Role != RoleTenantAdmin { t.Fatalf("erwartet tenant_admin, habe %q", assigned.Role) } got, err := store.Get(ctx, userID) if err != nil { t.Fatalf("get: %v", err) } if got.Role != RoleTenantAdmin || got.GrantedBy != "admin@acme.example" { t.Fatalf("zuweisung unerwartet: %+v", got) } } // Akzeptanzkriterium 1 + Pruefung 1: Zuweisung ausserhalb der erlaubten // Matrix (superadmin im tenant-gescopten Store) wird abgewiesen. func TestStore_RejectsSuperadminOutsideAllowedMatrix(t *testing.T) { store, userID, cleanup := setupStoreTest(t) defer cleanup() ctx := context.Background() _, err := store.Assign(ctx, userID, RoleSuperadmin, "admin@acme.example") if !errors.Is(err, ErrRoleNotAssignableInTenantScope) { t.Fatalf("erwartet ErrRoleNotAssignableInTenantScope, habe %v", err) } // Sicherstellen, dass der abgewiesene Versuch keine Zeile hinterlassen hat. if _, err := store.Get(ctx, userID); !errors.Is(err, ErrNotFound) { t.Fatalf("erwartet ErrNotFound nach abgewiesener zuweisung, habe %v", err) } } func TestStore_RejectsUnknownRole(t *testing.T) { store, userID, cleanup := setupStoreTest(t) defer cleanup() ctx := context.Background() if _, err := store.Assign(ctx, userID, Role("erfunden"), "admin@acme.example"); !errors.Is(err, ErrRoleNotAssignableInTenantScope) { t.Fatalf("erwartet ErrRoleNotAssignableInTenantScope fuer unbekannte rolle, habe %v", err) } } // Akzeptanzkriterium 3: Rollenzuweisung vollstaendig auditierbar. func TestStore_HistoryTracksWhoAndWhen(t *testing.T) { store, userID, cleanup := setupStoreTest(t) defer cleanup() ctx := context.Background() if _, err := store.Assign(ctx, userID, RoleUser, "system"); err != nil { t.Fatalf("assign 1: %v", err) } if _, err := store.Assign(ctx, userID, RoleTenantAdmin, "admin@acme.example"); err != nil { t.Fatalf("assign 2: %v", err) } history, err := store.History(ctx, userID) if err != nil { t.Fatalf("history: %v", err) } if len(history) != 2 { t.Fatalf("erwartet 2 historieneintraege, habe %d", len(history)) } if history[0].Role != RoleUser || history[0].GrantedBy != "system" { t.Fatalf("history[0] unerwartet: %+v", history[0]) } if history[1].Role != RoleTenantAdmin || history[1].GrantedBy != "admin@acme.example" { t.Fatalf("history[1] unerwartet: %+v", history[1]) } }