TenantAtlas/tests/Browser/OnboardingDraftVerificationResumeTest.php
2026-03-14 02:59:06 +01:00

493 lines
19 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\Tenant;
use App\Models\TenantPermission;
use App\Models\User;
use App\Models\Workspace;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Verification\VerificationReportWriter;
use App\Support\Workspaces\WorkspaceContext;
pest()->browser()->timeout(10_000);
it('keeps stale verification warnings and the selected provider connection stable after refresh', function (): void {
$workspace = Workspace::factory()->create();
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => '30303030-3030-3030-3030-303030303030',
'name' => 'Stale Verification Tenant',
'status' => Tenant::STATUS_ONBOARDING,
]);
$user = User::factory()->create(['name' => 'Verification Owner']);
createUserWithTenant(
tenant: $tenant,
user: $user,
role: 'owner',
workspaceRole: 'owner',
ensureDefaultMicrosoftProviderConnection: false,
);
$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' => 'Previously verified connection',
'is_default' => true,
'status' => 'connected',
]);
$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',
]);
$run = OperationRun::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'type' => 'provider.connection.check',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
'context' => [
'provider_connection_id' => (int) $verifiedConnection->getKey(),
'target_scope' => [
'entra_tenant_id' => (string) $tenant->tenant_id,
],
],
]);
$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(),
],
]);
$this->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$visibleSelectValue = <<<'JS'
(() => {
const select = [...document.querySelectorAll('select')].find((element) => {
const style = window.getComputedStyle(element);
return style.display !== 'none' && style.visibility !== 'hidden';
});
return select?.value ?? null;
})()
JS;
$page = visit(route('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]));
$page
->assertNoJavaScriptErrors()
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Verify access')
->assertSee('Status: Needs attention')
->assertSee('The selected provider connection has changed since this verification run. Start verification again to validate the current connection.')
->assertSee('Start verification')
->refresh()
->waitForText('The selected provider connection has changed since this verification run. Start verification again to validate the current connection.')
->assertNoJavaScriptErrors()
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Status: Needs attention')
->assertSee('Start verification')
->click('Provider connection')
->assertScript($visibleSelectValue, (string) $selectedConnection->getKey());
});
it('preserves bootstrap revisit state and blocked activation guards after refresh', function (): void {
$workspace = Workspace::factory()->create();
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => '40404040-4040-4040-4040-404040404040',
'name' => 'Blocked Review Tenant',
'status' => Tenant::STATUS_ONBOARDING,
]);
$user = User::factory()->create(['name' => 'Review Owner']);
createUserWithTenant(
tenant: $tenant,
user: $user,
role: 'owner',
workspaceRole: 'owner',
ensureDefaultMicrosoftProviderConnection: false,
);
$connection = 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' => 'Blocked review connection',
'is_default' => true,
'status' => 'connected',
]);
$verificationRun = OperationRun::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'type' => 'provider.connection.check',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Failed->value,
'context' => [
'provider_connection_id' => (int) $connection->getKey(),
'target_scope' => [
'entra_tenant_id' => (string) $tenant->tenant_id,
'entra_tenant_name' => (string) $tenant->name,
],
'verification_report' => VerificationReportWriter::build('provider.connection.check', [
[
'key' => 'permissions.admin_consent',
'title' => 'Required application permissions',
'status' => 'fail',
'severity' => 'critical',
'blocking' => true,
'reason_code' => 'permission_denied',
'message' => 'Missing required Graph permissions.',
'evidence' => [],
'next_steps' => [],
],
]),
],
]);
$bootstrapRun = createInventorySyncOperationRun($tenant, [
'workspace_id' => (int) $workspace->getKey(),
'status' => 'success',
]);
$draft = createOnboardingDraft([
'workspace' => $workspace,
'tenant' => $tenant,
'started_by' => $user,
'updated_by' => $user,
'current_step' => 'complete',
'state' => [
'entra_tenant_id' => (string) $tenant->tenant_id,
'tenant_name' => (string) $tenant->name,
'provider_connection_id' => (int) $connection->getKey(),
'verification_operation_run_id' => (int) $verificationRun->getKey(),
'bootstrap_operation_types' => ['inventory_sync'],
'bootstrap_operation_runs' => [
'inventory_sync' => (int) $bootstrapRun->getKey(),
],
],
]);
$this->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$page = visit(route('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]));
$activateButtonIsDisabled = <<<'JS'
(() => {
const button = [...document.querySelectorAll('button')].find((element) => element.textContent?.includes('Activate tenant'));
return button?.disabled ?? null;
})()
JS;
$openBootstrapStep = <<<'JS'
(() => {
const button = [...document.querySelectorAll('button')].find((element) => element.textContent?.includes('Bootstrap'));
if (! button) {
return false;
}
button.click();
return true;
})()
JS;
$page
->assertNoJavaScriptErrors()
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Complete')
->assertSee('Override blocked verification')
->assertSee('Blocked — 0/1 checks passed')
->assertSee('Started - 1 operation run(s) started')
->assertScript($activateButtonIsDisabled, true)
->refresh()
->waitForText('Override blocked verification')
->assertNoJavaScriptErrors()
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Blocked — 0/1 checks passed')
->assertSee('Started - 1 operation run(s) started')
->assertScript($activateButtonIsDisabled, true)
->assertScript($openBootstrapStep, true)
->assertSee('Started 1 bootstrap run(s).');
});
it('opens the full-page permissions deep dive in a new tab without replacing onboarding', function (): void {
[$user, $tenant] = createUserWithTenant(
role: 'owner',
ensureDefaultMicrosoftProviderConnection: false,
);
$workspace = $tenant->workspace()->firstOrFail();
$configured = array_merge(
config('intune_permissions.permissions', []),
config('entra_permissions.permissions', []),
);
$permissionKeys = array_values(array_filter(array_map(static function (mixed $permission): ?string {
if (! is_array($permission)) {
return null;
}
$key = $permission['key'] ?? null;
return is_string($key) && trim($key) !== '' ? trim($key) : null;
}, $configured)));
if ($permissionKeys === []) {
test()->markTestSkipped('No configured required permissions found.');
}
$missingKey = $permissionKeys[0];
foreach (array_slice($permissionKeys, 1) as $key) {
TenantPermission::query()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $workspace->getKey(),
'permission_key' => $key,
'status' => 'granted',
'details' => ['source' => 'db'],
'last_checked_at' => now(),
]);
}
$connection = 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' => 'Browser assist 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' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Blocked->value,
'context' => [
'provider_connection_id' => (int) $connection->getKey(),
'target_scope' => [
'entra_tenant_id' => (string) $tenant->tenant_id,
'entra_tenant_name' => (string) $tenant->name,
],
'verification_report' => VerificationReportWriter::build('provider.connection.check', [
[
'key' => 'permissions.admin_consent',
'title' => 'Required application permissions',
'status' => 'fail',
'severity' => 'critical',
'blocking' => true,
'reason_code' => 'provider_permission_missing',
'message' => "Missing required application permission: {$missingKey}",
'evidence' => [],
'next_steps' => [],
],
]),
],
]);
$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) $connection->getKey(),
'verification_operation_run_id' => (int) $run->getKey(),
],
]);
$this->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$page = visit(route('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]));
$page
->assertNoJavaScriptErrors()
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->click('Verify access')
->waitForText('View required permissions')
->click('View required permissions')
->waitForText('Open full page');
$page->script(<<<'JS'
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: {
writeText: async () => Promise.resolve(),
},
});
document.querySelector('[data-testid="verification-assist-copy-application"]')?.click();
JS);
$page
->waitForText('Copied')
->assertAttribute('[data-testid="verification-assist-full-page"]', 'target', '_blank')
->assertAttribute('[data-testid="verification-assist-full-page"]', 'rel', 'noopener noreferrer')
->click('Open full page')
->wait(1)
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Open full page')
->click('Close')
->click('Provider connection')
->assertSee('Select an existing connection or create a new one.');
});
it('opens the permissions assist from report remediation steps without leaving onboarding', function (): void {
[$user, $tenant] = createUserWithTenant(
role: 'owner',
ensureDefaultMicrosoftProviderConnection: false,
);
$workspace = $tenant->workspace()->firstOrFail();
$configured = array_merge(
config('intune_permissions.permissions', []),
config('entra_permissions.permissions', []),
);
$permissionKeys = array_values(array_filter(array_map(static function (mixed $permission): ?string {
if (! is_array($permission)) {
return null;
}
$key = $permission['key'] ?? null;
return is_string($key) && trim($key) !== '' ? trim($key) : null;
}, $configured)));
if ($permissionKeys === []) {
test()->markTestSkipped('No configured required permissions found.');
}
foreach (array_slice($permissionKeys, 1) as $key) {
TenantPermission::query()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $workspace->getKey(),
'permission_key' => $key,
'status' => 'granted',
'details' => ['source' => 'db'],
'last_checked_at' => now(),
]);
}
$connection = 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' => 'Browser next-step 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' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Blocked->value,
'context' => [
'provider_connection_id' => (int) $connection->getKey(),
'target_scope' => [
'entra_tenant_id' => (string) $tenant->tenant_id,
'entra_tenant_name' => (string) $tenant->name,
],
'verification_report' => VerificationReportWriter::build('provider.connection.check', [
[
'key' => 'provider.connection.preflight',
'title' => 'Provider connection preflight',
'status' => 'fail',
'severity' => 'critical',
'blocking' => true,
'reason_code' => 'provider_permission_missing',
'message' => 'Provider connection requires admin consent before use.',
'evidence' => [],
'next_steps' => [
[
'label' => 'Grant admin consent',
'url' => 'https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/grant-admin-consent',
],
[
'label' => 'Review platform connection',
'url' => route('filament.admin.resources.provider-connections.edit', ['tenant' => $tenant->external_id, 'record' => (int) $connection->getKey()]),
],
],
],
]),
],
]);
$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) $connection->getKey(),
'verification_operation_run_id' => (int) $run->getKey(),
],
]);
$this->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
visit(route('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]))
->assertNoJavaScriptErrors()
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->click('Verify access')
->waitForText('Grant admin consent')
->click('Grant admin consent')
->waitForText('Required permissions assist')
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Open full page')
->assertSee('Review platform connection');
});