Files
nextcloud-forum/tests/unit/Service/PermissionServiceTest.php

1243 lines
39 KiB
PHP

<?php
declare(strict_types=1);
namespace OCA\Forum\Tests\Service;
use OCA\Forum\Db\Category;
use OCA\Forum\Db\CategoryMapper;
use OCA\Forum\Db\CategoryPerm;
use OCA\Forum\Db\CategoryPermMapper;
use OCA\Forum\Db\Post;
use OCA\Forum\Db\PostMapper;
use OCA\Forum\Db\Role;
use OCA\Forum\Db\RoleMapper;
use OCA\Forum\Db\Thread;
use OCA\Forum\Db\ThreadMapper;
use OCA\Forum\Db\UserRole;
use OCA\Forum\Db\UserRoleMapper;
use OCA\Forum\Service\PermissionService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IUserManager;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class PermissionServiceTest extends TestCase {
private PermissionService $service;
/** @var UserRoleMapper&MockObject */
private UserRoleMapper $userRoleMapper;
/** @var RoleMapper&MockObject */
private RoleMapper $roleMapper;
/** @var CategoryPermMapper&MockObject */
private CategoryPermMapper $categoryPermMapper;
/** @var CategoryMapper&MockObject */
private CategoryMapper $categoryMapper;
/** @var ThreadMapper&MockObject */
private ThreadMapper $threadMapper;
/** @var PostMapper&MockObject */
private PostMapper $postMapper;
/** @var IUserManager&MockObject */
private IUserManager $userManager;
/** @var LoggerInterface&MockObject */
private LoggerInterface $logger;
protected function setUp(): void {
$this->userRoleMapper = $this->createMock(UserRoleMapper::class);
$this->roleMapper = $this->createMock(RoleMapper::class);
$this->categoryPermMapper = $this->createMock(CategoryPermMapper::class);
$this->categoryMapper = $this->createMock(CategoryMapper::class);
$this->threadMapper = $this->createMock(ThreadMapper::class);
$this->postMapper = $this->createMock(PostMapper::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new PermissionService(
$this->userRoleMapper,
$this->roleMapper,
$this->categoryPermMapper,
$this->categoryMapper,
$this->threadMapper,
$this->postMapper,
$this->userManager,
$this->logger
);
}
public function testHasGlobalPermissionReturnsTrueWhenUserHasPermission(): void {
$userId = 'user1';
$permission = 'canEditRoles';
$role = $this->createRole(1, 'Admin', true, true, true, true, Role::ROLE_TYPE_ADMIN);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with($userId)
->willReturn([$role]);
$result = $this->service->hasGlobalPermission($userId, $permission);
$this->assertTrue($result);
}
public function testHasGlobalPermissionReturnsFalseWhenUserLacksPermission(): void {
$userId = 'user1';
$permission = 'canEditRoles';
$role = $this->createRole(1, 'User', false, false, false, false, Role::ROLE_TYPE_CUSTOM);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with($userId)
->willReturn([$role]);
$result = $this->service->hasGlobalPermission($userId, $permission);
$this->assertFalse($result);
}
public function testHasGlobalPermissionReturnsFalseWhenUserHasNoRoles(): void {
$userId = 'user1';
$permission = 'canEditRoles';
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with($userId)
->willReturn([]);
$result = $this->service->hasGlobalPermission($userId, $permission);
$this->assertFalse($result);
}
public function testHasGlobalPermissionChecksMultipleRoles(): void {
$userId = 'user1';
$permission = 'canEditCategories';
$role1 = $this->createRole(1, 'User', false, false, false, false, Role::ROLE_TYPE_CUSTOM);
$role2 = $this->createRole(2, 'Moderator', true, false, true, true, Role::ROLE_TYPE_MODERATOR);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with($userId)
->willReturn([$role1, $role2]);
$result = $this->service->hasGlobalPermission($userId, $permission);
$this->assertTrue($result);
}
public function testHasGlobalPermissionCanManageUsers(): void {
$userId = 'user1';
$role = $this->createRole(1, 'Manager', false, false, false, false, Role::ROLE_TYPE_CUSTOM, true, false);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->assertTrue($this->service->hasGlobalPermission($userId, 'canManageUsers'));
$this->assertFalse($role->getCanEditBbcodes());
}
public function testHasGlobalPermissionCanEditBbcodes(): void {
$userId = 'user1';
$role = $this->createRole(1, 'BBCodeEditor', false, false, false, false, Role::ROLE_TYPE_CUSTOM, false, true);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->assertTrue($this->service->hasGlobalPermission($userId, 'canEditBbcodes'));
$this->assertFalse($role->getCanManageUsers());
}
public function testHasGlobalPermissionReturnsFalseForNewPermissionsWhenNotSet(): void {
$userId = 'user1';
$role = $this->createRole(1, 'Basic', false, false, false, false, Role::ROLE_TYPE_CUSTOM);
$this->roleMapper->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->assertFalse($this->service->hasGlobalPermission($userId, 'canManageUsers'));
}
public function testHasGlobalPermissionHandlesException(): void {
$userId = 'user1';
$permission = 'canEditRoles';
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with($userId)
->willThrowException(new \Exception('Database error'));
$result = $this->service->hasGlobalPermission($userId, $permission);
$this->assertFalse($result);
}
public function testHasCategoryPermissionReturnsTrueWhenUserHasPermission(): void {
$userId = 'user1';
$categoryId = 1;
$permission = 'canPost';
// Using role ID 3 (User) instead of 1 (Admin) to test normal permission check
$role = $this->createRole(3, 'User', false, false, false, true, Role::ROLE_TYPE_DEFAULT);
$categoryPerm = $this->createCategoryPerm(1, $categoryId, 3, true, true, true, false);
// findByUserId called twice: once in hasAdminRole(), once for permission check
$this->roleMapper->expects($this->exactly(2))
->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->expects($this->once())
->method('findByCategoryAndRole')
->with($categoryId, 3)
->willReturn($categoryPerm);
$result = $this->service->hasCategoryPermission($userId, $categoryId, $permission);
$this->assertTrue($result);
}
public function testHasCategoryPermissionReturnsFalseWhenUserLacksPermission(): void {
$userId = 'user1';
$categoryId = 1;
$permission = 'canModerate';
// Using role ID 2 (Moderator) instead of 1 (Admin) to test normal permission check
$role = $this->createRole(2, 'Moderator', true, false, true, true, Role::ROLE_TYPE_MODERATOR);
$categoryPerm = $this->createCategoryPerm(1, $categoryId, 2, true, true, true, false);
// findByUserId called twice: once in hasAdminRole(), once for permission check
$this->roleMapper->expects($this->exactly(2))
->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->expects($this->once())
->method('findByCategoryAndRole')
->with($categoryId, 2)
->willReturn($categoryPerm);
$result = $this->service->hasCategoryPermission($userId, $categoryId, $permission);
$this->assertFalse($result);
}
public function testHasCategoryPermissionReturnsFalseWhenNoPermissionEntryExists(): void {
$userId = 'user1';
$categoryId = 1;
$permission = 'canPost';
$role = $this->createRole(3, 'User', false, false, false, true, Role::ROLE_TYPE_DEFAULT);
// findByUserId called twice: once in hasAdminRole(), once for permission check
$this->roleMapper->expects($this->exactly(2))
->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->expects($this->once())
->method('findByCategoryAndRole')
->with($categoryId, 3)
->willThrowException(new DoesNotExistException('Permission not found'));
$result = $this->service->hasCategoryPermission($userId, $categoryId, $permission);
$this->assertFalse($result);
}
public function testHasCategoryPermissionReturnsTrueForAdminRoleRegardlessOfPermissions(): void {
$userId = 'admin1';
$categoryId = 1;
$permission = 'canModerate';
// User has Admin role (ID 1)
$adminRole = $this->createRole(1, 'Admin', true, true, true, true, Role::ROLE_TYPE_ADMIN);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with($userId)
->willReturn([$adminRole]);
// Should not even check category permissions for Admin
$this->categoryPermMapper->expects($this->never())
->method('findByCategoryAndRole');
$result = $this->service->hasCategoryPermission($userId, $categoryId, $permission);
$this->assertTrue($result);
}
public function testHasCategoryPermissionReturnsTrueForAdminEvenWithOtherRoles(): void {
$userId = 'admin1';
$categoryId = 1;
$permission = 'canView';
// User has both Admin (ID 1) and User (ID 3) roles
$adminRole = $this->createRole(1, 'Admin', true, true, true, true, Role::ROLE_TYPE_ADMIN);
$userRole = $this->createRole(3, 'User', false, false, false, true, Role::ROLE_TYPE_DEFAULT);
$this->roleMapper->expects($this->once())
->method('findByUserId')
->with($userId)
->willReturn([$adminRole, $userRole]);
// Should not check category permissions for Admin
$this->categoryPermMapper->expects($this->never())
->method('findByCategoryAndRole');
$result = $this->service->hasCategoryPermission($userId, $categoryId, $permission);
$this->assertTrue($result);
}
public function testHasCategoryPermissionCanModerateReturnsTrueForModeratorWithPermission(): void {
$userId = 'mod1';
$categoryId = 1;
$permission = 'canModerate';
$role = $this->createRole(2, 'Moderator', true, false, true, true, Role::ROLE_TYPE_MODERATOR);
$categoryPerm = $this->createCategoryPerm(1, $categoryId, 2, true, true, true, true);
$this->roleMapper->expects($this->exactly(2))
->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->expects($this->once())
->method('findByCategoryAndRole')
->with($categoryId, 2)
->willReturn($categoryPerm);
$result = $this->service->hasCategoryPermission($userId, $categoryId, $permission);
$this->assertTrue($result);
}
public function testHasCategoryPermissionCanModerateReturnsTrueForCustomRoleWithPermission(): void {
$userId = 'user1';
$categoryId = 1;
$permission = 'canModerate';
$role = $this->createRole(10, 'Category Moderator', false, false, false, false, Role::ROLE_TYPE_CUSTOM);
$categoryPerm = $this->createCategoryPerm(1, $categoryId, 10, true, true, true, true);
$this->roleMapper->expects($this->exactly(2))
->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->expects($this->once())
->method('findByCategoryAndRole')
->with($categoryId, 10)
->willReturn($categoryPerm);
$result = $this->service->hasCategoryPermission($userId, $categoryId, $permission);
$this->assertTrue($result);
}
public function testHasCategoryPermissionCanModerateReturnsFalseForCustomRoleWithoutPermission(): void {
$userId = 'user1';
$categoryId = 1;
$permission = 'canModerate';
$role = $this->createRole(10, 'Limited Role', false, false, false, false, Role::ROLE_TYPE_CUSTOM);
// Has view/post/reply but NOT moderate
$categoryPerm = $this->createCategoryPerm(1, $categoryId, 10, true, true, true, false);
$this->roleMapper->expects($this->exactly(2))
->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->expects($this->once())
->method('findByCategoryAndRole')
->with($categoryId, 10)
->willReturn($categoryPerm);
$result = $this->service->hasCategoryPermission($userId, $categoryId, $permission);
$this->assertFalse($result);
}
public function testHasCategoryPermissionCanModerateChecksAllRoles(): void {
$userId = 'user1';
$categoryId = 1;
$permission = 'canModerate';
// Default role has no moderate permission, custom role does
$defaultRole = $this->createRole(3, 'User', false, false, false, true, Role::ROLE_TYPE_DEFAULT);
$customRole = $this->createRole(10, 'Category Mod', false, false, false, false, Role::ROLE_TYPE_CUSTOM);
$defaultPerm = $this->createCategoryPerm(1, $categoryId, 3, true, true, true, false);
$customPerm = $this->createCategoryPerm(2, $categoryId, 10, true, true, true, true);
$this->roleMapper->expects($this->exactly(2))
->method('findByUserId')
->with($userId)
->willReturn([$defaultRole, $customRole]);
$this->categoryPermMapper->expects($this->exactly(2))
->method('findByCategoryAndRole')
->willReturnCallback(function ($catId, $roleId) use ($categoryId, $defaultPerm, $customPerm) {
$this->assertEquals($categoryId, $catId);
if ($roleId === 3) {
return $defaultPerm;
}
if ($roleId === 10) {
return $customPerm;
}
throw new DoesNotExistException('Not found');
});
$result = $this->service->hasCategoryPermission($userId, $categoryId, $permission);
$this->assertTrue($result);
}
public function testHasCategoryPermissionCanModerateIsCategorySpecific(): void {
$userId = 'user1';
$permission = 'canModerate';
// User can moderate category 1 but not category 2
$role = $this->createRole(10, 'Partial Mod', false, false, false, false, Role::ROLE_TYPE_CUSTOM);
$perm1 = $this->createCategoryPerm(1, 1, 10, true, true, true, true); // canModerate=true
$perm2 = $this->createCategoryPerm(2, 2, 10, true, true, true, false); // canModerate=false
$this->roleMapper->method('findByUserId')
->with($userId)
->willReturn([$role]);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willReturnCallback(function ($catId, $roleId) use ($perm1, $perm2) {
if ($catId === 1 && $roleId === 10) {
return $perm1;
}
if ($catId === 2 && $roleId === 10) {
return $perm2;
}
throw new DoesNotExistException('Not found');
});
$this->assertTrue($this->service->hasCategoryPermission($userId, 1, $permission));
$this->assertFalse($this->service->hasCategoryPermission($userId, 2, $permission));
}
public function testHasGlobalPermissionForGuestUserUsesGuestRole(): void {
$userId = null; // Guest user
$permission = 'canEditRoles';
$guestRole = $this->createRole(4, 'Guest', false, false, false, true, Role::ROLE_TYPE_GUEST);
$this->roleMapper->expects($this->once())
->method('findByRoleType')
->with(Role::ROLE_TYPE_GUEST)
->willReturn($guestRole);
$result = $this->service->hasGlobalPermission($userId, $permission);
$this->assertFalse($result);
}
public function testHasGlobalPermissionForGuestUserReturnsFalseWhenNoGuestRole(): void {
$userId = null; // Guest user
$permission = 'canEditRoles';
$this->roleMapper->expects($this->once())
->method('findByRoleType')
->with(Role::ROLE_TYPE_GUEST)
->willThrowException(new DoesNotExistException('Guest role not found'));
$result = $this->service->hasGlobalPermission($userId, $permission);
$this->assertFalse($result);
}
public function testHasCategoryPermissionForGuestUserUsesGuestRole(): void {
$userId = null; // Guest user
$categoryId = 1;
$permission = 'canView';
$guestRole = $this->createRole(4, 'Guest', false, false, false, true, Role::ROLE_TYPE_GUEST);
$categoryPerm = $this->createCategoryPerm(1, $categoryId, 4, true, false, false, false);
$this->roleMapper->expects($this->once())
->method('findByRoleType')
->with(Role::ROLE_TYPE_GUEST)
->willReturn($guestRole);
$this->categoryPermMapper->expects($this->once())
->method('findByCategoryAndRole')
->with($categoryId, 4)
->willReturn($categoryPerm);
$result = $this->service->hasCategoryPermission($userId, $categoryId, $permission);
$this->assertTrue($result);
}
public function testHasCategoryPermissionForGuestUserReturnsFalseWhenNoGuestRole(): void {
$userId = null; // Guest user
$categoryId = 1;
$permission = 'canView';
$this->roleMapper->expects($this->once())
->method('findByRoleType')
->with(Role::ROLE_TYPE_GUEST)
->willThrowException(new DoesNotExistException('Guest role not found'));
$result = $this->service->hasCategoryPermission($userId, $categoryId, $permission);
$this->assertFalse($result);
}
public function testHasCategoryPermissionForGuestUserReturnsFalseWhenNoPermission(): void {
$userId = null; // Guest user
$categoryId = 1;
$permission = 'canPost';
$guestRole = $this->createRole(4, 'Guest', false, false, false, true, Role::ROLE_TYPE_GUEST);
$categoryPerm = $this->createCategoryPerm(1, $categoryId, 4, true, false, false, false);
$this->roleMapper->expects($this->once())
->method('findByRoleType')
->with(Role::ROLE_TYPE_GUEST)
->willReturn($guestRole);
$this->categoryPermMapper->expects($this->once())
->method('findByCategoryAndRole')
->with($categoryId, 4)
->willReturn($categoryPerm);
$result = $this->service->hasCategoryPermission($userId, $categoryId, $permission);
$this->assertFalse($result);
}
public function testGetCategoryIdFromThreadReturnsCorrectId(): void {
$threadId = 1;
$categoryId = 5;
$thread = new Thread();
$thread->setId($threadId);
$thread->setCategoryId($categoryId);
$this->threadMapper->expects($this->once())
->method('find')
->with($threadId)
->willReturn($thread);
$result = $this->service->getCategoryIdFromThread($threadId);
$this->assertEquals($categoryId, $result);
}
public function testGetCategoryIdFromThreadThrowsWhenThreadNotFound(): void {
$threadId = 999;
$this->threadMapper->expects($this->once())
->method('find')
->with($threadId)
->willThrowException(new DoesNotExistException('Thread not found'));
$this->expectException(DoesNotExistException::class);
$this->service->getCategoryIdFromThread($threadId);
}
public function testGetCategoryIdFromPostReturnsCorrectId(): void {
$postId = 1;
$threadId = 2;
$categoryId = 3;
$post = new Post();
$post->setId($postId);
$post->setThreadId($threadId);
$post->setIsFirstPost(false);
$thread = new Thread();
$thread->setId($threadId);
$thread->setCategoryId($categoryId);
$this->postMapper->expects($this->once())
->method('find')
->with($postId)
->willReturn($post);
$this->threadMapper->expects($this->once())
->method('find')
->with($threadId)
->willReturn($thread);
$result = $this->service->getCategoryIdFromPost($postId);
$this->assertEquals($categoryId, $result);
}
public function testGetCategoryIdFromPostThrowsWhenPostNotFound(): void {
$postId = 999;
$this->postMapper->expects($this->once())
->method('find')
->with($postId)
->willThrowException(new DoesNotExistException('Post not found'));
$this->expectException(DoesNotExistException::class);
$this->service->getCategoryIdFromPost($postId);
}
public function testIsPostAuthorReturnsTrueWhenUserIsAuthor(): void {
$userId = 'user1';
$postId = 1;
$post = new Post();
$post->setId($postId);
$post->setAuthorId($userId);
$post->setIsFirstPost(false);
$this->postMapper->expects($this->once())
->method('find')
->with($postId)
->willReturn($post);
$result = $this->service->isPostAuthor($userId, $postId);
$this->assertTrue($result);
}
public function testIsPostAuthorReturnsFalseWhenUserIsNotAuthor(): void {
$userId = 'user1';
$postId = 1;
$post = new Post();
$post->setId($postId);
$post->setAuthorId('user2');
$post->setIsFirstPost(false);
$this->postMapper->expects($this->once())
->method('find')
->with($postId)
->willReturn($post);
$result = $this->service->isPostAuthor($userId, $postId);
$this->assertFalse($result);
}
public function testIsPostAuthorReturnsFalseWhenPostNotFound(): void {
$userId = 'user1';
$postId = 999;
$this->postMapper->expects($this->once())
->method('find')
->with($postId)
->willThrowException(new DoesNotExistException('Post not found'));
$result = $this->service->isPostAuthor($userId, $postId);
$this->assertFalse($result);
}
public function testIsThreadAuthorReturnsTrueWhenUserIsAuthor(): void {
$userId = 'user1';
$threadId = 1;
$thread = new Thread();
$thread->setId($threadId);
$thread->setAuthorId($userId);
$this->threadMapper->expects($this->once())
->method('find')
->with($threadId)
->willReturn($thread);
$result = $this->service->isThreadAuthor($userId, $threadId);
$this->assertTrue($result);
}
public function testIsThreadAuthorReturnsFalseWhenUserIsNotAuthor(): void {
$userId = 'user1';
$threadId = 1;
$thread = new Thread();
$thread->setId($threadId);
$thread->setAuthorId('user2');
$this->threadMapper->expects($this->once())
->method('find')
->with($threadId)
->willReturn($thread);
$result = $this->service->isThreadAuthor($userId, $threadId);
$this->assertFalse($result);
}
public function testIsThreadAuthorReturnsFalseWhenThreadNotFound(): void {
$userId = 'user1';
$threadId = 999;
$this->threadMapper->expects($this->once())
->method('find')
->with($threadId)
->willThrowException(new DoesNotExistException('Thread not found'));
$result = $this->service->isThreadAuthor($userId, $threadId);
$this->assertFalse($result);
}
public function testGetAccessibleCategoriesReturnsViewableCategory(): void {
$userId = 'user1';
$role = $this->createRole(1, 'User', false, false, false, false, Role::ROLE_TYPE_CUSTOM);
$category1 = $this->createCategory(1, 'Category 1', 'category-1');
$perm1 = $this->createCategoryPerm(1, 1, 1, true, false, false, false);
// Set up mocks without strict expectations
$this->roleMapper->method('findByUserId')
->willReturn([$role]);
$this->categoryMapper->method('findAll')
->willReturn([$category1]);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willReturn($perm1);
$result = $this->service->getAccessibleCategories($userId);
$this->assertCount(1, $result);
$this->assertContains(1, $result);
}
public function testGetAccessibleCategoriesForGuestUser(): void {
$guestRole = $this->createRole(1, 'Guest', false, false, false, true, Role::ROLE_TYPE_GUEST);
$category1 = $this->createCategory(1, 'Public Category', 'public-category');
$category2 = $this->createCategory(2, 'Private Category', 'private-category');
$perm1 = $this->createCategoryPerm(1, 1, 1, true, false, false, false);
// Category 2 has no guest view permission
$this->roleMapper->method('findByRoleType')
->willReturn($guestRole);
$this->categoryMapper->method('findAll')
->willReturn([$category1, $category2]);
$this->categoryPermMapper->method('findByCategoryAndRole')
->willReturnCallback(function ($categoryId, $roleId) use ($perm1) {
if ($categoryId == 1 && $roleId == 1) {
return $perm1;
}
throw new DoesNotExistException('Permission not found');
});
$result = $this->service->getAccessibleCategories(null);
$this->assertCount(1, $result);
$this->assertContains(1, $result);
$this->assertNotContains(2, $result);
}
public function testGetAccessibleCategoriesForGuestUserWithNoGuestRole(): void {
$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'));
$result = $this->service->getAccessibleCategories(null);
$this->assertCount(0, $result);
}
public function testGetAccessibleCategoriesReturnsEmptyWhenUserHasNoRoles(): void {
$userId = 'user1';
$category1 = $this->createCategory(1, 'Category 1', 'category-1');
$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);
}
public function testGetAccessibleCategoriesReturnsEmptyWhenNoCategoriesExist(): void {
$userId = 'user1';
$this->categoryMapper->expects($this->once())
->method('findAll')
->willReturn([]);
$result = $this->service->getAccessibleCategories($userId);
$this->assertCount(0, $result);
}
public function testGetAccessibleCategoriesHandlesExceptions(): void {
$userId = 'user1';
$this->categoryMapper->expects($this->once())
->method('findAll')
->willThrowException(new \Exception('Database error'));
$result = $this->service->getAccessibleCategories($userId);
$this->assertCount(0, $result);
}
private function createUserRole(int $id, string $userId, int $roleId): UserRole {
$userRole = new UserRole();
$userRole->setId($id);
$userRole->setUserId($userId);
$userRole->setRoleId($roleId);
$userRole->setCreatedAt(time());
return $userRole;
}
private function createRole(int $id, string $name, bool $canAccessAdminTools, bool $canEditRoles, bool $canEditCategories, bool $isSystemRole = false, string $roleType = Role::ROLE_TYPE_CUSTOM, bool $canManageUsers = false, bool $canEditBbcodes = false): Role {
$role = new Role();
$role->setId($id);
$role->setName($name);
$role->setCanAccessAdminTools($canAccessAdminTools);
$role->setCanManageUsers($canManageUsers);
$role->setCanEditRoles($canEditRoles);
$role->setCanEditCategories($canEditCategories);
$role->setCanEditBbcodes($canEditBbcodes);
$role->setIsSystemRole($isSystemRole);
$role->setRoleType($roleType);
$role->setCreatedAt(time());
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);
$perm->setCategoryId($categoryId);
$perm->setTargetType('role');
$perm->setTargetId((string)$roleId);
$perm->setCanView($canView);
$perm->setCanPost($canPost);
$perm->setCanReply($canReply);
$perm->setCanModerate($canModerate);
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);
$category->setName($name);
$category->setSlug($slug);
$category->setDescription('');
$category->setSortOrder(0);
$category->setThreadCount(0);
$category->setPostCount(0);
$category->setCreatedAt(time());
$category->setUpdatedAt(time());
return $category;
}
}