mirror of
https://github.com/chenasraf/nextcloud-forum.git
synced 2026-05-18 01:28:58 +00:00
1178 lines
38 KiB
PHP
1178 lines
38 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace OCA\Forum\Tests\Controller;
|
|
|
|
use OCA\Forum\AppInfo\Application;
|
|
use OCA\Forum\Controller\PostController;
|
|
use OCA\Forum\Db\BBCode;
|
|
use OCA\Forum\Db\BBCodeMapper;
|
|
use OCA\Forum\Db\Category;
|
|
use OCA\Forum\Db\CategoryMapper;
|
|
use OCA\Forum\Db\ForumUser;
|
|
use OCA\Forum\Db\ForumUserMapper;
|
|
use OCA\Forum\Db\Post;
|
|
use OCA\Forum\Db\PostMapper;
|
|
use OCA\Forum\Db\Reaction;
|
|
use OCA\Forum\Db\ReactionMapper;
|
|
use OCA\Forum\Db\ReadMarker;
|
|
use OCA\Forum\Db\ReadMarkerMapper;
|
|
use OCA\Forum\Db\Thread;
|
|
use OCA\Forum\Db\ThreadMapper;
|
|
use OCA\Forum\Db\ThreadSubscriptionMapper;
|
|
use OCA\Forum\Service\BBCodeService;
|
|
use OCA\Forum\Service\GuestService;
|
|
use OCA\Forum\Service\NotificationService;
|
|
use OCA\Forum\Service\PermissionService;
|
|
use OCA\Forum\Service\PostEnrichmentService;
|
|
use OCA\Forum\Service\PostHistoryService;
|
|
use OCA\Forum\Service\UserPreferencesService;
|
|
use OCA\Forum\Service\UserService;
|
|
use OCP\AppFramework\Db\DoesNotExistException;
|
|
use OCP\AppFramework\Http;
|
|
use OCP\IRequest;
|
|
use OCP\IUser;
|
|
use OCP\IUserSession;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
class PostControllerTest extends TestCase {
|
|
private PostController $controller;
|
|
/** @var PostMapper&MockObject */
|
|
private PostMapper $postMapper;
|
|
/** @var ThreadMapper&MockObject */
|
|
private ThreadMapper $threadMapper;
|
|
/** @var CategoryMapper&MockObject */
|
|
private CategoryMapper $categoryMapper;
|
|
/** @var ForumUserMapper&MockObject */
|
|
private ForumUserMapper $forumUserMapper;
|
|
/** @var ReactionMapper&MockObject */
|
|
private ReactionMapper $reactionMapper;
|
|
/** @var BBCodeService&MockObject */
|
|
private BBCodeService $bbCodeService;
|
|
/** @var BBCodeMapper&MockObject */
|
|
private BBCodeMapper $bbCodeMapper;
|
|
/** @var PermissionService&MockObject */
|
|
private PermissionService $permissionService;
|
|
/** @var ReadMarkerMapper&MockObject */
|
|
private ReadMarkerMapper $readMarkerMapper;
|
|
/** @var NotificationService&MockObject */
|
|
private NotificationService $notificationService;
|
|
/** @var PostEnrichmentService&MockObject */
|
|
private PostEnrichmentService $postEnrichmentService;
|
|
/** @var PostHistoryService&MockObject */
|
|
private PostHistoryService $postHistoryService;
|
|
/** @var UserService&MockObject */
|
|
private UserService $userService;
|
|
/** @var UserPreferencesService&MockObject */
|
|
private UserPreferencesService $userPreferencesService;
|
|
/** @var ThreadSubscriptionMapper&MockObject */
|
|
private ThreadSubscriptionMapper $threadSubscriptionMapper;
|
|
/** @var GuestService&MockObject */
|
|
private GuestService $guestService;
|
|
/** @var IUserSession&MockObject */
|
|
private IUserSession $userSession;
|
|
/** @var LoggerInterface&MockObject */
|
|
private LoggerInterface $logger;
|
|
/** @var IRequest&MockObject */
|
|
private IRequest $request;
|
|
|
|
protected function setUp(): void {
|
|
$this->request = $this->createMock(IRequest::class);
|
|
$this->postMapper = $this->createMock(PostMapper::class);
|
|
$this->threadMapper = $this->createMock(ThreadMapper::class);
|
|
$this->categoryMapper = $this->createMock(CategoryMapper::class);
|
|
$this->forumUserMapper = $this->createMock(ForumUserMapper::class);
|
|
$this->reactionMapper = $this->createMock(ReactionMapper::class);
|
|
$this->bbCodeService = $this->createMock(BBCodeService::class);
|
|
$this->bbCodeMapper = $this->createMock(BBCodeMapper::class);
|
|
$this->permissionService = $this->createMock(PermissionService::class);
|
|
$this->readMarkerMapper = $this->createMock(ReadMarkerMapper::class);
|
|
$this->notificationService = $this->createMock(NotificationService::class);
|
|
$this->postEnrichmentService = $this->createMock(PostEnrichmentService::class);
|
|
$this->postHistoryService = $this->createMock(PostHistoryService::class);
|
|
$this->userService = $this->createMock(UserService::class);
|
|
$this->userPreferencesService = $this->createMock(UserPreferencesService::class);
|
|
$this->threadSubscriptionMapper = $this->createMock(ThreadSubscriptionMapper::class);
|
|
$this->guestService = $this->createMock(GuestService::class);
|
|
$this->userSession = $this->createMock(IUserSession::class);
|
|
$this->logger = $this->createMock(LoggerInterface::class);
|
|
|
|
// Mock post enrichment service to return serialized post with mock data
|
|
$this->postEnrichmentService->method('enrichPost')
|
|
->willReturnCallback(function ($post) {
|
|
$data = is_array($post) ? $post : $post->jsonSerialize();
|
|
$data['author'] = ['userId' => $data['authorId'], 'displayName' => 'Test User'];
|
|
$data['reactions'] = [];
|
|
return $data;
|
|
});
|
|
|
|
$this->controller = new PostController(
|
|
Application::APP_ID,
|
|
$this->request,
|
|
$this->postMapper,
|
|
$this->threadMapper,
|
|
$this->categoryMapper,
|
|
$this->forumUserMapper,
|
|
$this->reactionMapper,
|
|
$this->bbCodeService,
|
|
$this->bbCodeMapper,
|
|
$this->permissionService,
|
|
$this->readMarkerMapper,
|
|
$this->notificationService,
|
|
$this->postEnrichmentService,
|
|
$this->postHistoryService,
|
|
$this->userService,
|
|
$this->userPreferencesService,
|
|
$this->threadSubscriptionMapper,
|
|
$this->guestService,
|
|
$this->userSession,
|
|
$this->logger
|
|
);
|
|
}
|
|
|
|
public function testByThreadReturnsPostsSuccessfully(): void {
|
|
$threadId = 1;
|
|
$limit = 50;
|
|
$offset = 0;
|
|
|
|
// Create mock posts
|
|
$post1 = $this->createMockPost(1, $threadId, 'user1', 'Test content 1');
|
|
$post2 = $this->createMockPost(2, $threadId, 'user2', 'Test content 2');
|
|
$posts = [$post1, $post2];
|
|
|
|
// Create mock BBCode
|
|
$bbcode = new BBCode();
|
|
$bbcode->setId(1);
|
|
$bbcode->setTag('b');
|
|
$bbcode->setReplacement('<strong>{content}</strong>');
|
|
$bbcode->setEnabled(true);
|
|
|
|
// Create mock reactions
|
|
$reaction1 = $this->createMockReaction(1, 1, 'user1', '👍');
|
|
$reaction2 = $this->createMockReaction(2, 1, 'user2', '👍');
|
|
|
|
// Mock user session
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
// Set up expectations
|
|
$this->postMapper->expects($this->once())
|
|
->method('findByThreadId')
|
|
->with($threadId, $limit, $offset)
|
|
->willReturn($posts);
|
|
|
|
$this->bbCodeMapper->expects($this->once())
|
|
->method('findAllEnabled')
|
|
->willReturn([$bbcode]);
|
|
|
|
$this->reactionMapper->expects($this->once())
|
|
->method('findByPostIds')
|
|
->with([1, 2])
|
|
->willReturn([$reaction1, $reaction2]);
|
|
|
|
// Mock userService to return enriched user data
|
|
$this->userService->expects($this->once())
|
|
->method('enrichMultipleUsers')
|
|
->with(['user1', 'user2'])
|
|
->willReturn([
|
|
'user1' => ['userId' => 'user1', 'displayName' => 'User 1', 'roles' => []],
|
|
'user2' => ['userId' => 'user2', 'displayName' => 'User 2', 'roles' => []],
|
|
]);
|
|
|
|
// Execute
|
|
$response = $this->controller->byThread($threadId, $limit, $offset);
|
|
|
|
// Assert
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertIsArray($data);
|
|
$this->assertCount(2, $data);
|
|
$this->assertEquals(1, $data[0]['id']);
|
|
$this->assertEquals(2, $data[1]['id']);
|
|
$this->assertArrayHasKey('reactions', $data[0]);
|
|
$this->assertArrayHasKey('reactions', $data[1]);
|
|
}
|
|
|
|
public function testByThreadHandlesEmptyPosts(): void {
|
|
$threadId = 1;
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('findByThreadId')
|
|
->willReturn([]);
|
|
|
|
$this->bbCodeMapper->expects($this->once())
|
|
->method('findAllEnabled')
|
|
->willReturn([]);
|
|
|
|
$this->reactionMapper->expects($this->once())
|
|
->method('findByPostIds')
|
|
->with([])
|
|
->willReturn([]);
|
|
|
|
$response = $this->controller->byThread($threadId);
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$this->assertIsArray($response->getData());
|
|
$this->assertCount(0, $response->getData());
|
|
}
|
|
|
|
public function testShowReturnsPostSuccessfully(): void {
|
|
$postId = 1;
|
|
$post = $this->createMockPost($postId, 1, 'user1', 'Test content');
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('find')
|
|
->with($postId)
|
|
->willReturn($post);
|
|
|
|
$response = $this->controller->show($postId);
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertEquals($postId, $data['id']);
|
|
$this->assertEquals('user1', $data['authorId']);
|
|
}
|
|
|
|
public function testShowReturnsNotFoundWhenPostDoesNotExist(): void {
|
|
$postId = 999;
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('find')
|
|
->with($postId)
|
|
->willThrowException(new DoesNotExistException('Post not found'));
|
|
|
|
$response = $this->controller->show($postId);
|
|
|
|
$this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus());
|
|
$this->assertEquals(['error' => 'Post not found'], $response->getData());
|
|
}
|
|
|
|
public function testCreatePostSuccessfully(): void {
|
|
$threadId = 1;
|
|
$content = 'New post content';
|
|
$userId = 'user1';
|
|
|
|
// Mock user
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn($userId);
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
// Mock forum user
|
|
$forumUser = new ForumUser();
|
|
$forumUser->setId(1);
|
|
$forumUser->setUserId($userId);
|
|
$forumUser->setPostCount(0);
|
|
$forumUser->setCreatedAt(time());
|
|
$forumUser->setUpdatedAt(time());
|
|
|
|
// Mock thread
|
|
$thread = new Thread();
|
|
$thread->setId($threadId);
|
|
$thread->setCategoryId(1);
|
|
$thread->setPostCount(1);
|
|
$thread->setCreatedAt(time());
|
|
$thread->setUpdatedAt(time());
|
|
|
|
// Mock category
|
|
$category = new Category();
|
|
$category->setId(1);
|
|
$category->setPostCount(1);
|
|
|
|
// Mock created post
|
|
$createdPost = $this->createMockPost(1, $threadId, $userId, $content);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('insert')
|
|
->willReturnCallback(function ($post) use ($createdPost) {
|
|
return $createdPost;
|
|
});
|
|
|
|
// Mock readMarkerMapper
|
|
$this->readMarkerMapper->expects($this->once())
|
|
->method('createOrUpdate')
|
|
->with($userId, $threadId, 1);
|
|
|
|
// Mock thread update
|
|
$this->threadMapper->expects($this->once())
|
|
->method('find')
|
|
->with($threadId)
|
|
->willReturn($thread);
|
|
|
|
$this->threadMapper->expects($this->once())
|
|
->method('update')
|
|
->willReturn($thread);
|
|
|
|
// Mock category update
|
|
$this->categoryMapper->expects($this->once())
|
|
->method('find')
|
|
->willReturn($category);
|
|
|
|
$this->categoryMapper->expects($this->once())
|
|
->method('update')
|
|
->willReturn($category);
|
|
|
|
// Mock forum user increment (void methods, no return value expectations)
|
|
$this->forumUserMapper->expects($this->once())
|
|
->method('incrementPostCount')
|
|
->with($userId);
|
|
|
|
// Mock notification service
|
|
$this->notificationService->expects($this->once())
|
|
->method('notifyThreadSubscribers')
|
|
->with($threadId, 1, $userId);
|
|
|
|
$response = $this->controller->create($threadId, $content);
|
|
|
|
$this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertEquals(1, $data['id']);
|
|
$this->assertEquals($threadId, $data['threadId']);
|
|
$this->assertEquals($userId, $data['authorId']);
|
|
}
|
|
|
|
public function testCreatePostReturnsUnauthorizedWhenUserNotAuthenticated(): void {
|
|
$threadId = 1;
|
|
$content = 'New post content';
|
|
|
|
$this->userSession->method('getUser')->willReturn(null);
|
|
|
|
$response = $this->controller->create($threadId, $content);
|
|
|
|
$this->assertEquals(Http::STATUS_UNAUTHORIZED, $response->getStatus());
|
|
$this->assertEquals(['error' => 'User not authenticated'], $response->getData());
|
|
}
|
|
|
|
public function testUpdatePostSuccessfullyAsAuthor(): void {
|
|
$postId = 1;
|
|
$userId = 'user1';
|
|
$newContent = 'Updated content';
|
|
$post = $this->createMockPost($postId, 1, $userId, 'Original content');
|
|
|
|
// Mock user (author)
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn($userId);
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
// Mock permission service
|
|
$this->permissionService->expects($this->once())
|
|
->method('getCategoryIdFromPost')
|
|
->with($postId)
|
|
->willReturn(1);
|
|
|
|
$this->permissionService->expects($this->once())
|
|
->method('hasCategoryPermission')
|
|
->with($userId, 1, 'canModerate')
|
|
->willReturn(false); // User is not a moderator, but is the author
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('find')
|
|
->with($postId)
|
|
->willReturn($post);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('update')
|
|
->willReturnCallback(function ($updatedPost) use ($newContent) {
|
|
$this->assertEquals($newContent, $updatedPost->getContent());
|
|
$this->assertTrue($updatedPost->getIsEdited());
|
|
$this->assertNotNull($updatedPost->getEditedAt());
|
|
return $updatedPost;
|
|
});
|
|
|
|
$response = $this->controller->update($postId, $newContent);
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertEquals($postId, $data['id']);
|
|
}
|
|
|
|
public function testUpdatePostSuccessfullyAsModerator(): void {
|
|
$postId = 1;
|
|
$userId = 'moderator1';
|
|
$postAuthorId = 'user1';
|
|
$newContent = 'Updated content';
|
|
$post = $this->createMockPost($postId, 1, $postAuthorId, 'Original content');
|
|
|
|
// Mock user (moderator, not author)
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn($userId);
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
// Mock permission service
|
|
$this->permissionService->expects($this->once())
|
|
->method('getCategoryIdFromPost')
|
|
->with($postId)
|
|
->willReturn(1);
|
|
|
|
$this->permissionService->expects($this->once())
|
|
->method('hasCategoryPermission')
|
|
->with($userId, 1, 'canModerate')
|
|
->willReturn(true); // User is a moderator
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('find')
|
|
->with($postId)
|
|
->willReturn($post);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('update')
|
|
->willReturnCallback(function ($updatedPost) use ($newContent) {
|
|
$this->assertEquals($newContent, $updatedPost->getContent());
|
|
$this->assertTrue($updatedPost->getIsEdited());
|
|
$this->assertNotNull($updatedPost->getEditedAt());
|
|
return $updatedPost;
|
|
});
|
|
|
|
$response = $this->controller->update($postId, $newContent);
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertEquals($postId, $data['id']);
|
|
}
|
|
|
|
public function testUpdatePostReturnsForbiddenWhenNotAuthorOrModerator(): void {
|
|
$postId = 1;
|
|
$userId = 'user2';
|
|
$postAuthorId = 'user1';
|
|
$newContent = 'Updated content';
|
|
$post = $this->createMockPost($postId, 1, $postAuthorId, 'Original content');
|
|
|
|
// Mock user (not author, not moderator)
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn($userId);
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
// Mock permission service
|
|
$this->permissionService->expects($this->once())
|
|
->method('getCategoryIdFromPost')
|
|
->with($postId)
|
|
->willReturn(1);
|
|
|
|
$this->permissionService->expects($this->once())
|
|
->method('hasCategoryPermission')
|
|
->with($userId, 1, 'canModerate')
|
|
->willReturn(false); // User is neither author nor moderator
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('find')
|
|
->with($postId)
|
|
->willReturn($post);
|
|
|
|
$response = $this->controller->update($postId, $newContent);
|
|
|
|
$this->assertEquals(Http::STATUS_FORBIDDEN, $response->getStatus());
|
|
$this->assertEquals(['error' => 'Insufficient permissions to edit this post'], $response->getData());
|
|
}
|
|
|
|
public function testUpdatePostReturnsUnauthorizedWhenUserNotAuthenticated(): void {
|
|
$postId = 1;
|
|
$newContent = 'Updated content';
|
|
|
|
$this->userSession->method('getUser')->willReturn(null);
|
|
|
|
$response = $this->controller->update($postId, $newContent);
|
|
|
|
$this->assertEquals(Http::STATUS_UNAUTHORIZED, $response->getStatus());
|
|
$this->assertEquals(['error' => 'User not authenticated'], $response->getData());
|
|
}
|
|
|
|
public function testUpdatePostReturnsNotFoundWhenPostDoesNotExist(): void {
|
|
$postId = 999;
|
|
$userId = 'user1';
|
|
$newContent = 'Updated content';
|
|
|
|
// Mock user
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn($userId);
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('find')
|
|
->with($postId)
|
|
->willThrowException(new DoesNotExistException('Post not found'));
|
|
|
|
$response = $this->controller->update($postId, $newContent);
|
|
|
|
$this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus());
|
|
$this->assertEquals(['error' => 'Post not found'], $response->getData());
|
|
}
|
|
|
|
public function testDestroyPostSuccessfully(): void {
|
|
$postId = 1;
|
|
$userId = 'user1';
|
|
$post = $this->createMockPost($postId, 1, $userId, 'Test content');
|
|
|
|
// Mock user session
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn($userId);
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
// Mock permission service
|
|
$this->permissionService->expects($this->once())
|
|
->method('getCategoryIdFromPost')
|
|
->with($postId)
|
|
->willReturn(1);
|
|
|
|
$this->permissionService->expects($this->once())
|
|
->method('hasCategoryPermission')
|
|
->with($userId, 1, 'canModerate')
|
|
->willReturn(false); // User is not moderator but is author
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('find')
|
|
->with($postId)
|
|
->willReturn($post);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('update')
|
|
->willReturnCallback(function ($updatedPost) {
|
|
$this->assertNotNull($updatedPost->getDeletedAt());
|
|
return $updatedPost;
|
|
});
|
|
|
|
$response = $this->controller->destroy($postId);
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$this->assertEquals(['success' => true], $response->getData());
|
|
}
|
|
|
|
public function testDestroyPostReturnsNotFoundWhenPostDoesNotExist(): void {
|
|
$postId = 999;
|
|
$userId = 'user1';
|
|
|
|
// Mock user session
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn($userId);
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('find')
|
|
->with($postId)
|
|
->willThrowException(new DoesNotExistException('Post not found'));
|
|
|
|
$response = $this->controller->destroy($postId);
|
|
|
|
$this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus());
|
|
$this->assertEquals(['error' => 'Post not found'], $response->getData());
|
|
}
|
|
|
|
private function createMockPost(int $id, int $threadId, string $authorId, string $content): Post {
|
|
$post = new Post();
|
|
$post->setId($id);
|
|
$post->setThreadId($threadId);
|
|
$post->setAuthorId($authorId);
|
|
$post->setContent($content);
|
|
$post->setIsEdited(false);
|
|
$post->setIsFirstPost(false);
|
|
$post->setCreatedAt(time());
|
|
$post->setUpdatedAt(time());
|
|
return $post;
|
|
}
|
|
|
|
private function createMockReaction(int $id, int $postId, string $userId, string $reactionType): Reaction {
|
|
$reaction = new Reaction();
|
|
$reaction->setId($id);
|
|
$reaction->setPostId($postId);
|
|
$reaction->setUserId($userId);
|
|
$reaction->setReactionType($reactionType);
|
|
$reaction->setCreatedAt(time());
|
|
return $reaction;
|
|
}
|
|
|
|
public function testCreatePostIncrementsThreadPostCount(): void {
|
|
$threadId = 1;
|
|
$content = 'New reply post content';
|
|
$userId = 'user1';
|
|
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn($userId);
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
$thread = new Thread();
|
|
$thread->setId($threadId);
|
|
$thread->setCategoryId(1);
|
|
$thread->setPostCount(5); // Thread has 5 replies
|
|
|
|
$category = new Category();
|
|
$category->setId(1);
|
|
$category->setPostCount(10); // Category has 10 total replies
|
|
|
|
$createdPost = $this->createMockPost(1, $threadId, $userId, $content);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('insert')
|
|
->willReturn($createdPost);
|
|
|
|
$this->readMarkerMapper->method('createOrUpdate');
|
|
|
|
$this->threadMapper->expects($this->once())
|
|
->method('find')
|
|
->willReturn($thread);
|
|
|
|
$this->threadMapper->expects($this->once())
|
|
->method('update')
|
|
->willReturnCallback(function ($updatedThread) {
|
|
// Thread post count should be incremented from 5 to 6
|
|
$this->assertEquals(6, $updatedThread->getPostCount());
|
|
return $updatedThread;
|
|
});
|
|
|
|
$this->categoryMapper->expects($this->once())
|
|
->method('find')
|
|
->willReturn($category);
|
|
|
|
$this->categoryMapper->expects($this->once())
|
|
->method('update')
|
|
->willReturnCallback(function ($updatedCategory) {
|
|
// Category post count should be incremented from 10 to 11
|
|
$this->assertEquals(11, $updatedCategory->getPostCount());
|
|
return $updatedCategory;
|
|
});
|
|
|
|
$this->forumUserMapper->expects($this->once())
|
|
->method('incrementPostCount')
|
|
->with($userId);
|
|
|
|
$this->notificationService->method('notifyThreadSubscribers');
|
|
|
|
$response = $this->controller->create($threadId, $content);
|
|
|
|
$this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
|
|
}
|
|
|
|
public function testDestroyPostDecrementsThreadPostCount(): void {
|
|
$postId = 1;
|
|
$userId = 'user1';
|
|
$post = $this->createMockPost($postId, 1, $userId, 'Test content');
|
|
$post->setIsFirstPost(false); // Regular reply post
|
|
|
|
$thread = new Thread();
|
|
$thread->setId(1);
|
|
$thread->setCategoryId(1);
|
|
$thread->setPostCount(5); // Thread has 5 replies
|
|
$thread->setLastPostId($postId); // This post is the last post
|
|
|
|
$category = new Category();
|
|
$category->setId(1);
|
|
$category->setPostCount(10); // Category has 10 total replies
|
|
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn($userId);
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
$this->permissionService->method('getCategoryIdFromPost')->willReturn(1);
|
|
$this->permissionService->method('hasCategoryPermission')->willReturn(false);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('find')
|
|
->willReturn($post);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('update')
|
|
->willReturnCallback(function ($updatedPost) {
|
|
$this->assertNotNull($updatedPost->getDeletedAt());
|
|
return $updatedPost;
|
|
});
|
|
|
|
$this->threadMapper->expects($this->once())
|
|
->method('find')
|
|
->willReturn($thread);
|
|
|
|
$this->threadMapper->expects($this->once())
|
|
->method('update')
|
|
->willReturnCallback(function ($updatedThread) {
|
|
// Thread post count should be decremented from 5 to 4
|
|
$this->assertEquals(4, $updatedThread->getPostCount());
|
|
return $updatedThread;
|
|
});
|
|
|
|
// Mock finding the last post (not the deleted one)
|
|
$lastPost = $this->createMockPost(2, 1, $userId, 'Last post');
|
|
$this->postMapper->expects($this->once())
|
|
->method('findLatestByThreadId')
|
|
->with(1, $postId)
|
|
->willReturn($lastPost);
|
|
|
|
$this->categoryMapper->expects($this->once())
|
|
->method('find')
|
|
->willReturn($category);
|
|
|
|
$this->categoryMapper->expects($this->once())
|
|
->method('update')
|
|
->willReturnCallback(function ($updatedCategory) {
|
|
// Category post count should be decremented from 10 to 9
|
|
$this->assertEquals(9, $updatedCategory->getPostCount());
|
|
return $updatedCategory;
|
|
});
|
|
|
|
$this->forumUserMapper->expects($this->once())
|
|
->method('decrementPostCount')
|
|
->with($userId);
|
|
|
|
$response = $this->controller->destroy($postId);
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
}
|
|
|
|
public function testDestroyFirstPostDecrementsThreadCount(): void {
|
|
$postId = 1;
|
|
$userId = 'user1';
|
|
$post = $this->createMockPost($postId, 1, $userId, 'First post content');
|
|
$post->setIsFirstPost(true); // First post
|
|
|
|
$thread = new Thread();
|
|
$thread->setId(1);
|
|
$thread->setCategoryId(1);
|
|
$thread->setPostCount(3); // Thread has 3 replies
|
|
$thread->setLastPostId($postId); // This post is the last post
|
|
|
|
$category = new Category();
|
|
$category->setId(1);
|
|
$category->setPostCount(10); // Category has 10 total replies
|
|
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn($userId);
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
$this->permissionService->method('getCategoryIdFromPost')->willReturn(1);
|
|
$this->permissionService->method('hasCategoryPermission')->willReturn(false);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('find')
|
|
->willReturn($post);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('update')
|
|
->willReturnCallback(function ($updatedPost) {
|
|
$this->assertNotNull($updatedPost->getDeletedAt());
|
|
return $updatedPost;
|
|
});
|
|
|
|
$this->threadMapper->expects($this->once())
|
|
->method('find')
|
|
->willReturn($thread);
|
|
|
|
$this->threadMapper->expects($this->once())
|
|
->method('update')
|
|
->willReturnCallback(function ($updatedThread) {
|
|
// Thread post count should stay at 3 (first posts don't count)
|
|
$this->assertEquals(3, $updatedThread->getPostCount());
|
|
return $updatedThread;
|
|
});
|
|
|
|
$lastPost = $this->createMockPost(2, 1, $userId, 'Last post');
|
|
$this->postMapper->expects($this->once())
|
|
->method('findLatestByThreadId')
|
|
->with(1, $postId)
|
|
->willReturn($lastPost);
|
|
|
|
// Category mapper should not be called for first post deletion
|
|
$this->categoryMapper->expects($this->never())
|
|
->method('find');
|
|
|
|
$this->categoryMapper->expects($this->never())
|
|
->method('update');
|
|
|
|
// First post deletion should decrement thread count, not post count
|
|
$this->forumUserMapper->expects($this->once())
|
|
->method('decrementThreadCount')
|
|
->with($userId);
|
|
|
|
$this->forumUserMapper->expects($this->never())
|
|
->method('decrementPostCount');
|
|
|
|
$response = $this->controller->destroy($postId);
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
}
|
|
|
|
public function testByThreadPaginatedReturnsPostsSuccessfully(): void {
|
|
$threadId = 1;
|
|
$page = 1;
|
|
$perPage = 20;
|
|
|
|
// Create mock first post
|
|
$firstPost = $this->createMockPost(1, $threadId, 'user1', 'First post content');
|
|
$firstPost->setIsFirstPost(true);
|
|
|
|
// Create mock replies
|
|
$reply1 = $this->createMockPost(2, $threadId, 'user1', 'Reply 1');
|
|
$reply2 = $this->createMockPost(3, $threadId, 'user2', 'Reply 2');
|
|
$replies = [$reply1, $reply2];
|
|
|
|
// Create mock BBCode
|
|
$bbcode = new BBCode();
|
|
$bbcode->setId(1);
|
|
$bbcode->setTag('b');
|
|
$bbcode->setReplacement('<strong>{content}</strong>');
|
|
$bbcode->setEnabled(true);
|
|
|
|
// Mock user session (authenticated user)
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
// Mock read marker (no existing marker = unread thread)
|
|
$this->readMarkerMapper->expects($this->once())
|
|
->method('findByUserAndThread')
|
|
->with('user1', $threadId)
|
|
->willThrowException(new DoesNotExistException('No read marker'));
|
|
|
|
// Set up PostMapper expectations
|
|
$this->postMapper->expects($this->once())
|
|
->method('countRepliesByThreadId')
|
|
->with($threadId)
|
|
->willReturn(2);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('findFirstPostByThreadId')
|
|
->with($threadId)
|
|
->willReturn($firstPost);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('findRepliesByThreadId')
|
|
->with($threadId, $perPage, 0)
|
|
->willReturn($replies);
|
|
|
|
$this->bbCodeMapper->expects($this->once())
|
|
->method('findAllEnabled')
|
|
->willReturn([$bbcode]);
|
|
|
|
$this->reactionMapper->expects($this->once())
|
|
->method('findByPostIds')
|
|
->with([1, 2, 3])
|
|
->willReturn([]);
|
|
|
|
$this->userService->expects($this->once())
|
|
->method('enrichMultipleUsers')
|
|
->willReturn([
|
|
'user1' => ['userId' => 'user1', 'displayName' => 'User 1', 'roles' => []],
|
|
'user2' => ['userId' => 'user2', 'displayName' => 'User 2', 'roles' => []],
|
|
]);
|
|
|
|
// Execute
|
|
$response = $this->controller->byThreadPaginated($threadId, $page, $perPage);
|
|
|
|
// Assert
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertArrayHasKey('firstPost', $data);
|
|
$this->assertArrayHasKey('replies', $data);
|
|
$this->assertArrayHasKey('pagination', $data);
|
|
$this->assertEquals(1, $data['firstPost']['id']);
|
|
$this->assertCount(2, $data['replies']);
|
|
$this->assertEquals(1, $data['pagination']['page']);
|
|
$this->assertEquals(20, $data['pagination']['perPage']);
|
|
$this->assertEquals(2, $data['pagination']['total']);
|
|
$this->assertEquals(1, $data['pagination']['totalPages']);
|
|
}
|
|
|
|
public function testByThreadPaginatedStartsAtLastPageForUnreadThread(): void {
|
|
$threadId = 1;
|
|
$perPage = 20;
|
|
|
|
// Create mock first post
|
|
$firstPost = $this->createMockPost(1, $threadId, 'user1', 'First post content');
|
|
$firstPost->setIsFirstPost(true);
|
|
|
|
// Create 25 mock replies (2 pages)
|
|
$replies = [];
|
|
for ($i = 2; $i <= 26; $i++) {
|
|
$reply = $this->createMockPost($i, $threadId, 'user1', "Reply $i");
|
|
$replies[] = $reply;
|
|
}
|
|
|
|
// Mock user session
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
// Mock read marker - no existing marker (never read)
|
|
$this->readMarkerMapper->expects($this->once())
|
|
->method('findByUserAndThread')
|
|
->with('user1', $threadId)
|
|
->willThrowException(new DoesNotExistException('No read marker'));
|
|
|
|
// 25 replies = 2 pages (20 per page)
|
|
$this->postMapper->expects($this->once())
|
|
->method('countRepliesByThreadId')
|
|
->with($threadId)
|
|
->willReturn(25);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('findFirstPostByThreadId')
|
|
->with($threadId)
|
|
->willReturn($firstPost);
|
|
|
|
// When page=0, it should calculate startPage=2 (last page) and fetch from offset 20
|
|
$this->postMapper->expects($this->once())
|
|
->method('findRepliesByThreadId')
|
|
->with($threadId, $perPage, 20) // offset = (2-1) * 20 = 20
|
|
->willReturn(array_slice($replies, 20)); // Last 5 replies
|
|
|
|
$this->bbCodeMapper->method('findAllEnabled')->willReturn([]);
|
|
$this->reactionMapper->method('findByPostIds')->willReturn([]);
|
|
$this->userService->method('enrichMultipleUsers')->willReturn([
|
|
'user1' => ['userId' => 'user1', 'displayName' => 'User 1', 'roles' => []],
|
|
]);
|
|
|
|
// Execute with page=0 (auto-calculate start page)
|
|
$response = $this->controller->byThreadPaginated($threadId, 0, $perPage);
|
|
|
|
// Assert
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertEquals(2, $data['pagination']['page']); // Should be on last page
|
|
$this->assertEquals(2, $data['pagination']['totalPages']);
|
|
$this->assertEquals(2, $data['pagination']['startPage']); // Unread = last page
|
|
$this->assertNull($data['pagination']['lastReadPostId']);
|
|
}
|
|
|
|
public function testByThreadPaginatedStartsAtOldestUnreadPage(): void {
|
|
$threadId = 1;
|
|
$perPage = 20;
|
|
|
|
// Create mock first post
|
|
$firstPost = $this->createMockPost(1, $threadId, 'user1', 'First post content');
|
|
$firstPost->setIsFirstPost(true);
|
|
|
|
// Create mock oldest unread reply (ID 32, which would be on page 2)
|
|
$oldestUnreadReply = $this->createMockPost(32, $threadId, 'user2', 'Oldest unread');
|
|
|
|
// Mock user session
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
// Mock read marker - last read post ID is 31
|
|
$readMarker = new ReadMarker();
|
|
$readMarker->setUserId('user1');
|
|
$readMarker->setEntityId($threadId);
|
|
$readMarker->setMarkerType(ReadMarker::TYPE_THREAD);
|
|
$readMarker->setLastReadPostId(31);
|
|
$readMarker->setReadAt(time());
|
|
|
|
$this->readMarkerMapper->expects($this->once())
|
|
->method('findByUserAndThread')
|
|
->with('user1', $threadId)
|
|
->willReturn($readMarker);
|
|
|
|
// 50 replies = 3 pages
|
|
$this->postMapper->expects($this->once())
|
|
->method('countRepliesByThreadId')
|
|
->with($threadId)
|
|
->willReturn(50);
|
|
|
|
// Find oldest unread reply
|
|
$this->postMapper->expects($this->once())
|
|
->method('findOldestUnreadReply')
|
|
->with($threadId, 31)
|
|
->willReturn($oldestUnreadReply);
|
|
|
|
// Get position of oldest unread reply (30 replies before it = page 2)
|
|
$this->postMapper->expects($this->once())
|
|
->method('getReplyPosition')
|
|
->with($threadId, 32)
|
|
->willReturn(30); // 0-indexed position 30 = page 2 (floor(30/20)+1)
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('findFirstPostByThreadId')
|
|
->with($threadId)
|
|
->willReturn($firstPost);
|
|
|
|
// Should fetch page 2 (offset 20)
|
|
$this->postMapper->expects($this->once())
|
|
->method('findRepliesByThreadId')
|
|
->with($threadId, $perPage, 20)
|
|
->willReturn([]);
|
|
|
|
$this->bbCodeMapper->method('findAllEnabled')->willReturn([]);
|
|
$this->reactionMapper->method('findByPostIds')->willReturn([]);
|
|
$this->userService->method('enrichMultipleUsers')->willReturn([
|
|
'user1' => ['userId' => 'user1', 'displayName' => 'User 1', 'roles' => []],
|
|
]);
|
|
|
|
// Execute with page=0 (auto-calculate start page)
|
|
$response = $this->controller->byThreadPaginated($threadId, 0, $perPage);
|
|
|
|
// Assert
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertEquals(2, $data['pagination']['page']); // Page with oldest unread
|
|
$this->assertEquals(3, $data['pagination']['totalPages']);
|
|
$this->assertEquals(2, $data['pagination']['startPage']);
|
|
$this->assertEquals(31, $data['pagination']['lastReadPostId']);
|
|
}
|
|
|
|
public function testByThreadPaginatedGoesToLastPageWhenAllRead(): void {
|
|
$threadId = 1;
|
|
$perPage = 20;
|
|
|
|
// Create mock first post
|
|
$firstPost = $this->createMockPost(1, $threadId, 'user1', 'First post content');
|
|
$firstPost->setIsFirstPost(true);
|
|
|
|
// Mock user session
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
// Mock read marker - all posts read (last read = 50)
|
|
$readMarker = new ReadMarker();
|
|
$readMarker->setUserId('user1');
|
|
$readMarker->setEntityId($threadId);
|
|
$readMarker->setMarkerType(ReadMarker::TYPE_THREAD);
|
|
$readMarker->setLastReadPostId(50);
|
|
$readMarker->setReadAt(time());
|
|
|
|
$this->readMarkerMapper->expects($this->once())
|
|
->method('findByUserAndThread')
|
|
->with('user1', $threadId)
|
|
->willReturn($readMarker);
|
|
|
|
// 40 replies = 2 pages
|
|
$this->postMapper->expects($this->once())
|
|
->method('countRepliesByThreadId')
|
|
->with($threadId)
|
|
->willReturn(40);
|
|
|
|
// No unread replies (all read)
|
|
$this->postMapper->expects($this->once())
|
|
->method('findOldestUnreadReply')
|
|
->with($threadId, 50)
|
|
->willReturn(null);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('findFirstPostByThreadId')
|
|
->with($threadId)
|
|
->willReturn($firstPost);
|
|
|
|
// Should fetch last page (page 2, offset 20)
|
|
$this->postMapper->expects($this->once())
|
|
->method('findRepliesByThreadId')
|
|
->with($threadId, $perPage, 20)
|
|
->willReturn([]);
|
|
|
|
$this->bbCodeMapper->method('findAllEnabled')->willReturn([]);
|
|
$this->reactionMapper->method('findByPostIds')->willReturn([]);
|
|
$this->userService->method('enrichMultipleUsers')->willReturn([
|
|
'user1' => ['userId' => 'user1', 'displayName' => 'User 1', 'roles' => []],
|
|
]);
|
|
|
|
// Execute with page=0 (auto-calculate start page)
|
|
$response = $this->controller->byThreadPaginated($threadId, 0, $perPage);
|
|
|
|
// Assert
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertEquals(2, $data['pagination']['page']); // Last page
|
|
$this->assertEquals(2, $data['pagination']['totalPages']);
|
|
$this->assertEquals(2, $data['pagination']['startPage']); // All read = last page
|
|
$this->assertEquals(50, $data['pagination']['lastReadPostId']);
|
|
}
|
|
|
|
public function testByThreadPaginatedWorksForGuestUser(): void {
|
|
$threadId = 1;
|
|
$page = 1;
|
|
$perPage = 20;
|
|
|
|
// Create mock first post
|
|
$firstPost = $this->createMockPost(1, $threadId, 'user1', 'First post content');
|
|
$firstPost->setIsFirstPost(true);
|
|
|
|
// Mock user session - guest (no user)
|
|
$this->userSession->method('getUser')->willReturn(null);
|
|
|
|
// 10 replies = 1 page
|
|
$this->postMapper->expects($this->once())
|
|
->method('countRepliesByThreadId')
|
|
->with($threadId)
|
|
->willReturn(10);
|
|
|
|
// Read marker should not be called for guests
|
|
$this->readMarkerMapper->expects($this->never())
|
|
->method('findByUserAndThread');
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('findFirstPostByThreadId')
|
|
->with($threadId)
|
|
->willReturn($firstPost);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('findRepliesByThreadId')
|
|
->with($threadId, $perPage, 0)
|
|
->willReturn([]);
|
|
|
|
$this->bbCodeMapper->method('findAllEnabled')->willReturn([]);
|
|
$this->reactionMapper->method('findByPostIds')->willReturn([]);
|
|
$this->userService->method('enrichMultipleUsers')->willReturn([
|
|
'user1' => ['userId' => 'user1', 'displayName' => 'User 1', 'roles' => []],
|
|
]);
|
|
|
|
// Execute
|
|
$response = $this->controller->byThreadPaginated($threadId, $page, $perPage);
|
|
|
|
// Assert
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertEquals(1, $data['pagination']['page']);
|
|
$this->assertEquals(1, $data['pagination']['startPage']); // Guests start at last page
|
|
$this->assertNull($data['pagination']['lastReadPostId']);
|
|
}
|
|
|
|
public function testByThreadPaginatedRespectsExplicitPageParameter(): void {
|
|
$threadId = 1;
|
|
$perPage = 20;
|
|
|
|
// Create mock first post
|
|
$firstPost = $this->createMockPost(1, $threadId, 'user1', 'First post content');
|
|
$firstPost->setIsFirstPost(true);
|
|
|
|
// Mock user session
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
// Mock read marker - no existing marker
|
|
$this->readMarkerMapper->expects($this->once())
|
|
->method('findByUserAndThread')
|
|
->willThrowException(new DoesNotExistException('No read marker'));
|
|
|
|
// 60 replies = 3 pages
|
|
$this->postMapper->expects($this->once())
|
|
->method('countRepliesByThreadId')
|
|
->with($threadId)
|
|
->willReturn(60);
|
|
|
|
$this->postMapper->expects($this->once())
|
|
->method('findFirstPostByThreadId')
|
|
->with($threadId)
|
|
->willReturn($firstPost);
|
|
|
|
// Request page 2 explicitly (even though startPage would be 3)
|
|
$this->postMapper->expects($this->once())
|
|
->method('findRepliesByThreadId')
|
|
->with($threadId, $perPage, 20) // Page 2 = offset 20
|
|
->willReturn([]);
|
|
|
|
$this->bbCodeMapper->method('findAllEnabled')->willReturn([]);
|
|
$this->reactionMapper->method('findByPostIds')->willReturn([]);
|
|
$this->userService->method('enrichMultipleUsers')->willReturn([
|
|
'user1' => ['userId' => 'user1', 'displayName' => 'User 1', 'roles' => []],
|
|
]);
|
|
|
|
// Execute with explicit page=2
|
|
$response = $this->controller->byThreadPaginated($threadId, 2, $perPage);
|
|
|
|
// Assert
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertEquals(2, $data['pagination']['page']); // Requested page 2
|
|
$this->assertEquals(3, $data['pagination']['totalPages']);
|
|
$this->assertEquals(3, $data['pagination']['startPage']); // StartPage is still 3 (last page for unread)
|
|
}
|
|
}
|