create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'permission_key' => $key, 'status' => in_array($key, $errorKeys, true) ? 'error' : 'granted', 'details' => in_array($key, $errorKeys, true) ? ['reason_code' => 'permission_denied'] : ['source' => 'db'], 'last_checked_at' => $staleDays === null ? now() : now()->subDays($staleDays), ]); } } /** * @return array{0:User,1:Tenant,2:\App\Models\TenantOnboardingSession,3:ProviderConnection,4:OperationRun,5:?string} */ function createVerificationAssistDraft( string $state = 'blocked', string $workspaceRole = 'owner', string $tenantRole = 'owner', bool $staleVerificationRun = false, ): array { [$user, $tenant] = createUserWithTenant( role: $tenantRole, workspaceRole: $workspaceRole, ensureDefaultMicrosoftProviderConnection: false, ); $workspace = $tenant->workspace()->firstOrFail(); $verifiedConnection = ProviderConnection::factory()->platform()->consentGranted()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'microsoft', 'entra_tenant_id' => (string) $tenant->tenant_id, 'display_name' => 'Verified connection', 'is_default' => true, 'status' => 'connected', ]); $selectedConnection = $verifiedConnection; if ($staleVerificationRun) { $selectedConnection = ProviderConnection::factory()->platform()->consentGranted()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'provider' => 'dummy', 'entra_tenant_id' => (string) $tenant->tenant_id, 'display_name' => 'Current selected connection', 'is_default' => false, 'status' => 'connected', ]); } $allPermissionKeys = assistConfiguredPermissionKeys(); $missingKey = $allPermissionKeys[0] ?? null; $errorKeys = []; $staleDays = null; $check = []; $outcome = OperationRunOutcome::Succeeded->value; if ($state === 'blocked') { seedAssistPermissionInventory($tenant, missingKey: $missingKey); $check = [ 'key' => 'permissions.admin_consent', 'title' => 'Required application permissions', 'status' => 'fail', 'severity' => 'critical', 'blocking' => true, 'reason_code' => ProviderReasonCodes::ProviderPermissionMissing, 'message' => 'Missing required Graph permissions.', 'evidence' => [], 'next_steps' => [], ]; $outcome = OperationRunOutcome::Blocked->value; } elseif ($state === 'needs_attention') { seedAssistPermissionInventory($tenant, staleDays: 45); $check = [ 'key' => 'permissions.admin_consent', 'title' => 'Required application permissions', 'status' => 'warn', 'severity' => 'medium', 'blocking' => false, 'reason_code' => ProviderReasonCodes::ProviderPermissionRefreshFailed, 'message' => 'Stored permission data needs review.', 'evidence' => [], 'next_steps' => [], ]; } elseif ($state === 'degraded') { $errorKeys = array_slice($allPermissionKeys, 0, 1); seedAssistPermissionInventory($tenant, staleDays: 45, errorKeys: $errorKeys); $check = [ 'key' => 'permissions.admin_consent', 'title' => 'Required application permissions', 'status' => 'warn', 'severity' => 'medium', 'blocking' => false, 'reason_code' => ProviderReasonCodes::ProviderPermissionRefreshFailed, 'message' => 'Stored permission data needs review.', 'evidence' => [], 'next_steps' => [], ]; } else { seedAssistPermissionInventory($tenant); $check = [ 'key' => 'provider.connection.check', 'title' => 'Provider connection check', 'status' => 'pass', 'severity' => 'info', 'blocking' => false, 'reason_code' => 'ok', 'message' => 'Connection is healthy.', 'evidence' => [], 'next_steps' => [], ]; } $run = OperationRun::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'type' => 'provider.connection.check', 'status' => OperationRunStatus::Completed->value, 'outcome' => $outcome, 'context' => [ 'provider_connection_id' => (int) $verifiedConnection->getKey(), 'target_scope' => [ 'entra_tenant_id' => (string) $tenant->tenant_id, 'entra_tenant_name' => (string) $tenant->name, ], 'verification_report' => VerificationReportWriter::build('provider.connection.check', [$check]), ], ]); $draft = createOnboardingDraft([ 'workspace' => $workspace, 'tenant' => $tenant, 'started_by' => $user, 'updated_by' => $user, 'current_step' => 'verify', 'state' => [ 'entra_tenant_id' => (string) $tenant->tenant_id, 'tenant_name' => (string) $tenant->name, 'provider_connection_id' => (int) $selectedConnection->getKey(), 'verification_operation_run_id' => (int) $run->getKey(), ], ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); return [$user, $tenant, $draft, $selectedConnection, $run, $missingKey]; } it('shows the assist trigger for blocked and needs-attention states and hides it when verification is ready', function (string $state, bool $shouldSeeTrigger): void { [$user, , $draft] = createVerificationAssistDraft($state); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $user->last_workspace_id]) ->get(route('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])) ->assertSuccessful() ->when( $shouldSeeTrigger, fn ($response) => $response->assertSee('View required permissions')->assertSee('Required permissions assist'), fn ($response) => $response->assertDontSee('View required permissions'), ); })->with([ 'blocked' => ['blocked', true], 'needs attention' => ['needs_attention', true], 'ready' => ['ready', false], ]); it('opens and closes the assist slideover without changing the verify step', function (): void { [$user, , $draft] = createVerificationAssistDraft('blocked'); session()->put(WorkspaceContext::SESSION_KEY, (int) $user->last_workspace_id); Livewire::actingAs($user) ->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $draft->getKey()]) ->assertWizardCurrentStep(3) ->mountAction('wizardVerificationRequiredPermissionsAssist') ->assertMountedActionModalSee('Required permissions assist') ->assertMountedActionModalSee('Open full page') ->unmountAction() ->assertWizardCurrentStep(3); }); it('renders summary metadata and missing application permissions in the assist slideover', function (): void { [$user, , $draft, , , $missingKey] = createVerificationAssistDraft('blocked'); session()->put(WorkspaceContext::SESSION_KEY, (int) $user->last_workspace_id); Livewire::actingAs($user) ->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $draft->getKey()]) ->mountAction('wizardVerificationRequiredPermissionsAssist') ->assertMountedActionModalSee('Missing application permissions') ->assertMountedActionModalSee('Copy missing application permissions') ->assertMountedActionModalSee((string) $missingKey) ->assertMountedActionModalDontSee('Copy missing delegated permissions'); }); it('renders degraded fallback and hides unavailable copy actions when stored detail is incomplete', function (): void { [$user, , $draft] = createVerificationAssistDraft('degraded', staleVerificationRun: true); session()->put(WorkspaceContext::SESSION_KEY, (int) $user->last_workspace_id); Livewire::actingAs($user) ->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $draft->getKey()]) ->mountAction('wizardVerificationRequiredPermissionsAssist') ->assertMountedActionModalSee('Verification result is stale') ->assertMountedActionModalSee('Stored permission data needs refresh') ->assertMountedActionModalSee('Compact detail is incomplete') ->assertMountedActionModalDontSee('Copy missing application permissions') ->assertMountedActionModalDontSee('Copy missing delegated permissions'); }); it('returns 404 for workspace members who are out of scope for the tenant assist surface', function (): void { [$authorizedUser, $tenant, $draft] = createVerificationAssistDraft('blocked'); $workspace = $tenant->workspace()->firstOrFail(); $outOfScopeUser = User::factory()->create(); WorkspaceMembership::query()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $outOfScopeUser->getKey(), 'role' => 'owner', ]); $this->actingAs($outOfScopeUser) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->get(route('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])) ->assertNotFound(); });