266 lines
9.5 KiB
PHP
266 lines
9.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\OperationRun;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\Tenant;
|
|
use App\Models\TenantOnboardingSession;
|
|
use App\Models\User;
|
|
use App\Models\WorkspaceMembership;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\ProductKnowledge\ContextualHelpCatalog;
|
|
use App\Support\Providers\ProviderReasonCodes;
|
|
use App\Support\Verification\VerificationReportWriter;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
/**
|
|
* @return array{0: User, 1: Tenant, 2: TenantOnboardingSession}
|
|
*/
|
|
function createProductKnowledgeOnboardingDraft(string $state, string $workspaceRole = 'owner', string $tenantRole = 'owner'): array
|
|
{
|
|
$tenant = Tenant::factory()->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();
|
|
});
|