create(); $user = User::factory()->create(); $this->actingAs($user) ->get("/admin/w/{$workspace->getKey()}/managed-tenants/onboarding") ->assertNotFound(); }); it('returns 403 for workspace members without onboarding capability', function (): void { $workspace = Workspace::factory()->create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => $workspace->getKey(), 'user_id' => $user->getKey(), 'role' => 'readonly', ]); $this->actingAs($user) ->get("/admin/w/{$workspace->getKey()}/managed-tenants/onboarding") ->assertForbidden(); }); it('renders onboarding wizard for workspace owners', 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) ->get("/admin/w/{$workspace->getKey()}/managed-tenants/onboarding") ->assertSuccessful(); }); it('allows owners to identify a managed tenant and creates a pending tenant + session', 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 = '11111111-1111-1111-1111-111111111111'; Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace]) ->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']); $tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail(); expect((int) $tenant->workspace_id)->toBe((int) $workspace->getKey()); expect($tenant->status)->toBe('pending'); $this->assertDatabaseHas('managed_tenant_onboarding_sessions', [ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'current_step' => 'identify', ]); $this->assertDatabaseHas('tenant_memberships', [ 'tenant_id' => (int) $tenant->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'owner', ]); expect( (int) \App\Models\TenantMembership::query() ->where('tenant_id', $tenant->getKey()) ->where('role', 'owner') ->count() )->toBe(1); }); it('upgrades the initiating user to owner if they already have a lower tenant role', function (): void { $workspace = Workspace::factory()->create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => $workspace->getKey(), 'user_id' => $user->getKey(), 'role' => 'owner', ]); $tenantGuid = '66666666-6666-6666-6666-666666666666'; $tenant = Tenant::factory()->create([ 'workspace_id' => $workspace->getKey(), 'tenant_id' => $tenantGuid, 'name' => 'Acme', 'status' => 'pending', ]); \App\Models\TenantMembership::query()->create([ 'tenant_id' => (int) $tenant->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'readonly', 'source' => 'manual', 'created_by_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']); $membership = \App\Models\TenantMembership::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('user_id', (int) $user->getKey()) ->firstOrFail(); expect($membership->role)->toBe('owner'); expect(\App\Models\TenantMembership::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('user_id', (int) $user->getKey()) ->count())->toBe(1); }); it('writes audit logs for onboarding start and resume', 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 = '44444444-4444-4444-4444-444444444444'; $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']); $tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail(); $this->assertDatabaseHas('audit_logs', [ 'workspace_id' => (int) $workspace->getKey(), 'actor_id' => (int) $user->getKey(), 'action' => 'managed_tenant_onboarding.start', 'resource_type' => 'tenant', 'resource_id' => (string) $tenant->getKey(), 'status' => 'success', ]); $this->assertDatabaseHas('audit_logs', [ 'workspace_id' => (int) $workspace->getKey(), 'actor_id' => (int) $user->getKey(), 'action' => 'managed_tenant_onboarding.resume', 'resource_type' => 'tenant', 'resource_id' => (string) $tenant->getKey(), 'status' => 'success', ]); expect(AuditLog::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('resource_type', 'tenant') ->where('resource_id', (string) $tenant->getKey()) ->whereIn('action', ['managed_tenant_onboarding.start', 'managed_tenant_onboarding.resume']) ->count())->toBeGreaterThanOrEqual(2); }); it('blocks demoting or removing the last remaining tenant owner', 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 = '55555555-5555-5555-5555-555555555555'; Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace]) ->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']); $tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail(); $membership = \App\Models\TenantMembership::query() ->where('tenant_id', $tenant->getKey()) ->where('user_id', $user->getKey()) ->firstOrFail(); expect(fn () => app(TenantMembershipManager::class)->changeRole($tenant, $user, $membership, 'manager')) ->toThrow(DomainException::class, 'You cannot demote the last remaining owner.'); expect(fn () => app(TenantMembershipManager::class)->removeMember($tenant, $user, $membership)) ->toThrow(DomainException::class, 'You cannot remove the last remaining owner.'); }); 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('is idempotent when identifying the same managed tenant twice', 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 = '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 selected bootstrap actions as separate operation runs and dispatches their jobs', 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', '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()) ->whereIn('type', ['inventory.sync', 'compliance.snapshot']) ->count())->toBe(2); $session->refresh(); $runs = $session->state['bootstrap_operation_runs'] ?? []; expect($runs)->toBeArray(); 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('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', ]); });