createMock(PermissionService::class); $permissionService->expects($this->once()) ->method('userHas') ->with(5, PermissionService::DEPARTMENTS_VIEW) ->willReturn(false); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->never())->method('getUserTenantIds'); $scopeGateway->expects($this->never())->method('isStrict'); $policy = new DepartmentAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_VIEW, [ 'actor_user_id' => 5, ]); $this->assertForbiddenDecision($decision); } public function testViewReturnsCreateCapabilityAndTenantScopeData(): void { $permissionService = $this->permissionGatewayAllowing([ 5 => [PermissionService::DEPARTMENTS_VIEW, PermissionService::DEPARTMENTS_CREATE], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('getUserTenantIds') ->with(5) ->willReturn([2, 3]); $scopeGateway->expects($this->once()) ->method('isStrict') ->willReturn(true); $policy = new DepartmentAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_VIEW, [ 'actor_user_id' => 5, ]); $capabilities = $decision->attribute('capabilities', []); $this->assertTrue($decision->isAllowed()); $this->assertTrue((bool) ($capabilities['can_create_department'] ?? false)); $this->assertSame([2, 3], $capabilities['allowed_tenant_ids'] ?? []); $this->assertTrue((bool) ($capabilities['is_strict_scope'] ?? false)); } public function testCreateRequiresDepartmentsCreatePermission(): void { $permissionService = $this->createMock(PermissionService::class); $permissionService->expects($this->once()) ->method('userHas') ->with(7, PermissionService::DEPARTMENTS_CREATE) ->willReturn(false); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->never())->method('getUserTenantIds'); $scopeGateway->expects($this->never())->method('isStrict'); $policy = new DepartmentAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_CREATE, [ 'actor_user_id' => 7, ]); $this->assertDeniedDecision($decision, 403, 'forbidden'); } public function testCreateDeniedInStrictScopeWithoutTenantAssignments(): void { $permissionService = $this->permissionGatewayAllowing([ 7 => [PermissionService::DEPARTMENTS_CREATE], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('getUserTenantIds') ->with(7) ->willReturn([]); $scopeGateway->expects($this->once()) ->method('isStrict') ->willReturn(true); $policy = new DepartmentAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_CREATE, [ 'actor_user_id' => 7, ]); $this->assertDeniedDecision($decision, 403, 'permission_denied'); } public function testCreateReturnsScopeCapabilitiesWhenAllowed(): void { $permissionService = $this->permissionGatewayAllowing([ 7 => [PermissionService::DEPARTMENTS_CREATE], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('getUserTenantIds') ->with(7) ->willReturn([4]); $scopeGateway->expects($this->once()) ->method('isStrict') ->willReturn(true); $policy = new DepartmentAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_CREATE, [ 'actor_user_id' => 7, ]); $capabilities = $decision->attribute('capabilities', []); $this->assertTrue($decision->isAllowed()); $this->assertSame([4], $capabilities['allowed_tenant_ids'] ?? []); $this->assertTrue((bool) ($capabilities['is_strict_scope'] ?? false)); } public function testEditContextRequiresDepartmentsViewPermission(): void { $permissionService = $this->createMock(PermissionService::class); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->never())->method('canAccess'); $policy = new DepartmentAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_EDIT_CONTEXT, [ 'actor_user_id' => 5, 'target_department_id' => 17, ]); $this->assertForbiddenDecision($decision); } public function testEditContextDeniesOutOfScopeDepartment(): void { $permissionService = $this->permissionGatewayAllowing([ 5 => [PermissionService::DEPARTMENTS_VIEW], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('departments', 17, 5) ->willReturn(false); $policy = new DepartmentAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_EDIT_CONTEXT, [ 'actor_user_id' => 5, 'target_department_id' => 17, ]); $this->assertDeniedDecision($decision, 403, 'permission_denied'); } public function testEditContextExposesDeleteCapabilityWhenDeletePermissionIsGranted(): void { $permissionService = $this->permissionGatewayAllowing([ 5 => [PermissionService::DEPARTMENTS_VIEW, PermissionService::DEPARTMENTS_DELETE], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('departments', 17, 5) ->willReturn(true); $scopeGateway->expects($this->once()) ->method('getUserTenantIds') ->with(5) ->willReturn([2]); $policy = new DepartmentAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_EDIT_CONTEXT, [ 'actor_user_id' => 5, 'target_department_id' => 17, ]); $capabilities = $decision->attribute('capabilities', []); $this->assertTrue($decision->isAllowed()); $this->assertTrue((bool) ($capabilities['can_delete_department'] ?? false)); } public function testEditContextExposesDeleteCapabilityAsFalseWithoutDeletePermission(): void { $permissionService = $this->permissionGatewayAllowing([ 5 => [PermissionService::DEPARTMENTS_VIEW], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('departments', 17, 5) ->willReturn(true); $scopeGateway->expects($this->once()) ->method('getUserTenantIds') ->with(5) ->willReturn([2]); $policy = new DepartmentAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_EDIT_CONTEXT, [ 'actor_user_id' => 5, 'target_department_id' => 17, ]); $capabilities = $decision->attribute('capabilities', []); $this->assertTrue($decision->isAllowed()); $this->assertFalse((bool) ($capabilities['can_delete_department'] ?? true)); } public function testEditSubmitRequiresDepartmentsUpdatePermission(): void { $permissionService = $this->permissionGatewayAllowing([ 5 => [PermissionService::DEPARTMENTS_VIEW], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('departments', 17, 5) ->willReturn(true); $scopeGateway->expects($this->once()) ->method('getUserTenantIds') ->with(5) ->willReturn([2]); $policy = new DepartmentAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_EDIT_SUBMIT, [ 'actor_user_id' => 5, 'target_department_id' => 17, ]); $this->assertDeniedDecision($decision, 403, 'forbidden'); } public function testDeleteRequiresPermissionAndScope(): void { $permissionService = $this->permissionGatewayAllowing([ 9 => [PermissionService::DEPARTMENTS_DELETE], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('departments', 44, 9) ->willReturn(true); $policy = new DepartmentAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_DELETE, [ 'actor_user_id' => 9, 'target_department_id' => 44, ]); $this->assertTrue($decision->isAllowed()); } public function testDeleteDeniesOutOfScopeDepartment(): void { $permissionService = $this->permissionGatewayAllowing([ 9 => [PermissionService::DEPARTMENTS_DELETE], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('departments', 44, 9) ->willReturn(false); $policy = new DepartmentAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_DELETE, [ 'actor_user_id' => 9, 'target_department_id' => 44, ]); $this->assertDeniedDecision($decision, 403, 'permission_denied'); } }