TenantAtlas/apps/platform/tests/Feature/ManagedTenantOnboardingWizardTest.php
ahmido 110245a9ec
Some checks are pending
Main Confidence / confidence (push) Waiting to run
feat: neutralize provider connection target-scope surfaces (#274)
## Summary
- add a shared provider target-scope descriptor, normalizer, identity-context metadata, and surface-summary layer
- update provider connection list, detail, create, edit, and onboarding surfaces to use neutral target-scope vocabulary while keeping Microsoft identity contextual
- align provider connection audit and resolver output with the neutral target-scope contract and add focused guard/unit/feature coverage for regressions

## Validation
- browser smoke: opened the tenant-scoped provider connection list, drilled into detail, and verified the edit/create surfaces in local admin context

## Notes
- this PR comes from the session branch created for the active feature work
- no additional runtime or persistence layer was introduced in this slice

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #274
2026-04-25 09:07:40 +00:00

1765 lines
64 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\Tenant;
use App\Models\TenantOnboardingSession;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Providers\ProviderConnectionType;
use App\Support\Verification\VerificationReportWriter;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Actions\Testing\TestAction;
use Livewire\Livewire;
it('returns 404 for non-members when starting onboarding with a selected workspace', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$this->actingAs($user)
->get('/admin/onboarding')
->assertNotFound();
});
it('forbids workspace members without onboarding capability from loading the wizard or executing actions', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => $user->getKey(),
'role' => 'readonly',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$this->actingAs($user)
->get('/admin/onboarding')
->assertForbidden();
Livewire::actingAs($user)
->test(ManagedTenantOnboardingWizard::class)
->assertForbidden();
expect(Tenant::query()->count())->toBe(0);
expect(TenantOnboardingSession::query()->count())->toBe(0);
});
it('renders onboarding wizard for workspace owners and allows identifying a managed tenant', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$this->actingAs($user)
->get('/admin/onboarding')
->assertSuccessful();
$entraTenantId = '22222222-2222-2222-2222-222222222222';
Livewire::actingAs($user)
->test(ManagedTenantOnboardingWizard::class)
->call('identifyManagedTenant', [
'entra_tenant_id' => $entraTenantId,
'environment' => 'prod',
'name' => 'Acme',
'primary_domain' => 'acme.example',
'notes' => 'Initial onboarding',
]);
$tenant = Tenant::query()->where('tenant_id', $entraTenantId)->firstOrFail();
expect((int) $tenant->workspace_id)->toBe((int) $workspace->getKey());
expect($tenant->status)->toBe(Tenant::STATUS_ONBOARDING);
$this->assertDatabaseHas('tenant_memberships', [
'tenant_id' => (int) $tenant->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
$this->assertDatabaseHas('managed_tenant_onboarding_sessions', [
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'entra_tenant_id' => $entraTenantId,
'current_step' => 'identify',
]);
});
it('renders neutral tenant id placeholder guidance for onboarding input', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$this->actingAs($user)
->get('/admin/onboarding')
->assertSuccessful()
->assertSee('Tenant ID (GUID)')
->assertSee('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', false);
});
it('uses target-scope wording in the onboarding provider setup step', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$entraTenantId = '34343434-3434-3434-3434-343434343434';
Livewire::actingAs($user)
->test(ManagedTenantOnboardingWizard::class)
->call('identifyManagedTenant', [
'entra_tenant_id' => $entraTenantId,
'environment' => 'prod',
'name' => 'Target Scope Tenant',
])
->set('data.connection_mode', 'new')
->assertSee('Target scope ID')
->assertSee('The provider connection will point to this tenant target scope.')
->assertSee($entraTenantId)
->assertDontSee('Directory (tenant) ID')
->assertDontSee('Graph API calls');
});
it('renders review summary guidance and activation consequences for ready onboarding sessions', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$entraTenantId = 'abababab-abab-abab-abab-abababababab';
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => $entraTenantId,
'status' => Tenant::STATUS_ONBOARDING,
'name' => 'Activation Ready Tenant',
]);
$connection = ProviderConnection::factory()->platform()->consentGranted()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => $entraTenantId,
'display_name' => 'Platform onboarding connection',
'is_default' => true,
'is_enabled' => true,
]);
$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) $connection->getKey(),
'target_scope' => [
'entra_tenant_id' => $entraTenantId,
'entra_tenant_name' => 'Activation Ready Tenant',
],
'verification_report' => VerificationReportWriter::build('provider.connection.check', [
[
'key' => 'consent',
'title' => 'Required application permissions',
'status' => 'pass',
'severity' => 'low',
'blocking' => false,
'reason_code' => 'ok',
'message' => 'Consent is ready.',
'evidence' => [],
'next_steps' => [],
],
]),
],
]);
TenantOnboardingSession::query()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'entra_tenant_id' => $entraTenantId,
'current_step' => 'bootstrap',
'state' => [
'provider_connection_id' => (int) $connection->getKey(),
'verification_operation_run_id' => (int) $run->getKey(),
],
'started_by_user_id' => (int) $user->getKey(),
'updated_by_user_id' => (int) $user->getKey(),
]);
$this->actingAs($user)
->followingRedirects()
->get('/admin/onboarding')
->assertSuccessful()
->assertSee('Skipped - No bootstrap actions selected')
->assertSee('Tenant status will be set to Active.')
->assertSee('The provider connection will be used for provider API calls.');
});
it('renders selected bootstrap actions in the review summary before any bootstrap run starts', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$entraTenantId = 'cdcdcdcd-cdcd-cdcd-cdcd-cdcdcdcdcdcd';
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => $entraTenantId,
'status' => Tenant::STATUS_ONBOARDING,
'name' => 'Bootstrap Selected Tenant',
]);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
$connection = ProviderConnection::factory()->platform()->consentGranted()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => $entraTenantId,
'display_name' => 'Platform onboarding connection',
'is_default' => true,
'is_enabled' => true,
]);
$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) $connection->getKey(),
'target_scope' => [
'entra_tenant_id' => $entraTenantId,
'entra_tenant_name' => 'Bootstrap Selected Tenant',
],
'verification_report' => VerificationReportWriter::build('provider.connection.check', [
[
'key' => 'consent',
'title' => 'Required application permissions',
'status' => 'pass',
'severity' => 'low',
'blocking' => false,
'reason_code' => 'ok',
'message' => 'Consent is ready.',
'evidence' => [],
'next_steps' => [],
],
]),
],
]);
$session = TenantOnboardingSession::query()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'entra_tenant_id' => $entraTenantId,
'current_step' => 'complete',
'state' => [
'provider_connection_id' => (int) $connection->getKey(),
'verification_operation_run_id' => (int) $run->getKey(),
'bootstrap_operation_types' => ['inventory_sync', 'compliance.snapshot'],
],
'started_by_user_id' => (int) $user->getKey(),
'updated_by_user_id' => (int) $user->getKey(),
]);
$component = Livewire::actingAs($user)
->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $session->getKey()]);
$component
->assertDontSee('Bootstrap needs attention')
->assertDontSee('Selected bootstrap actions must complete before activation. Return to Bootstrap to remove the selected actions and skip this optional step, or resolve the required permission and start the blocked action again.');
$summaryMethod = new \ReflectionMethod($component->instance(), 'completionSummaryBootstrapSummary');
$summaryMethod->setAccessible(true);
expect($summaryMethod->invoke($component->instance()))->toBe('Selected - 2 action(s) selected');
});
it('renders blocked bootstrap runs as action required in the review summary', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$entraTenantId = 'efefefef-efef-efef-efef-efefefefefef';
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => $entraTenantId,
'status' => Tenant::STATUS_ONBOARDING,
'name' => 'Bootstrap Blocked Tenant',
]);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
$connection = ProviderConnection::factory()->platform()->consentGranted()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => $entraTenantId,
'display_name' => 'Platform onboarding connection',
'is_default' => true,
'is_enabled' => true,
]);
$verificationRun = 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) $connection->getKey(),
'target_scope' => [
'entra_tenant_id' => $entraTenantId,
'entra_tenant_name' => 'Bootstrap Blocked Tenant',
],
'verification_report' => VerificationReportWriter::build('provider.connection.check', [
[
'key' => 'consent',
'title' => 'Required application permissions',
'status' => 'pass',
'severity' => 'low',
'blocking' => false,
'reason_code' => 'ok',
'message' => 'Consent is ready.',
'evidence' => [],
'next_steps' => [],
],
]),
],
]);
$bootstrapRun = OperationRun::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'type' => 'inventory_sync',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Blocked->value,
'context' => [
'provider_connection_id' => (int) $connection->getKey(),
'reason_translation' => [
'operator_label' => 'Permission required',
],
],
]);
$session = TenantOnboardingSession::query()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'entra_tenant_id' => $entraTenantId,
'current_step' => 'complete',
'state' => [
'provider_connection_id' => (int) $connection->getKey(),
'verification_operation_run_id' => (int) $verificationRun->getKey(),
'bootstrap_operation_types' => ['inventory_sync', 'compliance.snapshot'],
'bootstrap_operation_runs' => ['inventory_sync' => (int) $bootstrapRun->getKey()],
],
'started_by_user_id' => (int) $user->getKey(),
'updated_by_user_id' => (int) $user->getKey(),
]);
$component = Livewire::actingAs($user)
->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $session->getKey()]);
$summaryMethod = new \ReflectionMethod($component->instance(), 'completionSummaryBootstrapSummary');
$summaryMethod->setAccessible(true);
expect($summaryMethod->invoke($component->instance()))->toBe('Action required - Permission required');
});
it('initializes entangled wizard state keys to avoid Livewire entangle errors', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
Livewire::actingAs($user)
->test(ManagedTenantOnboardingWizard::class)
->assertSet('data.notes', '')
->assertSet('data.bootstrap_operation_types', [])
->assertSet('data.override_blocked', false)
->assertSet('data.override_reason', '');
});
it('persists selected bootstrap actions in the onboarding draft state', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$entraTenantId = 'dededede-dede-dede-dede-dededededede';
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => $entraTenantId,
'status' => Tenant::STATUS_ONBOARDING,
'name' => 'Persist Bootstrap Tenant',
]);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
$connection = ProviderConnection::factory()->platform()->consentGranted()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => $entraTenantId,
'display_name' => 'Platform onboarding connection',
'is_default' => true,
'is_enabled' => true,
]);
$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) $connection->getKey(),
'verification_report' => VerificationReportWriter::build('provider.connection.check', [
[
'key' => 'consent',
'title' => 'Required application permissions',
'status' => 'pass',
'severity' => 'low',
'blocking' => false,
'reason_code' => 'ok',
'message' => 'Consent is ready.',
'evidence' => [],
'next_steps' => [],
],
]),
],
]);
$session = TenantOnboardingSession::query()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'entra_tenant_id' => $entraTenantId,
'current_step' => 'bootstrap',
'state' => [
'provider_connection_id' => (int) $connection->getKey(),
'verification_operation_run_id' => (int) $run->getKey(),
],
'started_by_user_id' => (int) $user->getKey(),
'updated_by_user_id' => (int) $user->getKey(),
]);
$component = Livewire::actingAs($user)
->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $session->getKey()]);
$persistMethod = new \ReflectionMethod($component->instance(), 'persistBootstrapSelection');
$persistMethod->setAccessible(true);
$persistMethod->invoke($component->instance(), ['inventory_sync', 'compliance.snapshot']);
$session->refresh();
expect($session->state['bootstrap_operation_types'] ?? null)->toBe(['inventory_sync', 'compliance.snapshot']);
});
it('filters unsupported bootstrap selections from persisted onboarding drafts', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$tenantGuid = '12121212-1212-1212-1212-121212121212';
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => $tenantGuid,
'status' => Tenant::STATUS_ONBOARDING,
'name' => 'Acme',
]);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
$connection = ProviderConnection::factory()->platform()->consentGranted()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => $tenantGuid,
'display_name' => 'Platform onboarding connection',
'is_default' => true,
'is_enabled' => true,
]);
$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) $connection->getKey(),
'target_scope' => [
'entra_tenant_id' => $tenantGuid,
'entra_tenant_name' => 'Acme',
],
'verification_report' => VerificationReportWriter::build('provider.connection.check', [
[
'key' => 'consent',
'title' => 'Required application permissions',
'status' => 'pass',
'severity' => 'low',
'blocking' => false,
'reason_code' => 'ok',
'message' => 'Consent is ready.',
'evidence' => [],
'next_steps' => [],
],
]),
],
]);
$session = TenantOnboardingSession::query()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'entra_tenant_id' => $tenantGuid,
'current_step' => 'complete',
'state' => [
'provider_connection_id' => (int) $connection->getKey(),
'verification_operation_run_id' => (int) $run->getKey(),
'bootstrap_operation_types' => [
'inventory_sync',
'compliance.snapshot',
'restore.execute',
'entra_group_sync',
'directory_role_definitions.sync',
],
],
'started_by_user_id' => (int) $user->getKey(),
'updated_by_user_id' => (int) $user->getKey(),
]);
$component = Livewire::actingAs($user)
->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $session->getKey()]);
$normalizeMethod = new \ReflectionMethod($component->instance(), 'normalizeBootstrapOperationTypes');
$normalizeMethod->setAccessible(true);
expect($normalizeMethod->invoke($component->instance(), [
'inventory_sync',
'compliance.snapshot',
'restore.execute',
'entra_group_sync',
'directory_role_definitions.sync',
]))->toBe(['inventory_sync', 'compliance.snapshot']);
$optionsMethod = new \ReflectionMethod($component->instance(), 'bootstrapOperationOptions');
$optionsMethod->setAccessible(true);
expect(array_keys($optionsMethod->invoke($component->instance())))->toBe(['inventory_sync', 'compliance.snapshot']);
});
it('returns resumable drafts with missing provider connections to the provider connection step', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'status' => Tenant::STATUS_ONBOARDING,
]);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
$draft = TenantOnboardingSession::query()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'entra_tenant_id' => (string) $tenant->tenant_id,
'current_step' => 'verify',
'state' => [
'entra_tenant_id' => (string) $tenant->tenant_id,
'tenant_name' => (string) $tenant->name,
'provider_connection_id' => 999999,
],
'started_by_user_id' => (int) $user->getKey(),
'updated_by_user_id' => (int) $user->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
Livewire::actingAs($user)
->test(ManagedTenantOnboardingWizard::class, ['onboardingDraft' => (int) $draft->getKey()])
->assertWizardCurrentStep(2)
->assertSet('selectedProviderConnectionId', null)
->assertSet('data.provider_connection_id', null);
});
it('allows workspace owners to create a dedicated override connection explicitly during onboarding', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$entraTenantId = '55555555-5555-5555-5555-555555555555';
$component = Livewire::actingAs($user)->test(ManagedTenantOnboardingWizard::class);
$component->call('identifyManagedTenant', [
'entra_tenant_id' => $entraTenantId,
'environment' => 'prod',
'name' => 'Dedicated Tenant',
]);
$component
->set('data.connection_mode', 'new')
->assertSee('Dedicated override')
->call('createProviderConnection', [
'display_name' => 'Dedicated onboarding connection',
'connection_type' => ProviderConnectionType::Dedicated->value,
'client_id' => 'dedicated-onboarding-client',
'client_secret' => 'dedicated-onboarding-secret',
'is_default' => true,
]);
$tenant = Tenant::query()->where('tenant_id', $entraTenantId)->firstOrFail();
$connection = \App\Models\ProviderConnection::query()
->where('workspace_id', (int) $workspace->getKey())
->where('tenant_id', (int) $tenant->getKey())
->where('provider', 'microsoft')
->firstOrFail();
expect($connection->connection_type)->toBe(ProviderConnectionType::Dedicated)
->and($connection->credential)->not->toBeNull()
->and($connection->credential?->payload['client_id'] ?? null)->toBe('dedicated-onboarding-client');
});
it('forbids workspace managers from creating dedicated override connections during onboarding', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'manager',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$entraTenantId = '66666666-6666-6666-6666-666666666666';
$component = Livewire::actingAs($user)->test(ManagedTenantOnboardingWizard::class);
$component->call('identifyManagedTenant', [
'entra_tenant_id' => $entraTenantId,
'environment' => 'prod',
'name' => 'Managed Tenant',
]);
$component
->assertDontSee('Dedicated override')
->call('createProviderConnection', [
'display_name' => 'Forbidden dedicated connection',
'connection_type' => ProviderConnectionType::Dedicated->value,
'client_id' => 'forbidden-dedicated-client',
'client_secret' => 'forbidden-dedicated-secret',
'is_default' => true,
])
->assertForbidden();
});
it('is idempotent when identifying the same Entra tenant ID twice', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$entraTenantId = '33333333-3333-3333-3333-333333333333';
$component = Livewire::actingAs($user)->test(ManagedTenantOnboardingWizard::class);
$component->call('identifyManagedTenant', [
'entra_tenant_id' => $entraTenantId,
'environment' => 'prod',
'name' => 'Acme',
]);
$component->call('identifyManagedTenant', [
'entra_tenant_id' => $entraTenantId,
'environment' => 'prod',
'name' => 'Acme',
]);
expect(Tenant::query()->where('tenant_id', $entraTenantId)->count())->toBe(1);
expect(TenantOnboardingSession::query()
->where('workspace_id', (int) $workspace->getKey())
->where('entra_tenant_id', $entraTenantId)
->whereNull('completed_at')
->count())->toBe(1);
});
it('returns 404 and does not create anything when entra_tenant_id exists in another workspace', function (): void {
$entraTenantId = '44444444-4444-4444-4444-444444444444';
$workspaceA = Workspace::factory()->create();
$workspaceB = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspaceA->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspaceB->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
Tenant::factory()->create([
'workspace_id' => (int) $workspaceA->getKey(),
'tenant_id' => $entraTenantId,
'status' => Tenant::STATUS_ACTIVE,
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspaceB->getKey());
Livewire::actingAs($user)
->test(ManagedTenantOnboardingWizard::class)
->call('identifyManagedTenant', [
'entra_tenant_id' => $entraTenantId,
'environment' => 'prod',
'name' => 'Other Workspace',
])
->assertStatus(404);
});
it('returns 404 for legacy onboarding entry points', function (): void {
$user = User::factory()->create();
$this->actingAs($user);
$this->get('/admin/register-tenant')->assertNotFound();
$this->get('/admin/managed-tenants')->assertNotFound();
$this->get('/admin/managed-tenants/onboarding')->assertNotFound();
$this->get('/admin/new')->assertNotFound();
});
it('shows resume context and derived stage when loading a concrete draft route', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create(['name' => 'Draft Owner']);
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$draft = createOnboardingDraft([
'workspace' => $workspace,
'started_by' => $user,
'updated_by' => $user,
'current_step' => 'connection',
'state' => [
'entra_tenant_id' => '77777777-7777-7777-7777-777777777777',
'tenant_name' => 'Resume Draft',
'provider_connection_id' => 42,
],
]);
Livewire::actingAs($user)
->test(ManagedTenantOnboardingWizard::class, [
'onboardingDraft' => (int) $draft->getKey(),
])
->assertSuccessful()
->assertSee('Onboarding draft')
->assertSee('Resume Draft')
->assertSee('Verify access')
->assertSee('Draft Owner');
});
it('resumes an existing draft for the same tenant instead of creating a duplicate', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$existingDraft = createOnboardingDraft([
'workspace' => $workspace,
'started_by' => $user,
'updated_by' => $user,
'entra_tenant_id' => '88888888-8888-8888-8888-888888888888',
'state' => [
'entra_tenant_id' => '88888888-8888-8888-8888-888888888888',
'tenant_name' => 'Existing Draft Tenant',
],
]);
createOnboardingDraft([
'workspace' => $workspace,
'started_by' => $user,
'updated_by' => $user,
'entra_tenant_id' => '12121212-1212-1212-1212-121212121212',
'state' => [
'entra_tenant_id' => '12121212-1212-1212-1212-121212121212',
'tenant_name' => 'Keep Landing Stable',
],
]);
Livewire::actingAs($user)
->test(ManagedTenantOnboardingWizard::class)
->call('identifyManagedTenant', [
'entra_tenant_id' => '88888888-8888-8888-8888-888888888888',
'environment' => 'prod',
'name' => 'Existing Draft Tenant',
])
->assertRedirect(route('admin.onboarding.draft', ['onboardingDraft' => $existingDraft->getKey()]));
expect(TenantOnboardingSession::query()
->where('workspace_id', (int) $workspace->getKey())
->where('entra_tenant_id', '88888888-8888-8888-8888-888888888888')
->count())->toBe(1);
});
it('allows resuming a selected draft from the picker', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$draftToResume = createOnboardingDraft([
'workspace' => $workspace,
'started_by' => $user,
'updated_by' => $user,
'state' => [
'entra_tenant_id' => '99999999-9999-9999-9999-999999999999',
'tenant_name' => 'Resume Me',
],
]);
createOnboardingDraft([
'workspace' => $workspace,
'started_by' => $user,
'updated_by' => $user,
'state' => [
'entra_tenant_id' => 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'tenant_name' => 'Leave Me',
],
]);
Livewire::actingAs($user)
->test(ManagedTenantOnboardingWizard::class)
->callAction(
TestAction::make('resume_draft_'.$draftToResume->getKey())
->schemaComponent('draft_picker_actions_'.$draftToResume->getKey())
)
->assertRedirect(route('admin.onboarding.draft', ['onboardingDraft' => $draftToResume->getKey()]));
});
/*
|--------------------------------------------------------------------------
| Legacy onboarding suite (deprecated)
|--------------------------------------------------------------------------
|
| The remainder of this file previously contained an end-to-end onboarding
| suite that relied on deprecated routes and pre-enterprise state semantics.
| Spec 073 replaces it with focused coverage under tests/Feature/Onboarding
| and tests/Feature/Rbac.
|
| Keeping the legacy assertions around (commented) is intentional to avoid
| reintroducing removed routes or old semantics.
$this->actingAs($user);
$tenantGuid = '22222222-2222-2222-2222-222222222222';
$component = Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace]);
$component->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
$component->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
expect(Tenant::query()->where('tenant_id', $tenantGuid)->count())->toBe(1);
$tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail();
expect(TenantOnboardingSession::query()
->where('workspace_id', $workspace->getKey())
->where('tenant_id', $tenant->getKey())
->count())->toBe(1);
$this->assertDatabaseHas('managed_tenant_onboarding_sessions', [
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
]);
});
it('returns 404 and does not create anything when tenant_id exists in another workspace', function (): void {
$workspaceA = Workspace::factory()->create();
$workspaceB = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspaceB->getKey(),
'user_id' => $user->getKey(),
'role' => 'owner',
]);
$tenantGuid = '33333333-3333-3333-3333-333333333333';
Tenant::factory()->create([
'workspace_id' => $workspaceA->getKey(),
'tenant_id' => $tenantGuid,
'name' => 'Acme',
'status' => 'active',
]);
$this->actingAs($user);
Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspaceB])
->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme'])
->assertStatus(404);
expect(Tenant::query()->where('tenant_id', $tenantGuid)->count())->toBe(1);
expect(TenantOnboardingSession::query()
->where('workspace_id', $workspaceB->getKey())
->whereIn('tenant_id', Tenant::query()->where('tenant_id', $tenantGuid)->pluck('id'))
->count())->toBe(0);
});
it('binds an unscoped existing tenant to the current workspace when safe and allows identifying it', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $user->getKey(),
'role' => 'owner',
]);
$tenantGuid = '77777777-7777-7777-7777-777777777777';
$tenant = Tenant::factory()->create([
'workspace_id' => null,
'tenant_id' => $tenantGuid,
'name' => 'Acme',
'status' => 'active',
]);
\App\Models\TenantMembership::query()->create([
'tenant_id' => (int) $tenant->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
'source' => 'manual',
'created_by_user_id' => (int) $user->getKey(),
]);
$this->assertDatabaseHas('tenants', [
'id' => (int) $tenant->getKey(),
'workspace_id' => null,
'tenant_id' => $tenantGuid,
]);
$this->assertDatabaseHas('tenant_memberships', [
'tenant_id' => (int) $tenant->getKey(),
'user_id' => (int) $user->getKey(),
]);
$this->actingAs($user);
Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace])
->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme'])
->assertOk();
$this->assertDatabaseHas('tenants', [
'id' => (int) $tenant->getKey(),
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => $tenantGuid,
]);
});
it('auto-selects the default provider connection and allows switching', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $user->getKey(),
'role' => 'owner',
]);
$tenantGuid = '99999999-9999-9999-9999-999999999999';
$tenant = Tenant::factory()->create([
'workspace_id' => $workspace->getKey(),
'tenant_id' => $tenantGuid,
'name' => 'Acme',
'status' => 'pending',
]);
$default = ProviderConnection::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => $tenantGuid,
'display_name' => 'Default',
'is_default' => true,
]);
$other = ProviderConnection::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft_alt',
'entra_tenant_id' => $tenantGuid,
'display_name' => 'Other',
'is_default' => false,
]);
$this->actingAs($user);
$component = Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace]);
$component->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
$component->assertSet('selectedProviderConnectionId', (int) $default->getKey());
$session = TenantOnboardingSession::query()
->where('workspace_id', (int) $workspace->getKey())
->where('tenant_id', (int) $tenant->getKey())
->firstOrFail();
expect($session->state['provider_connection_id'] ?? null)->toBe((int) $default->getKey());
$component->call('selectProviderConnection', (int) $other->getKey());
$component->assertSet('selectedProviderConnectionId', (int) $other->getKey());
$session->refresh();
expect($session->state['provider_connection_id'] ?? null)->toBe((int) $other->getKey());
});
it('dedupes verification runs: starting verification twice returns the active run and dispatches only once', function (): void {
Bus::fake();
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $user->getKey(),
'role' => 'owner',
]);
$this->actingAs($user);
$tenantGuid = '77777777-7777-7777-7777-777777777777';
$component = Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace]);
$component->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
$tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail();
$connection = ProviderConnection::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'is_default' => true,
]);
$component->set('selectedProviderConnectionId', (int) $connection->getKey());
$component->call('startVerification');
$component->call('startVerification');
expect(OperationRun::query()
->where('tenant_id', (int) $tenant->getKey())
->where('type', 'provider.connection.check')
->count())->toBe(1);
Bus::assertDispatchedTimes(\App\Jobs\ProviderConnectionHealthCheckJob::class, 1);
$this->assertDatabaseHas('audit_logs', [
'workspace_id' => (int) $workspace->getKey(),
'actor_id' => (int) $user->getKey(),
'action' => 'managed_tenant_onboarding.verification_start',
'resource_type' => 'operation_run',
]);
$session = TenantOnboardingSession::query()
->where('workspace_id', (int) $workspace->getKey())
->where('tenant_id', (int) $tenant->getKey())
->firstOrFail();
expect($session->state['verification_operation_run_id'] ?? null)->not->toBeNull();
});
it('creates a provider connection with encrypted credentials and does not persist secrets in session state', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $user->getKey(),
'role' => 'owner',
]);
$this->actingAs($user);
$tenantGuid = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
$secret = 'super-secret-123';
$component = Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace]);
$component
->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme'])
->call('createProviderConnection', [
'display_name' => 'Onboarding Connection',
'client_id' => 'client-id-1',
'client_secret' => $secret,
'is_default' => true,
])
->assertDontSee($secret);
$tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail();
$connection = ProviderConnection::query()
->where('tenant_id', (int) $tenant->getKey())
->where('provider', 'microsoft')
->where('entra_tenant_id', $tenantGuid)
->firstOrFail();
expect($connection->credential)->toBeInstanceOf(ProviderCredential::class);
expect($connection->credential->toArray())->not->toHaveKey('payload');
$session = TenantOnboardingSession::query()
->where('workspace_id', (int) $workspace->getKey())
->where('tenant_id', (int) $tenant->getKey())
->firstOrFail();
$state = $session->state ?? [];
expect($state)->not->toHaveKey('client_id');
expect($state)->not->toHaveKey('client_secret');
expect($state['provider_connection_id'] ?? null)->toBe((int) $connection->getKey());
});
it('starts verification, creates an operation run, dispatches the job, and does not include secrets in run context', function (): void {
Bus::fake();
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $user->getKey(),
'role' => 'owner',
]);
$this->actingAs($user);
$tenantGuid = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
$component = Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace]);
$component->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
$tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail();
$connection = ProviderConnection::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => $tenantGuid,
'is_default' => true,
]);
$component->set('selectedProviderConnectionId', (int) $connection->getKey());
$component->call('startVerification');
$run = OperationRun::query()
->where('tenant_id', (int) $tenant->getKey())
->where('type', 'provider.connection.check')
->latest('id')
->firstOrFail();
expect($run->context)->toBeArray();
expect($run->context['provider_connection_id'] ?? null)->toBe((int) $connection->getKey());
expect($run->context)->not->toHaveKey('client_id');
expect($run->context)->not->toHaveKey('client_secret');
Bus::assertDispatched(\App\Jobs\ProviderConnectionHealthCheckJob::class);
});
it('can resume the latest onboarding session as a different authorized workspace member', function (): void {
Bus::fake();
$workspace = Workspace::factory()->create();
$initiator = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $initiator->getKey(),
'role' => 'owner',
]);
$resumer = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $resumer->getKey(),
'role' => 'manager',
]);
$tenantGuid = 'cccccccc-cccc-cccc-cccc-cccccccccccc';
$this->actingAs($initiator);
$component = Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace]);
$component->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
$tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail();
$connection = ProviderConnection::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => $tenantGuid,
'is_default' => true,
]);
$session = TenantOnboardingSession::query()
->where('workspace_id', (int) $workspace->getKey())
->where('tenant_id', (int) $tenant->getKey())
->firstOrFail();
$session->update([
'state' => array_merge($session->state ?? [], [
'provider_connection_id' => (int) $connection->getKey(),
]),
]);
$this->actingAs($resumer);
Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace])
->call('startVerification');
Bus::assertDispatched(\App\Jobs\ProviderConnectionHealthCheckJob::class);
});
it('completes onboarding only after verification succeeded and redirects to tenant dashboard', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $user->getKey(),
'role' => 'owner',
]);
$this->actingAs($user);
$tenantGuid = 'dddddddd-dddd-dddd-dddd-dddddddddddd';
$component = Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace]);
$component->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
$tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail();
$connection = ProviderConnection::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => $tenantGuid,
'is_default' => true,
]);
$run = OperationRun::query()->create([
'tenant_id' => (int) $tenant->getKey(),
'user_id' => (int) $user->getKey(),
'initiator_name' => $user->name,
'type' => 'provider.connection.check',
'status' => 'completed',
'outcome' => 'succeeded',
'run_identity_hash' => sha1('verify-ok-'.(string) $connection->getKey()),
'context' => [
'provider_connection_id' => (int) $connection->getKey(),
],
]);
$session = TenantOnboardingSession::query()
->where('workspace_id', (int) $workspace->getKey())
->where('tenant_id', (int) $tenant->getKey())
->firstOrFail();
$session->update([
'state' => array_merge($session->state ?? [], [
'provider_connection_id' => (int) $connection->getKey(),
'verification_operation_run_id' => (int) $run->getKey(),
]),
]);
$component
->call('completeOnboarding')
->assertRedirect(TenantDashboard::getUrl(tenant: $tenant));
$tenant->refresh();
expect($tenant->status)->toBe('active');
$session->refresh();
expect($session->completed_at)->not->toBeNull();
});
it('starts one selected bootstrap action at a time and persists the remaining selections', function (): void {
Bus::fake();
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $user->getKey(),
'role' => 'owner',
]);
$this->actingAs($user);
$tenantGuid = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee';
$component = Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace]);
$component->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
$tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail();
$connection = ProviderConnection::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => $tenantGuid,
'is_default' => true,
]);
$verificationRun = OperationRun::query()->create([
'tenant_id' => (int) $tenant->getKey(),
'user_id' => (int) $user->getKey(),
'initiator_name' => $user->name,
'type' => 'provider.connection.check',
'status' => 'completed',
'outcome' => 'succeeded',
'run_identity_hash' => sha1('verify-ok-bootstrap-'.(string) $connection->getKey()),
'context' => [
'provider_connection_id' => (int) $connection->getKey(),
],
]);
$session = TenantOnboardingSession::query()
->where('workspace_id', (int) $workspace->getKey())
->where('tenant_id', (int) $tenant->getKey())
->firstOrFail();
$session->update([
'state' => array_merge($session->state ?? [], [
'provider_connection_id' => (int) $connection->getKey(),
'verification_operation_run_id' => (int) $verificationRun->getKey(),
]),
]);
$component->call('startBootstrap', [
'inventory_sync' => true,
'compliance.snapshot' => true,
]);
Bus::assertDispatchedTimes(\App\Jobs\ProviderInventorySyncJob::class, 1);
Bus::assertNotDispatched(\App\Jobs\ProviderComplianceSnapshotJob::class);
expect(OperationRun::query()
->where('tenant_id', (int) $tenant->getKey())
->where('type', 'inventory_sync')
->count())->toBe(1);
expect(OperationRun::query()
->where('tenant_id', (int) $tenant->getKey())
->where('type', 'compliance.snapshot')
->count())->toBe(0);
$session->refresh();
$runs = $session->state['bootstrap_operation_runs'] ?? [];
expect($runs)->toBeArray();
expect($runs['inventory_sync'] ?? null)->toBeInt();
expect($runs['compliance.snapshot'] ?? null)->toBeNull();
expect($session->state['bootstrap_operation_types'] ?? null)->toBe(['inventory_sync', 'compliance.snapshot']);
});
it('starts the next pending bootstrap action after the prior one completes successfully', function (): void {
Bus::fake();
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $user->getKey(),
'role' => 'owner',
]);
$this->actingAs($user);
$tenantGuid = 'ffffffff-ffff-ffff-ffff-ffffffffffff';
$component = Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace]);
$component->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
$tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail();
$connection = ProviderConnection::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => $tenantGuid,
'is_default' => true,
]);
$verificationRun = OperationRun::query()->create([
'tenant_id' => (int) $tenant->getKey(),
'user_id' => (int) $user->getKey(),
'initiator_name' => $user->name,
'type' => 'provider.connection.check',
'status' => 'completed',
'outcome' => 'succeeded',
'run_identity_hash' => sha1('verify-ok-bootstrap-next-'.(string) $connection->getKey()),
'context' => [
'provider_connection_id' => (int) $connection->getKey(),
],
]);
$session = TenantOnboardingSession::query()
->where('workspace_id', (int) $workspace->getKey())
->where('tenant_id', (int) $tenant->getKey())
->firstOrFail();
$session->update([
'state' => array_merge($session->state ?? [], [
'provider_connection_id' => (int) $connection->getKey(),
'verification_operation_run_id' => (int) $verificationRun->getKey(),
]),
]);
$component->call('startBootstrap', ['inventory_sync', 'compliance.snapshot']);
$inventoryRun = OperationRun::query()
->where('tenant_id', (int) $tenant->getKey())
->where('type', 'inventory_sync')
->latest('id')
->firstOrFail();
$inventoryRun->forceFill([
'status' => 'completed',
'outcome' => 'succeeded',
])->save();
$component->call('startBootstrap', ['inventory_sync', 'compliance.snapshot']);
Bus::assertDispatchedTimes(\App\Jobs\ProviderInventorySyncJob::class, 1);
Bus::assertDispatchedTimes(\App\Jobs\ProviderComplianceSnapshotJob::class, 1);
expect(OperationRun::query()
->where('tenant_id', (int) $tenant->getKey())
->where('type', 'compliance.snapshot')
->count())->toBe(1);
$session->refresh();
$runs = $session->state['bootstrap_operation_runs'] ?? [];
expect($runs['inventory_sync'] ?? null)->toBeInt();
expect($runs['compliance.snapshot'] ?? null)->toBeInt();
});
it('returns scope-busy semantics for verification when another run is active for the same connection scope', function (): void {
Bus::fake();
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $user->getKey(),
'role' => 'owner',
]);
$this->actingAs($user);
$tenantGuid = '88888888-8888-8888-8888-888888888888';
$component = Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace]);
$component->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
$tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail();
$connection = ProviderConnection::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'is_default' => true,
]);
OperationRun::query()->create([
'tenant_id' => (int) $tenant->getKey(),
'user_id' => (int) $user->getKey(),
'initiator_name' => $user->name,
'type' => 'inventory_sync',
'status' => 'queued',
'outcome' => 'pending',
'run_identity_hash' => sha1('busy-'.(string) $connection->getKey()),
'context' => [
'provider_connection_id' => (int) $connection->getKey(),
],
]);
$component->set('selectedProviderConnectionId', (int) $connection->getKey());
$component->call('startVerification');
expect(OperationRun::query()
->where('tenant_id', (int) $tenant->getKey())
->where('type', 'provider.connection.check')
->count())->toBe(0);
Bus::assertNotDispatched(\App\Jobs\ProviderConnectionHealthCheckJob::class);
});
it('fails a stale queued verification run and allows starting a new verification run', function (): void {
Bus::fake();
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $user->getKey(),
'role' => 'owner',
]);
$this->actingAs($user);
$tenantGuid = '99999999-9999-9999-9999-999999999999';
$component = Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace]);
$component->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
$tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail();
$connection = ProviderConnection::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => $tenantGuid,
'is_default' => true,
]);
$staleRun = OperationRun::query()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'initiator_name' => $user->name,
'type' => 'provider.connection.check',
'status' => 'queued',
'outcome' => 'pending',
'run_identity_hash' => sha1('stale-queued-verify-'.(string) $connection->getKey()),
'context' => [
'provider_connection_id' => (int) $connection->getKey(),
],
'created_at' => now()->subMinutes(10),
'updated_at' => now()->subMinutes(10),
]);
$component->set('selectedProviderConnectionId', (int) $connection->getKey());
$component->call('startVerification');
$staleRun->refresh();
expect($staleRun->status)->toBe('completed');
expect($staleRun->outcome)->toBe('failed');
expect($staleRun->context)->toBeArray();
expect($staleRun->context['verification_report'] ?? null)->toBeArray();
$report = $staleRun->context['verification_report'] ?? null;
expect($report['checks'] ?? null)->toBeArray();
expect($report['checks'][0]['message'] ?? null)->toBe('Run was queued but never started. A queue worker may not be running.');
$newRun = OperationRun::query()
->where('tenant_id', (int) $tenant->getKey())
->where('type', 'provider.connection.check')
->latest('id')
->firstOrFail();
expect((int) $newRun->getKey())->not->toBe((int) $staleRun->getKey());
Bus::assertDispatched(\App\Jobs\ProviderConnectionHealthCheckJob::class);
});
it('registers the onboarding capability in the canonical registry', function (): void {
expect(Capabilities::isKnown(Capabilities::WORKSPACE_MANAGED_TENANT_ONBOARD))->toBeTrue();
});
it('maps onboarding capability to owner and manager workspace roles', function (): void {
expect(WorkspaceRoleCapabilityMap::hasCapability('owner', Capabilities::WORKSPACE_MANAGED_TENANT_ONBOARD))->toBeTrue();
expect(WorkspaceRoleCapabilityMap::hasCapability('manager', Capabilities::WORKSPACE_MANAGED_TENANT_ONBOARD))->toBeTrue();
expect(WorkspaceRoleCapabilityMap::hasCapability('operator', Capabilities::WORKSPACE_MANAGED_TENANT_ONBOARD))->toBeFalse();
expect(WorkspaceRoleCapabilityMap::hasCapability('readonly', Capabilities::WORKSPACE_MANAGED_TENANT_ONBOARD))->toBeFalse();
});
it('authorizes onboarding via Gate for owner and manager memberships', function (): void {
$workspace = Workspace::factory()->create();
$owner = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $owner->getKey(),
'role' => 'owner',
]);
$manager = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $manager->getKey(),
'role' => 'manager',
]);
$readonly = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => $workspace->getKey(),
'user_id' => $readonly->getKey(),
'role' => 'readonly',
]);
expect(Gate::forUser($owner)->allows(Capabilities::WORKSPACE_MANAGED_TENANT_ONBOARD, $workspace))->toBeTrue();
expect(Gate::forUser($manager)->allows(Capabilities::WORKSPACE_MANAGED_TENANT_ONBOARD, $workspace))->toBeTrue();
expect(Gate::forUser($readonly)->allows(Capabilities::WORKSPACE_MANAGED_TENANT_ONBOARD, $workspace))->toBeFalse();
});
it('keeps filament tenant routing key stable (external_id resolves /admin/t/{tenant})', function (): void {
[$user, $tenant] = createUserWithTenant(
Tenant::factory()->create([
'workspace_id' => null,
'tenant_id' => '11111111-1111-1111-1111-111111111111',
]),
role: 'owner',
);
$this->actingAs($user)
->get(TenantDashboard::getUrl(tenant: $tenant))
->assertSuccessful();
$tenant->refresh();
expect($tenant->external_id)->toBe($tenant->tenant_id);
});
it('can persist a tenant onboarding session row', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$session = TenantOnboardingSession::query()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'current_step' => 'identify',
'state' => ['example' => 'value'],
'started_by_user_id' => (int) $user->getKey(),
'updated_by_user_id' => (int) $user->getKey(),
]);
expect($session->exists)->toBeTrue();
$this->assertDatabaseHas('managed_tenant_onboarding_sessions', [
'id' => $session->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'current_step' => 'identify',
]);
});
*/