feat/031-tenant-portfolio-context-switch (#32)
Tenant Switch implemented Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local> Reviewed-on: #32
This commit is contained in:
parent
817ad208da
commit
2ca989c00f
83
app/Filament/Pages/Tenancy/RegisterTenant.php
Normal file
83
app/Filament/Pages/Tenancy/RegisterTenant.php
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Pages\Tenancy;
|
||||||
|
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Support\TenantRole;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Pages\Tenancy\RegisterTenant as BaseRegisterTenant;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class RegisterTenant extends BaseRegisterTenant
|
||||||
|
{
|
||||||
|
public static function getLabel(): string
|
||||||
|
{
|
||||||
|
return 'Register tenant';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canView(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $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()
|
||||||
|
->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<string, mixed> $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -112,8 +112,16 @@ public function table(Table $table): Table
|
|||||||
Actions\ActionGroup::make([
|
Actions\ActionGroup::make([
|
||||||
Actions\ViewAction::make()
|
Actions\ViewAction::make()
|
||||||
->label('View policy')
|
->label('View policy')
|
||||||
->url(fn ($record) => $record->policy_id ? PolicyResource::getUrl('view', ['record' => $record->policy_id]) : null)
|
->url(function (BackupItem $record): ?string {
|
||||||
->hidden(fn ($record) => ! $record->policy_id)
|
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),
|
->openUrlInNewTab(true),
|
||||||
Actions\Action::make('remove')
|
Actions\Action::make('remove')
|
||||||
->label('Remove')
|
->label('Remove')
|
||||||
|
|||||||
@ -186,9 +186,7 @@ public static function table(Table $table): Table
|
|||||||
->falseLabel('Archived'),
|
->falseLabel('Archived'),
|
||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
Actions\ViewAction::make()
|
Actions\ViewAction::make(),
|
||||||
->url(fn (PolicyVersion $record) => static::getUrl('view', ['record' => $record]))
|
|
||||||
->openUrlInNewTab(false),
|
|
||||||
Actions\ActionGroup::make([
|
Actions\ActionGroup::make([
|
||||||
Actions\Action::make('restore_via_wizard')
|
Actions\Action::make('restore_via_wizard')
|
||||||
->label('Restore via Wizard')
|
->label('Restore via Wizard')
|
||||||
|
|||||||
@ -4,13 +4,18 @@
|
|||||||
|
|
||||||
use App\Filament\Resources\TenantResource\Pages;
|
use App\Filament\Resources\TenantResource\Pages;
|
||||||
use App\Http\Controllers\RbacDelegatedAuthController;
|
use App\Http\Controllers\RbacDelegatedAuthController;
|
||||||
|
use App\Jobs\BulkTenantSyncJob;
|
||||||
|
use App\Jobs\SyncPoliciesJob;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\BulkOperationService;
|
||||||
use App\Services\Graph\GraphClientInterface;
|
use App\Services\Graph\GraphClientInterface;
|
||||||
use App\Services\Intune\AuditLogger;
|
use App\Services\Intune\AuditLogger;
|
||||||
use App\Services\Intune\RbacHealthService;
|
use App\Services\Intune\RbacHealthService;
|
||||||
use App\Services\Intune\RbacOnboardingService;
|
use App\Services\Intune\RbacOnboardingService;
|
||||||
use App\Services\Intune\TenantConfigService;
|
use App\Services\Intune\TenantConfigService;
|
||||||
use App\Services\Intune\TenantPermissionService;
|
use App\Services\Intune\TenantPermissionService;
|
||||||
|
use App\Support\TenantRole;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Actions\ActionGroup;
|
use Filament\Actions\ActionGroup;
|
||||||
@ -23,6 +28,8 @@
|
|||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@ -33,6 +40,8 @@ class TenantResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Tenant::class;
|
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|BackedEnum|null $navigationIcon = 'heroicon-o-building-office-2';
|
||||||
|
|
||||||
protected static string|UnitEnum|null $navigationGroup = 'Settings';
|
protected static string|UnitEnum|null $navigationGroup = 'Settings';
|
||||||
@ -44,6 +53,15 @@ public static function form(Schema $schema): Schema
|
|||||||
Forms\Components\TextInput::make('name')
|
Forms\Components\TextInput::make('name')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->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')
|
Forms\Components\TextInput::make('tenant_id')
|
||||||
->label('Tenant ID (GUID)')
|
->label('Tenant ID (GUID)')
|
||||||
->required()
|
->required()
|
||||||
@ -69,10 +87,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
|
public static function table(Table $table): Table
|
||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->modifyQueryUsing(fn (\Illuminate\Database\Eloquent\Builder $query) => $query->withTrashed())
|
|
||||||
->columns([
|
->columns([
|
||||||
Tables\Columns\TextColumn::make('name')
|
Tables\Columns\TextColumn::make('name')
|
||||||
->searchable(),
|
->searchable(),
|
||||||
@ -80,6 +116,23 @@ public static function table(Table $table): Table
|
|||||||
->label('Tenant ID')
|
->label('Tenant ID')
|
||||||
->copyable()
|
->copyable()
|
||||||
->searchable(),
|
->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')
|
Tables\Columns\TextColumn::make('domain')
|
||||||
->copyable()
|
->copyable()
|
||||||
->toggleable(),
|
->toggleable(),
|
||||||
@ -102,6 +155,13 @@ public static function table(Table $table): Table
|
|||||||
->trueLabel('All')
|
->trueLabel('All')
|
||||||
->falseLabel('Archived')
|
->falseLabel('Archived')
|
||||||
->default(true),
|
->default(true),
|
||||||
|
Tables\Filters\SelectFilter::make('environment')
|
||||||
|
->options([
|
||||||
|
'prod' => 'PROD',
|
||||||
|
'dev' => 'DEV',
|
||||||
|
'staging' => 'STAGING',
|
||||||
|
'other' => 'Other',
|
||||||
|
]),
|
||||||
Tables\Filters\SelectFilter::make('app_status')
|
Tables\Filters\SelectFilter::make('app_status')
|
||||||
->options([
|
->options([
|
||||||
'ok' => 'OK',
|
'ok' => 'OK',
|
||||||
@ -113,6 +173,51 @@ public static function table(Table $table): Table
|
|||||||
->actions([
|
->actions([
|
||||||
Actions\ViewAction::make(),
|
Actions\ViewAction::make(),
|
||||||
ActionGroup::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()
|
||||||
|
->sendToDatabase(auth()->user())
|
||||||
|
->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\EditAction::make(),
|
||||||
Actions\RestoreAction::make()
|
Actions\RestoreAction::make()
|
||||||
->label('Restore')
|
->label('Restore')
|
||||||
@ -242,7 +347,106 @@ public static function table(Table $table): Table
|
|||||||
}),
|
}),
|
||||||
])->icon('heroicon-o-ellipsis-vertical'),
|
])->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));
|
||||||
|
|
||||||
|
if ($eligible->isEmpty()) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Bulk sync skipped')
|
||||||
|
->body('No eligible tenants selected.')
|
||||||
|
->icon('heroicon-o-information-circle')
|
||||||
|
->info()
|
||||||
|
->sendToDatabase($user)
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenantContext = Tenant::current() ?? $eligible->first();
|
||||||
|
|
||||||
|
if (! $tenantContext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = $eligible->pluck('id')->toArray();
|
||||||
|
$count = $eligible->count();
|
||||||
|
|
||||||
|
$service = app(BulkOperationService::class);
|
||||||
|
$run = $service->createRun($tenantContext, $user, 'tenant', 'sync', $ids, $count);
|
||||||
|
|
||||||
|
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("Syncing {$count} tenant(s) in the background. Check the progress bar in the bottom right corner.")
|
||||||
|
->icon('heroicon-o-arrow-path')
|
||||||
|
->iconColor('warning')
|
||||||
|
->success()
|
||||||
|
->duration(8000)
|
||||||
|
->sendToDatabase($user)
|
||||||
|
->send();
|
||||||
|
|
||||||
|
BulkTenantSyncJob::dispatch($run->id);
|
||||||
|
})
|
||||||
|
->deselectRecordsAfterCompletion(),
|
||||||
|
])
|
||||||
->headerActions([]);
|
->headerActions([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -440,7 +644,10 @@ public static function rbacAction(): Actions\Action
|
|||||||
->label('Open RBAC login')
|
->label('Open RBAC login')
|
||||||
->url(route('admin.rbac.start', [
|
->url(route('admin.rbac.start', [
|
||||||
'tenant' => $record->graphTenantId(),
|
'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()
|
->warning()
|
||||||
@ -579,7 +786,10 @@ private static function loginToSearchRolesAction(?Tenant $tenant): ?Actions\Acti
|
|||||||
->label('Login to load roles')
|
->label('Login to load roles')
|
||||||
->url(route('admin.rbac.start', [
|
->url(route('admin.rbac.start', [
|
||||||
'tenant' => $tenant->graphTenantId(),
|
'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 +971,10 @@ private static function loginToSearchGroupsAction(?Tenant $tenant): ?Actions\Act
|
|||||||
->label('Login to search groups')
|
->label('Login to search groups')
|
||||||
->url(route('admin.rbac.start', [
|
->url(route('admin.rbac.start', [
|
||||||
'tenant' => $tenant->graphTenantId(),
|
'tenant' => $tenant->graphTenantId(),
|
||||||
'return' => route('filament.admin.resources.tenants.view', $tenant),
|
'return' => route('filament.admin.resources.tenants.view', [
|
||||||
|
'tenant' => $tenant->external_id,
|
||||||
|
'record' => $tenant,
|
||||||
|
]),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,24 @@
|
|||||||
namespace App\Filament\Resources\TenantResource\Pages;
|
namespace App\Filament\Resources\TenantResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\TenantResource;
|
use App\Filament\Resources\TenantResource;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Support\TenantRole;
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
class CreateTenant extends CreateRecord
|
class CreateTenant extends CreateRecord
|
||||||
{
|
{
|
||||||
protected static string $resource = TenantResource::class;
|
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],
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
152
app/Jobs/BulkTenantSyncJob.php
Normal file
152
app/Jobs/BulkTenantSyncJob.php
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use App\Models\BulkOperationRun;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Services\BulkOperationService;
|
||||||
|
use App\Services\Intune\PolicySyncService;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class BulkTenantSyncJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public function __construct(public int $bulkRunId) {}
|
||||||
|
|
||||||
|
public function handle(BulkOperationService $service, PolicySyncService $syncService): void
|
||||||
|
{
|
||||||
|
$run = BulkOperationRun::with(['tenant', 'user'])->find($this->bulkRunId);
|
||||||
|
|
||||||
|
if (! $run || $run->status !== 'pending') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$service->start($run);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10));
|
||||||
|
$itemCount = 0;
|
||||||
|
|
||||||
|
$supported = config('tenantpilot.supported_policy_types');
|
||||||
|
|
||||||
|
$totalItems = $run->total_items ?: count($run->item_ids ?? []);
|
||||||
|
$failureThreshold = (int) floor($totalItems / 2);
|
||||||
|
|
||||||
|
foreach (($run->item_ids ?? []) as $tenantId) {
|
||||||
|
$itemCount++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$tenant = Tenant::query()->whereKey($tenantId)->first();
|
||||||
|
|
||||||
|
if (! $tenant) {
|
||||||
|
$service->recordFailure($run, (string) $tenantId, 'Tenant not found');
|
||||||
|
|
||||||
|
if ($run->failed > $failureThreshold) {
|
||||||
|
$service->abort($run, 'Circuit breaker: more than 50% of items failed.');
|
||||||
|
|
||||||
|
if ($run->user) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Bulk Sync Aborted')
|
||||||
|
->body('Circuit breaker triggered: too many failures (>50%).')
|
||||||
|
->icon('heroicon-o-exclamation-triangle')
|
||||||
|
->danger()
|
||||||
|
->sendToDatabase($run->user)
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $tenant->isActive()) {
|
||||||
|
$service->recordSkippedWithReason($run, (string) $tenantId, 'Tenant is not active');
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $run->user || ! $run->user->canSyncTenant($tenant)) {
|
||||||
|
$service->recordSkippedWithReason($run, (string) $tenantId, 'Not authorized to sync tenant');
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$syncService->syncPolicies($tenant, $supported);
|
||||||
|
|
||||||
|
$service->recordSuccess($run);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$service->recordFailure($run, (string) $tenantId, $e->getMessage());
|
||||||
|
|
||||||
|
if ($run->failed > $failureThreshold) {
|
||||||
|
$service->abort($run, 'Circuit breaker: more than 50% of items failed.');
|
||||||
|
|
||||||
|
if ($run->user) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Bulk Sync Aborted')
|
||||||
|
->body('Circuit breaker triggered: too many failures (>50%).')
|
||||||
|
->icon('heroicon-o-exclamation-triangle')
|
||||||
|
->danger()
|
||||||
|
->sendToDatabase($run->user)
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($itemCount % $chunkSize === 0) {
|
||||||
|
$run->refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$service->complete($run);
|
||||||
|
|
||||||
|
if ($run->user) {
|
||||||
|
$message = "Synced {$run->succeeded} tenant(s)";
|
||||||
|
|
||||||
|
if ($run->skipped > 0) {
|
||||||
|
$message .= " ({$run->skipped} skipped)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($run->failed > 0) {
|
||||||
|
$message .= " ({$run->failed} failed)";
|
||||||
|
}
|
||||||
|
|
||||||
|
$message .= '.';
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Bulk Sync Completed')
|
||||||
|
->body($message)
|
||||||
|
->icon('heroicon-o-check-circle')
|
||||||
|
->success()
|
||||||
|
->sendToDatabase($run->user)
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$service->fail($run, $e->getMessage());
|
||||||
|
|
||||||
|
$run->refresh();
|
||||||
|
$run->load('user');
|
||||||
|
|
||||||
|
if ($run->user) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Bulk Sync Failed')
|
||||||
|
->body($e->getMessage())
|
||||||
|
->icon('heroicon-o-x-circle')
|
||||||
|
->danger()
|
||||||
|
->sendToDatabase($run->user)
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,16 +2,19 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Filament\Models\Contracts\HasName;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
class Tenant extends Model
|
class Tenant extends Model implements HasName
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use SoftDeletes;
|
use SoftDeletes;
|
||||||
@ -114,6 +117,12 @@ public function makeCurrent(): void
|
|||||||
|
|
||||||
public static function current(): self
|
public static function current(): self
|
||||||
{
|
{
|
||||||
|
$filamentTenant = Filament::getTenant();
|
||||||
|
|
||||||
|
if ($filamentTenant instanceof self) {
|
||||||
|
return $filamentTenant;
|
||||||
|
}
|
||||||
|
|
||||||
$envTenantId = getenv('INTUNE_TENANT_ID') ?: null;
|
$envTenantId = getenv('INTUNE_TENANT_ID') ?: null;
|
||||||
|
|
||||||
if ($envTenantId) {
|
if ($envTenantId) {
|
||||||
@ -142,6 +151,20 @@ public static function current(): self
|
|||||||
return $tenant;
|
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
|
public function policies(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Policy::class);
|
return $this->hasMany(Policy::class);
|
||||||
|
|||||||
@ -2,13 +2,21 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Support\TenantRole;
|
||||||
use Filament\Models\Contracts\FilamentUser;
|
use Filament\Models\Contracts\FilamentUser;
|
||||||
|
use Filament\Models\Contracts\HasDefaultTenant;
|
||||||
|
use Filament\Models\Contracts\HasTenants;
|
||||||
use Filament\Panel;
|
use Filament\Panel;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
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\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
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<\Database\Factories\UserFactory> */
|
||||||
use HasFactory, Notifiable;
|
use HasFactory, Notifiable;
|
||||||
@ -51,4 +59,113 @@ public function canAccessPanel(Panel $panel): bool
|
|||||||
{
|
{
|
||||||
return true;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
app/Models/UserTenantPreference.php
Normal file
26
app/Models/UserTenantPreference.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class UserTenantPreference extends Model
|
||||||
|
{
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_favorite' => 'boolean',
|
||||||
|
'last_used_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tenant(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Tenant::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\UserTenantPreference;
|
||||||
use App\Services\Graph\GraphClientInterface;
|
use App\Services\Graph\GraphClientInterface;
|
||||||
use App\Services\Graph\MicrosoftGraphClient;
|
use App\Services\Graph\MicrosoftGraphClient;
|
||||||
use App\Services\Graph\NullGraphClient;
|
use App\Services\Graph\NullGraphClient;
|
||||||
@ -18,6 +21,9 @@
|
|||||||
use App\Services\Intune\WindowsFeatureUpdateProfileNormalizer;
|
use App\Services\Intune\WindowsFeatureUpdateProfileNormalizer;
|
||||||
use App\Services\Intune\WindowsQualityUpdateProfileNormalizer;
|
use App\Services\Intune\WindowsQualityUpdateProfileNormalizer;
|
||||||
use App\Services\Intune\WindowsUpdateRingNormalizer;
|
use App\Services\Intune\WindowsUpdateRingNormalizer;
|
||||||
|
use Filament\Events\TenantSet;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
@ -66,6 +72,35 @@ public function register(): void
|
|||||||
*/
|
*/
|
||||||
public function boot(): 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(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Providers\Filament;
|
namespace App\Providers\Filament;
|
||||||
|
|
||||||
|
use App\Filament\Pages\Tenancy\RegisterTenant;
|
||||||
|
use App\Models\Tenant;
|
||||||
use Filament\Http\Middleware\Authenticate;
|
use Filament\Http\Middleware\Authenticate;
|
||||||
use Filament\Http\Middleware\AuthenticateSession;
|
use Filament\Http\Middleware\AuthenticateSession;
|
||||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||||
@ -29,6 +31,10 @@ public function panel(Panel $panel): Panel
|
|||||||
->id('admin')
|
->id('admin')
|
||||||
->path('admin')
|
->path('admin')
|
||||||
->login()
|
->login()
|
||||||
|
->tenant(Tenant::class, slugAttribute: 'external_id')
|
||||||
|
->tenantRoutePrefix('t')
|
||||||
|
->searchableTenantMenu()
|
||||||
|
->tenantRegistration(RegisterTenant::class)
|
||||||
->colors([
|
->colors([
|
||||||
'primary' => Color::Amber,
|
'primary' => Color::Amber,
|
||||||
])
|
])
|
||||||
|
|||||||
21
app/Support/TenantRole.php
Normal file
21
app/Support/TenantRole.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
enum TenantRole: string
|
||||||
|
{
|
||||||
|
case Owner = 'owner';
|
||||||
|
case Manager = 'manager';
|
||||||
|
case Operator = 'operator';
|
||||||
|
case Readonly = 'readonly';
|
||||||
|
|
||||||
|
public function canSync(): bool
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::Owner,
|
||||||
|
self::Manager,
|
||||||
|
self::Operator => true,
|
||||||
|
self::Readonly => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,6 +26,7 @@ public function definition(): array
|
|||||||
'app_status' => 'ok',
|
'app_status' => 'ok',
|
||||||
'app_notes' => null,
|
'app_notes' => null,
|
||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
|
'environment' => 'other',
|
||||||
'is_current' => false,
|
'is_current' => false,
|
||||||
'metadata' => [],
|
'metadata' => [],
|
||||||
];
|
];
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('tenants', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('tenant_user', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('user_tenant_preferences', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -31,7 +31,11 @@
|
|||||||
<p>Admin consent wurde bestätigt.</p>
|
<p>Admin consent wurde bestätigt.</p>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<p><a href="{{ route('filament.admin.resources.tenants.view', $tenant) }}">Zurück zur Tenant-Detailseite</a></p>
|
<p>
|
||||||
|
<a href="{{ route('filament.admin.resources.tenants.view', ['tenant' => $tenant->external_id, 'record' => $tenant]) }}">
|
||||||
|
Zurück zur Tenant-Detailseite
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
# Requirements Checklist (031)
|
||||||
|
|
||||||
|
**Created**: 2026-01-04
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
|
- [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.
|
||||||
34
specs/031-tenant-portfolio-context-switch/plan.md
Normal file
34
specs/031-tenant-portfolio-context-switch/plan.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Plan: Tenant Portfolio & Context Switch (031)
|
||||||
|
|
||||||
|
**Branch**: `feat/031-tenant-portfolio-context-switch`
|
||||||
|
**Date**: 2026-01-04
|
||||||
|
**Input**: [spec.md](./spec.md)
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
1. Decide on the tenant context mechanism:
|
||||||
|
- Preferred: Filament tenancy (tenant in URL) + built-in tenant switcher.
|
||||||
|
- Fallback: session-based Current Tenant + visible banner (avoid global/DB state as “source of truth”).
|
||||||
|
2. Add data model pieces:
|
||||||
|
- tenant membership/role mapping
|
||||||
|
- tenant environment attribute
|
||||||
|
- optional user preferences (favorites + last used)
|
||||||
|
3. Implement a single TenantContext resolver (HTTP + console) and central authorization gate/policy:
|
||||||
|
- deny-by-default if no access
|
||||||
|
- keep `INTUNE_TENANT_ID` as console override for automation
|
||||||
|
4. Update tenant-scoped resources/services to use TenantContext instead of `Tenant::current()` and ensure base queries are tenant-scoped.
|
||||||
|
5. Extend `TenantResource` into a portfolio view:
|
||||||
|
- access-scoped query
|
||||||
|
- environment/health columns
|
||||||
|
- “Open” action + “Sync” action
|
||||||
|
- bulk “Sync selected”
|
||||||
|
6. Add restore guardrails:
|
||||||
|
- target tenant badge/header on restore pages
|
||||||
|
- type-to-confirm includes tenant/environment (e.g. `RESTORE PROD`)
|
||||||
|
7. Add targeted Pest tests for authorization, context switching, and bulk sync.
|
||||||
|
8. Run Pint + targeted tests; document rollout/migration notes.
|
||||||
|
|
||||||
|
## Decisions / Notes
|
||||||
|
- Avoid a global `tenants.is_current` UI context (unsafe for MSP); prefer per-user context.
|
||||||
|
- Avoid storing Current Tenant in the `users` table as the source of truth (cross-tab risk); prefer route/session context, optionally persisting “last used” separately.
|
||||||
|
- Start with user-based tenant memberships; extend to organization/group principals later if needed.
|
||||||
|
- Prefer deriving portfolio stats via relationships (`withCount`, `withMax`) initially; add denormalized summary columns only if needed for performance.
|
||||||
89
specs/031-tenant-portfolio-context-switch/spec.md
Normal file
89
specs/031-tenant-portfolio-context-switch/spec.md
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# Feature Specification: Tenant Portfolio & Context Switch (031)
|
||||||
|
|
||||||
|
**Feature Branch**: `feat/031-tenant-portfolio-context-switch`
|
||||||
|
**Created**: 2026-01-04
|
||||||
|
**Status**: Implemented (ready to merge)
|
||||||
|
**Risk**: Medium
|
||||||
|
**Priority**: P1
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Today TenantPilot behaves like a single-tenant app:
|
||||||
|
- The “current tenant” is global (`tenants.is_current` + `Tenant::current()`), not per user.
|
||||||
|
- Most tenant-scoped screens implicitly use `Tenant::current()`.
|
||||||
|
|
||||||
|
This is limiting and potentially unsafe for:
|
||||||
|
- Customers running multiple tenants (PROD/DEV/STAGING).
|
||||||
|
- MSPs managing many customer tenants.
|
||||||
|
|
||||||
|
We need a tenant-agnostic **Portfolio** view plus an explicit, always-visible **Current Tenant** context for all tenant-scoped areas (Policies, Backups, Restore Runs, etc.).
|
||||||
|
|
||||||
|
## Design Considerations (Best Practice)
|
||||||
|
- Prefer an explicit tenant context (route parameter or session) over hidden global state.
|
||||||
|
- Avoid storing Current Tenant in the `users` table as the source of truth (cross-tab risk). If persistence is needed, store **“last used”** separately and treat it as a default for new sessions.
|
||||||
|
- Keep console/automation behavior stable: `INTUNE_TENANT_ID` can remain a console override, but tenant-scoped UI must not depend on it.
|
||||||
|
|
||||||
|
## User Scenarios & Testing
|
||||||
|
|
||||||
|
### User Story 1 — Portfolio overview (P1)
|
||||||
|
As a user with access to multiple tenants, I can see a portfolio overview with health/status and key counts.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**
|
||||||
|
1. Tenants list shows only tenants the user can access.
|
||||||
|
2. Portfolio shows environment badge (PROD/DEV/STAGING/OTHER) and connection/health indicators.
|
||||||
|
3. Portfolio columns can be filtered by environment and connection status.
|
||||||
|
|
||||||
|
### User Story 2 — Safe tenant context switching (P1)
|
||||||
|
As a user, I can switch the Current Tenant via a topbar switcher or by clicking “Open” in the portfolio, and all tenant-scoped screens reflect that tenant.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**
|
||||||
|
1. Switching tenant updates the visible Current Tenant badge and redirects to a default tenant-scoped landing page (e.g. Policies).
|
||||||
|
2. Policies, Backups, Restore Runs, and Policy Versions are scoped to the selected tenant.
|
||||||
|
3. Restore flows always show the target tenant and environment prominently and require tenant-aware type-to-confirm.
|
||||||
|
|
||||||
|
### User Story 3 — Multi-tenant bulk actions (P2)
|
||||||
|
As an operator, I can select multiple tenants in the portfolio and run safe bulk actions (initially Sync).
|
||||||
|
|
||||||
|
**Acceptance Scenarios**
|
||||||
|
1. Bulk “Sync selected” dispatches a sync job per tenant (batch) and shows progress.
|
||||||
|
2. Readonly users cannot trigger bulk sync.
|
||||||
|
|
||||||
|
### User Story 4 — Authorization hardening (P1)
|
||||||
|
As a user, I cannot access tenants or tenant-scoped data I am not authorized for.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**
|
||||||
|
1. Attempting to open a tenant without access is denied (403) and does not change Current Tenant.
|
||||||
|
2. Direct URL access to tenant-scoped pages for an unauthorized tenant returns 403/404.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
- **FR-001**: Introduce a per-user Current Tenant context for all tenant-scoped screens.
|
||||||
|
- **FR-002**: Current Tenant context must be always visible in the UI (topbar) to reduce “wrong tenant” operations.
|
||||||
|
- **FR-003**: Add an “Open” action from the portfolio to set Current Tenant and redirect into the tenant-scoped area.
|
||||||
|
- **FR-004**: Portfolio view is tenant-agnostic and supports filtering, search, and safe bulk actions.
|
||||||
|
- **FR-005**: Tenant access is enforced centrally (single `canAccessTenant(...)` gate/policy used by UI + routes + services).
|
||||||
|
- **FR-006**: Restore remains single-tenant; restore actions must include explicit tenant/environment confirmations and never rely on hidden global context.
|
||||||
|
- **FR-007**: Bulk Sync is tenant-safe: per-tenant authorization, per-tenant job execution, and audit logs for each tenant sync trigger.
|
||||||
|
|
||||||
|
### UX / UI Requirements
|
||||||
|
- **UX-001**: Topbar shows “Tenant: <name>” with an environment badge (PROD/DEV/STAGING/OTHER) and is accessible from all tenant-scoped pages.
|
||||||
|
- **UX-002**: Tenant switcher is searchable (typeahead); favorites (if enabled) appear at the top.
|
||||||
|
- **UX-003**: Portfolio table includes (at minimum): Name, Tenant ID (short/copy), Environment, Connection/App status, RBAC/Health indicator, Last Sync (time), Policies count; optional Restore runs (last 30d).
|
||||||
|
- **UX-004**: Portfolio “Open” action makes the tenant context explicit and navigates into the tenant-scoped area.
|
||||||
|
- **UX-005**: Restore screens show “Target Tenant” prominently (name + environment badge) and require tenant-aware type-to-confirm (e.g. `RESTORE PROD`).
|
||||||
|
|
||||||
|
### Data Model Requirements
|
||||||
|
- **DM-001**: Introduce tenant access/membership mapping (user ↔ tenant) with a role (`owner|manager|operator|readonly`).
|
||||||
|
- **DM-002**: Add tenant environment classification (`prod|dev|staging|other`) as a first-class attribute (column or indexed JSONB).
|
||||||
|
- **DM-003 (Optional)**: Persist per-user tenant preferences (favorites + last used) without coupling it to cross-tab safety.
|
||||||
|
- **DM-004 (Optional)**: Support grouping tenants by customer (MSP use case) via a lightweight “customer label” or a dedicated Customer model (future).
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
- No multi-tenant policy detail view in one screen.
|
||||||
|
- No multi-tenant restore; restore run always targets exactly one tenant.
|
||||||
|
- No cross-tenant diff/promotion (separate feature).
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
- **SC-001**: A user can switch Current Tenant quickly and always understands which tenant they are operating on.
|
||||||
|
- **SC-002**: All tenant-scoped data is strictly filtered and authorization-safe.
|
||||||
|
- **SC-003**: Bulk Sync works across selected tenants with clear feedback and role gating.
|
||||||
33
specs/031-tenant-portfolio-context-switch/tasks.md
Normal file
33
specs/031-tenant-portfolio-context-switch/tasks.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Tasks: Tenant Portfolio & Context Switch (031)
|
||||||
|
|
||||||
|
**Branch**: `feat/031-tenant-portfolio-context-switch`
|
||||||
|
**Date**: 2026-01-04
|
||||||
|
**Input**: [spec.md](./spec.md), [plan.md](./plan.md)
|
||||||
|
|
||||||
|
## Phase 1: Setup
|
||||||
|
- [x] T001 Create spec/plan/tasks and checklist.
|
||||||
|
|
||||||
|
## Phase 2: Research & Design
|
||||||
|
- [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)
|
||||||
|
- [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
|
||||||
|
- [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
|
||||||
|
- [x] T017 Run targeted tests.
|
||||||
|
- [x] T018 Run Pint (`./vendor/bin/pint --dirty`).
|
||||||
@ -7,6 +7,7 @@
|
|||||||
use App\Models\RestoreRun;
|
use App\Models\RestoreRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -14,8 +15,12 @@
|
|||||||
|
|
||||||
test('backup sets table bulk archive creates a run and archives selected sets', function () {
|
test('backup sets table bulk archive creates a run and archives selected sets', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$tenant->makeCurrent();
|
|
||||||
$user = User::factory()->create();
|
$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) {
|
$sets = collect(range(1, 3))->map(function (int $i) use ($tenant) {
|
||||||
return BackupSet::create([
|
return BackupSet::create([
|
||||||
@ -58,8 +63,12 @@
|
|||||||
|
|
||||||
test('backup sets can be archived even when referenced by restore runs', function () {
|
test('backup sets can be archived even when referenced by restore runs', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$tenant->makeCurrent();
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$set = BackupSet::create([
|
$set = BackupSet::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
@ -87,8 +96,12 @@
|
|||||||
|
|
||||||
test('backup sets table bulk archive requires type-to-confirm for 10+ sets', function () {
|
test('backup sets table bulk archive requires type-to-confirm for 10+ sets', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$tenant->makeCurrent();
|
|
||||||
$user = User::factory()->create();
|
$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) {
|
$sets = collect(range(1, 10))->map(function (int $i) use ($tenant) {
|
||||||
return BackupSet::create([
|
return BackupSet::create([
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\RestoreRun;
|
use App\Models\RestoreRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -13,8 +14,12 @@
|
|||||||
|
|
||||||
test('bulk delete restore runs skips running items', function () {
|
test('bulk delete restore runs skips running items', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$tenant->makeCurrent();
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$backupSet = BackupSet::create([
|
$backupSet = BackupSet::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\RestoreRun;
|
use App\Models\RestoreRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -13,8 +14,12 @@
|
|||||||
|
|
||||||
test('bulk delete restore runs soft deletes selected runs', function () {
|
test('bulk delete restore runs soft deletes selected runs', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$tenant->makeCurrent();
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$backupSet = BackupSet::create([
|
$backupSet = BackupSet::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\BulkOperationRun;
|
use App\Models\BulkOperationRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -13,8 +14,12 @@
|
|||||||
|
|
||||||
test('backup sets table bulk force delete permanently deletes archived sets and their items', function () {
|
test('backup sets table bulk force delete permanently deletes archived sets and their items', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$tenant->makeCurrent();
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$set = BackupSet::create([
|
$set = BackupSet::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\PolicyVersion;
|
use App\Models\PolicyVersion;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -14,6 +15,11 @@
|
|||||||
test('policy versions table bulk force delete creates a run and skips non-archived records', function () {
|
test('policy versions table bulk force delete creates a run and skips non-archived records', function () {
|
||||||
$tenant = Tenant::factory()->create(['is_current' => true]);
|
$tenant = Tenant::factory()->create(['is_current' => true]);
|
||||||
$user = User::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]);
|
$policy = Policy::factory()->create(['tenant_id' => $tenant->id]);
|
||||||
$version = PolicyVersion::factory()->create([
|
$version = PolicyVersion::factory()->create([
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\RestoreRun;
|
use App\Models\RestoreRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -13,8 +14,12 @@
|
|||||||
|
|
||||||
test('bulk force delete restore runs permanently deletes archived runs', function () {
|
test('bulk force delete restore runs permanently deletes archived runs', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$tenant->makeCurrent();
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$backupSet = BackupSet::create([
|
$backupSet = BackupSet::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\PolicyVersion;
|
use App\Models\PolicyVersion;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -14,6 +15,11 @@
|
|||||||
test('bulk prune records skip reasons', function () {
|
test('bulk prune records skip reasons', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$user = User::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]);
|
$policyA = Policy::factory()->create(['tenant_id' => $tenant->id]);
|
||||||
$current = PolicyVersion::factory()->create([
|
$current = PolicyVersion::factory()->create([
|
||||||
@ -37,8 +43,6 @@
|
|||||||
'captured_at' => now()->subDays(10),
|
'captured_at' => now()->subDays(10),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$tenant->forceFill(['is_current' => true])->save();
|
|
||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(PolicyVersionResource\Pages\ListPolicyVersions::class)
|
->test(PolicyVersionResource\Pages\ListPolicyVersions::class)
|
||||||
->callTableBulkAction('bulk_prune_versions', collect([$current, $tooRecent]), data: [
|
->callTableBulkAction('bulk_prune_versions', collect([$current, $tooRecent]), data: [
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
use App\Models\PolicyVersion;
|
use App\Models\PolicyVersion;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -13,6 +14,11 @@
|
|||||||
test('bulk prune archives eligible policy versions', function () {
|
test('bulk prune archives eligible policy versions', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$user = User::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]);
|
$policy = Policy::factory()->create(['tenant_id' => $tenant->id]);
|
||||||
|
|
||||||
@ -30,8 +36,6 @@
|
|||||||
'captured_at' => now()->subDays(120),
|
'captured_at' => now()->subDays(120),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$tenant->forceFill(['is_current' => true])->save();
|
|
||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(PolicyVersionResource\Pages\ListPolicyVersions::class)
|
->test(PolicyVersionResource\Pages\ListPolicyVersions::class)
|
||||||
->callTableBulkAction('bulk_prune_versions', collect([$eligible, $current]), data: [
|
->callTableBulkAction('bulk_prune_versions', collect([$eligible, $current]), data: [
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\BulkOperationRun;
|
use App\Models\BulkOperationRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -13,8 +14,12 @@
|
|||||||
|
|
||||||
test('backup sets table bulk restore restores archived sets and their items', function () {
|
test('backup sets table bulk restore restores archived sets and their items', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$tenant->makeCurrent();
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$set = BackupSet::create([
|
$set = BackupSet::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\PolicyVersion;
|
use App\Models\PolicyVersion;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -14,6 +15,11 @@
|
|||||||
test('policy versions table bulk restore creates a run and restores archived records', function () {
|
test('policy versions table bulk restore creates a run and restores archived records', function () {
|
||||||
$tenant = Tenant::factory()->create(['is_current' => true]);
|
$tenant = Tenant::factory()->create(['is_current' => true]);
|
||||||
$user = User::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]);
|
$policy = Policy::factory()->create(['tenant_id' => $tenant->id]);
|
||||||
$version = PolicyVersion::factory()->create([
|
$version = PolicyVersion::factory()->create([
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\RestoreRun;
|
use App\Models\RestoreRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -13,8 +14,12 @@
|
|||||||
|
|
||||||
test('restore runs table bulk restore creates a run and restores archived records', function () {
|
test('restore runs table bulk restore creates a run and restores archived records', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$tenant->makeCurrent();
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$backupSet = BackupSet::create([
|
$backupSet = BackupSet::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
use App\Models\Policy;
|
use App\Models\Policy;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -11,8 +12,12 @@
|
|||||||
|
|
||||||
test('bulk delete requires confirmation string for large batches', function () {
|
test('bulk delete requires confirmation string for large batches', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$tenant->makeCurrent();
|
|
||||||
$user = User::factory()->create();
|
$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]);
|
$policies = Policy::factory()->count(20)->create(['tenant_id' => $tenant->id]);
|
||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
@ -27,8 +32,12 @@
|
|||||||
|
|
||||||
test('bulk delete fails with incorrect confirmation string', function () {
|
test('bulk delete fails with incorrect confirmation string', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$tenant->makeCurrent();
|
|
||||||
$user = User::factory()->create();
|
$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]);
|
$policies = Policy::factory()->count(20)->create(['tenant_id' => $tenant->id]);
|
||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
@ -43,8 +52,12 @@
|
|||||||
|
|
||||||
test('bulk delete does not require confirmation string for small batches', function () {
|
test('bulk delete does not require confirmation string for small batches', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$tenant->makeCurrent();
|
|
||||||
$user = User::factory()->create();
|
$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]);
|
$policies = Policy::factory()->count(10)->create(['tenant_id' => $tenant->id]);
|
||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
|
|||||||
@ -47,9 +47,12 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertSee('Data Protection');
|
$response->assertSee('Data Protection');
|
||||||
|
|||||||
@ -22,6 +22,9 @@
|
|||||||
|
|
||||||
$this->tenant = $tenant;
|
$this->tenant = $tenant;
|
||||||
$this->user = User::factory()->create();
|
$this->user = User::factory()->create();
|
||||||
|
$this->user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('policy detail renders normalized settings for Autopilot profiles', function () {
|
test('policy detail renders normalized settings for Autopilot profiles', function () {
|
||||||
@ -54,7 +57,7 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($this->user)
|
$response = $this->actingAs($this->user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $this->tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertSee('Settings');
|
$response->assertSee('Settings');
|
||||||
@ -95,7 +98,7 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($this->user)
|
$response = $this->actingAs($this->user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $this->tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertSee('Settings');
|
$response->assertSee('Settings');
|
||||||
@ -139,7 +142,7 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($this->user)
|
$response = $this->actingAs($this->user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $this->tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertSee('Settings');
|
$response->assertSee('Settings');
|
||||||
|
|||||||
@ -133,10 +133,13 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
);
|
);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this
|
$response = $this
|
||||||
->actingAs($user)
|
->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->assertOk();
|
||||||
$response->assertSee('Block legacy auth');
|
$response->assertSee('Block legacy auth');
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
use App\Models\RestoreRun;
|
use App\Models\RestoreRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -43,6 +44,10 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ListBackupSets::class)
|
Livewire::test(ListBackupSets::class)
|
||||||
->callTableAction('archive', $backupSet);
|
->callTableAction('archive', $backupSet);
|
||||||
@ -78,6 +83,10 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ListBackupSets::class)
|
Livewire::test(ListBackupSets::class)
|
||||||
->callTableAction('archive', $backupSet);
|
->callTableAction('archive', $backupSet);
|
||||||
@ -117,6 +126,10 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ListBackupSets::class)
|
Livewire::test(ListBackupSets::class)
|
||||||
->callTableAction('archive', $backupSet)
|
->callTableAction('archive', $backupSet)
|
||||||
@ -158,6 +171,10 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ListBackupSets::class)
|
Livewire::test(ListBackupSets::class)
|
||||||
->callTableAction('archive', $backupSet)
|
->callTableAction('archive', $backupSet)
|
||||||
@ -197,6 +214,10 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ListRestoreRuns::class)
|
Livewire::test(ListRestoreRuns::class)
|
||||||
->callTableAction('archive', $restoreRun)
|
->callTableAction('archive', $restoreRun)
|
||||||
@ -235,6 +256,10 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ListRestoreRuns::class)
|
Livewire::test(ListRestoreRuns::class)
|
||||||
->callTableAction('archive', $restoreRun)
|
->callTableAction('archive', $restoreRun)
|
||||||
@ -269,6 +294,10 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ListPolicies::class)
|
Livewire::test(ListPolicies::class)
|
||||||
->callTableAction('ignore', $policy);
|
->callTableAction('ignore', $policy);
|
||||||
@ -309,6 +338,10 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ListPolicyVersions::class)
|
Livewire::test(ListPolicyVersions::class)
|
||||||
->callTableAction('archive', $version);
|
->callTableAction('archive', $version);
|
||||||
@ -346,6 +379,10 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ListPolicyVersions::class)
|
Livewire::test(ListPolicyVersions::class)
|
||||||
->callTableAction('archive', $version)
|
->callTableAction('archive', $version)
|
||||||
@ -368,6 +405,10 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ListTenants::class)
|
Livewire::test(ListTenants::class)
|
||||||
->callTableAction('archive', $tenant);
|
->callTableAction('archive', $tenant);
|
||||||
@ -409,6 +450,11 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$active->getKey() => ['role' => 'owner'],
|
||||||
|
$archived->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($active, true);
|
||||||
|
|
||||||
$component = Livewire::test(ListTenants::class)
|
$component = Livewire::test(ListTenants::class)
|
||||||
->assertSee($active->name)
|
->assertSee($active->name)
|
||||||
@ -433,8 +479,18 @@
|
|||||||
|
|
||||||
$tenant->delete();
|
$tenant->delete();
|
||||||
|
|
||||||
|
$contextTenant = Tenant::create([
|
||||||
|
'tenant_id' => 'tenant-restore-context',
|
||||||
|
'name' => 'Restore Context Tenant',
|
||||||
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
$contextTenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($contextTenant, true);
|
||||||
|
|
||||||
Livewire::test(ListTenants::class)
|
Livewire::test(ListTenants::class)
|
||||||
->set('tableFilters.trashed.value', 1)
|
->set('tableFilters.trashed.value', 1)
|
||||||
|
|||||||
@ -41,14 +41,17 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$policyResponse = $this->actingAs($user)
|
$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');
|
$policyResponse->assertSee('This snapshot may be incomplete or malformed');
|
||||||
|
|
||||||
$versionResponse = $this->actingAs($user)
|
$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');
|
$versionResponse->assertSee('This snapshot may be incomplete or malformed');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -100,9 +100,12 @@ public function getServicePrincipalPermissions(array $options = []): GraphRespon
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$detailResponse = $this->actingAs($user)
|
$detailResponse = $this->actingAs($user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||||
|
|
||||||
$detailResponse->assertSee('@odata.type mismatch');
|
$detailResponse->assertSee('@odata.type mismatch');
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
use App\Services\Graph\AssignmentFetcher;
|
use App\Services\Graph\AssignmentFetcher;
|
||||||
use App\Services\Graph\ScopeTagResolver;
|
use App\Services\Graph\ScopeTagResolver;
|
||||||
use App\Services\Intune\PolicySnapshotService;
|
use App\Services\Intune\PolicySnapshotService;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Mockery\MockInterface;
|
use Mockery\MockInterface;
|
||||||
@ -22,6 +23,10 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$this->mock(PolicySnapshotService::class, function (MockInterface $mock) use ($policy) {
|
$this->mock(PolicySnapshotService::class, function (MockInterface $mock) use ($policy) {
|
||||||
$mock->shouldReceive('fetch')
|
$mock->shouldReceive('fetch')
|
||||||
|
|||||||
@ -7,13 +7,7 @@
|
|||||||
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
|
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
|
||||||
|
|
||||||
test('policies are listed for the active tenant', function () {
|
test('policies are listed for the active tenant', function () {
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::factory()->create();
|
||||||
'tenant_id' => 'local-tenant',
|
|
||||||
'name' => 'Tenant One',
|
|
||||||
'metadata' => [],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$tenant->makeCurrent();
|
|
||||||
|
|
||||||
Policy::create([
|
Policy::create([
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
@ -24,11 +18,7 @@
|
|||||||
'last_synced_at' => now(),
|
'last_synced_at' => now(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$otherTenant = Tenant::create([
|
$otherTenant = Tenant::factory()->create();
|
||||||
'tenant_id' => 'tenant-2',
|
|
||||||
'name' => 'Tenant Two',
|
|
||||||
'metadata' => [],
|
|
||||||
]);
|
|
||||||
|
|
||||||
Policy::create([
|
Policy::create([
|
||||||
'tenant_id' => $otherTenant->id,
|
'tenant_id' => $otherTenant->id,
|
||||||
@ -40,9 +30,13 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
$otherTenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->get(route('filament.admin.resources.policies.index'))
|
->get(route('filament.admin.resources.policies.index', filamentTenantRouteParams($tenant)))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Policy A')
|
->assertSee('Policy A')
|
||||||
->assertDontSee('Policy B');
|
->assertDontSee('Policy B');
|
||||||
|
|||||||
@ -49,9 +49,12 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertSee('Settings');
|
$response->assertSee('Settings');
|
||||||
|
|||||||
@ -48,9 +48,12 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
$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->assertOk();
|
||||||
$response->assertSee('Settings');
|
$response->assertSee('Settings');
|
||||||
|
|||||||
@ -58,9 +58,12 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version]));
|
->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertSee('Normalized settings');
|
$response->assertSee('Normalized settings');
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Graph\GroupResolver;
|
use App\Services\Graph\GroupResolver;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Mockery\MockInterface;
|
use Mockery\MockInterface;
|
||||||
@ -49,11 +50,15 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create(['email' => 'tester@example.com']);
|
$user = User::factory()->create(['email' => 'tester@example.com']);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ListPolicyVersions::class)
|
Livewire::test(ListPolicyVersions::class)
|
||||||
->callTableAction('restore_via_wizard', $version)
|
->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();
|
$backupSet = BackupSet::query()->where('metadata->source', 'policy_version')->first();
|
||||||
expect($backupSet)->not->toBeNull();
|
expect($backupSet)->not->toBeNull();
|
||||||
@ -141,7 +146,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$component = Livewire::withQueryParams([
|
$component = Livewire::withQueryParams([
|
||||||
'backup_set_id' => $backupSet->id,
|
'backup_set_id' => $backupSet->id,
|
||||||
|
|||||||
@ -47,9 +47,12 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version]));
|
->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertSee('Scope Tags');
|
$response->assertSee('Scope Tags');
|
||||||
|
|||||||
@ -45,9 +45,12 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version]));
|
->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertSee('Raw JSON');
|
$response->assertSee('Raw JSON');
|
||||||
@ -132,9 +135,12 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
$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->assertOk();
|
||||||
$response->assertSee('Enrollment notifications');
|
$response->assertSee('Enrollment notifications');
|
||||||
|
|||||||
@ -31,9 +31,12 @@
|
|||||||
$service->captureVersion($policy, ['value' => 2], 'tester');
|
$service->captureVersion($policy, ['value' => 2], 'tester');
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->get(route('filament.admin.resources.policy-versions.index'))
|
->get(route('filament.admin.resources.policy-versions.index', filamentTenantRouteParams($tenant)))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Policy A')
|
->assertSee('Policy A')
|
||||||
->assertSee((string) PolicyVersion::max('version_number'));
|
->assertSee((string) PolicyVersion::max('version_number'));
|
||||||
|
|||||||
@ -71,9 +71,12 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertSee('Settings'); // Settings tab should appear for Settings Catalog
|
$response->assertSee('Settings'); // Settings tab should appear for Settings Catalog
|
||||||
@ -130,9 +133,12 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
// TODO: Manual verification - check UI for display name "Allow Real-time Monitoring"
|
// TODO: Manual verification - check UI for display name "Allow Real-time Monitoring"
|
||||||
@ -181,9 +187,12 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
// TODO: Manual verification - check UI shows prettified fallback label
|
// TODO: Manual verification - check UI shows prettified fallback label
|
||||||
@ -225,9 +234,12 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertSee('General');
|
$response->assertSee('General');
|
||||||
@ -281,8 +293,11 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
// Policy view should render successfully with Settings Catalog data
|
// Policy view should render successfully with Settings Catalog data
|
||||||
@ -356,8 +371,11 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
// Value formatting verified by manual UI inspection
|
// Value formatting verified by manual UI inspection
|
||||||
@ -419,8 +437,11 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
// Search functionality is Alpine.js client-side, requires browser testing
|
// Search functionality is Alpine.js client-side, requires browser testing
|
||||||
@ -465,8 +486,11 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
// Page renders without crash - actual fallback display requires UI verification
|
// Page renders without crash - actual fallback display requires UI verification
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
use App\Models\Policy;
|
use App\Models\Policy;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -99,6 +100,10 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(CreateRestoreRun::class)
|
Livewire::test(CreateRestoreRun::class)
|
||||||
->fillForm([
|
->fillForm([
|
||||||
|
|||||||
@ -12,9 +12,12 @@
|
|||||||
$originalEnv = getenv('INTUNE_TENANT_ID');
|
$originalEnv = getenv('INTUNE_TENANT_ID');
|
||||||
putenv('INTUNE_TENANT_ID=');
|
putenv('INTUNE_TENANT_ID=');
|
||||||
|
|
||||||
$this->actingAs(User::factory()->create());
|
|
||||||
|
|
||||||
$tenant = Tenant::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);
|
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
||||||
$tenant->makeCurrent();
|
$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();
|
->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();
|
->assertSuccessful();
|
||||||
|
|
||||||
$originalEnv !== false
|
$originalEnv !== false
|
||||||
@ -71,14 +74,17 @@
|
|||||||
$originalEnv = getenv('INTUNE_TENANT_ID');
|
$originalEnv = getenv('INTUNE_TENANT_ID');
|
||||||
putenv('INTUNE_TENANT_ID=');
|
putenv('INTUNE_TENANT_ID=');
|
||||||
|
|
||||||
$this->actingAs(User::factory()->create());
|
|
||||||
|
|
||||||
config([
|
config([
|
||||||
'tenantpilot.display.show_script_content' => true,
|
'tenantpilot.display.show_script_content' => true,
|
||||||
'tenantpilot.display.max_script_content_chars' => 5000,
|
'tenantpilot.display.max_script_content_chars' => 5000,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$tenant = Tenant::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);
|
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
||||||
$tenant->makeCurrent();
|
$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')
|
$this->get($url.'?tab=diff')
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
@ -136,14 +142,17 @@
|
|||||||
$originalEnv = getenv('INTUNE_TENANT_ID');
|
$originalEnv = getenv('INTUNE_TENANT_ID');
|
||||||
putenv('INTUNE_TENANT_ID=');
|
putenv('INTUNE_TENANT_ID=');
|
||||||
|
|
||||||
$this->actingAs(User::factory()->create());
|
|
||||||
|
|
||||||
config([
|
config([
|
||||||
'tenantpilot.display.show_script_content' => true,
|
'tenantpilot.display.show_script_content' => true,
|
||||||
'tenantpilot.display.max_script_content_chars' => 5000,
|
'tenantpilot.display.max_script_content_chars' => 5000,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$tenant = Tenant::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);
|
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
|
||||||
$tenant->makeCurrent();
|
$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')
|
$this->get($url.'?tab=diff')
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
|
|||||||
@ -105,10 +105,13 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
);
|
);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this
|
$response = $this
|
||||||
->actingAs($user)
|
->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->assertOk();
|
||||||
$response->assertSee('Setting A');
|
$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');
|
$versions->captureFromGraph($tenant, $policy, createdBy: 'tester@example.com');
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this
|
$response = $this
|
||||||
->actingAs($user)
|
->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->assertOk();
|
||||||
$response->assertSee('Setting A');
|
$response->assertSee('Setting A');
|
||||||
|
|||||||
@ -90,9 +90,12 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$policyResponse = $this->actingAs($user)
|
$policyResponse = $this->actingAs($user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||||
|
|
||||||
$policyResponse->assertOk();
|
$policyResponse->assertOk();
|
||||||
$policyResponse->assertSee('Definition');
|
$policyResponse->assertSee('Definition');
|
||||||
@ -104,7 +107,7 @@
|
|||||||
$policyResponse->assertSee('tp-policy-general-card');
|
$policyResponse->assertSee('tp-policy-general-card');
|
||||||
|
|
||||||
$versionResponse = $this->actingAs($user)
|
$versionResponse = $this->actingAs($user)
|
||||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version]));
|
->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant));
|
||||||
|
|
||||||
$versionResponse->assertOk();
|
$versionResponse->assertOk();
|
||||||
$versionResponse->assertSee('Normalized settings');
|
$versionResponse->assertSee('Normalized settings');
|
||||||
|
|||||||
@ -111,10 +111,13 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this
|
$response = $this
|
||||||
->actingAs($user)
|
->actingAs($user)
|
||||||
->get(route('filament.admin.resources.policies.index'));
|
->get(route('filament.admin.resources.policies.index', filamentTenantRouteParams($tenant)));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertSee('Settings Catalog Policy');
|
$response->assertSee('Settings Catalog Policy');
|
||||||
|
|||||||
@ -147,6 +147,9 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
$service = app(RestoreService::class);
|
$service = app(RestoreService::class);
|
||||||
@ -185,7 +188,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
|
|
||||||
$run->update(['results' => $results]);
|
$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->assertOk();
|
||||||
$response->assertSee('Graph bulk apply failed');
|
$response->assertSee('Graph bulk apply failed');
|
||||||
$response->assertSee('Setting missing');
|
$response->assertSee('Setting missing');
|
||||||
|
|||||||
@ -162,6 +162,9 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
$service = app(RestoreService::class);
|
$service = app(RestoreService::class);
|
||||||
@ -201,7 +204,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
->toBe('#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance');
|
->toBe('#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance');
|
||||||
|
|
||||||
$response = $this
|
$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->assertOk();
|
||||||
$response->assertSee('settings are read-only');
|
$response->assertSee('settings are read-only');
|
||||||
|
|||||||
@ -56,9 +56,12 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$policyResponse = $this->actingAs($user)
|
$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->assertOk();
|
||||||
$policyResponse->assertSee('fi-width-full');
|
$policyResponse->assertSee('fi-width-full');
|
||||||
@ -69,7 +72,7 @@
|
|||||||
$policyResponse->assertSee('fi-ta-table');
|
$policyResponse->assertSee('fi-ta-table');
|
||||||
|
|
||||||
$versionResponse = $this->actingAs($user)
|
$versionResponse = $this->actingAs($user)
|
||||||
->get(PolicyVersionResource::getUrl('view', ['record' => $version]));
|
->get(PolicyVersionResource::getUrl('view', ['record' => $version], tenant: $tenant));
|
||||||
|
|
||||||
$versionResponse->assertOk();
|
$versionResponse->assertOk();
|
||||||
$versionResponse->assertSee('fi-width-full');
|
$versionResponse->assertSee('fi-width-full');
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
use App\Filament\Resources\TenantResource\Pages\ListTenants;
|
use App\Filament\Resources\TenantResource\Pages\ListTenants;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -26,6 +27,11 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$first->getKey() => ['role' => 'owner'],
|
||||||
|
$second->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($first, true);
|
||||||
|
|
||||||
Livewire::test(ListTenants::class)
|
Livewire::test(ListTenants::class)
|
||||||
->callTableAction('makeCurrent', $second);
|
->callTableAction('makeCurrent', $second);
|
||||||
|
|||||||
104
tests/Feature/Filament/TenantPortfolioContextSwitchTest.php
Normal file
104
tests/Feature/Filament/TenantPortfolioContextSwitchTest.php
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Filament\Resources\TenantResource\Pages\ListTenants;
|
||||||
|
use App\Jobs\BulkTenantSyncJob;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Models\User;
|
||||||
|
use Filament\Events\TenantSet;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Bus;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
test('tenant-scoped pages return 404 for unauthorized tenant', function () {
|
||||||
|
[$user, $authorizedTenant] = createUserWithTenant();
|
||||||
|
$unauthorizedTenant = Tenant::factory()->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(BulkTenantSyncJob::class, 1);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('bulk_operation_runs', [
|
||||||
|
'tenant_id' => $tenantA->id,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'resource' => 'tenant',
|
||||||
|
'action' => 'sync',
|
||||||
|
'total_items' => 2,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
});
|
||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Graph\GraphClientInterface;
|
use App\Services\Graph\GraphClientInterface;
|
||||||
use App\Services\Graph\GraphResponse;
|
use App\Services\Graph\GraphResponse;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
@ -32,6 +33,10 @@ function tenantWithApp(): Tenant
|
|||||||
$tenant = tenantWithApp();
|
$tenant = tenantWithApp();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||||
->mountAction('setup_rbac')
|
->mountAction('setup_rbac')
|
||||||
@ -51,6 +56,10 @@ function tenantWithApp(): Tenant
|
|||||||
$tenant = tenantWithApp();
|
$tenant = tenantWithApp();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
|
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
|
||||||
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
|
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
|
||||||
@ -155,6 +164,10 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
$tenant = tenantWithApp();
|
$tenant = tenantWithApp();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
|
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
|
||||||
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
|
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
|
||||||
@ -265,6 +278,10 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
$tenant = tenantWithApp();
|
$tenant = tenantWithApp();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
|
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
|
||||||
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
|
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
|
||||||
@ -365,6 +382,10 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
$tenant = tenantWithApp();
|
$tenant = tenantWithApp();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||||
->mountAction('setup_rbac')
|
->mountAction('setup_rbac')
|
||||||
@ -380,6 +401,10 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
$tenant = tenantWithApp();
|
$tenant = tenantWithApp();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||||
->mountAction('setup_rbac')
|
->mountAction('setup_rbac')
|
||||||
@ -394,6 +419,10 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
$tenant = tenantWithApp();
|
$tenant = tenantWithApp();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
|
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
|
||||||
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
|
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
|
||||||
@ -505,6 +534,10 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
$tenant = tenantWithApp();
|
$tenant = tenantWithApp();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
|
$cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null);
|
||||||
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
|
Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5));
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Graph\GraphClientInterface;
|
use App\Services\Graph\GraphClientInterface;
|
||||||
use App\Services\Graph\GraphResponse;
|
use App\Services\Graph\GraphResponse;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -54,9 +55,19 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$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)
|
Livewire::test(CreateTenant::class)
|
||||||
->fillForm([
|
->fillForm([
|
||||||
'name' => 'Contoso',
|
'name' => 'Contoso',
|
||||||
|
'environment' => 'other',
|
||||||
'tenant_id' => 'tenant-guid',
|
'tenant_id' => 'tenant-guid',
|
||||||
'domain' => 'contoso.com',
|
'domain' => 'contoso.com',
|
||||||
'app_client_id' => 'client-123',
|
'app_client_id' => 'client-123',
|
||||||
@ -65,7 +76,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
->call('create')
|
->call('create')
|
||||||
->assertHasNoFormErrors();
|
->assertHasNoFormErrors();
|
||||||
|
|
||||||
$tenant = Tenant::first();
|
$tenant = Tenant::query()->where('tenant_id', 'tenant-guid')->first();
|
||||||
expect($tenant)->not->toBeNull();
|
expect($tenant)->not->toBeNull();
|
||||||
|
|
||||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
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',
|
'tenant_id' => 'tenant-error',
|
||||||
'name' => 'Error Tenant',
|
'name' => 'Error Tenant',
|
||||||
]);
|
]);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||||
->callAction('verify');
|
->callAction('verify');
|
||||||
@ -157,6 +173,9 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
'tenant_id' => 'tenant-ui',
|
'tenant_id' => 'tenant-ui',
|
||||||
'name' => 'UI Tenant',
|
'name' => 'UI Tenant',
|
||||||
]);
|
]);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
config(['intune_permissions.granted_stub' => []]);
|
config(['intune_permissions.granted_stub' => []]);
|
||||||
|
|
||||||
@ -169,7 +188,7 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
'status' => 'ok',
|
'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->assertOk();
|
||||||
$response->assertSee('Actions');
|
$response->assertSee('Actions');
|
||||||
@ -182,13 +201,17 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'tenant_id' => 'tenant-ui-list',
|
'tenant_id' => 'tenant-ui-list',
|
||||||
'name' => 'UI Tenant List',
|
'name' => 'UI Tenant List',
|
||||||
'app_client_id' => 'client-123',
|
'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->assertOk();
|
||||||
$response->assertSee('Open in Entra');
|
$response->assertSee('Open in Entra');
|
||||||
@ -202,6 +225,11 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
'tenant_id' => 'tenant-ui-deactivate',
|
'tenant_id' => 'tenant-ui-deactivate',
|
||||||
'name' => 'UI Tenant Deactivate',
|
'name' => 'UI Tenant Deactivate',
|
||||||
]);
|
]);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||||
->callAction('archive');
|
->callAction('archive');
|
||||||
|
|||||||
@ -48,9 +48,12 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->get(PolicyResource::getUrl('view', ['record' => $policy]));
|
->get(PolicyResource::getUrl('view', ['record' => $policy], tenant: $tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
|
|
||||||
|
|||||||
@ -10,11 +10,13 @@
|
|||||||
unset($_ENV['INTUNE_TENANT_ID'], $_SERVER['INTUNE_TENANT_ID']);
|
unset($_ENV['INTUNE_TENANT_ID'], $_SERVER['INTUNE_TENANT_ID']);
|
||||||
|
|
||||||
$this->tenant = Tenant::factory()->create();
|
$this->tenant = Tenant::factory()->create();
|
||||||
$this->tenant->makeCurrent();
|
|
||||||
$this->policy = Policy::factory()->create([
|
$this->policy = Policy::factory()->create([
|
||||||
'tenant_id' => $this->tenant->id,
|
'tenant_id' => $this->tenant->id,
|
||||||
]);
|
]);
|
||||||
$this->user = User::factory()->create();
|
$this->user = User::factory()->create();
|
||||||
|
$this->user->tenants()->syncWithoutDetaching([
|
||||||
|
$this->tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays policy version page', function () {
|
it('displays policy version page', function () {
|
||||||
@ -26,7 +28,10 @@
|
|||||||
|
|
||||||
$this->actingAs($this->user);
|
$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->assertOk();
|
||||||
});
|
});
|
||||||
@ -67,7 +72,10 @@
|
|||||||
|
|
||||||
$this->actingAs($this->user);
|
$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->assertOk();
|
||||||
$response->assertSeeLivewire('policy-version-assignments-widget');
|
$response->assertSeeLivewire('policy-version-assignments-widget');
|
||||||
@ -87,7 +95,10 @@
|
|||||||
|
|
||||||
$this->actingAs($this->user);
|
$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->assertOk();
|
||||||
$response->assertSee('Assignments were not captured for this version');
|
$response->assertSee('Assignments were not captured for this version');
|
||||||
@ -107,7 +118,10 @@
|
|||||||
|
|
||||||
$this->actingAs($this->user);
|
$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->assertOk();
|
||||||
$response->assertSee('No assignments found for this version');
|
$response->assertSee('No assignments found for this version');
|
||||||
@ -137,7 +151,10 @@
|
|||||||
|
|
||||||
$this->actingAs($this->user);
|
$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->assertOk();
|
||||||
$response->assertSee('Compliance notifications');
|
$response->assertSee('Compliance notifications');
|
||||||
@ -169,7 +186,10 @@
|
|||||||
|
|
||||||
$this->actingAs($this->user);
|
$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->assertOk();
|
||||||
$response->assertSee('Compliance notifications');
|
$response->assertSee('Compliance notifications');
|
||||||
@ -192,7 +212,10 @@
|
|||||||
|
|
||||||
$this->actingAs($this->user);
|
$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->assertOk();
|
||||||
$response->assertSee('Password & Access');
|
$response->assertSee('Password & Access');
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Graph\GroupResolver;
|
use App\Services\Graph\GroupResolver;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Mockery\MockInterface;
|
use Mockery\MockInterface;
|
||||||
@ -76,6 +77,11 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$component = Livewire::test(CreateRestoreRun::class)
|
$component = Livewire::test(CreateRestoreRun::class)
|
||||||
->fillForm([
|
->fillForm([
|
||||||
@ -157,6 +163,11 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(CreateRestoreRun::class)
|
Livewire::test(CreateRestoreRun::class)
|
||||||
->fillForm([
|
->fillForm([
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
use App\Models\RestoreRun;
|
use App\Models\RestoreRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -86,6 +87,11 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$component = Livewire::test(CreateRestoreRun::class)
|
$component = Livewire::test(CreateRestoreRun::class)
|
||||||
->fillForm([
|
->fillForm([
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Graph\GroupResolver;
|
use App\Services\Graph\GroupResolver;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Mockery\MockInterface;
|
use Mockery\MockInterface;
|
||||||
@ -77,6 +78,11 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$component = Livewire::test(CreateRestoreRun::class)
|
$component = Livewire::test(CreateRestoreRun::class)
|
||||||
->fillForm([
|
->fillForm([
|
||||||
@ -188,6 +194,11 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$component = Livewire::test(CreateRestoreRun::class)
|
$component = Livewire::test(CreateRestoreRun::class)
|
||||||
->fillForm([
|
->fillForm([
|
||||||
@ -270,6 +281,11 @@
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
$component = Livewire::test(CreateRestoreRun::class)
|
$component = Livewire::test(CreateRestoreRun::class)
|
||||||
->fillForm([
|
->fillForm([
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
use App\Models\RestoreRun;
|
use App\Models\RestoreRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -28,6 +29,11 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(ListRestoreRuns::class)
|
->test(ListRestoreRuns::class)
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\RestoreRun;
|
use App\Models\RestoreRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -13,7 +14,6 @@
|
|||||||
|
|
||||||
test('rerun action creates a new restore run with the same selections', function () {
|
test('rerun action creates a new restore run with the same selections', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$tenant->makeCurrent();
|
|
||||||
|
|
||||||
$backupSet = BackupSet::factory()->for($tenant)->create([
|
$backupSet = BackupSet::factory()->for($tenant)->create([
|
||||||
'status' => 'completed',
|
'status' => 'completed',
|
||||||
@ -47,6 +47,11 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::factory()->create(['email' => 'tester@example.com']);
|
$user = User::factory()->create(['email' => 'tester@example.com']);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(ListRestoreRuns::class)
|
->test(ListRestoreRuns::class)
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Support\RestoreRunStatus;
|
use App\Support\RestoreRunStatus;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Bus;
|
use Illuminate\Support\Facades\Bus;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
@ -62,6 +63,11 @@
|
|||||||
'name' => 'Tester',
|
'name' => 'Tester',
|
||||||
]);
|
]);
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(CreateRestoreRun::class)
|
Livewire::test(CreateRestoreRun::class)
|
||||||
->fillForm([
|
->fillForm([
|
||||||
@ -130,6 +136,11 @@
|
|||||||
'name' => 'Executor',
|
'name' => 'Executor',
|
||||||
]);
|
]);
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(CreateRestoreRun::class)
|
Livewire::test(CreateRestoreRun::class)
|
||||||
->fillForm([
|
->fillForm([
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\RestoreRun;
|
use App\Models\RestoreRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
@ -50,6 +51,11 @@
|
|||||||
'name' => 'Tester',
|
'name' => 'Tester',
|
||||||
]);
|
]);
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$tenant->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
Livewire::test(CreateRestoreRun::class)
|
Livewire::test(CreateRestoreRun::class)
|
||||||
->fillForm([
|
->fillForm([
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -60,3 +62,26 @@ function something()
|
|||||||
{
|
{
|
||||||
// ..
|
// ..
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{0: User, 1: Tenant}
|
||||||
|
*/
|
||||||
|
function createUserWithTenant(?Tenant $tenant = null, ?User $user = null, string $role = 'owner'): array
|
||||||
|
{
|
||||||
|
$user ??= User::factory()->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];
|
||||||
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
use App\Models\Policy;
|
use App\Models\Policy;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
@ -12,8 +13,12 @@
|
|||||||
|
|
||||||
test('policies bulk actions are available for authenticated users', function () {
|
test('policies bulk actions are available for authenticated users', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
$tenant->makeCurrent();
|
|
||||||
$user = User::factory()->create();
|
$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]);
|
$policies = Policy::factory()->count(2)->create(['tenant_id' => $tenant->id]);
|
||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user