mirror of
https://github.com/chenasraf/nextcloud-forum.git
synced 2026-05-18 01:28:58 +00:00
500 lines
16 KiB
PHP
500 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace OCA\Forum\Tests\Controller;
|
|
|
|
use OCA\Forum\AppInfo\Application;
|
|
use OCA\Forum\Controller\SearchController;
|
|
use OCA\Forum\Db\Post;
|
|
use OCA\Forum\Db\PostMapper;
|
|
use OCA\Forum\Db\Thread;
|
|
use OCA\Forum\Db\ThreadMapper;
|
|
use OCA\Forum\Service\PostEnrichmentService;
|
|
use OCA\Forum\Service\SearchService;
|
|
use OCA\Forum\Service\ThreadEnrichmentService;
|
|
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 SearchControllerTest extends TestCase {
|
|
private SearchController $controller;
|
|
/** @var SearchService&MockObject */
|
|
private SearchService $searchService;
|
|
/** @var PostMapper&MockObject */
|
|
private PostMapper $postMapper;
|
|
/** @var ThreadMapper&MockObject */
|
|
private ThreadMapper $threadMapper;
|
|
/** @var PostEnrichmentService&MockObject */
|
|
private PostEnrichmentService $postEnrichmentService;
|
|
/** @var ThreadEnrichmentService&MockObject */
|
|
private ThreadEnrichmentService $threadEnrichmentService;
|
|
/** @var UserService&MockObject */
|
|
private UserService $userService;
|
|
/** @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->searchService = $this->createMock(SearchService::class);
|
|
$this->postMapper = $this->createMock(PostMapper::class);
|
|
$this->threadMapper = $this->createMock(ThreadMapper::class);
|
|
$this->postEnrichmentService = $this->createMock(PostEnrichmentService::class);
|
|
$this->threadEnrichmentService = $this->createMock(ThreadEnrichmentService::class);
|
|
$this->userService = $this->createMock(UserService::class);
|
|
$this->userSession = $this->createMock(IUserSession::class);
|
|
$this->logger = $this->createMock(LoggerInterface::class);
|
|
|
|
// Mock post enrichment service
|
|
$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;
|
|
});
|
|
|
|
// Mock thread enrichment service
|
|
$this->threadEnrichmentService->method('enrichThread')
|
|
->willReturnCallback(function ($thread) {
|
|
$data = is_array($thread) ? $thread : $thread->jsonSerialize();
|
|
$data['author'] = ['userId' => $data['authorId'], 'displayName' => 'Test User'];
|
|
$data['categorySlug'] = 'test-category';
|
|
$data['categoryName'] = 'Test Category';
|
|
$data['isSubscribed'] = false;
|
|
return $data;
|
|
});
|
|
|
|
$this->controller = new SearchController(
|
|
Application::APP_ID,
|
|
$this->request,
|
|
$this->searchService,
|
|
$this->postMapper,
|
|
$this->threadMapper,
|
|
$this->postEnrichmentService,
|
|
$this->threadEnrichmentService,
|
|
$this->userService,
|
|
$this->userSession,
|
|
$this->logger
|
|
);
|
|
}
|
|
|
|
public function testIndexAllowsGuestUsers(): void {
|
|
// Guest user (null user session)
|
|
$this->userSession->method('getUser')
|
|
->willReturn(null);
|
|
|
|
$thread1 = $this->createMockThread(1, 'Test Thread', 'test-thread', 1);
|
|
|
|
$searchResults = [
|
|
'threads' => [$thread1],
|
|
'posts' => [],
|
|
'threadCount' => 1,
|
|
'postCount' => 0,
|
|
];
|
|
|
|
// Should call search service with null userId for guests
|
|
$this->searchService->expects($this->once())
|
|
->method('search')
|
|
->with('test query', null, true, true, null, 50, 0)
|
|
->willReturn($searchResults);
|
|
|
|
$this->userService->expects($this->once())
|
|
->method('enrichMultipleUsers')
|
|
->willReturn(['user1' => ['userId' => 'user1', 'displayName' => 'User 1']]);
|
|
|
|
$response = $this->controller->index('test query');
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertArrayHasKey('threads', $data);
|
|
$this->assertEquals(1, $data['threadCount']);
|
|
}
|
|
|
|
public function testIndexReturnsErrorWhenQueryIsEmpty(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
$response = $this->controller->index('');
|
|
|
|
$this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertArrayHasKey('error', $data);
|
|
$this->assertEquals('Search query is required', $data['error']);
|
|
}
|
|
|
|
public function testIndexReturnsErrorWhenQueryIsWhitespace(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
$response = $this->controller->index(' ');
|
|
|
|
$this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertArrayHasKey('error', $data);
|
|
$this->assertEquals('Search query is required', $data['error']);
|
|
}
|
|
|
|
public function testIndexReturnsErrorWhenNoSearchScopeSelected(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
$response = $this->controller->index('test query', false, false);
|
|
|
|
$this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertArrayHasKey('error', $data);
|
|
$this->assertEquals('At least one search scope must be selected (threads or posts)', $data['error']);
|
|
}
|
|
|
|
public function testIndexReturnsSearchResults(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
$thread1 = $this->createMockThread(1, 'Test Thread', 'test-thread', 1);
|
|
$thread2 = $this->createMockThread(2, 'Another Thread', 'another-thread', 1);
|
|
|
|
$post1 = $this->createMockPost(1, 1, 'user1', 'Test post content');
|
|
$post2 = $this->createMockPost(2, 2, 'user2', 'Another post content');
|
|
|
|
$searchResults = [
|
|
'threads' => [$thread1, $thread2],
|
|
'posts' => [$post1, $post2],
|
|
'threadCount' => 2,
|
|
'postCount' => 2,
|
|
];
|
|
|
|
$this->searchService->expects($this->once())
|
|
->method('search')
|
|
->with('test query', 'user1', true, true, null, 50, 0)
|
|
->willReturn($searchResults);
|
|
|
|
// Mock thread mapper for enriching posts
|
|
$this->threadMapper->expects($this->exactly(2))
|
|
->method('find')
|
|
->willReturnMap([
|
|
[1, $thread1],
|
|
[2, $thread2],
|
|
]);
|
|
|
|
// Mock userService
|
|
$this->userService->expects($this->once())
|
|
->method('enrichMultipleUsers')
|
|
->willReturn([
|
|
'user1' => ['userId' => 'user1', 'displayName' => 'User 1'],
|
|
'user2' => ['userId' => 'user2', 'displayName' => 'User 2'],
|
|
]);
|
|
|
|
$response = $this->controller->index('test query');
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertArrayHasKey('threads', $data);
|
|
$this->assertArrayHasKey('posts', $data);
|
|
$this->assertArrayHasKey('threadCount', $data);
|
|
$this->assertArrayHasKey('postCount', $data);
|
|
$this->assertArrayHasKey('query', $data);
|
|
$this->assertEquals('test query', $data['query']);
|
|
$this->assertEquals(2, $data['threadCount']);
|
|
$this->assertEquals(2, $data['postCount']);
|
|
$this->assertCount(2, $data['threads']);
|
|
$this->assertCount(2, $data['posts']);
|
|
}
|
|
|
|
public function testIndexWithThreadsOnlySearch(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
$thread1 = $this->createMockThread(1, 'Test Thread', 'test-thread', 1);
|
|
|
|
$searchResults = [
|
|
'threads' => [$thread1],
|
|
'posts' => [],
|
|
'threadCount' => 1,
|
|
'postCount' => 0,
|
|
];
|
|
|
|
$this->searchService->expects($this->once())
|
|
->method('search')
|
|
->with('test query', 'user1', true, false, null, 50, 0)
|
|
->willReturn($searchResults);
|
|
|
|
$this->userService->expects($this->once())
|
|
->method('enrichMultipleUsers')
|
|
->willReturn(['user1' => ['userId' => 'user1', 'displayName' => 'User 1']]);
|
|
|
|
$response = $this->controller->index('test query', true, false);
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertEquals(1, $data['threadCount']);
|
|
$this->assertEquals(0, $data['postCount']);
|
|
$this->assertCount(1, $data['threads']);
|
|
$this->assertCount(0, $data['posts']);
|
|
}
|
|
|
|
public function testIndexWithPostsOnlySearch(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
$post1 = $this->createMockPost(1, 1, 'user1', 'Test post content');
|
|
$thread1 = $this->createMockThread(1, 'Test Thread', 'test-thread', 1);
|
|
|
|
$searchResults = [
|
|
'threads' => [],
|
|
'posts' => [$post1],
|
|
'threadCount' => 0,
|
|
'postCount' => 1,
|
|
];
|
|
|
|
$this->searchService->expects($this->once())
|
|
->method('search')
|
|
->with('test query', 'user1', false, true, null, 50, 0)
|
|
->willReturn($searchResults);
|
|
|
|
// Mock thread mapper for enriching posts
|
|
$this->threadMapper->expects($this->once())
|
|
->method('find')
|
|
->with(1)
|
|
->willReturn($thread1);
|
|
|
|
$this->userService->expects($this->once())
|
|
->method('enrichMultipleUsers')
|
|
->willReturn(['user1' => ['userId' => 'user1', 'displayName' => 'User 1']]);
|
|
|
|
$response = $this->controller->index('test query', false, true);
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertEquals(0, $data['threadCount']);
|
|
$this->assertEquals(1, $data['postCount']);
|
|
$this->assertCount(0, $data['threads']);
|
|
$this->assertCount(1, $data['posts']);
|
|
}
|
|
|
|
public function testIndexWithCategoryFilter(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
$thread1 = $this->createMockThread(1, 'Test Thread', 'test-thread', 1);
|
|
|
|
$searchResults = [
|
|
'threads' => [$thread1],
|
|
'posts' => [],
|
|
'threadCount' => 1,
|
|
'postCount' => 0,
|
|
];
|
|
|
|
$this->searchService->expects($this->once())
|
|
->method('search')
|
|
->with('test query', 'user1', true, true, 1, 50, 0)
|
|
->willReturn($searchResults);
|
|
|
|
$this->userService->expects($this->once())
|
|
->method('enrichMultipleUsers')
|
|
->willReturn(['user1' => ['userId' => 'user1', 'displayName' => 'User 1']]);
|
|
|
|
$response = $this->controller->index('test query', true, true, 1);
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertEquals(1, $data['threadCount']);
|
|
}
|
|
|
|
public function testIndexWithCustomLimitAndOffset(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
$searchResults = [
|
|
'threads' => [],
|
|
'posts' => [],
|
|
'threadCount' => 0,
|
|
'postCount' => 0,
|
|
];
|
|
|
|
$this->searchService->expects($this->once())
|
|
->method('search')
|
|
->with('test query', 'user1', true, true, null, 25, 10)
|
|
->willReturn($searchResults);
|
|
|
|
$response = $this->controller->index('test query', true, true, null, 25, 10);
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
}
|
|
|
|
public function testIndexHandlesDeletedThread(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
$post1 = $this->createMockPost(1, 1, 'user1', 'Test post content');
|
|
|
|
$searchResults = [
|
|
'threads' => [],
|
|
'posts' => [$post1],
|
|
'threadCount' => 0,
|
|
'postCount' => 1,
|
|
];
|
|
|
|
$this->searchService->expects($this->once())
|
|
->method('search')
|
|
->willReturn($searchResults);
|
|
|
|
// Thread not found (deleted)
|
|
$this->threadMapper->expects($this->once())
|
|
->method('find')
|
|
->with(1)
|
|
->willThrowException(new DoesNotExistException('Thread not found'));
|
|
|
|
$this->userService->expects($this->once())
|
|
->method('enrichMultipleUsers')
|
|
->willReturn(['user1' => ['userId' => 'user1', 'displayName' => 'User 1']]);
|
|
|
|
$response = $this->controller->index('test query');
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertCount(1, $data['posts']);
|
|
// Check that thread context is null
|
|
$this->assertNull($data['posts'][0]['threadTitle']);
|
|
$this->assertNull($data['posts'][0]['threadSlug']);
|
|
}
|
|
|
|
public function testIndexHandlesSearchServiceException(): void {
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')->willReturn('user1');
|
|
$this->userSession->method('getUser')->willReturn($user);
|
|
|
|
$this->searchService->expects($this->once())
|
|
->method('search')
|
|
->willThrowException(new \Exception('Database error'));
|
|
|
|
$this->logger->expects($this->once())
|
|
->method('error');
|
|
|
|
$response = $this->controller->index('test query');
|
|
|
|
$this->assertEquals(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertArrayHasKey('error', $data);
|
|
$this->assertEquals('Failed to perform search', $data['error']);
|
|
}
|
|
|
|
public function testIndexGuestUserWithEmptyQuery(): void {
|
|
// Guest user should still get validation errors
|
|
$this->userSession->method('getUser')
|
|
->willReturn(null);
|
|
|
|
$response = $this->controller->index('');
|
|
|
|
$this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertArrayHasKey('error', $data);
|
|
$this->assertEquals('Search query is required', $data['error']);
|
|
}
|
|
|
|
public function testIndexGuestUserWithCategoryFilter(): void {
|
|
// Guest user searching within a specific category
|
|
$this->userSession->method('getUser')
|
|
->willReturn(null);
|
|
|
|
$thread1 = $this->createMockThread(1, 'Test Thread', 'test-thread', 5);
|
|
|
|
$searchResults = [
|
|
'threads' => [$thread1],
|
|
'posts' => [],
|
|
'threadCount' => 1,
|
|
'postCount' => 0,
|
|
];
|
|
|
|
$this->searchService->expects($this->once())
|
|
->method('search')
|
|
->with('test query', null, true, true, 5, 50, 0)
|
|
->willReturn($searchResults);
|
|
|
|
$this->userService->expects($this->once())
|
|
->method('enrichMultipleUsers')
|
|
->willReturn(['user1' => ['userId' => 'user1', 'displayName' => 'User 1']]);
|
|
|
|
$response = $this->controller->index('test query', true, true, 5);
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertEquals(1, $data['threadCount']);
|
|
}
|
|
|
|
public function testIndexGuestUserWithNoResults(): void {
|
|
// Guest user with no accessible categories or no results
|
|
$this->userSession->method('getUser')
|
|
->willReturn(null);
|
|
|
|
$searchResults = [
|
|
'threads' => [],
|
|
'posts' => [],
|
|
'threadCount' => 0,
|
|
'postCount' => 0,
|
|
];
|
|
|
|
$this->searchService->expects($this->once())
|
|
->method('search')
|
|
->with('test query', null, true, true, null, 50, 0)
|
|
->willReturn($searchResults);
|
|
|
|
$response = $this->controller->index('test query');
|
|
|
|
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
|
$data = $response->getData();
|
|
$this->assertEquals(0, $data['threadCount']);
|
|
$this->assertEquals(0, $data['postCount']);
|
|
$this->assertCount(0, $data['threads']);
|
|
$this->assertCount(0, $data['posts']);
|
|
}
|
|
|
|
private function createMockThread(int $id, string $title, string $slug, int $categoryId): Thread {
|
|
$thread = new Thread();
|
|
$thread->setId($id);
|
|
$thread->setTitle($title);
|
|
$thread->setSlug($slug);
|
|
$thread->setCategoryId($categoryId);
|
|
$thread->setAuthorId('user1');
|
|
$thread->setCreatedAt(time());
|
|
$thread->setUpdatedAt(time());
|
|
$thread->setIsPinned(false);
|
|
$thread->setIsLocked(false);
|
|
$thread->setIsHidden(false);
|
|
$thread->setPostCount(1);
|
|
$thread->setViewCount(0);
|
|
return $thread;
|
|
}
|
|
|
|
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->setCreatedAt(time());
|
|
$post->setUpdatedAt(time());
|
|
$post->setIsFirstPost(false);
|
|
return $post;
|
|
}
|
|
}
|