onboarding()->create(); [$user, $tenant] = createUserWithTenant( tenant: $tenant, role: $tenantRole, workspaceRole: $workspaceRole, ensureDefaultMicrosoftProviderConnection: false, ); $workspace = $tenant->workspace()->firstOrFail(); $verificationConnection = 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' => 'Verification connection', 'is_default' => true, 'consent_status' => 'granted', ]); $selectedConnection = $verificationConnection; $checks = []; $outcome = OperationRunOutcome::Blocked->value; if ($state === 'admin_consent') { $checks[] = [ 'key' => 'permissions.admin_consent', 'title' => 'Admin consent', 'status' => 'fail', 'severity' => 'critical', 'blocking' => true, 'reason_code' => ProviderReasonCodes::ProviderConsentMissing, 'message' => 'Admin consent is required before verification can proceed.', 'evidence' => [], 'next_steps' => [], ]; } elseif ($state === 'required_permissions') { $checks[] = [ 'key' => 'permissions.required', 'title' => 'Required application permissions', 'status' => 'fail', 'severity' => 'critical', 'blocking' => true, 'reason_code' => ProviderReasonCodes::ProviderPermissionMissing, 'message' => 'Missing required application permissions.', 'evidence' => [], 'next_steps' => [], ]; } elseif ($state === 'connection_unhealthy') { $checks[] = [ 'key' => 'provider.connection.check', 'title' => 'Provider connection check', 'status' => 'fail', 'severity' => 'critical', 'blocking' => true, 'reason_code' => ProviderReasonCodes::ProviderAuthFailed, 'message' => 'Stored provider credentials are no longer valid.', 'evidence' => [], 'next_steps' => [], ]; } elseif ($state === 'verification_stale') { $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' => 'Currently selected connection', 'is_default' => false, 'consent_status' => 'granted', ]); $checks[] = [ 'key' => 'provider.connection.check', 'title' => 'Provider connection check', 'status' => 'pass', 'severity' => 'info', 'blocking' => false, 'reason_code' => 'ok', 'message' => 'Connection is healthy.', 'evidence' => [], 'next_steps' => [], ]; $outcome = OperationRunOutcome::Succeeded->value; } elseif ($state === 'verification_failed') { $checks[] = [ 'key' => 'provider.connection.check', 'title' => 'Provider connection check', 'status' => 'fail', 'severity' => 'critical', 'blocking' => true, 'reason_code' => '', 'message' => 'Verification failed after the prerequisite checks ran.', 'evidence' => [], 'next_steps' => [], ]; $outcome = OperationRunOutcome::Failed->value; } $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) $verificationConnection->getKey(), 'target_scope' => [ 'entra_tenant_id' => (string) $tenant->tenant_id, 'entra_tenant_name' => (string) $tenant->name, ], 'verification_report' => VerificationReportWriter::build('provider.connection.check', $checks), ], ]); $draft = createOnboardingDraft([ 'workspace' => $workspace, 'tenant' => $tenant, 'started_by' => $user, 'updated_by' => $user, 'current_step' => 'verify', 'entra_tenant_id' => (string) $tenant->tenant_id, '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]; } it('renders onboarding contextual help for each in-scope verification topic', function ( string $state, string $headline, string $safeNextAction, ?string $linkLabel, ): void { [$user, , $draft] = createProductKnowledgeOnboardingDraft($state); $response = $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $user->last_workspace_id]) ->followingRedirects() ->get(route('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])); $response->assertSuccessful() ->assertSee('Verification report') ->assertSee('Stored verification details') ->assertSee($headline) ->assertDontSee('Permission diagnostics') ->assertSee($safeNextAction); $dom = new \DOMDocument(); @$dom->loadHTML($response->getContent()); $xpath = new \DOMXPath($dom); $headlineNodes = $xpath->query(sprintf( '//*[@data-testid="contextual-help-block"]//*[normalize-space(text())="%s"]', $headline, )); $storedVerificationDetailsHeadings = $xpath->query( '//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6][normalize-space(text())="Stored verification details"]', ); $verificationReportHeadings = $xpath->query( '//*[self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6][normalize-space(text())="Verification report"]', ); expect($headlineNodes?->length)->toBe(1); expect($storedVerificationDetailsHeadings?->length)->toBe(1); expect($verificationReportHeadings?->length)->toBeLessThanOrEqual(1); if ($state === 'admin_consent') { $primaryNextActionNode = $xpath->query( '//*[normalize-space(text())="Primary next action"]/following::*[(self::a or self::button) and normalize-space(text())!=""][1]', ); expect(trim((string) $primaryNextActionNode?->item(0)?->textContent))->toContain('Grant admin consent'); } if ($linkLabel !== null) { $response->assertSee($linkLabel); } })->with([ 'admin consent required' => [ 'admin_consent', 'Admin consent required', 'Grant admin consent and re-run verification.', 'Grant admin consent', ], 'required permissions missing' => [ 'required_permissions', 'Required permissions missing', 'Open required permissions and confirm the missing grants.', 'Open required permissions', ], 'connection unhealthy' => [ 'connection_unhealthy', 'Provider connection needs review', 'Review the provider connection before retrying.', null, ], 'verification stale' => [ 'verification_stale', 'Verification result is stale', 'Refresh verification before continuing onboarding.', null, ], 'verification failed' => [ 'verification_failed', 'Verification failed', 'Review the blocking reason and retry verification.', null, ], ]); it('keeps onboarding contextual help deny-as-not-found for workspace members outside the tenant scope', function (): void { [$authorizedUser, $tenant, $draft] = createProductKnowledgeOnboardingDraft('admin_consent'); $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(); });