set('tenantpilot.features.conditional_access', false); }); function tenantWithApp(): Tenant { return Tenant::create([ 'tenant_id' => 'tenant-guid', 'name' => 'Tenant One', 'app_client_id' => 'client-123', 'app_client_secret' => 'secret', 'status' => 'active', ]); } test('rbac action prompts login when no delegated token', function () { $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->mountAction('setup_rbac') ->setActionData([ 'role_definition_id' => 'role-1', 'role_display_name' => 'Policy and Profile Manager', 'scope' => 'all_devices', 'group_mode' => 'create', ]) ->callMountedAction() ->assertHasNoActionErrors(); expect(Cache::get(RbacDelegatedAuthController::cacheKey($tenant, $user->id, session()->getId())))->toBeNull(); }); test('rbac action succeeds and clears token cache', function () { $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); $cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null); Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5)); app()->bind(GraphClientInterface::class, function () { return new class implements GraphClientInterface { public function listPolicies(string $policyType, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getOrganization(array $options = []): GraphResponse { return new GraphResponse(true, []); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(true, []); } public function request(string $method, string $path, array $options = []): GraphResponse { $filter = $options['query']['$filter'] ?? ''; if ($method === 'GET' && $path === 'servicePrincipals') { return new GraphResponse(true, ['value' => [['id' => 'sp-1']]]); } if ($method === 'GET' && $path === 'groups' && str_contains($filter, 'displayName eq')) { return new GraphResponse(true, ['value' => []]); } if ($method === 'POST' && $path === 'groups') { return new GraphResponse(true, ['id' => 'group-1']); } if ($method === 'POST' && str_contains($path, '/members/$ref')) { return new GraphResponse(true, []); } if ($method === 'GET' && $path === 'deviceManagement/roleDefinitions') { return new GraphResponse(true, ['value' => [['id' => 'role-1', 'displayName' => 'Policy and Profile Manager']]]); } if ($method === 'GET' && $path === 'deviceManagement/roleAssignments') { return new GraphResponse(true, ['value' => []]); } if ($method === 'POST' && $path === 'deviceManagement/roleAssignments') { return new GraphResponse(true, ['id' => 'assign-1']); } if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceConfigurations')) { return new GraphResponse(true, ['value' => []]); } if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceCompliancePolicies')) { return new GraphResponse(true, ['value' => []]); } return new GraphResponse(true, ['value' => []]); } }; }); Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->mountAction('setup_rbac') ->setActionData([ 'role_definition_id' => 'role-1', 'role_display_name' => 'Policy and Profile Manager', 'scope' => 'all_devices', 'group_mode' => 'create', ]) ->callMountedAction() ->assertHasNoActionErrors(); $tenant->refresh(); expect($tenant->rbac_group_id)->toBe('group-1'); expect($tenant->rbac_role_assignment_id)->toBe('assign-1'); expect($tenant->rbac_canary_results)->toMatchArray([ 'deviceConfigurations' => 'ok', 'deviceCompliancePolicies' => 'ok', ]); expect($tenant->rbac_last_warnings)->toContain('ca_canary_disabled'); expect(Cache::has($cacheKey))->toBeFalse(); }); test('rbac action is idempotent on rerun', function () { $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); $cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null); Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5)); app()->bind(GraphClientInterface::class, function () { return new class implements GraphClientInterface { public function listPolicies(string $policyType, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getOrganization(array $options = []): GraphResponse { return new GraphResponse(true, []); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(true, []); } public function request(string $method, string $path, array $options = []): GraphResponse { if ($method === 'GET' && $path === 'servicePrincipals') { return new GraphResponse(true, ['value' => [['id' => 'sp-1']]]); } if ($method === 'POST' && str_contains($path, '/members/$ref')) { return new GraphResponse(false, [], 400, [ [ 'error' => [ 'code' => 'Request_BadRequest', 'message' => 'One or more added object references already exist for the following modified objects', ], ], ]); } if ($method === 'GET' && $path === 'deviceManagement/roleDefinitions') { return new GraphResponse(true, ['value' => [['id' => 'role-1', 'displayName' => 'Policy and Profile Manager']]]); } if ($method === 'GET' && $path === 'deviceManagement/roleAssignments') { return new GraphResponse(true, ['value' => [[ 'id' => 'assign-1', 'members' => ['group-1'], 'resourceScopes' => ['/'], 'roleDefinition' => ['id' => 'role-1'], ]]]); } if ($method === 'PATCH' && str_contains($path, 'deviceManagement/roleAssignments/assign-1')) { return new GraphResponse(true, ['id' => 'assign-1']); } if ($method === 'GET' && str_starts_with($path, 'groups/')) { $id = str_replace('groups/', '', $path); return new GraphResponse(true, ['id' => $id, 'displayName' => "Group {$id}"]); } if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceConfigurations')) { return new GraphResponse(true, ['value' => []]); } if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceCompliancePolicies')) { return new GraphResponse(true, ['value' => []]); } return new GraphResponse(true, ['value' => []]); } }; }); Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->mountAction('setup_rbac') ->setActionData([ 'role_definition_id' => 'role-1', 'role_display_name' => 'Policy and Profile Manager', 'scope' => 'scope_group', 'scope_group_id' => 'group-scope', 'group_mode' => 'existing', 'existing_group_id' => 'group-1', ]) ->callMountedAction() ->assertHasNoActionErrors(); $tenant->refresh(); expect($tenant->rbac_group_id)->toBe('group-1'); expect($tenant->rbac_role_assignment_id)->toBe('assign-1'); expect($tenant->rbac_scope_mode)->toBe('scope_group'); expect($tenant->rbac_last_warnings)->toContain('scope_limited'); expect($tenant->rbac_status)->toBe('ok'); }); test('existing group membership error from Graph json payload is treated idempotently', function () { $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); $cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null); Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5)); app()->bind(GraphClientInterface::class, function () { return new class implements GraphClientInterface { public function listPolicies(string $policyType, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getOrganization(array $options = []): GraphResponse { return new GraphResponse(true, []); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(true, []); } public function request(string $method, string $path, array $options = []): GraphResponse { if ($method === 'GET' && $path === 'servicePrincipals') { return new GraphResponse(true, ['value' => [['id' => 'sp-1']]]); } if ($method === 'GET' && $path === 'groups' && str_contains($options['query']['$filter'] ?? '', 'displayName')) { return new GraphResponse(true, ['value' => [['id' => 'group-1', 'displayName' => 'Existing Group']]]); } if ($method === 'POST' && str_contains($path, '/members/$ref')) { return new GraphResponse(false, [], 400, [ [ 'error' => [ 'code' => 'Request_BadRequest', 'message' => 'One or more added object references already exist for the following modified objects', ], ], ]); } if ($method === 'GET' && $path === 'deviceManagement/roleDefinitions') { return new GraphResponse(true, ['value' => [['id' => 'role-1', 'displayName' => 'Policy and Profile Manager']]]); } if ($method === 'GET' && $path === 'deviceManagement/roleAssignments') { return new GraphResponse(true, ['value' => []]); } if ($method === 'POST' && $path === 'deviceManagement/roleAssignments') { return new GraphResponse(true, ['id' => 'assign-1']); } if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceConfigurations')) { return new GraphResponse(true, ['value' => []]); } if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceCompliancePolicies')) { return new GraphResponse(true, ['value' => []]); } return new GraphResponse(true, ['value' => []]); } }; }); Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->mountAction('setup_rbac') ->setActionData([ 'role_definition_id' => 'role-1', 'role_display_name' => 'Policy and Profile Manager', 'scope' => 'all_devices', 'group_mode' => 'existing', 'existing_group_id' => 'group-1', ]) ->callMountedAction() ->assertHasNoActionErrors(); $tenant->refresh(); expect($tenant->rbac_group_id)->toBe('group-1'); expect($tenant->rbac_role_assignment_id)->toBe('assign-1'); expect($tenant->rbac_status)->toBe('ok'); }); test('group picker is disabled without delegated token', function () { $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->mountAction('setup_rbac') ->setActionData([ 'group_mode' => 'existing', ]) ->assertFormFieldDisabled('existing_group_id'); expect(\App\Filament\Resources\TenantResource::groupSearchHelper($tenant))->toBe('Login to search groups'); }); test('group picker toggles when switching modes', function () { $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->mountAction('setup_rbac') ->setActionData([ 'group_mode' => 'existing', ]) ->assertFormFieldVisible('existing_group_id') ->assertFormFieldHidden('group_name'); }); test('delegated group search returns options and persists selection', function () { $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); $cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null); Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5)); app()->bind(GraphClientInterface::class, function () { return new class implements GraphClientInterface { public function listPolicies(string $policyType, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getOrganization(array $options = []): GraphResponse { return new GraphResponse(true, []); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(true, []); } public function request(string $method, string $path, array $options = []): GraphResponse { $filter = $options['query']['$filter'] ?? ''; if ($method === 'GET' && $path === 'servicePrincipals') { return new GraphResponse(true, ['value' => [['id' => 'sp-1']]]); } if ($method === 'GET' && $path === 'groups' && str_contains($filter, 'securityEnabled eq true')) { return new GraphResponse(true, ['value' => [ ['id' => 'group-123', 'displayName' => 'Ops Team'], ['id' => 'group-456', 'displayName' => 'Helpdesk'], ]]); } if ($method === 'GET' && str_starts_with($path, 'groups/')) { $id = str_replace('groups/', '', $path); return new GraphResponse(true, ['id' => $id, 'displayName' => 'Ops Team']); } if ($method === 'POST' && str_contains($path, '/members/$ref')) { return new GraphResponse(true, []); } if ($method === 'GET' && $path === 'deviceManagement/roleDefinitions') { return new GraphResponse(true, ['value' => [['id' => 'role-1', 'displayName' => 'Policy and Profile Manager']]]); } if ($method === 'GET' && $path === 'deviceManagement/roleAssignments') { return new GraphResponse(true, ['value' => []]); } if ($method === 'POST' && $path === 'deviceManagement/roleAssignments') { return new GraphResponse(true, ['id' => 'assign-1']); } if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceConfigurations')) { return new GraphResponse(true, ['value' => []]); } if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceCompliancePolicies')) { return new GraphResponse(true, ['value' => []]); } return new GraphResponse(true, ['value' => []]); } }; }); $options = \App\Filament\Resources\TenantResource::groupSearchOptions($tenant, 'Ops'); expect($options)->toHaveKey('group-123'); expect($options['group-123'])->toContain('Ops Team'); Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->mountAction('setup_rbac') ->setActionData([ 'group_mode' => 'existing', 'role_definition_id' => 'role-1', 'role_display_name' => 'Policy and Profile Manager', 'scope' => 'all_devices', 'existing_group_id' => 'group-123', ]) ->assertFormFieldEnabled('existing_group_id') ->callMountedAction() ->assertHasNoActionErrors(); $tenant->refresh(); expect($tenant->rbac_group_id)->toBe('group-123'); expect($tenant->rbac_role_assignment_id)->toBe('assign-1'); expect(Cache::has($cacheKey))->toBeFalse(); }); test('delegated role search returns options and persists role definition id', function () { $tenant = tenantWithApp(); $user = User::factory()->create(); $this->actingAs($user); $cacheKey = RbacDelegatedAuthController::cacheKey($tenant, $user->id, null); Cache::put($cacheKey, 'delegated-token', now()->addMinutes(5)); app()->bind(GraphClientInterface::class, function () { return new class implements GraphClientInterface { public function listPolicies(string $policyType, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getOrganization(array $options = []): GraphResponse { return new GraphResponse(true, []); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { return new GraphResponse(true, []); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(true, []); } public function request(string $method, string $path, array $options = []): GraphResponse { $filter = $options['query']['$filter'] ?? ''; if ($method === 'GET' && $path === 'deviceManagement/roleDefinitions') { return new GraphResponse(true, ['value' => [ ['id' => 'role-1', 'displayName' => 'Policy and Profile Manager'], ['id' => 'role-2', 'displayName' => 'Helpdesk Operator'], ]]); } if ($method === 'GET' && $path === 'deviceManagement/roleDefinitions/role-1') { return new GraphResponse(true, ['id' => 'role-1', 'displayName' => 'Policy and Profile Manager']); } if ($method === 'GET' && $path === 'servicePrincipals') { return new GraphResponse(true, ['value' => [['id' => 'sp-1']]]); } if ($method === 'GET' && $path === 'groups' && str_contains($filter, 'displayName eq')) { return new GraphResponse(true, ['value' => []]); } if ($method === 'POST' && $path === 'groups') { return new GraphResponse(true, ['id' => 'group-1']); } if ($method === 'POST' && str_contains($path, '/members/$ref')) { return new GraphResponse(true, []); } if ($method === 'GET' && $path === 'deviceManagement/roleAssignments') { return new GraphResponse(true, ['value' => []]); } if ($method === 'POST' && $path === 'deviceManagement/roleAssignments') { return new GraphResponse(true, ['id' => 'assign-1']); } if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceConfigurations')) { return new GraphResponse(true, ['value' => []]); } if ($method === 'GET' && str_starts_with($path, 'deviceManagement/deviceCompliancePolicies')) { return new GraphResponse(true, ['value' => []]); } return new GraphResponse(true, ['value' => []]); } }; }); $roles = \App\Filament\Resources\TenantResource::roleSearchOptions($tenant, 'Policy'); expect($roles)->toHaveKey('role-1'); expect($roles['role-1'])->toContain('Policy and Profile Manager'); Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->mountAction('setup_rbac') ->setActionData([ 'role_definition_id' => 'role-1', 'role_display_name' => 'Policy and Profile Manager', 'scope' => 'all_devices', 'group_mode' => 'create', ]) ->callMountedAction() ->assertHasNoActionErrors(); $tenant->refresh(); expect($tenant->rbac_role_definition_id)->toBe('role-1'); expect($tenant->rbac_role_display_name)->toBe('Policy and Profile Manager'); expect(Cache::has($cacheKey))->toBeFalse(); });