fix: fix permission checks for roles/circles

This commit is contained in:
2026-03-13 16:40:12 +02:00
parent 9deb266f7f
commit 09a74a29c4
7 changed files with 502 additions and 667 deletions

View File

@@ -17,6 +17,7 @@ use OCA\Forum\Db\ReadMarkerMapper;
use OCA\Forum\Db\Role;
use OCA\Forum\Db\RoleMapper;
use OCA\Forum\Db\ThreadMapper;
use OCA\Forum\Service\PermissionService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\IRequest;
@@ -40,6 +41,8 @@ class CategoryControllerTest extends TestCase {
private ReadMarkerMapper $readMarkerMapper;
/** @var RoleMapper&MockObject */
private RoleMapper $roleMapper;
/** @var PermissionService&MockObject */
private PermissionService $permissionService;
/** @var IUserSession&MockObject */
private IUserSession $userSession;
/** @var LoggerInterface&MockObject */
@@ -55,6 +58,13 @@ class CategoryControllerTest extends TestCase {
$this->threadMapper = $this->createMock(ThreadMapper::class);
$this->readMarkerMapper = $this->createMock(ReadMarkerMapper::class);
$this->roleMapper = $this->createMock(RoleMapper::class);
$this->permissionService = $this->createMock(PermissionService::class);
// By default, grant access to all categories (tests that need filtering can override)
$this->permissionService->method('getAccessibleCategories')
->willReturnCallback(function () {
// Return IDs 1-100 to cover all test categories
return range(1, 100);
});
$this->userSession = $this->createMock(IUserSession::class);
$this->logger = $this->createMock(LoggerInterface::class);
@@ -67,6 +77,7 @@ class CategoryControllerTest extends TestCase {
$this->threadMapper,
$this->readMarkerMapper,
$this->roleMapper,
$this->permissionService,
$this->userSession,
$this->logger
);
@@ -592,31 +603,10 @@ class CategoryControllerTest extends TestCase {
$user->method('getUID')->willReturn($userId);
$this->userSession->method('getUser')->willReturn($user);
// User has a role
$role = new Role();
$role->setId(1);
$role->setName('User');
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with($userId)
->willReturn([$role]);
// Category permission allows viewing
$categoryPerm = new CategoryPerm();
$categoryPerm->setId(1);
$categoryPerm->setCategoryId($categoryId);
$categoryPerm->setTargetType('role');
$categoryPerm->setTargetId('1');
$categoryPerm->setCanView(true);
$categoryPerm->setCanPost(false);
$categoryPerm->setCanReply(false);
$categoryPerm->setCanModerate(false);
$this->categoryPermMapper->expects($this->once())
->method('findByCategoryAndRoles')
->with($categoryId, [1])
->willReturn([$categoryPerm]);
$this->permissionService->expects($this->once())
->method('hasCategoryPermission')
->with($userId, $categoryId, $permission)
->willReturn(true);
$response = $this->controller->checkPermission($categoryId, $permission);
@@ -634,29 +624,10 @@ class CategoryControllerTest extends TestCase {
$user->method('getUID')->willReturn($userId);
$this->userSession->method('getUser')->willReturn($user);
$role = new Role();
$role->setId(1);
$role->setName('User');
$this->roleMapper->expects($this->once())
->method('findByUserId')
->willReturn([$role]);
// Category permission does not allow moderating
$categoryPerm = new CategoryPerm();
$categoryPerm->setId(1);
$categoryPerm->setCategoryId($categoryId);
$categoryPerm->setTargetType('role');
$categoryPerm->setTargetId('1');
$categoryPerm->setCanView(true);
$categoryPerm->setCanPost(false);
$categoryPerm->setCanReply(false);
$categoryPerm->setCanModerate(false);
$this->categoryPermMapper->expects($this->once())
->method('findByCategoryAndRoles')
->with($categoryId, [1])
->willReturn([$categoryPerm]);
$this->permissionService->expects($this->once())
->method('hasCategoryPermission')
->with($userId, $categoryId, $permission)
->willReturn(false);
$response = $this->controller->checkPermission($categoryId, $permission);
@@ -674,16 +645,10 @@ class CategoryControllerTest extends TestCase {
$user->method('getUID')->willReturn($userId);
$this->userSession->method('getUser')->willReturn($user);
// User has Admin role
$adminRole = new Role();
$adminRole->setId(1);
$adminRole->setName('Admin');
$adminRole->setRoleType(Role::ROLE_TYPE_ADMIN);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with($userId)
->willReturn([$adminRole]);
$this->permissionService->expects($this->once())
->method('hasCategoryPermission')
->with($userId, $categoryId, $permission)
->willReturn(true);
$response = $this->controller->checkPermission($categoryId, $permission);

View File

@@ -4,404 +4,25 @@ declare(strict_types=1);
namespace OCA\Forum\Tests\Db;
use OCA\Forum\Db\Category;
use OCA\Forum\Db\CategoryMapper;
use OCA\Forum\Db\Role;
use OCA\Forum\Db\RoleMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IDBConnection;
use OCP\IUser;
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class CategoryMapperTest extends TestCase {
private CategoryMapper $mapper;
/** @var IDBConnection&MockObject */
private IDBConnection $db;
/** @var IUserSession&MockObject */
private IUserSession $userSession;
/** @var RoleMapper&MockObject */
private RoleMapper $roleMapper;
/** @var LoggerInterface&MockObject */
private LoggerInterface $logger;
protected function setUp(): void {
$this->db = $this->createMock(IDBConnection::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->roleMapper = $this->createMock(RoleMapper::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->mapper = new CategoryMapper(
$this->db,
$this->userSession,
$this->roleMapper,
$this->logger
);
}
public function testIsCurrentUserAdminReturnsTrueForAdminRole(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('admin1');
$this->userSession->method('getUser')->willReturn($user);
$adminRole = new Role();
$adminRole->setId(1);
$adminRole->setRoleType(Role::ROLE_TYPE_ADMIN);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with('admin1')
->willReturn([$adminRole]);
// Use reflection to call private method
$reflection = new \ReflectionClass($this->mapper);
$method = $reflection->getMethod('isCurrentUserAdmin');
$method->setAccessible(true);
$result = $method->invoke($this->mapper);
$this->assertTrue($result);
}
public function testIsCurrentUserAdminReturnsFalseForNonAdminRole(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user1');
$this->userSession->method('getUser')->willReturn($user);
$userRole = new Role();
$userRole->setId(3);
$userRole->setRoleType(Role::ROLE_TYPE_DEFAULT);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with('user1')
->willReturn([$userRole]);
// Use reflection to call private method
$reflection = new \ReflectionClass($this->mapper);
$method = $reflection->getMethod('isCurrentUserAdmin');
$method->setAccessible(true);
$result = $method->invoke($this->mapper);
$this->assertFalse($result);
}
public function testIsCurrentUserAdminReturnsFalseWhenNotAuthenticated(): void {
$this->userSession->method('getUser')->willReturn(null);
// Use reflection to call private method
$reflection = new \ReflectionClass($this->mapper);
$method = $reflection->getMethod('isCurrentUserAdmin');
$method->setAccessible(true);
$result = $method->invoke($this->mapper);
$this->assertFalse($result);
}
public function testGetUserRoleIdsReturnsGuestRoleForUnauthenticatedUser(): void {
$this->userSession->method('getUser')->willReturn(null);
$guestRole = new Role();
$guestRole->setId(4);
$guestRole->setRoleType(Role::ROLE_TYPE_GUEST);
$this->roleMapper->expects($this->once())
->method('findByRoleType')
->with('guest')
->willReturn($guestRole);
// Use reflection to call private method
$reflection = new \ReflectionClass($this->mapper);
$method = $reflection->getMethod('getUserRoleIds');
$method->setAccessible(true);
$result = $method->invoke($this->mapper);
$this->assertEquals([4], $result);
}
public function testGetUserRoleIdsReturnsEmptyArrayWhenGuestRoleNotFound(): void {
$this->userSession->method('getUser')->willReturn(null);
$this->roleMapper->expects($this->once())
->method('findByRoleType')
->with('guest')
->willThrowException(new DoesNotExistException('Guest role not found'));
// Expect logger to be called when guest role is not found
$this->logger->expects($this->once())
->method('error')
->with($this->stringContains('Guest role not found'));
// Use reflection to call private method
$reflection = new \ReflectionClass($this->mapper);
$method = $reflection->getMethod('getUserRoleIds');
$method->setAccessible(true);
$result = $method->invoke($this->mapper);
$this->assertEquals([], $result);
}
public function testGetUserRoleIdsReturnsUserRolesForAuthenticatedUser(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user1');
$this->userSession->method('getUser')->willReturn($user);
$role1 = new Role();
$role1->setId(3);
$role1->setRoleType(Role::ROLE_TYPE_DEFAULT);
$role2 = new Role();
$role2->setId(5);
$role2->setRoleType(Role::ROLE_TYPE_CUSTOM);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with('user1')
->willReturn([$role1, $role2]);
// Use reflection to call private method
$reflection = new \ReflectionClass($this->mapper);
$method = $reflection->getMethod('getUserRoleIds');
$method->setAccessible(true);
$result = $method->invoke($this->mapper);
$this->assertEquals([3, 5], $result);
}
public function testFilterByPermissionsSkipsFilteringForAdminUser(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('admin1');
$this->userSession->method('getUser')->willReturn($user);
$adminRole = new Role();
$adminRole->setId(1);
$adminRole->setRoleType(Role::ROLE_TYPE_ADMIN);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with('admin1')
->willReturn([$adminRole]);
$category1 = new Category();
$category1->setId(1);
$category2 = new Category();
$category2->setId(2);
$categories = [$category1, $category2];
// Use reflection to call private method
$reflection = new \ReflectionClass($this->mapper);
$method = $reflection->getMethod('filterByPermissions');
$method->setAccessible(true);
$result = $method->invoke($this->mapper, $categories);
// Admin should see all categories
$this->assertEquals($categories, $result);
}
public function testFilterByPermissionsReturnsEmptyArrayWhenNoCategories(): void {
// Use reflection to call private method
$reflection = new \ReflectionClass($this->mapper);
$method = $reflection->getMethod('filterByPermissions');
$method->setAccessible(true);
$result = $method->invoke($this->mapper, []);
$this->assertEquals([], $result);
}
public function testIsCurrentUserAdminReturnsTrueForModeratorRole(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('mod1');
$this->userSession->method('getUser')->willReturn($user);
$modRole = new Role();
$modRole->setId(2);
$modRole->setRoleType(Role::ROLE_TYPE_MODERATOR);
$adminRole = new Role();
$adminRole->setId(1);
$adminRole->setRoleType(Role::ROLE_TYPE_ADMIN);
// User has both moderator and admin roles
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with('mod1')
->willReturn([$modRole, $adminRole]);
// Use reflection to call private method
$reflection = new \ReflectionClass($this->mapper);
$method = $reflection->getMethod('isCurrentUserAdmin');
$method->setAccessible(true);
$result = $method->invoke($this->mapper);
// Should return true because one of the roles is admin
$this->assertTrue($result);
}
public function testGetUserRoleIdsReturnsMultipleRoles(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user1');
$this->userSession->method('getUser')->willReturn($user);
$role1 = new Role();
$role1->setId(3);
$role1->setRoleType(Role::ROLE_TYPE_DEFAULT);
$role2 = new Role();
$role2->setId(5);
$role2->setRoleType(Role::ROLE_TYPE_CUSTOM);
$role3 = new Role();
$role3->setId(7);
$role3->setRoleType(Role::ROLE_TYPE_CUSTOM);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with('user1')
->willReturn([$role1, $role2, $role3]);
// Use reflection to call private method
$reflection = new \ReflectionClass($this->mapper);
$method = $reflection->getMethod('getUserRoleIds');
$method->setAccessible(true);
$result = $method->invoke($this->mapper);
$this->assertEquals([3, 5, 7], $result);
}
public function testIsCurrentUserAdminReturnsFalseWhenUserHasNoRoles(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user1');
$this->userSession->method('getUser')->willReturn($user);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with('user1')
->willReturn([]);
// Use reflection to call private method
$reflection = new \ReflectionClass($this->mapper);
$method = $reflection->getMethod('isCurrentUserAdmin');
$method->setAccessible(true);
$result = $method->invoke($this->mapper);
$this->assertFalse($result);
}
public function testGetUserRoleIdsReturnsEmptyArrayWhenUserHasNoRoles(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user1');
$this->userSession->method('getUser')->willReturn($user);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with('user1')
->willReturn([]);
// Use reflection to call private method
$reflection = new \ReflectionClass($this->mapper);
$method = $reflection->getMethod('getUserRoleIds');
$method->setAccessible(true);
$result = $method->invoke($this->mapper);
$this->assertEquals([], $result);
}
public function testIsCurrentUserAdminWithModeratorRoleOnly(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('mod1');
$this->userSession->method('getUser')->willReturn($user);
$modRole = new Role();
$modRole->setId(2);
$modRole->setRoleType(Role::ROLE_TYPE_MODERATOR);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with('mod1')
->willReturn([$modRole]);
// Use reflection to call private method
$reflection = new \ReflectionClass($this->mapper);
$method = $reflection->getMethod('isCurrentUserAdmin');
$method->setAccessible(true);
$result = $method->invoke($this->mapper);
// Moderator is not admin
$this->assertFalse($result);
}
public function testIsCurrentUserAdminWithDefaultRoleType(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user1');
$this->userSession->method('getUser')->willReturn($user);
$defaultRole = new Role();
$defaultRole->setId(3);
$defaultRole->setRoleType(Role::ROLE_TYPE_DEFAULT);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with('user1')
->willReturn([$defaultRole]);
// Use reflection to call private method
$reflection = new \ReflectionClass($this->mapper);
$method = $reflection->getMethod('isCurrentUserAdmin');
$method->setAccessible(true);
$result = $method->invoke($this->mapper);
$this->assertFalse($result);
}
public function testIsCurrentUserAdminWithCustomRoleType(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user1');
$this->userSession->method('getUser')->willReturn($user);
$customRole = new Role();
$customRole->setId(10);
$customRole->setRoleType(Role::ROLE_TYPE_CUSTOM);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with('user1')
->willReturn([$customRole]);
// Use reflection to call private method
$reflection = new \ReflectionClass($this->mapper);
$method = $reflection->getMethod('isCurrentUserAdmin');
$method->setAccessible(true);
$result = $method->invoke($this->mapper);
$this->assertFalse($result);
public function testConstructor(): void {
$this->assertInstanceOf(CategoryMapper::class, $this->mapper);
}
}

View File

@@ -710,8 +710,12 @@ class PermissionServiceTest extends TestCase {
}
public function testGetAccessibleCategoriesForGuestUserWithNoGuestRole(): void {
$this->roleMapper->expects($this->once())
->method('findByRoleType')
$category1 = $this->createCategory(1, 'Category 1', 'category-1');
$this->categoryMapper->method('findAll')
->willReturn([$category1]);
$this->roleMapper->method('findByRoleType')
->with(Role::ROLE_TYPE_GUEST)
->willThrowException(new DoesNotExistException('Guest role not found'));
@@ -722,12 +726,18 @@ class PermissionServiceTest extends TestCase {
public function testGetAccessibleCategoriesReturnsEmptyWhenUserHasNoRoles(): void {
$userId = 'user1';
$category1 = $this->createCategory(1, 'Category 1', 'category-1');
$this->roleMapper->expects($this->once())
->method('findByUserId')
$this->categoryMapper->method('findAll')
->willReturn([$category1]);
$this->roleMapper->method('findByUserId')
->with($userId)
->willReturn([]);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willThrowException(new DoesNotExistException('Not found'));
$result = $this->service->getAccessibleCategories($userId);
$this->assertCount(0, $result);
@@ -735,12 +745,6 @@ class PermissionServiceTest extends TestCase {
public function testGetAccessibleCategoriesReturnsEmptyWhenNoCategoriesExist(): void {
$userId = 'user1';
$role = $this->createRole(1, 'User', false, false, false, false, Role::ROLE_TYPE_CUSTOM);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryMapper->expects($this->once())
->method('findAll')
@@ -754,9 +758,8 @@ class PermissionServiceTest extends TestCase {
public function testGetAccessibleCategoriesHandlesExceptions(): void {
$userId = 'user1';
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with($userId)
$this->categoryMapper->expects($this->once())
->method('findAll')
->willThrowException(new \Exception('Database error'));
$result = $this->service->getAccessibleCategories($userId);
@@ -786,6 +789,378 @@ class PermissionServiceTest extends TestCase {
return $role;
}
// ---- Team (circle) permission tests ----
/**
* Create a PermissionService partial mock where getUserCircleIds is overridden
* to return the given circle IDs, allowing team permission paths to be tested
* without the Circles app installed.
*
* @param array<string>|null $circleIds Circle IDs to return, or null for "Circles unavailable"
*/
private function createServiceWithCircleIds(?array $circleIds): PermissionService {
$service = $this->getMockBuilder(PermissionService::class)
->setConstructorArgs([
$this->userRoleMapper,
$this->roleMapper,
$this->categoryPermMapper,
$this->categoryMapper,
$this->threadMapper,
$this->postMapper,
$this->userManager,
$this->logger,
])
->onlyMethods(['getUserCircleIds'])
->getMock();
$service->method('getUserCircleIds')
->willReturn($circleIds);
return $service;
}
public function testHasCategoryPermissionGrantedByTeamWhenRoleDenies(): void {
$userId = 'user1';
$categoryId = 3;
// User role denies view on this category
$role = $this->createRole(3, 'User', false, false, false, false, Role::ROLE_TYPE_DEFAULT);
$rolePerm = $this->createCategoryPerm(1, $categoryId, 3, false, false, false, false);
// But team grants view
$teamPerm = $this->createTeamCategoryPerm(10, $categoryId, 'circle-abc', true, false, false, false);
$this->roleMapper->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willReturn($rolePerm);
$this->categoryPermMapper->method('findByCategoryAndTeamIds')
->with($categoryId, ['circle-abc'])
->willReturn([$teamPerm]);
$service = $this->createServiceWithCircleIds(['circle-abc']);
$this->assertTrue($service->hasCategoryPermission($userId, $categoryId, 'canView'));
}
public function testHasCategoryPermissionGrantedByTeamWhenNoRolePermEntryExists(): void {
$userId = 'user1';
$categoryId = 3;
// User role has no permission entry for this category at all
$role = $this->createRole(3, 'User', false, false, false, false, Role::ROLE_TYPE_DEFAULT);
$teamPerm = $this->createTeamCategoryPerm(10, $categoryId, 'circle-abc', true, true, true, false);
$this->roleMapper->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willThrowException(new DoesNotExistException('Not found'));
$this->categoryPermMapper->method('findByCategoryAndTeamIds')
->with($categoryId, ['circle-abc'])
->willReturn([$teamPerm]);
$service = $this->createServiceWithCircleIds(['circle-abc']);
$this->assertTrue($service->hasCategoryPermission($userId, $categoryId, 'canPost'));
}
public function testHasCategoryPermissionDeniedByBothRoleAndTeam(): void {
$userId = 'user1';
$categoryId = 3;
$role = $this->createRole(3, 'User', false, false, false, false, Role::ROLE_TYPE_DEFAULT);
$rolePerm = $this->createCategoryPerm(1, $categoryId, 3, false, false, false, false);
// Team also denies
$teamPerm = $this->createTeamCategoryPerm(10, $categoryId, 'circle-abc', false, false, false, false);
$this->roleMapper->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willReturn($rolePerm);
$this->categoryPermMapper->method('findByCategoryAndTeamIds')
->with($categoryId, ['circle-abc'])
->willReturn([$teamPerm]);
$service = $this->createServiceWithCircleIds(['circle-abc']);
$this->assertFalse($service->hasCategoryPermission($userId, $categoryId, 'canView'));
}
public function testHasCategoryPermissionTeamNotCheckedForGuestUser(): void {
$categoryId = 1;
$guestRole = $this->createRole(4, 'Guest', false, false, false, true, Role::ROLE_TYPE_GUEST);
$rolePerm = $this->createCategoryPerm(1, $categoryId, 4, false, false, false, false);
$this->roleMapper->method('findByRoleType')
->with(Role::ROLE_TYPE_GUEST)
->willReturn($guestRole);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willReturn($rolePerm);
// Team mapper should never be called for guest users
$this->categoryPermMapper->expects($this->never())
->method('findByCategoryAndTeamIds');
$result = $this->service->hasCategoryPermission(null, $categoryId, 'canView');
$this->assertFalse($result);
}
public function testHasCategoryPermissionTeamNotCheckedWhenCirclesUnavailable(): void {
$userId = 'user1';
$categoryId = 3;
$role = $this->createRole(3, 'User', false, false, false, false, Role::ROLE_TYPE_DEFAULT);
$this->roleMapper->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willThrowException(new DoesNotExistException('Not found'));
// Circles unavailable → null circle IDs → no team check
$this->categoryPermMapper->expects($this->never())
->method('findByCategoryAndTeamIds');
$service = $this->createServiceWithCircleIds(null);
$this->assertFalse($service->hasCategoryPermission($userId, $categoryId, 'canView'));
}
public function testHasCategoryPermissionTeamNotCheckedWhenUserHasNoCircles(): void {
$userId = 'user1';
$categoryId = 3;
$role = $this->createRole(3, 'User', false, false, false, false, Role::ROLE_TYPE_DEFAULT);
$this->roleMapper->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willThrowException(new DoesNotExistException('Not found'));
// User is in no circles
$this->categoryPermMapper->expects($this->never())
->method('findByCategoryAndTeamIds');
$service = $this->createServiceWithCircleIds([]);
$this->assertFalse($service->hasCategoryPermission($userId, $categoryId, 'canView'));
}
public function testHasCategoryPermissionMultipleTeamsOneGrants(): void {
$userId = 'user1';
$categoryId = 3;
$role = $this->createRole(3, 'User', false, false, false, false, Role::ROLE_TYPE_DEFAULT);
$this->roleMapper->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willThrowException(new DoesNotExistException('Not found'));
// User is in two teams; first denies, second grants
$teamPerm1 = $this->createTeamCategoryPerm(10, $categoryId, 'circle-aaa', false, false, false, false);
$teamPerm2 = $this->createTeamCategoryPerm(11, $categoryId, 'circle-bbb', true, true, false, false);
$this->categoryPermMapper->method('findByCategoryAndTeamIds')
->with($categoryId, ['circle-aaa', 'circle-bbb'])
->willReturn([$teamPerm1, $teamPerm2]);
$service = $this->createServiceWithCircleIds(['circle-aaa', 'circle-bbb']);
$this->assertTrue($service->hasCategoryPermission($userId, $categoryId, 'canView'));
}
public function testHasCategoryPermissionTeamGrantsSpecificPermissionOnly(): void {
$userId = 'user1';
$categoryId = 3;
$role = $this->createRole(3, 'User', false, false, false, false, Role::ROLE_TYPE_DEFAULT);
$this->roleMapper->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willThrowException(new DoesNotExistException('Not found'));
// Team grants view but not post
$teamPerm = $this->createTeamCategoryPerm(10, $categoryId, 'circle-abc', true, false, false, false);
$this->categoryPermMapper->method('findByCategoryAndTeamIds')
->willReturn([$teamPerm]);
$service = $this->createServiceWithCircleIds(['circle-abc']);
$this->assertTrue($service->hasCategoryPermission($userId, $categoryId, 'canView'));
$this->assertFalse($service->hasCategoryPermission($userId, $categoryId, 'canPost'));
}
public function testHasCategoryPermissionAdminBypassesTeamCheck(): void {
$userId = 'admin1';
$categoryId = 3;
$adminRole = $this->createRole(1, 'Admin', true, true, true, true, Role::ROLE_TYPE_ADMIN);
$this->roleMapper->method('findByUserId')
->with($userId)
->willReturn([$adminRole]);
// Neither role perms nor team perms should be checked
$this->categoryPermMapper->expects($this->never())
->method('findByCategoryAndRole');
$this->categoryPermMapper->expects($this->never())
->method('findByCategoryAndTeamIds');
$service = $this->createServiceWithCircleIds(['circle-abc']);
$this->assertTrue($service->hasCategoryPermission($userId, $categoryId, 'canView'));
}
public function testHasCategoryPermissionRoleGrantsBeforeTeamCheck(): void {
$userId = 'user1';
$categoryId = 1;
// Role already grants the permission
$role = $this->createRole(3, 'User', false, false, false, false, Role::ROLE_TYPE_DEFAULT);
$rolePerm = $this->createCategoryPerm(1, $categoryId, 3, true, true, true, false);
$this->roleMapper->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willReturn($rolePerm);
// Team mapper should not be called since role already granted
$this->categoryPermMapper->expects($this->never())
->method('findByCategoryAndTeamIds');
$service = $this->createServiceWithCircleIds(['circle-abc']);
$this->assertTrue($service->hasCategoryPermission($userId, $categoryId, 'canView'));
}
public function testGetAccessibleCategoriesIncludesTeamOnlyCategory(): void {
$userId = 'user1';
$role = $this->createRole(3, 'User', false, false, false, false, Role::ROLE_TYPE_DEFAULT);
$category1 = $this->createCategory(1, 'Role Access', 'role-access');
$category2 = $this->createCategory(2, 'Team Only', 'team-only');
$category3 = $this->createCategory(3, 'No Access', 'no-access');
$rolePerm1 = $this->createCategoryPerm(1, 1, 3, true, true, true, false);
$teamPerm2 = $this->createTeamCategoryPerm(10, 2, 'circle-abc', true, false, false, false);
$this->roleMapper->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryMapper->method('findAll')
->willReturn([$category1, $category2, $category3]);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willReturnCallback(function ($catId, $roleId) use ($rolePerm1) {
if ($catId === 1 && $roleId === 3) {
return $rolePerm1;
}
throw new DoesNotExistException('Not found');
});
$this->categoryPermMapper->method('findByCategoryAndTeamIds')
->willReturnCallback(function ($catId, $teamIds) use ($teamPerm2) {
if ($catId === 2) {
return [$teamPerm2];
}
return [];
});
$service = $this->createServiceWithCircleIds(['circle-abc']);
$result = $service->getAccessibleCategories($userId);
$this->assertCount(2, $result);
$this->assertContains(1, $result);
$this->assertContains(2, $result);
$this->assertNotContains(3, $result);
}
public function testGetAccessibleCategoriesNoTeamAccessWhenCirclesUnavailable(): void {
$userId = 'user1';
$role = $this->createRole(3, 'User', false, false, false, false, Role::ROLE_TYPE_DEFAULT);
$category1 = $this->createCategory(1, 'Role Access', 'role-access');
$category2 = $this->createCategory(2, 'Team Only', 'team-only');
$rolePerm1 = $this->createCategoryPerm(1, 1, 3, true, true, true, false);
$this->roleMapper->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryMapper->method('findAll')
->willReturn([$category1, $category2]);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willReturnCallback(function ($catId, $roleId) use ($rolePerm1) {
if ($catId === 1 && $roleId === 3) {
return $rolePerm1;
}
throw new DoesNotExistException('Not found');
});
// Circles unavailable
$service = $this->createServiceWithCircleIds(null);
$result = $service->getAccessibleCategories($userId);
// Only role-granted category, not the team-only one
$this->assertCount(1, $result);
$this->assertContains(1, $result);
}
public function testHasCategoryPermissionTeamMapperExceptionHandledGracefully(): void {
$userId = 'user1';
$categoryId = 3;
$role = $this->createRole(3, 'User', false, false, false, false, Role::ROLE_TYPE_DEFAULT);
$this->roleMapper->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willThrowException(new DoesNotExistException('Not found'));
$this->categoryPermMapper->method('findByCategoryAndTeamIds')
->willThrowException(new \Exception('Database error'));
$service = $this->createServiceWithCircleIds(['circle-abc']);
// Should return false, not throw
$this->assertFalse($service->hasCategoryPermission($userId, $categoryId, 'canView'));
}
// ---- Helper methods ----
private function createCategoryPerm(int $id, int $categoryId, int $roleId, bool $canView, bool $canPost, bool $canReply, bool $canModerate): CategoryPerm {
$perm = new CategoryPerm();
$perm->setId($id);
@@ -799,6 +1174,19 @@ class PermissionServiceTest extends TestCase {
return $perm;
}
private function createTeamCategoryPerm(int $id, int $categoryId, string $teamId, bool $canView, bool $canPost, bool $canReply, bool $canModerate): CategoryPerm {
$perm = new CategoryPerm();
$perm->setId($id);
$perm->setCategoryId($categoryId);
$perm->setTargetType(CategoryPerm::TARGET_TYPE_TEAM);
$perm->setTargetId($teamId);
$perm->setCanView($canView);
$perm->setCanPost($canPost);
$perm->setCanReply($canReply);
$perm->setCanModerate($canModerate);
return $perm;
}
private function createCategory(int $id, string $name, string $slug): Category {
$category = new Category();
$category->setId($id);