create(); $user = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $user->getKey(), 'role' => 'readonly', ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); Livewire::actingAs($user) ->test(ManagedTenantOnboardingWizard::class) ->assertForbidden(); }); it('denies dedicated provider connection creation for manager members', function (): void { $workspace = Workspace::factory()->create(); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'status' => Tenant::STATUS_ONBOARDING, ]); $user = User::factory()->create(); createUserWithTenant( tenant: $tenant, user: $user, role: 'manager', workspaceRole: 'manager', ensureDefaultMicrosoftProviderConnection: false, ); $draft = createOnboardingDraft([ 'workspace' => $workspace, 'tenant' => $tenant, 'started_by' => $user, 'updated_by' => $user, 'current_step' => 'connection', 'state' => [ 'entra_tenant_id' => (string) $tenant->tenant_id, 'tenant_name' => (string) $tenant->name, ], ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); Livewire::actingAs($user) ->test(ManagedTenantOnboardingWizard::class, [ 'onboardingDraft' => (int) $draft->getKey(), ]) ->call('createProviderConnection', [ 'display_name' => 'Acme connection', 'client_id' => '00000000-0000-0000-0000-000000000000', 'client_secret' => 'super-secret', 'is_default' => true, ]) ->assertForbidden(); }); it('allows manager members to start verification for an existing onboarding session', function (): void { Queue::fake(); $workspace = Workspace::factory()->create(); $user = User::factory()->create(); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'status' => Tenant::STATUS_ONBOARDING, ]); createUserWithTenant( tenant: $tenant, user: $user, role: 'manager', workspaceRole: 'manager', ensureDefaultMicrosoftProviderConnection: false, ); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $connection = ProviderConnection::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'entra_tenant_id' => (string) $tenant->tenant_id, ]); TenantOnboardingSession::query()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'entra_tenant_id' => (string) $tenant->tenant_id, 'current_step' => 'connection', 'state' => [ 'provider_connection_id' => (int) $connection->getKey(), ], 'started_by_user_id' => (int) $user->getKey(), 'updated_by_user_id' => (int) $user->getKey(), ]); $draft = TenantOnboardingSession::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('tenant_id', (int) $tenant->getKey()) ->firstOrFail(); Livewire::actingAs($user) ->test(ManagedTenantOnboardingWizard::class, [ 'onboardingDraft' => (int) $draft->getKey(), ]) ->call('startVerification'); $run = OperationRun::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('type', 'provider.connection.check') ->latest('id') ->first(); expect($run)->not->toBeNull(); Queue::assertNothingPushed(); }); it('returns 403 for in-scope members missing onboarding capability on assist-backed drafts', function (): void { $configured = array_merge( config('intune_permissions.permissions', []), config('entra_permissions.permissions', []), ); $permissionKeys = array_values(array_filter(array_map(static function (mixed $permission): ?string { if (! is_array($permission)) { return null; } $key = $permission['key'] ?? null; return is_string($key) && trim($key) !== '' ? trim($key) : null; }, $configured))); if ($permissionKeys === []) { test()->markTestSkipped('No configured required permissions found.'); } [$user, $tenant] = createUserWithTenant( role: 'operator', workspaceRole: 'operator', ensureDefaultMicrosoftProviderConnection: false, ); $workspace = $tenant->workspace()->firstOrFail(); foreach ($permissionKeys as $key) { TenantPermission::query()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $workspace->getKey(), 'permission_key' => $key, 'status' => 'granted', 'details' => ['source' => 'db'], 'last_checked_at' => now()->subDays(45), ]); } $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, 'is_default' => true, 'status' => 'connected', ]); $run = OperationRun::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'type' => 'provider.connection.check', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Succeeded->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' => 'warn', 'severity' => 'medium', 'blocking' => false, 'reason_code' => ProviderReasonCodes::ProviderPermissionRefreshFailed, 'message' => 'Stored permission data needs review.', 'evidence' => [], 'next_steps' => [], ], ]), ], ]); $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' => [ '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) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->get(route('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])) ->assertForbidden(); }); it('returns 403 when a manager tries to complete onboarding without owner capability', function (): void { $workspace = Workspace::factory()->create(); $user = User::factory()->create(); $tenant = Tenant::factory()->onboarding()->create([ 'workspace_id' => (int) $workspace->getKey(), ]); createUserWithTenant( tenant: $tenant, user: $user, role: 'manager', workspaceRole: 'manager', ensureDefaultMicrosoftProviderConnection: false, ); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); $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, 'is_default' => true, 'status' => 'connected', ]); $run = OperationRun::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'type' => 'provider.connection.check', 'status' => OperationRunStatus::Completed->value, 'outcome' => OperationRunOutcome::Succeeded->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' => 'consent', 'title' => 'Required application permissions', 'status' => 'pass', 'severity' => 'low', 'blocking' => false, 'reason_code' => 'ok', 'message' => 'Consent is ready.', 'evidence' => [], 'next_steps' => [], ], ]), ], ]); $draft = createOnboardingDraft([ 'workspace' => $workspace, 'tenant' => $tenant, 'started_by' => $user, 'updated_by' => $user, 'current_step' => 'bootstrap', 'state' => [ 'entra_tenant_id' => (string) $tenant->tenant_id, 'tenant_name' => (string) $tenant->name, 'provider_connection_id' => (int) $connection->getKey(), 'verification_operation_run_id' => (int) $run->getKey(), ], ]); Livewire::actingAs($user) ->test(ManagedTenantOnboardingWizard::class, [ 'onboardingDraft' => (int) $draft->getKey(), ]) ->call('completeOnboarding') ->assertForbidden(); }); });