TenantAtlas/tests/Browser/OnboardingDraftVerificationResumeTest.php
ahmido 98e2b5acd9 feat: managed tenant onboarding draft identity and resume semantics (#167)
## Summary
- add canonical managed-tenant onboarding draft routing with explicit draft identity and landing vs concrete draft behavior
- implement draft lifecycle, authorization, attribution, picker UX, resume-stage resolution, and auditable cancel or completion semantics
- add focused feature, unit, and browser coverage plus Spec 138 artifacts for the onboarding draft resume flow

## Validation
- `vendor/bin/sail artisan test --compact tests/Feature/ManagedTenantOnboardingWizardTest.php tests/Feature/Audit/OnboardingDraftAuditTest.php tests/Feature/Onboarding/OnboardingDraftAccessTest.php tests/Feature/Onboarding/OnboardingDraftAuthorizationTest.php tests/Feature/Onboarding/OnboardingDraftLifecycleTest.php tests/Feature/Onboarding/OnboardingDraftMultiTabTest.php tests/Feature/Onboarding/OnboardingDraftPickerTest.php tests/Feature/Onboarding/OnboardingDraftRoutingTest.php tests/Feature/Onboarding/OnboardingRbacSemanticsTest.php tests/Feature/Onboarding/OnboardingVerificationClustersTest.php tests/Feature/Onboarding/OnboardingVerificationTest.php tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php tests/Feature/Verification/VerificationReportViewerDbOnlyTest.php tests/Unit/Onboarding tests/Unit/VerificationReportSanitizerEvidenceKindsTest.php tests/Browser/OnboardingDraftRefreshTest.php tests/Browser/OnboardingDraftVerificationResumeTest.php`
- passed: 69 tests, 251 assertions

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #167
2026-03-13 23:45:23 +00:00

245 lines
9.1 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\Tenant;
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).');
});