diff --git a/app/Filament/Pages/Tenancy/RegisterTenant.php b/app/Filament/Pages/Tenancy/RegisterTenant.php new file mode 100644 index 0000000..b39b512 --- /dev/null +++ b/app/Filament/Pages/Tenancy/RegisterTenant.php @@ -0,0 +1,83 @@ +schema([ + Forms\Components\TextInput::make('name') + ->required() + ->maxLength(255), + Forms\Components\Select::make('environment') + ->options([ + 'prod' => 'PROD', + 'dev' => 'DEV', + 'staging' => 'STAGING', + 'other' => 'Other', + ]) + ->default('other') + ->required(), + Forms\Components\TextInput::make('tenant_id') + ->label('Tenant ID (GUID)') + ->required() + ->maxLength(255) + ->unique(ignoreRecord: true), + Forms\Components\TextInput::make('domain') + ->label('Primary domain') + ->maxLength(255), + Forms\Components\TextInput::make('app_client_id') + ->label('App Client ID') + ->maxLength(255), + Forms\Components\TextInput::make('app_client_secret') + ->label('App Client Secret') + ->password() + ->dehydrateStateUsing(fn ($state) => filled($state) ? $state : null) + ->dehydrated(fn ($state) => filled($state)), + Forms\Components\TextInput::make('app_certificate_thumbprint') + ->label('Certificate thumbprint') + ->maxLength(255), + Forms\Components\Textarea::make('app_notes') + ->label('Notes') + ->rows(3), + ]); + } + + /** + * @param array $data + */ + protected function handleRegistration(array $data): Model + { + $tenant = Tenant::create($data); + + $user = auth()->user(); + + if ($user instanceof User) { + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => TenantRole::Owner->value], + ]); + } + + return $tenant; + } +} diff --git a/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php b/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php index 914991f..e5e8035 100644 --- a/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php +++ b/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php @@ -112,8 +112,16 @@ public function table(Table $table): Table Actions\ActionGroup::make([ Actions\ViewAction::make() ->label('View policy') - ->url(fn ($record) => $record->policy_id ? PolicyResource::getUrl('view', ['record' => $record->policy_id]) : null) - ->hidden(fn ($record) => ! $record->policy_id) + ->url(function (BackupItem $record): ?string { + if (! $record->policy_id) { + return null; + } + + $tenant = $this->getOwnerRecord()->tenant ?? \App\Models\Tenant::current(); + + return PolicyResource::getUrl('view', ['record' => $record->policy_id], tenant: $tenant); + }) + ->hidden(fn (BackupItem $record) => ! $record->policy_id) ->openUrlInNewTab(true), Actions\Action::make('remove') ->label('Remove') diff --git a/app/Filament/Resources/PolicyVersionResource.php b/app/Filament/Resources/PolicyVersionResource.php index b9dd732..9d6259a 100644 --- a/app/Filament/Resources/PolicyVersionResource.php +++ b/app/Filament/Resources/PolicyVersionResource.php @@ -186,9 +186,7 @@ public static function table(Table $table): Table ->falseLabel('Archived'), ]) ->actions([ - Actions\ViewAction::make() - ->url(fn (PolicyVersion $record) => static::getUrl('view', ['record' => $record])) - ->openUrlInNewTab(false), + Actions\ViewAction::make(), Actions\ActionGroup::make([ Actions\Action::make('restore_via_wizard') ->label('Restore via Wizard') diff --git a/app/Filament/Resources/TenantResource.php b/app/Filament/Resources/TenantResource.php index 92f5156..1b31a54 100644 --- a/app/Filament/Resources/TenantResource.php +++ b/app/Filament/Resources/TenantResource.php @@ -4,13 +4,16 @@ use App\Filament\Resources\TenantResource\Pages; use App\Http\Controllers\RbacDelegatedAuthController; +use App\Jobs\SyncPoliciesJob; use App\Models\Tenant; +use App\Models\User; use App\Services\Graph\GraphClientInterface; use App\Services\Intune\AuditLogger; use App\Services\Intune\RbacHealthService; use App\Services\Intune\RbacOnboardingService; use App\Services\Intune\TenantConfigService; use App\Services\Intune\TenantPermissionService; +use App\Support\TenantRole; use BackedEnum; use Filament\Actions; use Filament\Actions\ActionGroup; @@ -23,6 +26,8 @@ use Filament\Schemas\Schema; use Filament\Tables; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; @@ -33,6 +38,8 @@ class TenantResource extends Resource { protected static ?string $model = Tenant::class; + protected static bool $isScopedToTenant = false; + protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-building-office-2'; protected static string|UnitEnum|null $navigationGroup = 'Settings'; @@ -44,6 +51,15 @@ public static function form(Schema $schema): Schema Forms\Components\TextInput::make('name') ->required() ->maxLength(255), + Forms\Components\Select::make('environment') + ->options([ + 'prod' => 'PROD', + 'dev' => 'DEV', + 'staging' => 'STAGING', + 'other' => 'Other', + ]) + ->default('other') + ->required(), Forms\Components\TextInput::make('tenant_id') ->label('Tenant ID (GUID)') ->required() @@ -69,10 +85,28 @@ public static function form(Schema $schema): Schema ]); } + public static function getEloquentQuery(): Builder + { + $user = auth()->user(); + + if (! $user instanceof User) { + return parent::getEloquentQuery()->whereRaw('1 = 0'); + } + + $tenantIds = $user->tenants() + ->withTrashed() + ->pluck('tenants.id'); + + return parent::getEloquentQuery() + ->withTrashed() + ->whereIn('id', $tenantIds) + ->withCount('policies') + ->withMax('policies as last_policy_sync_at', 'last_synced_at'); + } + public static function table(Table $table): Table { return $table - ->modifyQueryUsing(fn (\Illuminate\Database\Eloquent\Builder $query) => $query->withTrashed()) ->columns([ Tables\Columns\TextColumn::make('name') ->searchable(), @@ -80,6 +114,23 @@ public static function table(Table $table): Table ->label('Tenant ID') ->copyable() ->searchable(), + Tables\Columns\TextColumn::make('environment') + ->badge() + ->color(fn (?string $state) => match ($state) { + 'prod' => 'danger', + 'dev' => 'warning', + 'staging' => 'info', + default => 'gray', + }) + ->sortable(), + Tables\Columns\TextColumn::make('policies_count') + ->label('Policies') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('last_policy_sync_at') + ->label('Last Sync') + ->since() + ->sortable(), Tables\Columns\TextColumn::make('domain') ->copyable() ->toggleable(), @@ -102,6 +153,13 @@ public static function table(Table $table): Table ->trueLabel('All') ->falseLabel('Archived') ->default(true), + Tables\Filters\SelectFilter::make('environment') + ->options([ + 'prod' => 'PROD', + 'dev' => 'DEV', + 'staging' => 'STAGING', + 'other' => 'Other', + ]), Tables\Filters\SelectFilter::make('app_status') ->options([ 'ok' => 'OK', @@ -113,6 +171,50 @@ public static function table(Table $table): Table ->actions([ Actions\ViewAction::make(), ActionGroup::make([ + Actions\Action::make('syncTenant') + ->label('Sync') + ->icon('heroicon-o-arrow-path') + ->color('warning') + ->requiresConfirmation() + ->visible(function (Tenant $record): bool { + if (! $record->isActive()) { + return false; + } + + $user = auth()->user(); + + if (! $user instanceof User) { + return false; + } + + return $user->canSyncTenant($record); + }) + ->action(function (Tenant $record, AuditLogger $auditLogger): void { + SyncPoliciesJob::dispatch($record->getKey()); + + $auditLogger->log( + tenant: $record, + action: 'tenant.sync_dispatched', + resourceType: 'tenant', + resourceId: (string) $record->id, + status: 'success', + context: ['metadata' => ['tenant_id' => $record->tenant_id]], + ); + + Notification::make() + ->title('Sync started') + ->body("Sync dispatched for {$record->name}.") + ->icon('heroicon-o-arrow-path') + ->iconColor('warning') + ->success() + ->send(); + }), + Actions\Action::make('openTenant') + ->label('Open') + ->icon('heroicon-o-arrow-right') + ->color('primary') + ->url(fn (Tenant $record) => \App\Filament\Resources\PolicyResource::getUrl('index', tenant: $record)) + ->visible(fn (Tenant $record) => $record->isActive()), Actions\EditAction::make(), Actions\RestoreAction::make() ->label('Restore') @@ -242,7 +344,78 @@ public static function table(Table $table): Table }), ])->icon('heroicon-o-ellipsis-vertical'), ]) - ->bulkActions([]) + ->bulkActions([ + Actions\BulkAction::make('syncSelected') + ->label('Sync selected') + ->icon('heroicon-o-arrow-path') + ->color('warning') + ->requiresConfirmation() + ->visible(function (): bool { + $user = auth()->user(); + + if (! $user instanceof User) { + return false; + } + + return $user->tenants() + ->whereIn('role', [ + TenantRole::Owner->value, + TenantRole::Manager->value, + TenantRole::Operator->value, + ]) + ->exists(); + }) + ->authorize(function (): bool { + $user = auth()->user(); + + if (! $user instanceof User) { + return false; + } + + return $user->tenants() + ->whereIn('role', [ + TenantRole::Owner->value, + TenantRole::Manager->value, + TenantRole::Operator->value, + ]) + ->exists(); + }) + ->action(function (Collection $records, AuditLogger $auditLogger): void { + $user = auth()->user(); + + if (! $user instanceof User) { + return; + } + + $eligible = $records + ->filter(fn ($record) => $record instanceof Tenant && $record->isActive()) + ->filter(fn (Tenant $tenant) => $user->canSyncTenant($tenant)); + + foreach ($eligible as $tenant) { + SyncPoliciesJob::dispatch($tenant->getKey()); + + $auditLogger->log( + tenant: $tenant, + action: 'tenant.sync_dispatched', + resourceType: 'tenant', + resourceId: (string) $tenant->id, + status: 'success', + context: ['metadata' => ['tenant_id' => $tenant->tenant_id]], + ); + } + + $count = $eligible->count(); + + Notification::make() + ->title('Bulk sync started') + ->body("Dispatched sync for {$count} tenant(s).") + ->icon('heroicon-o-arrow-path') + ->iconColor('warning') + ->success() + ->send(); + }) + ->deselectRecordsAfterCompletion(), + ]) ->headerActions([]); } @@ -440,7 +613,10 @@ public static function rbacAction(): Actions\Action ->label('Open RBAC login') ->url(route('admin.rbac.start', [ 'tenant' => $record->graphTenantId(), - 'return' => route('filament.admin.resources.tenants.view', $record), + 'return' => route('filament.admin.resources.tenants.view', [ + 'tenant' => $record->external_id, + 'record' => $record, + ]), ])), ]) ->warning() @@ -579,7 +755,10 @@ private static function loginToSearchRolesAction(?Tenant $tenant): ?Actions\Acti ->label('Login to load roles') ->url(route('admin.rbac.start', [ 'tenant' => $tenant->graphTenantId(), - 'return' => route('filament.admin.resources.tenants.view', $tenant), + 'return' => route('filament.admin.resources.tenants.view', [ + 'tenant' => $tenant->external_id, + 'record' => $tenant, + ]), ])); } @@ -761,7 +940,10 @@ private static function loginToSearchGroupsAction(?Tenant $tenant): ?Actions\Act ->label('Login to search groups') ->url(route('admin.rbac.start', [ 'tenant' => $tenant->graphTenantId(), - 'return' => route('filament.admin.resources.tenants.view', $tenant), + 'return' => route('filament.admin.resources.tenants.view', [ + 'tenant' => $tenant->external_id, + 'record' => $tenant, + ]), ])); } diff --git a/app/Filament/Resources/TenantResource/Pages/CreateTenant.php b/app/Filament/Resources/TenantResource/Pages/CreateTenant.php index 2a0c9ed..6e592c6 100644 --- a/app/Filament/Resources/TenantResource/Pages/CreateTenant.php +++ b/app/Filament/Resources/TenantResource/Pages/CreateTenant.php @@ -3,9 +3,24 @@ namespace App\Filament\Resources\TenantResource\Pages; use App\Filament\Resources\TenantResource; +use App\Models\User; +use App\Support\TenantRole; use Filament\Resources\Pages\CreateRecord; class CreateTenant extends CreateRecord { protected static string $resource = TenantResource::class; + + protected function afterCreate(): void + { + $user = auth()->user(); + + if (! $user instanceof User) { + return; + } + + $user->tenants()->syncWithoutDetaching([ + $this->record->getKey() => ['role' => TenantRole::Owner->value], + ]); + } } diff --git a/app/Models/Tenant.php b/app/Models/Tenant.php index 3944280..29c5af5 100644 --- a/app/Models/Tenant.php +++ b/app/Models/Tenant.php @@ -2,16 +2,19 @@ namespace App\Models; +use Filament\Facades\Filament; +use Filament\Models\Contracts\HasName; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; use RuntimeException; -class Tenant extends Model +class Tenant extends Model implements HasName { use HasFactory; use SoftDeletes; @@ -114,6 +117,12 @@ public function makeCurrent(): void public static function current(): self { + $filamentTenant = Filament::getTenant(); + + if ($filamentTenant instanceof self) { + return $filamentTenant; + } + $envTenantId = getenv('INTUNE_TENANT_ID') ?: null; if ($envTenantId) { @@ -142,6 +151,20 @@ public static function current(): self return $tenant; } + public function getFilamentName(): string + { + $environment = strtoupper((string) ($this->environment ?? 'other')); + + return "{$this->name} ({$environment})"; + } + + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class) + ->withPivot('role') + ->withTimestamps(); + } + public function policies(): HasMany { return $this->hasMany(Policy::class); diff --git a/app/Models/User.php b/app/Models/User.php index ddf23da..8c08d23 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,13 +2,21 @@ namespace App\Models; +use App\Support\TenantRole; use Filament\Models\Contracts\FilamentUser; +use Filament\Models\Contracts\HasDefaultTenant; +use Filament\Models\Contracts\HasTenants; use Filament\Panel; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Schema; -class User extends Authenticatable implements FilamentUser +class User extends Authenticatable implements FilamentUser, HasDefaultTenant, HasTenants { /** @use HasFactory<\Database\Factories\UserFactory> */ use HasFactory, Notifiable; @@ -51,4 +59,113 @@ public function canAccessPanel(Panel $panel): bool { return true; } + + public function tenants(): BelongsToMany + { + return $this->belongsToMany(Tenant::class) + ->withPivot('role') + ->withTimestamps(); + } + + public function tenantPreferences(): HasMany + { + return $this->hasMany(UserTenantPreference::class); + } + + private function tenantPivotTableExists(): bool + { + static $exists; + + return $exists ??= Schema::hasTable('tenant_user'); + } + + private function tenantPreferencesTableExists(): bool + { + static $exists; + + return $exists ??= Schema::hasTable('user_tenant_preferences'); + } + + public function tenantRole(Tenant $tenant): ?TenantRole + { + if (! $this->tenantPivotTableExists()) { + return null; + } + + $role = $this->tenants() + ->whereKey($tenant->getKey()) + ->value('role'); + + if (! is_string($role)) { + return null; + } + + return TenantRole::tryFrom($role); + } + + public function canSyncTenant(Tenant $tenant): bool + { + $role = $this->tenantRole($tenant); + + return $role?->canSync() ?? false; + } + + public function canAccessTenant(Model $tenant): bool + { + if (! $tenant instanceof Tenant) { + return false; + } + + if (! $this->tenantPivotTableExists()) { + return false; + } + + return $this->tenants() + ->whereKey($tenant->getKey()) + ->exists(); + } + + public function getTenants(Panel $panel): array|Collection + { + if (! $this->tenantPivotTableExists()) { + return collect(); + } + + return $this->tenants() + ->where('status', 'active') + ->orderBy('name') + ->get(); + } + + public function getDefaultTenant(Panel $panel): ?Model + { + if (! $this->tenantPivotTableExists()) { + return null; + } + + $tenantId = null; + + if ($this->tenantPreferencesTableExists()) { + $tenantId = $this->tenantPreferences() + ->whereNotNull('last_used_at') + ->orderByDesc('last_used_at') + ->value('tenant_id'); + } + + if ($tenantId !== null) { + $tenant = $this->tenants() + ->where('status', 'active') + ->whereKey($tenantId) + ->first(); + + if ($tenant !== null) { + return $tenant; + } + } + + return $this->tenants() + ->where('status', 'active') + ->orderBy('name') + ->first(); + } } diff --git a/app/Models/UserTenantPreference.php b/app/Models/UserTenantPreference.php new file mode 100644 index 0000000..dab7b6c --- /dev/null +++ b/app/Models/UserTenantPreference.php @@ -0,0 +1,26 @@ + 'boolean', + 'last_used_at' => 'datetime', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index fbe5206..9da238b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,9 @@ namespace App\Providers; +use App\Models\Tenant; +use App\Models\User; +use App\Models\UserTenantPreference; use App\Services\Graph\GraphClientInterface; use App\Services\Graph\MicrosoftGraphClient; use App\Services\Graph\NullGraphClient; @@ -18,6 +21,9 @@ use App\Services\Intune\WindowsFeatureUpdateProfileNormalizer; use App\Services\Intune\WindowsQualityUpdateProfileNormalizer; use App\Services\Intune\WindowsUpdateRingNormalizer; +use Filament\Events\TenantSet; +use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Schema; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -66,6 +72,35 @@ public function register(): void */ public function boot(): void { - // + Event::listen(TenantSet::class, function (TenantSet $event): void { + static $hasPreferencesTable; + + $hasPreferencesTable ??= Schema::hasTable('user_tenant_preferences'); + + if (! $hasPreferencesTable) { + return; + } + + $tenant = $event->getTenant(); + $user = $event->getUser(); + + if (! $tenant instanceof Tenant) { + return; + } + + if (! $user instanceof User) { + return; + } + + UserTenantPreference::query()->updateOrCreate( + [ + 'user_id' => $user->getKey(), + 'tenant_id' => $tenant->getKey(), + ], + [ + 'last_used_at' => now(), + ], + ); + }); } } diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 9827abc..351d2bd 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -2,6 +2,8 @@ namespace App\Providers\Filament; +use App\Filament\Pages\Tenancy\RegisterTenant; +use App\Models\Tenant; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\DisableBladeIconComponents; @@ -29,6 +31,10 @@ public function panel(Panel $panel): Panel ->id('admin') ->path('admin') ->login() + ->tenant(Tenant::class, slugAttribute: 'external_id') + ->tenantRoutePrefix('t') + ->searchableTenantMenu() + ->tenantRegistration(RegisterTenant::class) ->colors([ 'primary' => Color::Amber, ]) diff --git a/app/Support/TenantRole.php b/app/Support/TenantRole.php new file mode 100644 index 0000000..db82a29 --- /dev/null +++ b/app/Support/TenantRole.php @@ -0,0 +1,21 @@ + true, + self::Readonly => false, + }; + } +} diff --git a/database/factories/TenantFactory.php b/database/factories/TenantFactory.php index 0938ebe..3abfbdd 100644 --- a/database/factories/TenantFactory.php +++ b/database/factories/TenantFactory.php @@ -26,6 +26,7 @@ public function definition(): array 'app_status' => 'ok', 'app_notes' => null, 'status' => 'active', + 'environment' => 'other', 'is_current' => false, 'metadata' => [], ]; diff --git a/database/migrations/2026_01_04_135956_add_environment_to_tenants_table.php b/database/migrations/2026_01_04_135956_add_environment_to_tenants_table.php new file mode 100644 index 0000000..8576c8c --- /dev/null +++ b/database/migrations/2026_01_04_135956_add_environment_to_tenants_table.php @@ -0,0 +1,24 @@ +string('environment')->default('other')->after('status'); + $table->index('environment'); + }); + } + + public function down(): void + { + Schema::table('tenants', function (Blueprint $table) { + $table->dropIndex(['environment']); + $table->dropColumn('environment'); + }); + } +}; diff --git a/database/migrations/2026_01_04_135957_create_tenant_user_table.php b/database/migrations/2026_01_04_135957_create_tenant_user_table.php new file mode 100644 index 0000000..c0b5dcf --- /dev/null +++ b/database/migrations/2026_01_04_135957_create_tenant_user_table.php @@ -0,0 +1,61 @@ +foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('role')->default('owner'); + $table->timestamps(); + + $table->unique(['tenant_id', 'user_id']); + }); + + $now = now(); + + $tenantIds = DB::table('tenants') + ->whereNull('deleted_at') + ->pluck('id'); + + $userIds = DB::table('users')->pluck('id'); + + if ($tenantIds->isEmpty() || $userIds->isEmpty()) { + return; + } + + $rows = []; + + foreach ($tenantIds as $tenantId) { + foreach ($userIds as $userId) { + $rows[] = [ + 'tenant_id' => $tenantId, + 'user_id' => $userId, + 'role' => 'owner', + 'created_at' => $now, + 'updated_at' => $now, + ]; + + if (count($rows) >= 500) { + DB::table('tenant_user')->insertOrIgnore($rows); + $rows = []; + } + } + } + + if ($rows !== []) { + DB::table('tenant_user')->insertOrIgnore($rows); + } + } + + public function down(): void + { + Schema::dropIfExists('tenant_user'); + } +}; diff --git a/database/migrations/2026_01_04_135957_create_user_tenant_preferences_table.php b/database/migrations/2026_01_04_135957_create_user_tenant_preferences_table.php new file mode 100644 index 0000000..e460b08 --- /dev/null +++ b/database/migrations/2026_01_04_135957_create_user_tenant_preferences_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->boolean('is_favorite')->default(false); + $table->timestamp('last_used_at')->nullable(); + $table->timestamps(); + + $table->unique(['user_id', 'tenant_id']); + $table->index(['user_id', 'last_used_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('user_tenant_preferences'); + } +}; diff --git a/resources/views/admin-consent-callback.blade.php b/resources/views/admin-consent-callback.blade.php index 9e7cb3f..3cfe8df 100644 --- a/resources/views/admin-consent-callback.blade.php +++ b/resources/views/admin-consent-callback.blade.php @@ -31,7 +31,11 @@

Admin consent wurde bestätigt.

@endif -

Zurück zur Tenant-Detailseite

+

+ + Zurück zur Tenant-Detailseite + +

diff --git a/specs/031-tenant-portfolio-context-switch/checklists/requirements.md b/specs/031-tenant-portfolio-context-switch/checklists/requirements.md index 5162cf1..7834e92 100644 --- a/specs/031-tenant-portfolio-context-switch/checklists/requirements.md +++ b/specs/031-tenant-portfolio-context-switch/checklists/requirements.md @@ -3,12 +3,11 @@ # Requirements Checklist (031) **Created**: 2026-01-04 **Feature**: [spec.md](../spec.md) -- [ ] Tenant memberships/roles exist and are enforced. -- [ ] Current Tenant context is per-user and always visible. -- [ ] Portfolio shows only accessible tenants with environment + health/status. -- [ ] “Open tenant” changes context and redirects into tenant-scoped area. -- [ ] Tenant-scoped resources are filtered by context and deny unauthorized access. -- [ ] Bulk “Sync selected” dispatches per-tenant jobs and is role-gated. -- [ ] Restore flows show target tenant + environment and require tenant-aware confirmation. -- [ ] Pest tests cover authorization + context switching + bulk actions. - +- [x] Tenant memberships/roles exist and are enforced. +- [x] Current Tenant context is per-user and always visible. +- [x] Portfolio shows only accessible tenants with environment + health/status. +- [x] “Open tenant” changes context and redirects into tenant-scoped area. +- [x] Tenant-scoped resources are filtered by context and deny unauthorized access. +- [x] Bulk “Sync selected” dispatches per-tenant jobs and is role-gated. +- [x] Restore flows show target tenant + environment and require tenant-aware confirmation. +- [x] Pest tests cover authorization + context switching + bulk actions. diff --git a/specs/031-tenant-portfolio-context-switch/spec.md b/specs/031-tenant-portfolio-context-switch/spec.md index d90ade4..a538dad 100644 --- a/specs/031-tenant-portfolio-context-switch/spec.md +++ b/specs/031-tenant-portfolio-context-switch/spec.md @@ -2,7 +2,7 @@ # Feature Specification: Tenant Portfolio & Context Switch (031) **Feature Branch**: `feat/031-tenant-portfolio-context-switch` **Created**: 2026-01-04 -**Status**: Proposed +**Status**: Implemented (ready to merge) **Risk**: Medium **Priority**: P1 diff --git a/specs/031-tenant-portfolio-context-switch/tasks.md b/specs/031-tenant-portfolio-context-switch/tasks.md index 3a2579a..70acd11 100644 --- a/specs/031-tenant-portfolio-context-switch/tasks.md +++ b/specs/031-tenant-portfolio-context-switch/tasks.md @@ -8,27 +8,26 @@ ## Phase 1: Setup - [x] T001 Create spec/plan/tasks and checklist. ## Phase 2: Research & Design -- [ ] T002 Review Filament tenancy support and choose the context mechanism (route vs session). -- [ ] T003 Define tenant access roles and mapping (user memberships; future org/group principals). -- [ ] T004 Decide how to store `environment` (column vs JSONB) and whether MSP “customer grouping” is in scope. -- [ ] T005 Define context precedence rules (env override, route tenant, session/default tenant) and cross-tab safety expectations. +- [x] T002 Review Filament tenancy support and choose the context mechanism (route vs session). +- [x] T003 Define tenant access roles and mapping (user memberships; future org/group principals). +- [x] T004 Decide how to store `environment` (column vs JSONB) and whether MSP “customer grouping” is in scope. +- [x] T005 Define context precedence rules (env override, route tenant, session/default tenant) and cross-tab safety expectations. ## Phase 3: Tests (TDD) -- [ ] T006 Authorization: user cannot open unauthorized tenant (403). -- [ ] T007 Authorization: tenant-scoped resources deny cross-tenant access via URL (403/404). -- [ ] T008 Context switching: “Open tenant” sets context and tenant-scoped pages filter correctly. -- [ ] T009 Bulk sync: dispatches one job per selected tenant; readonly role cannot run it. +- [x] T006 Authorization: user cannot access unauthorized tenant (404). +- [x] T007 Authorization: tenant-scoped resources deny cross-tenant access via URL (404). +- [x] T008 Context switching: “Open tenant” navigates into tenant-scoped pages (tenant in URL) and data filters correctly. +- [x] T009 Bulk sync: dispatches one job per selected tenant; readonly role cannot run it. - [ ] T010 UI (optional browser tests): tenant switcher visible and environment badge shown. ## Phase 4: Implementation -- [ ] T011 Add migrations for tenant memberships/roles and environment attribute (and optional preferences). -- [ ] T012 Implement `TenantContext` + authorization gate/policy (`canAccessTenant`). -- [ ] T013 Integrate tenant switcher into Filament topbar and make Current Tenant always visible. -- [ ] T014 Scope tenant resources (Policies/Backups/RestoreRuns/etc.) via TenantContext; replace direct `Tenant::current()` usage. -- [ ] T015 Update `TenantResource` into a portfolio view: access-scoped query, columns, filters, “Open”, “Sync”, bulk “Sync selected”. -- [ ] T016 Add restore guardrails (target tenant header + tenant-aware confirmations). +- [x] T011 Add migrations for tenant memberships/roles and environment attribute (and optional preferences). +- [x] T012 Implement `TenantContext` + authorization gate/policy (`canAccessTenant`). +- [x] T013 Integrate tenant switcher into Filament topbar and make Current Tenant always visible. +- [x] T014 Scope tenant resources (Policies/Backups/RestoreRuns/etc.) via TenantContext; replace direct `Tenant::current()` usage. +- [x] T015 Update `TenantResource` into a portfolio view: access-scoped query, columns, filters, “Open”, “Sync”, bulk “Sync selected”. +- [x] T016 Add restore guardrails (target tenant header + tenant-aware confirmations). ## Phase 5: Verification -- [ ] T017 Run targeted tests. -- [ ] T018 Run Pint (`./vendor/bin/pint --dirty`). - +- [x] T017 Run targeted tests. +- [x] T018 Run Pint (`./vendor/bin/pint --dirty`). diff --git a/tests/Feature/BulkDeleteBackupSetsTest.php b/tests/Feature/BulkDeleteBackupSetsTest.php index 260b458..b3d81bd 100644 --- a/tests/Feature/BulkDeleteBackupSetsTest.php +++ b/tests/Feature/BulkDeleteBackupSetsTest.php @@ -7,6 +7,7 @@ use App\Models\RestoreRun; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -14,8 +15,12 @@ test('backup sets table bulk archive creates a run and archives selected sets', function () { $tenant = Tenant::factory()->create(); - $tenant->makeCurrent(); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $sets = collect(range(1, 3))->map(function (int $i) use ($tenant) { return BackupSet::create([ @@ -58,8 +63,12 @@ test('backup sets can be archived even when referenced by restore runs', function () { $tenant = Tenant::factory()->create(); - $tenant->makeCurrent(); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $set = BackupSet::create([ 'tenant_id' => $tenant->id, @@ -87,8 +96,12 @@ test('backup sets table bulk archive requires type-to-confirm for 10+ sets', function () { $tenant = Tenant::factory()->create(); - $tenant->makeCurrent(); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $sets = collect(range(1, 10))->map(function (int $i) use ($tenant) { return BackupSet::create([ diff --git a/tests/Feature/BulkDeleteMixedStatusTest.php b/tests/Feature/BulkDeleteMixedStatusTest.php index 17dad2b..1a35e66 100644 --- a/tests/Feature/BulkDeleteMixedStatusTest.php +++ b/tests/Feature/BulkDeleteMixedStatusTest.php @@ -6,6 +6,7 @@ use App\Models\RestoreRun; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -13,8 +14,12 @@ test('bulk delete restore runs skips running items', function () { $tenant = Tenant::factory()->create(); - $tenant->makeCurrent(); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $backupSet = BackupSet::create([ 'tenant_id' => $tenant->id, diff --git a/tests/Feature/BulkDeleteRestoreRunsTest.php b/tests/Feature/BulkDeleteRestoreRunsTest.php index 9e41b4b..3ad115c 100644 --- a/tests/Feature/BulkDeleteRestoreRunsTest.php +++ b/tests/Feature/BulkDeleteRestoreRunsTest.php @@ -6,6 +6,7 @@ use App\Models\RestoreRun; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -13,8 +14,12 @@ test('bulk delete restore runs soft deletes selected runs', function () { $tenant = Tenant::factory()->create(); - $tenant->makeCurrent(); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $backupSet = BackupSet::create([ 'tenant_id' => $tenant->id, diff --git a/tests/Feature/BulkForceDeleteBackupSetsTest.php b/tests/Feature/BulkForceDeleteBackupSetsTest.php index 7cb8aec..abc6316 100644 --- a/tests/Feature/BulkForceDeleteBackupSetsTest.php +++ b/tests/Feature/BulkForceDeleteBackupSetsTest.php @@ -6,6 +6,7 @@ use App\Models\BulkOperationRun; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -13,8 +14,12 @@ test('backup sets table bulk force delete permanently deletes archived sets and their items', function () { $tenant = Tenant::factory()->create(); - $tenant->makeCurrent(); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $set = BackupSet::create([ 'tenant_id' => $tenant->id, diff --git a/tests/Feature/BulkForceDeletePolicyVersionsTest.php b/tests/Feature/BulkForceDeletePolicyVersionsTest.php index 106239b..701bd44 100644 --- a/tests/Feature/BulkForceDeletePolicyVersionsTest.php +++ b/tests/Feature/BulkForceDeletePolicyVersionsTest.php @@ -6,6 +6,7 @@ use App\Models\PolicyVersion; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -14,6 +15,11 @@ test('policy versions table bulk force delete creates a run and skips non-archived records', function () { $tenant = Tenant::factory()->create(['is_current' => true]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $policy = Policy::factory()->create(['tenant_id' => $tenant->id]); $version = PolicyVersion::factory()->create([ diff --git a/tests/Feature/BulkForceDeleteRestoreRunsTest.php b/tests/Feature/BulkForceDeleteRestoreRunsTest.php index d527954..cd383d8 100644 --- a/tests/Feature/BulkForceDeleteRestoreRunsTest.php +++ b/tests/Feature/BulkForceDeleteRestoreRunsTest.php @@ -6,6 +6,7 @@ use App\Models\RestoreRun; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -13,8 +14,12 @@ test('bulk force delete restore runs permanently deletes archived runs', function () { $tenant = Tenant::factory()->create(); - $tenant->makeCurrent(); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $backupSet = BackupSet::create([ 'tenant_id' => $tenant->id, diff --git a/tests/Feature/BulkPruneSkipReasonsTest.php b/tests/Feature/BulkPruneSkipReasonsTest.php index 759ff54..de35dcb 100644 --- a/tests/Feature/BulkPruneSkipReasonsTest.php +++ b/tests/Feature/BulkPruneSkipReasonsTest.php @@ -6,6 +6,7 @@ use App\Models\PolicyVersion; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -14,6 +15,11 @@ test('bulk prune records skip reasons', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $policyA = Policy::factory()->create(['tenant_id' => $tenant->id]); $current = PolicyVersion::factory()->create([ @@ -37,8 +43,6 @@ 'captured_at' => now()->subDays(10), ]); - $tenant->forceFill(['is_current' => true])->save(); - Livewire::actingAs($user) ->test(PolicyVersionResource\Pages\ListPolicyVersions::class) ->callTableBulkAction('bulk_prune_versions', collect([$current, $tooRecent]), data: [ diff --git a/tests/Feature/BulkPruneVersionsTest.php b/tests/Feature/BulkPruneVersionsTest.php index ec62444..d9cbe48 100644 --- a/tests/Feature/BulkPruneVersionsTest.php +++ b/tests/Feature/BulkPruneVersionsTest.php @@ -5,6 +5,7 @@ use App\Models\PolicyVersion; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -13,6 +14,11 @@ test('bulk prune archives eligible policy versions', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $policy = Policy::factory()->create(['tenant_id' => $tenant->id]); @@ -30,8 +36,6 @@ 'captured_at' => now()->subDays(120), ]); - $tenant->forceFill(['is_current' => true])->save(); - Livewire::actingAs($user) ->test(PolicyVersionResource\Pages\ListPolicyVersions::class) ->callTableBulkAction('bulk_prune_versions', collect([$eligible, $current]), data: [ diff --git a/tests/Feature/BulkRestoreBackupSetsTest.php b/tests/Feature/BulkRestoreBackupSetsTest.php index 3908e6d..6132119 100644 --- a/tests/Feature/BulkRestoreBackupSetsTest.php +++ b/tests/Feature/BulkRestoreBackupSetsTest.php @@ -6,6 +6,7 @@ use App\Models\BulkOperationRun; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -13,8 +14,12 @@ test('backup sets table bulk restore restores archived sets and their items', function () { $tenant = Tenant::factory()->create(); - $tenant->makeCurrent(); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $set = BackupSet::create([ 'tenant_id' => $tenant->id, diff --git a/tests/Feature/BulkRestorePolicyVersionsTest.php b/tests/Feature/BulkRestorePolicyVersionsTest.php index 41d6a81..8f3b429 100644 --- a/tests/Feature/BulkRestorePolicyVersionsTest.php +++ b/tests/Feature/BulkRestorePolicyVersionsTest.php @@ -6,6 +6,7 @@ use App\Models\PolicyVersion; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -14,6 +15,11 @@ test('policy versions table bulk restore creates a run and restores archived records', function () { $tenant = Tenant::factory()->create(['is_current' => true]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $policy = Policy::factory()->create(['tenant_id' => $tenant->id]); $version = PolicyVersion::factory()->create([ diff --git a/tests/Feature/BulkRestoreRestoreRunsTest.php b/tests/Feature/BulkRestoreRestoreRunsTest.php index 35d2bec..a6fa5e9 100644 --- a/tests/Feature/BulkRestoreRestoreRunsTest.php +++ b/tests/Feature/BulkRestoreRestoreRunsTest.php @@ -6,6 +6,7 @@ use App\Models\RestoreRun; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -13,8 +14,12 @@ test('restore runs table bulk restore creates a run and restores archived records', function () { $tenant = Tenant::factory()->create(); - $tenant->makeCurrent(); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $backupSet = BackupSet::create([ 'tenant_id' => $tenant->id, diff --git a/tests/Feature/BulkTypeToConfirmTest.php b/tests/Feature/BulkTypeToConfirmTest.php index 1b7748a..43a78a9 100644 --- a/tests/Feature/BulkTypeToConfirmTest.php +++ b/tests/Feature/BulkTypeToConfirmTest.php @@ -4,6 +4,7 @@ use App\Models\Policy; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -11,8 +12,12 @@ test('bulk delete requires confirmation string for large batches', function () { $tenant = Tenant::factory()->create(); - $tenant->makeCurrent(); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $policies = Policy::factory()->count(20)->create(['tenant_id' => $tenant->id]); Livewire::actingAs($user) @@ -27,8 +32,12 @@ test('bulk delete fails with incorrect confirmation string', function () { $tenant = Tenant::factory()->create(); - $tenant->makeCurrent(); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $policies = Policy::factory()->count(20)->create(['tenant_id' => $tenant->id]); Livewire::actingAs($user) @@ -43,8 +52,12 @@ test('bulk delete does not require confirmation string for small batches', function () { $tenant = Tenant::factory()->create(); - $tenant->makeCurrent(); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $policies = Policy::factory()->count(10)->create(['tenant_id' => $tenant->id]); Livewire::actingAs($user) diff --git a/tests/Feature/Filament/AppProtectionPolicySettingsDisplayTest.php b/tests/Feature/Filament/AppProtectionPolicySettingsDisplayTest.php index 809b9ed..8f00a23 100644 --- a/tests/Feature/Filament/AppProtectionPolicySettingsDisplayTest.php +++ b/tests/Feature/Filament/AppProtectionPolicySettingsDisplayTest.php @@ -47,9 +47,12 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant)); $response->assertOk(); $response->assertSee('Data Protection'); diff --git a/tests/Feature/Filament/EnrollmentAutopilotSettingsDisplayTest.php b/tests/Feature/Filament/EnrollmentAutopilotSettingsDisplayTest.php index 7ad9258..0ed7cc6 100644 --- a/tests/Feature/Filament/EnrollmentAutopilotSettingsDisplayTest.php +++ b/tests/Feature/Filament/EnrollmentAutopilotSettingsDisplayTest.php @@ -22,6 +22,9 @@ $this->tenant = $tenant; $this->user = User::factory()->create(); + $this->user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); }); test('policy detail renders normalized settings for Autopilot profiles', function () { @@ -54,7 +57,7 @@ ]); $response = $this->actingAs($this->user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $this->tenant)); $response->assertOk(); $response->assertSee('Settings'); @@ -95,7 +98,7 @@ ]); $response = $this->actingAs($this->user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $this->tenant)); $response->assertOk(); $response->assertSee('Settings'); @@ -139,7 +142,7 @@ ]); $response = $this->actingAs($this->user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $this->tenant)); $response->assertOk(); $response->assertSee('Settings'); diff --git a/tests/Feature/Filament/GroupPolicyConfigurationHydrationTest.php b/tests/Feature/Filament/GroupPolicyConfigurationHydrationTest.php index e7b424e..3254355 100644 --- a/tests/Feature/Filament/GroupPolicyConfigurationHydrationTest.php +++ b/tests/Feature/Filament/GroupPolicyConfigurationHydrationTest.php @@ -133,10 +133,13 @@ public function request(string $method, string $path, array $options = []): Grap ); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this ->actingAs($user) - ->get(route('filament.admin.resources.policies.view', ['record' => $policy])); + ->get(route('filament.admin.resources.policies.view', array_merge(filamentTenantRouteParams($tenant), ['record' => $policy]))); $response->assertOk(); $response->assertSee('Block legacy auth'); diff --git a/tests/Feature/Filament/HousekeepingTest.php b/tests/Feature/Filament/HousekeepingTest.php index fb2a62e..7c2bd74 100644 --- a/tests/Feature/Filament/HousekeepingTest.php +++ b/tests/Feature/Filament/HousekeepingTest.php @@ -12,6 +12,7 @@ use App\Models\RestoreRun; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -43,6 +44,10 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); Livewire::test(ListBackupSets::class) ->callTableAction('archive', $backupSet); @@ -78,6 +83,10 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); Livewire::test(ListBackupSets::class) ->callTableAction('archive', $backupSet); @@ -117,6 +126,10 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); Livewire::test(ListBackupSets::class) ->callTableAction('archive', $backupSet) @@ -158,6 +171,10 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); Livewire::test(ListBackupSets::class) ->callTableAction('archive', $backupSet) @@ -197,6 +214,10 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); Livewire::test(ListRestoreRuns::class) ->callTableAction('archive', $restoreRun) @@ -235,6 +256,10 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); Livewire::test(ListRestoreRuns::class) ->callTableAction('archive', $restoreRun) @@ -269,6 +294,10 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); Livewire::test(ListPolicies::class) ->callTableAction('ignore', $policy); @@ -309,6 +338,10 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); Livewire::test(ListPolicyVersions::class) ->callTableAction('archive', $version); @@ -346,6 +379,10 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); Livewire::test(ListPolicyVersions::class) ->callTableAction('archive', $version) @@ -368,6 +405,10 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); Livewire::test(ListTenants::class) ->callTableAction('archive', $tenant); @@ -409,6 +450,11 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $active->getKey() => ['role' => 'owner'], + $archived->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($active, true); $component = Livewire::test(ListTenants::class) ->assertSee($active->name) @@ -433,8 +479,18 @@ $tenant->delete(); + $contextTenant = Tenant::create([ + 'tenant_id' => 'tenant-restore-context', + 'name' => 'Restore Context Tenant', + ]); + $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + $contextTenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($contextTenant, true); Livewire::test(ListTenants::class) ->set('tableFilters.trashed.value', 1) diff --git a/tests/Feature/Filament/MalformedSnapshotWarningTest.php b/tests/Feature/Filament/MalformedSnapshotWarningTest.php index 07bcea1..cd5abbb 100644 --- a/tests/Feature/Filament/MalformedSnapshotWarningTest.php +++ b/tests/Feature/Filament/MalformedSnapshotWarningTest.php @@ -41,14 +41,17 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $policyResponse = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant)); $policyResponse->assertSee('This snapshot may be incomplete or malformed'); $versionResponse = $this->actingAs($user) - ->get(PolicyVersionResource::getUrl('view', ['record' => $version])); + ->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant)); $versionResponse->assertSee('This snapshot may be incomplete or malformed'); }); diff --git a/tests/Feature/Filament/ODataTypeMismatchTest.php b/tests/Feature/Filament/ODataTypeMismatchTest.php index 3aca519..1d1d8d5 100644 --- a/tests/Feature/Filament/ODataTypeMismatchTest.php +++ b/tests/Feature/Filament/ODataTypeMismatchTest.php @@ -100,9 +100,12 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $detailResponse = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant)); $detailResponse->assertSee('@odata.type mismatch'); diff --git a/tests/Feature/Filament/PolicyCaptureSnapshotOptionsTest.php b/tests/Feature/Filament/PolicyCaptureSnapshotOptionsTest.php index f547b1b..6b7227c 100644 --- a/tests/Feature/Filament/PolicyCaptureSnapshotOptionsTest.php +++ b/tests/Feature/Filament/PolicyCaptureSnapshotOptionsTest.php @@ -7,6 +7,7 @@ use App\Services\Graph\AssignmentFetcher; use App\Services\Graph\ScopeTagResolver; use App\Services\Intune\PolicySnapshotService; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; use Mockery\MockInterface; @@ -22,6 +23,10 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); $this->mock(PolicySnapshotService::class, function (MockInterface $mock) use ($policy) { $mock->shouldReceive('fetch') diff --git a/tests/Feature/Filament/PolicyListingTest.php b/tests/Feature/Filament/PolicyListingTest.php index 72ba98f..49c2e81 100644 --- a/tests/Feature/Filament/PolicyListingTest.php +++ b/tests/Feature/Filament/PolicyListingTest.php @@ -7,13 +7,7 @@ uses(\Illuminate\Foundation\Testing\RefreshDatabase::class); test('policies are listed for the active tenant', function () { - $tenant = Tenant::create([ - 'tenant_id' => 'local-tenant', - 'name' => 'Tenant One', - 'metadata' => [], - ]); - - $tenant->makeCurrent(); + $tenant = Tenant::factory()->create(); Policy::create([ 'tenant_id' => $tenant->id, @@ -24,11 +18,7 @@ 'last_synced_at' => now(), ]); - $otherTenant = Tenant::create([ - 'tenant_id' => 'tenant-2', - 'name' => 'Tenant Two', - 'metadata' => [], - ]); + $otherTenant = Tenant::factory()->create(); Policy::create([ 'tenant_id' => $otherTenant->id, @@ -40,9 +30,13 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + $otherTenant->getKey() => ['role' => 'owner'], + ]); $this->actingAs($user) - ->get(route('filament.admin.resources.policies.index')) + ->get(route('filament.admin.resources.policies.index', filamentTenantRouteParams($tenant))) ->assertOk() ->assertSee('Policy A') ->assertDontSee('Policy B'); diff --git a/tests/Feature/Filament/PolicySettingsDisplayTest.php b/tests/Feature/Filament/PolicySettingsDisplayTest.php index 56acd96..1d2c934 100644 --- a/tests/Feature/Filament/PolicySettingsDisplayTest.php +++ b/tests/Feature/Filament/PolicySettingsDisplayTest.php @@ -49,9 +49,12 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant)); $response->assertOk(); $response->assertSee('Settings'); diff --git a/tests/Feature/Filament/PolicySettingsStandardRendersArraysTest.php b/tests/Feature/Filament/PolicySettingsStandardRendersArraysTest.php index 566c323..dcd3999 100644 --- a/tests/Feature/Filament/PolicySettingsStandardRendersArraysTest.php +++ b/tests/Feature/Filament/PolicySettingsStandardRendersArraysTest.php @@ -48,9 +48,12 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy]).'?tab=settings'); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant).'?tab=settings'); $response->assertOk(); $response->assertSee('Settings'); diff --git a/tests/Feature/Filament/PolicyVersionReadableLayoutTest.php b/tests/Feature/Filament/PolicyVersionReadableLayoutTest.php index e53e7ab..516bdc8 100644 --- a/tests/Feature/Filament/PolicyVersionReadableLayoutTest.php +++ b/tests/Feature/Filament/PolicyVersionReadableLayoutTest.php @@ -58,9 +58,12 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyVersionResource::getUrl('view', ['record' => $version])); + ->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant)); $response->assertOk(); $response->assertSee('Normalized settings'); diff --git a/tests/Feature/Filament/PolicyVersionRestoreViaWizardTest.php b/tests/Feature/Filament/PolicyVersionRestoreViaWizardTest.php index 7a84965..02e9efe 100644 --- a/tests/Feature/Filament/PolicyVersionRestoreViaWizardTest.php +++ b/tests/Feature/Filament/PolicyVersionRestoreViaWizardTest.php @@ -10,6 +10,7 @@ use App\Models\Tenant; use App\Models\User; use App\Services\Graph\GroupResolver; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; use Mockery\MockInterface; @@ -49,11 +50,15 @@ ]); $user = User::factory()->create(['email' => 'tester@example.com']); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $this->actingAs($user); + Filament::setTenant($tenant, true); Livewire::test(ListPolicyVersions::class) ->callTableAction('restore_via_wizard', $version) - ->assertRedirectContains(RestoreRunResource::getUrl('create', [], false)); + ->assertRedirectContains(RestoreRunResource::getUrl('create', [], false, tenant: $tenant)); $backupSet = BackupSet::query()->where('metadata->source', 'policy_version')->first(); expect($backupSet)->not->toBeNull(); @@ -141,7 +146,11 @@ }); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $this->actingAs($user); + Filament::setTenant($tenant, true); $component = Livewire::withQueryParams([ 'backup_set_id' => $backupSet->id, diff --git a/tests/Feature/Filament/PolicyVersionScopeTagsDisplayTest.php b/tests/Feature/Filament/PolicyVersionScopeTagsDisplayTest.php index 63a3dce..2f96209 100644 --- a/tests/Feature/Filament/PolicyVersionScopeTagsDisplayTest.php +++ b/tests/Feature/Filament/PolicyVersionScopeTagsDisplayTest.php @@ -47,9 +47,12 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyVersionResource::getUrl('view', ['record' => $version])); + ->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant)); $response->assertOk(); $response->assertSee('Scope Tags'); diff --git a/tests/Feature/Filament/PolicyVersionSettingsTest.php b/tests/Feature/Filament/PolicyVersionSettingsTest.php index a7495c7..79c9d71 100644 --- a/tests/Feature/Filament/PolicyVersionSettingsTest.php +++ b/tests/Feature/Filament/PolicyVersionSettingsTest.php @@ -45,9 +45,12 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyVersionResource::getUrl('view', ['record' => $version])); + ->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant)); $response->assertOk(); $response->assertSee('Raw JSON'); @@ -132,9 +135,12 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyVersionResource::getUrl('view', ['record' => $version]).'?tab=normalized-settings'); + ->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant).'?tab=normalized-settings'); $response->assertOk(); $response->assertSee('Enrollment notifications'); diff --git a/tests/Feature/Filament/PolicyVersionTest.php b/tests/Feature/Filament/PolicyVersionTest.php index ed14f79..4a104e9 100644 --- a/tests/Feature/Filament/PolicyVersionTest.php +++ b/tests/Feature/Filament/PolicyVersionTest.php @@ -31,9 +31,12 @@ $service->captureVersion($policy, ['value' => 2], 'tester'); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $this->actingAs($user) - ->get(route('filament.admin.resources.policy-versions.index')) + ->get(route('filament.admin.resources.policy-versions.index', filamentTenantRouteParams($tenant))) ->assertOk() ->assertSee('Policy A') ->assertSee((string) PolicyVersion::max('version_number')); diff --git a/tests/Feature/Filament/PolicyViewSettingsCatalogReadableTest.php b/tests/Feature/Filament/PolicyViewSettingsCatalogReadableTest.php index 49b7cbd..bfbebb7 100644 --- a/tests/Feature/Filament/PolicyViewSettingsCatalogReadableTest.php +++ b/tests/Feature/Filament/PolicyViewSettingsCatalogReadableTest.php @@ -71,9 +71,12 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant)); $response->assertOk(); $response->assertSee('Settings'); // Settings tab should appear for Settings Catalog @@ -130,9 +133,12 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant)); $response->assertOk(); // TODO: Manual verification - check UI for display name "Allow Real-time Monitoring" @@ -181,9 +187,12 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant)); $response->assertOk(); // TODO: Manual verification - check UI shows prettified fallback label @@ -225,9 +234,12 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant)); $response->assertOk(); $response->assertSee('General'); @@ -281,8 +293,11 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant)); $response->assertOk(); // Policy view should render successfully with Settings Catalog data @@ -356,8 +371,11 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant)); $response->assertOk(); // Value formatting verified by manual UI inspection @@ -419,8 +437,11 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant)); $response->assertOk(); // Search functionality is Alpine.js client-side, requires browser testing @@ -465,8 +486,11 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant)); $response->assertOk(); // Page renders without crash - actual fallback display requires UI verification diff --git a/tests/Feature/Filament/RestoreItemSelectionTest.php b/tests/Feature/Filament/RestoreItemSelectionTest.php index 7ac2911..a80766d 100644 --- a/tests/Feature/Filament/RestoreItemSelectionTest.php +++ b/tests/Feature/Filament/RestoreItemSelectionTest.php @@ -7,6 +7,7 @@ use App\Models\Policy; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -99,6 +100,10 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); Livewire::test(CreateRestoreRun::class) ->fillForm([ diff --git a/tests/Feature/Filament/ScriptPoliciesNormalizedDisplayTest.php b/tests/Feature/Filament/ScriptPoliciesNormalizedDisplayTest.php index 7673fe7..9b0e106 100644 --- a/tests/Feature/Filament/ScriptPoliciesNormalizedDisplayTest.php +++ b/tests/Feature/Filament/ScriptPoliciesNormalizedDisplayTest.php @@ -12,9 +12,12 @@ $originalEnv = getenv('INTUNE_TENANT_ID'); putenv('INTUNE_TENANT_ID='); - $this->actingAs(User::factory()->create()); - $tenant = Tenant::factory()->create(); + $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + $this->actingAs($user); putenv('INTUNE_TENANT_ID='.$tenant->tenant_id); $tenant->makeCurrent(); @@ -51,10 +54,10 @@ ], ]); - $this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('index')) + $this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('index', tenant: $tenant)) ->assertSuccessful(); - $this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $version]).'?tab=normalized-settings') + $this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant).'?tab=normalized-settings') ->assertSuccessful(); $originalEnv !== false @@ -71,14 +74,17 @@ $originalEnv = getenv('INTUNE_TENANT_ID'); putenv('INTUNE_TENANT_ID='); - $this->actingAs(User::factory()->create()); - config([ 'tenantpilot.display.show_script_content' => true, 'tenantpilot.display.max_script_content_chars' => 5000, ]); $tenant = Tenant::factory()->create(); + $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + $this->actingAs($user); putenv('INTUNE_TENANT_ID='.$tenant->tenant_id); $tenant->makeCurrent(); @@ -117,7 +123,7 @@ ], ]); - $url = \App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $v2]); + $url = \App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $v2], tenant: $tenant); $this->get($url.'?tab=diff') ->assertSuccessful() @@ -136,14 +142,17 @@ $originalEnv = getenv('INTUNE_TENANT_ID'); putenv('INTUNE_TENANT_ID='); - $this->actingAs(User::factory()->create()); - config([ 'tenantpilot.display.show_script_content' => true, 'tenantpilot.display.max_script_content_chars' => 5000, ]); $tenant = Tenant::factory()->create(); + $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + $this->actingAs($user); putenv('INTUNE_TENANT_ID='.$tenant->tenant_id); $tenant->makeCurrent(); @@ -182,7 +191,7 @@ ], ]); - $url = \App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $v2]); + $url = \App\Filament\Resources\PolicyVersionResource::getUrl('view', ['record' => $v2], tenant: $tenant); $this->get($url.'?tab=diff') ->assertSuccessful() diff --git a/tests/Feature/Filament/SettingsCatalogPolicyHydrationTest.php b/tests/Feature/Filament/SettingsCatalogPolicyHydrationTest.php index 67c0793..0f6f0cf 100644 --- a/tests/Feature/Filament/SettingsCatalogPolicyHydrationTest.php +++ b/tests/Feature/Filament/SettingsCatalogPolicyHydrationTest.php @@ -105,10 +105,13 @@ public function request(string $method, string $path, array $options = []): Grap ); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this ->actingAs($user) - ->get(route('filament.admin.resources.policies.view', ['record' => $policy])); + ->get(route('filament.admin.resources.policies.view', array_merge(filamentTenantRouteParams($tenant), ['record' => $policy]))); $response->assertOk(); $response->assertSee('Setting A'); @@ -145,10 +148,13 @@ public function request(string $method, string $path, array $options = []): Grap $versions->captureFromGraph($tenant, $policy, createdBy: 'tester@example.com'); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this ->actingAs($user) - ->get(route('filament.admin.resources.policies.view', ['record' => $policy])); + ->get(route('filament.admin.resources.policies.view', array_merge(filamentTenantRouteParams($tenant), ['record' => $policy]))); $response->assertOk(); $response->assertSee('Setting A'); diff --git a/tests/Feature/Filament/SettingsCatalogPolicyNormalizedDisplayTest.php b/tests/Feature/Filament/SettingsCatalogPolicyNormalizedDisplayTest.php index a83f411..0c1dadf 100644 --- a/tests/Feature/Filament/SettingsCatalogPolicyNormalizedDisplayTest.php +++ b/tests/Feature/Filament/SettingsCatalogPolicyNormalizedDisplayTest.php @@ -90,9 +90,12 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $policyResponse = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant)); $policyResponse->assertOk(); $policyResponse->assertSee('Definition'); @@ -104,7 +107,7 @@ $policyResponse->assertSee('tp-policy-general-card'); $versionResponse = $this->actingAs($user) - ->get(PolicyVersionResource::getUrl('view', ['record' => $version])); + ->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant)); $versionResponse->assertOk(); $versionResponse->assertSee('Normalized settings'); diff --git a/tests/Feature/Filament/SettingsCatalogPolicySyncTest.php b/tests/Feature/Filament/SettingsCatalogPolicySyncTest.php index f34e7c3..c8dca6f 100644 --- a/tests/Feature/Filament/SettingsCatalogPolicySyncTest.php +++ b/tests/Feature/Filament/SettingsCatalogPolicySyncTest.php @@ -111,10 +111,13 @@ public function request(string $method, string $path, array $options = []): Grap ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this ->actingAs($user) - ->get(route('filament.admin.resources.policies.index')); + ->get(route('filament.admin.resources.policies.index', filamentTenantRouteParams($tenant))); $response->assertOk(); $response->assertSee('Settings Catalog Policy'); diff --git a/tests/Feature/Filament/SettingsCatalogRestoreApplySettingsPatchTest.php b/tests/Feature/Filament/SettingsCatalogRestoreApplySettingsPatchTest.php index ec69858..68e7dcd 100644 --- a/tests/Feature/Filament/SettingsCatalogRestoreApplySettingsPatchTest.php +++ b/tests/Feature/Filament/SettingsCatalogRestoreApplySettingsPatchTest.php @@ -147,6 +147,9 @@ public function request(string $method, string $path, array $options = []): Grap ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $this->actingAs($user); $service = app(RestoreService::class); @@ -185,7 +188,7 @@ public function request(string $method, string $path, array $options = []): Grap $run->update(['results' => $results]); - $response = $this->get(route('filament.admin.resources.restore-runs.view', ['record' => $run])); + $response = $this->get(route('filament.admin.resources.restore-runs.view', array_merge(filamentTenantRouteParams($tenant), ['record' => $run]))); $response->assertOk(); $response->assertSee('Graph bulk apply failed'); $response->assertSee('Setting missing'); diff --git a/tests/Feature/Filament/SettingsCatalogRestoreTest.php b/tests/Feature/Filament/SettingsCatalogRestoreTest.php index 0570dad..5b37350 100644 --- a/tests/Feature/Filament/SettingsCatalogRestoreTest.php +++ b/tests/Feature/Filament/SettingsCatalogRestoreTest.php @@ -162,6 +162,9 @@ public function request(string $method, string $path, array $options = []): Grap ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $this->actingAs($user); $service = app(RestoreService::class); @@ -201,7 +204,7 @@ public function request(string $method, string $path, array $options = []): Grap ->toBe('#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance'); $response = $this - ->get(route('filament.admin.resources.restore-runs.view', ['record' => $run])); + ->get(route('filament.admin.resources.restore-runs.view', array_merge(filamentTenantRouteParams($tenant), ['record' => $run]))); $response->assertOk(); $response->assertSee('settings are read-only'); diff --git a/tests/Feature/Filament/SettingsCatalogSettingsTableRenderTest.php b/tests/Feature/Filament/SettingsCatalogSettingsTableRenderTest.php index ddc67fb..0bab050 100644 --- a/tests/Feature/Filament/SettingsCatalogSettingsTableRenderTest.php +++ b/tests/Feature/Filament/SettingsCatalogSettingsTableRenderTest.php @@ -56,9 +56,12 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $policyResponse = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy]).'?tab=settings'); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant).'?tab=settings'); $policyResponse->assertOk(); $policyResponse->assertSee('fi-width-full'); @@ -69,7 +72,7 @@ $policyResponse->assertSee('fi-ta-table'); $versionResponse = $this->actingAs($user) - ->get(PolicyVersionResource::getUrl('view', ['record' => $version])); + ->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant)); $versionResponse->assertOk(); $versionResponse->assertSee('fi-width-full'); diff --git a/tests/Feature/Filament/TenantMakeCurrentTest.php b/tests/Feature/Filament/TenantMakeCurrentTest.php index 8da5bbc..d9e000f 100644 --- a/tests/Feature/Filament/TenantMakeCurrentTest.php +++ b/tests/Feature/Filament/TenantMakeCurrentTest.php @@ -3,6 +3,7 @@ use App\Filament\Resources\TenantResource\Pages\ListTenants; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -26,6 +27,11 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $first->getKey() => ['role' => 'owner'], + $second->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($first, true); Livewire::test(ListTenants::class) ->callTableAction('makeCurrent', $second); diff --git a/tests/Feature/Filament/TenantPortfolioContextSwitchTest.php b/tests/Feature/Filament/TenantPortfolioContextSwitchTest.php new file mode 100644 index 0000000..26a6c4c --- /dev/null +++ b/tests/Feature/Filament/TenantPortfolioContextSwitchTest.php @@ -0,0 +1,99 @@ +create(); + + $this->actingAs($user) + ->get(route('filament.admin.resources.policies.index', filamentTenantRouteParams($unauthorizedTenant))) + ->assertNotFound(); +}); + +test('tenant portfolio lists only tenants the user can access', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $authorizedTenant = Tenant::factory()->create([ + 'tenant_id' => 'tenant-portfolio-authorized', + 'name' => 'Authorized Tenant', + ]); + + $unauthorizedTenant = Tenant::factory()->create([ + 'tenant_id' => 'tenant-portfolio-unauthorized', + 'name' => 'Unauthorized Tenant', + ]); + + $user->tenants()->syncWithoutDetaching([ + $authorizedTenant->getKey() => ['role' => 'owner'], + ]); + + $this->get(route('filament.admin.resources.tenants.index', filamentTenantRouteParams($authorizedTenant))) + ->assertOk() + ->assertSee($authorizedTenant->name) + ->assertDontSee($unauthorizedTenant->name); +}); + +test('tenant portfolio bulk sync dispatches one job per eligible tenant', function () { + Bus::fake(); + + $user = User::factory()->create(); + $this->actingAs($user); + + $tenantA = Tenant::factory()->create(['tenant_id' => 'tenant-bulk-a']); + $tenantB = Tenant::factory()->create(['tenant_id' => 'tenant-bulk-b']); + + $user->tenants()->syncWithoutDetaching([ + $tenantA->getKey() => ['role' => 'owner'], + $tenantB->getKey() => ['role' => 'operator'], + ]); + + Filament::setTenant($tenantA, true); + + Livewire::test(ListTenants::class) + ->assertTableBulkActionVisible('syncSelected') + ->callTableBulkAction('syncSelected', collect([$tenantA, $tenantB])); + + Bus::assertDispatchedTimes(SyncPoliciesJob::class, 2); + + Bus::assertDispatched(SyncPoliciesJob::class, fn (SyncPoliciesJob $job) => $job->tenantId === $tenantA->id); + Bus::assertDispatched(SyncPoliciesJob::class, fn (SyncPoliciesJob $job) => $job->tenantId === $tenantB->id); +}); + +test('tenant portfolio bulk sync is hidden for readonly users', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $tenant = Tenant::factory()->create(['tenant_id' => 'tenant-bulk-readonly']); + + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'readonly'], + ]); + + Filament::setTenant($tenant, true); + + Livewire::test(ListTenants::class) + ->assertTableBulkActionHidden('syncSelected'); +}); + +test('tenant set event updates user tenant preference last used timestamp', function () { + [$user, $tenant] = createUserWithTenant(); + + TenantSet::dispatch($tenant, $user); + + $this->assertDatabaseHas('user_tenant_preferences', [ + 'user_id' => $user->id, + 'tenant_id' => $tenant->id, + ]); +}); diff --git a/tests/Feature/Filament/TenantRbacWizardTest.php b/tests/Feature/Filament/TenantRbacWizardTest.php index dd55e28..d9cd858 100644 --- a/tests/Feature/Filament/TenantRbacWizardTest.php +++ b/tests/Feature/Filament/TenantRbacWizardTest.php @@ -6,6 +6,7 @@ use App\Models\User; use App\Services\Graph\GraphClientInterface; use App\Services\Graph\GraphResponse; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Cache; use Livewire\Livewire; @@ -32,6 +33,10 @@ function tenantWithApp(): Tenant $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->mountAction('setup_rbac') @@ -51,6 +56,10 @@ function tenantWithApp(): Tenant $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); $cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null); Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5)); @@ -155,6 +164,10 @@ public function request(string $method, string $path, array $options = []): Grap $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); $cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null); Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5)); @@ -265,6 +278,10 @@ public function request(string $method, string $path, array $options = []): Grap $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); $cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null); Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5)); @@ -365,6 +382,10 @@ public function request(string $method, string $path, array $options = []): Grap $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->mountAction('setup_rbac') @@ -380,6 +401,10 @@ public function request(string $method, string $path, array $options = []): Grap $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->mountAction('setup_rbac') @@ -394,6 +419,10 @@ public function request(string $method, string $path, array $options = []): Grap $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); $cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null); Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5)); @@ -505,6 +534,10 @@ public function request(string $method, string $path, array $options = []): Grap $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($tenant, true); $cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null); Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5)); diff --git a/tests/Feature/Filament/TenantSetupTest.php b/tests/Feature/Filament/TenantSetupTest.php index 10c6ec8..b950406 100644 --- a/tests/Feature/Filament/TenantSetupTest.php +++ b/tests/Feature/Filament/TenantSetupTest.php @@ -7,6 +7,7 @@ use App\Models\User; use App\Services\Graph\GraphClientInterface; use App\Services\Graph\GraphResponse; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -54,9 +55,19 @@ public function request(string $method, string $path, array $options = []): Grap $user = User::factory()->create(); $this->actingAs($user); + $contextTenant = Tenant::create([ + 'tenant_id' => 'tenant-context', + 'name' => 'Context Tenant', + ]); + $user->tenants()->syncWithoutDetaching([ + $contextTenant->getKey() => ['role' => 'owner'], + ]); + Filament::setTenant($contextTenant, true); + Livewire::test(CreateTenant::class) ->fillForm([ 'name' => 'Contoso', + 'environment' => 'other', 'tenant_id' => 'tenant-guid', 'domain' => 'contoso.com', 'app_client_id' => 'client-123', @@ -65,7 +76,7 @@ public function request(string $method, string $path, array $options = []): Grap ->call('create') ->assertHasNoFormErrors(); - $tenant = Tenant::first(); + $tenant = Tenant::query()->where('tenant_id', 'tenant-guid')->first(); expect($tenant)->not->toBeNull(); Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) @@ -129,6 +140,11 @@ public function request(string $method, string $path, array $options = []): Grap 'tenant_id' => 'tenant-error', 'name' => 'Error Tenant', ]); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->callAction('verify'); @@ -157,6 +173,9 @@ public function request(string $method, string $path, array $options = []): Grap 'tenant_id' => 'tenant-ui', 'name' => 'UI Tenant', ]); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); config(['intune_permissions.granted_stub' => []]); @@ -169,7 +188,7 @@ public function request(string $method, string $path, array $options = []): Grap 'status' => 'ok', ]); - $response = $this->get(route('filament.admin.resources.tenants.view', $tenant)); + $response = $this->get(route('filament.admin.resources.tenants.view', array_merge(filamentTenantRouteParams($tenant), ['record' => $tenant]))); $response->assertOk(); $response->assertSee('Actions'); @@ -182,13 +201,17 @@ public function request(string $method, string $path, array $options = []): Grap $user = User::factory()->create(); $this->actingAs($user); - Tenant::create([ + $tenant = Tenant::create([ 'tenant_id' => 'tenant-ui-list', 'name' => 'UI Tenant List', 'app_client_id' => 'client-123', ]); - $response = $this->get(route('filament.admin.resources.tenants.index')); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + $response = $this->get(route('filament.admin.resources.tenants.index', filamentTenantRouteParams($tenant))); $response->assertOk(); $response->assertSee('Open in Entra'); @@ -202,6 +225,11 @@ public function request(string $method, string $path, array $options = []): Grap 'tenant_id' => 'tenant-ui-deactivate', 'name' => 'UI Tenant Deactivate', ]); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->callAction('archive'); diff --git a/tests/Feature/Filament/WindowsUpdateRingPolicyTest.php b/tests/Feature/Filament/WindowsUpdateRingPolicyTest.php index fccc528..bfc70b8 100644 --- a/tests/Feature/Filament/WindowsUpdateRingPolicyTest.php +++ b/tests/Feature/Filament/WindowsUpdateRingPolicyTest.php @@ -48,9 +48,12 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); $response = $this->actingAs($user) - ->get(PolicyResource::getUrl('view', ['record' => $policy])); + ->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant)); $response->assertOk(); diff --git a/tests/Feature/PolicyVersionViewAssignmentsTest.php b/tests/Feature/PolicyVersionViewAssignmentsTest.php index ff174ad..5b6cdad 100644 --- a/tests/Feature/PolicyVersionViewAssignmentsTest.php +++ b/tests/Feature/PolicyVersionViewAssignmentsTest.php @@ -10,11 +10,13 @@ unset($_ENV['INTUNE_TENANT_ID'], $_SERVER['INTUNE_TENANT_ID']); $this->tenant = Tenant::factory()->create(); - $this->tenant->makeCurrent(); $this->policy = Policy::factory()->create([ 'tenant_id' => $this->tenant->id, ]); $this->user = User::factory()->create(); + $this->user->tenants()->syncWithoutDetaching([ + $this->tenant->getKey() => ['role' => 'owner'], + ]); }); it('displays policy version page', function () { @@ -26,7 +28,10 @@ $this->actingAs($this->user); - $response = $this->get("/admin/policy-versions/{$version->id}"); + $response = $this->get(route('filament.admin.resources.policy-versions.view', array_merge( + filamentTenantRouteParams($this->tenant), + ['record' => $version], + ))); $response->assertOk(); }); @@ -67,7 +72,10 @@ $this->actingAs($this->user); - $response = $this->get("/admin/policy-versions/{$version->id}"); + $response = $this->get(route('filament.admin.resources.policy-versions.view', array_merge( + filamentTenantRouteParams($this->tenant), + ['record' => $version], + ))); $response->assertOk(); $response->assertSeeLivewire('policy-version-assignments-widget'); @@ -87,7 +95,10 @@ $this->actingAs($this->user); - $response = $this->get("/admin/policy-versions/{$version->id}"); + $response = $this->get(route('filament.admin.resources.policy-versions.view', array_merge( + filamentTenantRouteParams($this->tenant), + ['record' => $version], + ))); $response->assertOk(); $response->assertSee('Assignments were not captured for this version'); @@ -107,7 +118,10 @@ $this->actingAs($this->user); - $response = $this->get("/admin/policy-versions/{$version->id}"); + $response = $this->get(route('filament.admin.resources.policy-versions.view', array_merge( + filamentTenantRouteParams($this->tenant), + ['record' => $version], + ))); $response->assertOk(); $response->assertSee('No assignments found for this version'); @@ -137,7 +151,10 @@ $this->actingAs($this->user); - $response = $this->get("/admin/policy-versions/{$version->id}"); + $response = $this->get(route('filament.admin.resources.policy-versions.view', array_merge( + filamentTenantRouteParams($this->tenant), + ['record' => $version], + ))); $response->assertOk(); $response->assertSee('Compliance notifications'); @@ -169,7 +186,10 @@ $this->actingAs($this->user); - $response = $this->get("/admin/policy-versions/{$version->id}"); + $response = $this->get(route('filament.admin.resources.policy-versions.view', array_merge( + filamentTenantRouteParams($this->tenant), + ['record' => $version], + ))); $response->assertOk(); $response->assertSee('Compliance notifications'); @@ -192,7 +212,10 @@ $this->actingAs($this->user); - $response = $this->get("/admin/policy-versions/{$version->id}?tab=normalized-settings"); + $response = $this->get(route('filament.admin.resources.policy-versions.view', array_merge( + filamentTenantRouteParams($this->tenant), + ['record' => $version], + )).'?tab=normalized-settings'); $response->assertOk(); $response->assertSee('Password & Access'); diff --git a/tests/Feature/RestoreGroupMappingTest.php b/tests/Feature/RestoreGroupMappingTest.php index 5746b5b..1ddd818 100644 --- a/tests/Feature/RestoreGroupMappingTest.php +++ b/tests/Feature/RestoreGroupMappingTest.php @@ -8,6 +8,7 @@ use App\Models\Tenant; use App\Models\User; use App\Services\Graph\GroupResolver; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; use Mockery\MockInterface; @@ -76,6 +77,11 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $component = Livewire::test(CreateRestoreRun::class) ->fillForm([ @@ -157,6 +163,11 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); Livewire::test(CreateRestoreRun::class) ->fillForm([ diff --git a/tests/Feature/RestorePreviewDiffWizardTest.php b/tests/Feature/RestorePreviewDiffWizardTest.php index ab62af1..90c0caa 100644 --- a/tests/Feature/RestorePreviewDiffWizardTest.php +++ b/tests/Feature/RestorePreviewDiffWizardTest.php @@ -8,6 +8,7 @@ use App\Models\RestoreRun; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -86,6 +87,11 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $component = Livewire::test(CreateRestoreRun::class) ->fillForm([ diff --git a/tests/Feature/RestoreRiskChecksWizardTest.php b/tests/Feature/RestoreRiskChecksWizardTest.php index 1fe987b..32a88f0 100644 --- a/tests/Feature/RestoreRiskChecksWizardTest.php +++ b/tests/Feature/RestoreRiskChecksWizardTest.php @@ -8,6 +8,7 @@ use App\Models\Tenant; use App\Models\User; use App\Services\Graph\GroupResolver; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; use Mockery\MockInterface; @@ -77,6 +78,11 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $component = Livewire::test(CreateRestoreRun::class) ->fillForm([ @@ -188,6 +194,11 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $component = Livewire::test(CreateRestoreRun::class) ->fillForm([ @@ -270,6 +281,11 @@ $user = User::factory()->create(); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $component = Livewire::test(CreateRestoreRun::class) ->fillForm([ diff --git a/tests/Feature/RestoreRunArchiveGuardTest.php b/tests/Feature/RestoreRunArchiveGuardTest.php index 50c7c57..0520342 100644 --- a/tests/Feature/RestoreRunArchiveGuardTest.php +++ b/tests/Feature/RestoreRunArchiveGuardTest.php @@ -5,6 +5,7 @@ use App\Models\RestoreRun; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -28,6 +29,11 @@ ]); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); Livewire::actingAs($user) ->test(ListRestoreRuns::class) diff --git a/tests/Feature/RestoreRunRerunTest.php b/tests/Feature/RestoreRunRerunTest.php index a6014ef..924a03a 100644 --- a/tests/Feature/RestoreRunRerunTest.php +++ b/tests/Feature/RestoreRunRerunTest.php @@ -6,6 +6,7 @@ use App\Models\RestoreRun; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -13,7 +14,6 @@ test('rerun action creates a new restore run with the same selections', function () { $tenant = Tenant::factory()->create(); - $tenant->makeCurrent(); $backupSet = BackupSet::factory()->for($tenant)->create([ 'status' => 'completed', @@ -47,6 +47,11 @@ ]); $user = User::factory()->create(['email' => 'tester@example.com']); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); Livewire::actingAs($user) ->test(ListRestoreRuns::class) diff --git a/tests/Feature/RestoreRunWizardExecuteTest.php b/tests/Feature/RestoreRunWizardExecuteTest.php index 4332917..8528c5c 100644 --- a/tests/Feature/RestoreRunWizardExecuteTest.php +++ b/tests/Feature/RestoreRunWizardExecuteTest.php @@ -9,6 +9,7 @@ use App\Models\Tenant; use App\Models\User; use App\Support\RestoreRunStatus; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Bus; use Livewire\Livewire; @@ -62,6 +63,11 @@ 'name' => 'Tester', ]); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); Livewire::test(CreateRestoreRun::class) ->fillForm([ @@ -130,6 +136,11 @@ 'name' => 'Executor', ]); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); Livewire::test(CreateRestoreRun::class) ->fillForm([ diff --git a/tests/Feature/RestoreRunWizardMetadataTest.php b/tests/Feature/RestoreRunWizardMetadataTest.php index 10c9697..9d29991 100644 --- a/tests/Feature/RestoreRunWizardMetadataTest.php +++ b/tests/Feature/RestoreRunWizardMetadataTest.php @@ -6,6 +6,7 @@ use App\Models\RestoreRun; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -50,6 +51,11 @@ 'name' => 'Tester', ]); $this->actingAs($user); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); Livewire::test(CreateRestoreRun::class) ->fillForm([ diff --git a/tests/Pest.php b/tests/Pest.php index 4baf965..62cf209 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,7 @@ create(); + $tenant ??= Tenant::factory()->create(); + + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => $role], + ]); + + return [$user, $tenant]; +} + +/** + * @return array{tenant: string} + */ +function filamentTenantRouteParams(Tenant $tenant): array +{ + return ['tenant' => (string) $tenant->external_id]; +} diff --git a/tests/Unit/BulkActionPermissionTest.php b/tests/Unit/BulkActionPermissionTest.php index a9da5b9..02e99b6 100644 --- a/tests/Unit/BulkActionPermissionTest.php +++ b/tests/Unit/BulkActionPermissionTest.php @@ -4,6 +4,7 @@ use App\Models\Policy; use App\Models\Tenant; use App\Models\User; +use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; use Tests\TestCase; @@ -12,8 +13,12 @@ test('policies bulk actions are available for authenticated users', function () { $tenant = Tenant::factory()->create(); - $tenant->makeCurrent(); $user = User::factory()->create(); + $user->tenants()->syncWithoutDetaching([ + $tenant->getKey() => ['role' => 'owner'], + ]); + + Filament::setTenant($tenant, true); $policies = Policy::factory()->count(2)->create(['tenant_id' => $tenant->id]); Livewire::actingAs($user)