## Summary - Removes the legacy Tenant CRUD create page (`/admin/tenants/create`) so tenant creation is handled exclusively via the onboarding wizard. - Updates tenant selection flows and pages to prevent Livewire polling/notification-related 404s on workspace-scoped routes. - Aligns empty-state UX with enterprise patterns (avoid duplicate CTAs). ## Key changes - Tenant creation - Removed `CreateTenant` page + route from `TenantResource`. - `TenantResource::canCreate()` now returns `false` (CRUD creation disabled). - Tenants list now surfaces an **Add tenant** action that links to onboarding (`admin.onboarding`). - Onboarding wizard - Removed redundant legacy step-cards from the blade view (Wizard schema is the source of truth). - Disabled topbar on the onboarding page to avoid lazy-loaded notifications. - Choose tenant - Enterprise UI redesign + workspace context. - Uses Livewire `selectTenant()` instead of a form POST. - Disabled topbar and gated BODY_END hook to avoid background polling. - Baseline profiles - Hide header create action when table is empty to avoid duplicate CTAs. ## Tests - `vendor/bin/sail artisan test --compact --filter='Onboarding|ManagedTenantOnboarding'` - `vendor/bin/sail artisan test --compact --filter='ManagedTenantsLivewireUpdate'` - `vendor/bin/sail artisan test --compact --filter='TenantSetup|TenantResourceAuth|TenantAdminAuth|ListTenants'` - `vendor/bin/sail artisan test --compact --filter='BaselineProfile'` - `vendor/bin/sail artisan test --compact --filter='ChooseTenant|TenantMake|TenantScoping|AdminTenantScoped|AdminHomeRedirect|WorkspaceContext'` ## Notes - Filament v5 / Livewire v4 compatible. - No new assets introduced; no deploy pipeline changes required. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #131
202 lines
6.7 KiB
PHP
202 lines
6.7 KiB
PHP
<?php
|
|
|
|
use App\Filament\Resources\TenantResource\Pages\ViewTenant;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ProviderCredential;
|
|
use App\Models\Tenant;
|
|
use App\Models\TenantPermission;
|
|
use App\Models\User;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\Providers\ProviderReasonCodes;
|
|
use App\Support\Verification\VerificationReportSchema;
|
|
use Filament\Facades\Filament;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
test('verification start enqueues an operation run for a tenant', function () {
|
|
Queue::fake();
|
|
bindFailHardGraphClient();
|
|
|
|
$user = User::factory()->create();
|
|
$this->actingAs($user);
|
|
|
|
$tenant = Tenant::factory()->create([
|
|
'tenant_id' => 'tenant-guid',
|
|
'name' => 'Contoso',
|
|
'environment' => 'other',
|
|
'domain' => 'contoso.com',
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant($tenant, $user, role: 'owner', ensureDefaultMicrosoftProviderConnection: false);
|
|
$this->actingAs($user);
|
|
Filament::setTenant($tenant, true);
|
|
|
|
$connection = ProviderConnection::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'is_default' => true,
|
|
'status' => 'enabled',
|
|
]);
|
|
|
|
ProviderCredential::factory()->create([
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'type' => 'client_secret',
|
|
'payload' => [
|
|
'client_id' => 'client-id',
|
|
'client_secret' => 'client-secret',
|
|
],
|
|
]);
|
|
|
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
|
->callAction('verify');
|
|
|
|
$run = OperationRun::query()
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->where('type', 'provider.connection.check')
|
|
->latest('id')
|
|
->first();
|
|
|
|
expect($run)->not->toBeNull()
|
|
->and($run?->status)->toBe('queued')
|
|
->and($run?->outcome)->toBe('pending')
|
|
->and($run?->context['provider_connection_id'] ?? null)->toBe((int) $connection->getKey());
|
|
|
|
$notificationActionUrls = collect(session('filament.notifications', []))
|
|
->flatMap(static fn (array $notification): array => is_array($notification['actions'] ?? null)
|
|
? $notification['actions']
|
|
: [])
|
|
->pluck('url')
|
|
->filter(static fn (mixed $url): bool => is_string($url) && trim($url) !== '')
|
|
->values()
|
|
->all();
|
|
|
|
expect($notificationActionUrls)->toContain(OperationRunLinks::tenantlessView($run));
|
|
|
|
Queue::assertPushed(\App\Jobs\ProviderConnectionHealthCheckJob::class, 1);
|
|
|
|
$this->assertDatabaseMissing('audit_logs', [
|
|
'tenant_id' => $tenant->id,
|
|
'action' => 'tenant.config.verified',
|
|
]);
|
|
});
|
|
|
|
test('verify configuration creates a blocked run when default connection credentials are missing', function () {
|
|
Queue::fake();
|
|
|
|
$user = User::factory()->create();
|
|
|
|
$tenant = Tenant::factory()->create([
|
|
'tenant_id' => 'tenant-error',
|
|
'name' => 'Error Tenant',
|
|
'status' => 'active',
|
|
]);
|
|
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, user: $user, role: 'owner', ensureDefaultMicrosoftProviderConnection: false);
|
|
$this->actingAs($user);
|
|
Filament::setTenant($tenant, true);
|
|
|
|
$connection = ProviderConnection::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'is_default' => true,
|
|
'status' => 'connected',
|
|
]);
|
|
|
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
|
->callAction('verify');
|
|
|
|
$run = OperationRun::query()
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->where('type', 'provider.connection.check')
|
|
->latest('id')
|
|
->first();
|
|
|
|
expect($run)->not->toBeNull()
|
|
->and($run?->outcome)->toBe('blocked')
|
|
->and($run?->context['provider_connection_id'] ?? null)->toBe((int) $connection->getKey())
|
|
->and($run?->context['reason_code'] ?? null)->toBe(ProviderReasonCodes::ProviderCredentialMissing);
|
|
|
|
expect(VerificationReportSchema::isValidReport($run?->context['verification_report'] ?? []))->toBeTrue();
|
|
|
|
Queue::assertNothingPushed();
|
|
});
|
|
|
|
test('tenant detail shows required permissions with statuses', function () {
|
|
$user = User::factory()->create();
|
|
$this->actingAs($user);
|
|
|
|
$tenant = Tenant::create([
|
|
'tenant_id' => 'tenant-ui',
|
|
'name' => 'UI Tenant',
|
|
]);
|
|
[$user, $tenant] = createUserWithTenant($tenant, $user, role: 'owner');
|
|
$this->actingAs($user);
|
|
|
|
config(['intune_permissions.granted_stub' => []]);
|
|
|
|
$permissions = config('intune_permissions.permissions', []);
|
|
$firstKey = $permissions[0]['key'] ?? 'DeviceManagementConfiguration.ReadWrite.All';
|
|
|
|
TenantPermission::create([
|
|
'tenant_id' => $tenant->id,
|
|
'permission_key' => $firstKey,
|
|
'status' => 'ok',
|
|
]);
|
|
|
|
$response = $this->get(route('filament.admin.resources.tenants.view', array_merge(filamentTenantRouteParams($tenant), ['record' => $tenant])));
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('Actions');
|
|
$response->assertSee($firstKey);
|
|
$response->assertSee('ok');
|
|
$response->assertSee('Missing');
|
|
});
|
|
|
|
test('tenant list shows Open in Entra action', function () {
|
|
$user = User::factory()->create();
|
|
$this->actingAs($user);
|
|
|
|
$tenant = Tenant::create([
|
|
'tenant_id' => 'tenant-ui-list',
|
|
'name' => 'UI Tenant List',
|
|
'app_client_id' => 'client-123',
|
|
]);
|
|
|
|
[$user, $tenant] = createUserWithTenant($tenant, $user, role: 'owner');
|
|
$this->actingAs($user);
|
|
|
|
$response = $this->get(route('filament.admin.resources.tenants.index', filamentTenantRouteParams($tenant)));
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('Open in Entra');
|
|
});
|
|
|
|
test('tenant can be deactivated from the tenant detail action menu', function () {
|
|
$user = User::factory()->create();
|
|
$this->actingAs($user);
|
|
|
|
$tenant = Tenant::create([
|
|
'tenant_id' => 'tenant-ui-deactivate',
|
|
'name' => 'UI Tenant Deactivate',
|
|
]);
|
|
|
|
[$user, $tenant] = createUserWithTenant($tenant, $user, role: 'owner');
|
|
$this->actingAs($user);
|
|
|
|
Filament::setTenant($tenant, true);
|
|
|
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
|
->mountAction('archive')
|
|
->callMountedAction()
|
|
->assertHasNoActionErrors();
|
|
|
|
$this->assertSoftDeleted('tenants', ['id' => $tenant->id]);
|
|
});
|