Files
nextcloud-forum/tests/unit/Controller/SearchControllerTest.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;
}
}