create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $entraTenantId = '77777777-7777-7777-7777-777777777777'; $component = Livewire::actingAs($user)->test(ManagedTenantOnboardingWizard::class); $component->call('identifyManagedTenant', [ 'entra_tenant_id' => $entraTenantId, 'environment' => 'prod', 'name' => 'Acme', ]); $component->call('createProviderConnection', [ 'display_name' => 'Acme connection', 'client_id' => '00000000-0000-0000-0000-000000000000', 'client_secret' => 'super-secret', 'is_default' => true, ]); $tenant = Tenant::query()->where('tenant_id', $entraTenantId)->firstOrFail(); ProviderConnection::query() ->where('tenant_id', (int) $tenant->getKey()) ->update([ 'status' => 'connected', 'consent_status' => 'granted', 'last_error_reason_code' => null, 'last_error_message' => null, ]); $component->call('startVerification'); $component->call('startVerification'); expect(OperationRun::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('type', 'provider.connection.check') ->count())->toBe(1); $runId = (int) OperationRun::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('type', 'provider.connection.check') ->value('id'); $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($runId)); $session = TenantOnboardingSession::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('entra_tenant_id', $entraTenantId) ->whereNull('completed_at') ->firstOrFail(); expect($session->state['verification_operation_run_id'] ?? null)->toBe($runId); expect(AuditLog::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('action', 'managed_tenant_onboarding.verification_start') ->exists())->toBeTrue() ->and(AuditLog::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('action', 'managed_tenant_onboarding.verification_persisted') ->exists())->toBeTrue(); }); it('stores a blocked verification report and canonical link when onboarding verification cannot proceed', function (): void { Queue::fake(); $workspace = Workspace::factory()->create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $entraTenantId = '72727272-7272-7272-7272-727272727272'; $component = Livewire::actingAs($user)->test(ManagedTenantOnboardingWizard::class); $component->call('identifyManagedTenant', [ 'entra_tenant_id' => $entraTenantId, 'environment' => 'prod', 'name' => 'Blocked Tenant', ]); $tenant = Tenant::query()->where('tenant_id', $entraTenantId)->firstOrFail(); $connection = ProviderConnection::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => $entraTenantId, 'display_name' => 'Blocked connection', 'is_default' => true, 'status' => 'connected', ]); $component->call('selectProviderConnection', (int) $connection->getKey()); $component->call('startVerification'); $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'); $report = $run?->context['verification_report'] ?? null; expect($report)->toBeArray(); expect(VerificationReportSchema::isValidReport($report))->toBeTrue(); $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((int) $run?->getKey())); Queue::assertNothingPushed(); }); it('does not start verification or write audit history when the workspace changes after mount', function (): void { Queue::fake(); $workspaceA = Workspace::factory()->create(); $workspaceB = Workspace::factory()->create(); $user = User::factory()->create(); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspaceA->getKey(), 'status' => Tenant::STATUS_ONBOARDING, ]); createUserWithTenant( tenant: $tenant, user: $user, role: 'owner', workspaceRole: 'owner', ensureDefaultMicrosoftProviderConnection: false, ); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspaceB->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); $connection = ProviderConnection::factory()->platform()->consentGranted()->create([ 'workspace_id' => (int) $workspaceA->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => (string) $tenant->tenant_id, 'display_name' => 'Verified connection', 'is_default' => true, 'status' => 'connected', ]); $draft = createOnboardingDraft([ 'workspace' => $workspaceA, 'tenant' => $tenant, 'started_by' => $user, 'updated_by' => $user, 'current_step' => 'connection', 'state' => [ 'entra_tenant_id' => (string) $tenant->tenant_id, 'tenant_name' => (string) $tenant->name, 'provider_connection_id' => (int) $connection->getKey(), ], ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspaceA->getKey()); $component = Livewire::actingAs($user)->test(ManagedTenantOnboardingWizard::class, [ 'onboardingDraft' => (int) $draft->getKey(), ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspaceB->getKey()); $component->call('startVerification')->assertNotFound(); expect(OperationRun::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('type', 'provider.connection.check') ->exists())->toBeFalse() ->and(AuditLog::query() ->where('workspace_id', (int) $workspaceA->getKey()) ->whereIn('action', [ 'managed_tenant_onboarding.verification_start', 'managed_tenant_onboarding.verification_persisted', ]) ->exists())->toBeFalse(); }); it('renders stored verification findings in the wizard report section', function (): void { $workspace = Workspace::factory()->create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $entraTenantId = '99999999-9999-9999-9999-999999999999'; $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => $entraTenantId, 'status' => 'onboarding', ]); $user->tenants()->syncWithoutDetaching([ $tenant->getKey() => ['role' => 'owner'], ]); $connection = ProviderConnection::factory()->platform()->consentGranted()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => $entraTenantId, 'display_name' => 'Contoso platform connection', 'is_default' => true, 'status' => 'connected', ]); $run = OperationRun::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'type' => 'provider.connection.check', 'status' => 'completed', 'outcome' => 'succeeded', 'context' => [ 'provider_connection_id' => (int) $connection->getKey(), 'target_scope' => [ 'entra_tenant_id' => $entraTenantId, 'entra_tenant_name' => 'Contoso', ], 'verification_report' => VerificationReportWriter::build('provider.connection.check', [ [ 'key' => 'permission_check', 'title' => 'Graph permissions', 'status' => 'fail', 'severity' => 'high', 'blocking' => true, 'reason_code' => 'permission_denied', 'message' => 'Missing required Graph permissions.', 'evidence' => [], 'next_steps' => [], ], ]), ], ]); TenantOnboardingSession::query()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'entra_tenant_id' => $entraTenantId, 'current_step' => 'verify', 'state' => [ 'provider_connection_id' => (int) $connection->getKey(), 'verification_operation_run_id' => (int) $run->getKey(), ], 'started_by_user_id' => (int) $user->getKey(), 'updated_by_user_id' => (int) $user->getKey(), ]); $this->actingAs($user) ->followingRedirects() ->get('/admin/onboarding') ->assertSuccessful() ->assertSee('Status: Blocked') ->assertSee('Missing required Graph permissions.') ->assertSee('Graph permissions') ->assertSee($entraTenantId); }); it('clears the stored verification run id when switching provider connections', function (): void { Queue::fake(); $workspace = Workspace::factory()->create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $entraTenantId = '12121212-1212-1212-1212-121212121212'; $component = Livewire::actingAs($user)->test(ManagedTenantOnboardingWizard::class); $component->call('identifyManagedTenant', [ 'entra_tenant_id' => $entraTenantId, 'environment' => 'prod', 'name' => 'Acme', ]); $component->call('createProviderConnection', [ 'display_name' => 'Acme connection', 'client_id' => '00000000-0000-0000-0000-000000000000', 'client_secret' => 'super-secret', 'is_default' => true, ]); $tenant = Tenant::query()->where('tenant_id', $entraTenantId)->firstOrFail(); $otherConnection = ProviderConnection::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'dummy', 'entra_tenant_id' => $entraTenantId, 'display_name' => 'Dummy connection', 'is_default' => false, ]); $component->call('startVerification'); $session = TenantOnboardingSession::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('entra_tenant_id', $entraTenantId) ->whereNull('completed_at') ->firstOrFail(); expect($session->state['verification_operation_run_id'] ?? null)->toBeInt(); $component->call('selectProviderConnection', (int) $otherConnection->getKey()); $session->refresh(); expect($session->state['verification_operation_run_id'] ?? null)->toBeNull(); }); it('ignores forged onboarding session and managed tenant state when starting verification', function (): void { Queue::fake(); $workspace = Workspace::factory()->create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $entraTenantId = '17171717-1717-1717-1717-171717171717'; $component = Livewire::actingAs($user)->test(ManagedTenantOnboardingWizard::class); $component->call('identifyManagedTenant', [ 'entra_tenant_id' => $entraTenantId, 'environment' => 'prod', 'name' => 'Primary Tenant', ]); $primaryTenant = Tenant::query()->where('tenant_id', $entraTenantId)->firstOrFail(); $otherTenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => '18181818-1818-1818-1818-181818181818', 'status' => Tenant::STATUS_ONBOARDING, ]); $otherConnection = ProviderConnection::factory()->platform()->consentGranted()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $otherTenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => (string) $otherTenant->tenant_id, 'display_name' => 'Forged verification connection', 'is_default' => true, 'status' => 'connected', ]); $otherDraft = TenantOnboardingSession::query()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $otherTenant->getKey(), 'entra_tenant_id' => (string) $otherTenant->tenant_id, 'current_step' => 'connection', 'state' => [ 'provider_connection_id' => (int) $otherConnection->getKey(), ], 'started_by_user_id' => (int) $user->getKey(), 'updated_by_user_id' => (int) $user->getKey(), ]); $component ->set('onboardingSession', $otherDraft) ->set('managedTenant', $otherTenant) ->set('selectedProviderConnectionId', (int) $otherConnection->getKey()) ->call('startVerification'); expect(OperationRun::query() ->where('tenant_id', (int) $otherTenant->getKey()) ->where('type', 'provider.connection.check') ->exists())->toBeFalse() ->and(OperationRun::query() ->where('tenant_id', (int) $primaryTenant->getKey()) ->where('type', 'provider.connection.check') ->exists())->toBeFalse(); }); it('treats a completed verification run as stale when it belongs to a different provider connection', function (): void { $workspace = Workspace::factory()->create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $entraTenantId = '13131313-1313-1313-1313-131313131313'; $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => $entraTenantId, 'status' => Tenant::STATUS_ONBOARDING, ]); $user->tenants()->syncWithoutDetaching([ $tenant->getKey() => ['role' => 'owner'], ]); $microsoftConnection = ProviderConnection::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => $entraTenantId, 'display_name' => 'Microsoft connection', 'is_default' => true, ]); $otherConnection = ProviderConnection::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'dummy', 'entra_tenant_id' => $entraTenantId, 'display_name' => 'Dummy connection', 'is_default' => false, ]); $run = OperationRun::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'type' => 'provider.connection.check', 'status' => 'completed', 'outcome' => 'succeeded', 'context' => [ 'provider_connection_id' => (int) $microsoftConnection->getKey(), 'target_scope' => [ 'entra_tenant_id' => $entraTenantId, ], ], ]); $session = TenantOnboardingSession::query()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'entra_tenant_id' => $entraTenantId, 'current_step' => 'verify', 'state' => [ 'provider_connection_id' => (int) $otherConnection->getKey(), 'verification_operation_run_id' => (int) $run->getKey(), ], 'started_by_user_id' => (int) $user->getKey(), 'updated_by_user_id' => (int) $user->getKey(), ]); $component = Livewire::actingAs($user)->test(ManagedTenantOnboardingWizard::class, [ 'onboardingDraft' => (int) $session->getKey(), ]); expect($component->instance()->verificationSucceeded())->toBeFalse(); });