mirror of
https://github.com/chenasraf/nextcloud-forum.git
synced 2026-05-18 01:28:58 +00:00
feat: allow updating thread title (author/admin/moderator)
This commit is contained in:
@@ -15,6 +15,7 @@ use OCA\Forum\Db\Thread;
|
||||
use OCA\Forum\Db\ThreadMapper;
|
||||
use OCA\Forum\Db\ThreadSubscriptionMapper;
|
||||
use OCA\Forum\Db\UserStatsMapper;
|
||||
use OCA\Forum\Service\PermissionService;
|
||||
use OCA\Forum\Service\ThreadEnrichmentService;
|
||||
use OCA\Forum\Service\UserPreferencesService;
|
||||
use OCA\Forum\Service\UserService;
|
||||
@@ -41,6 +42,7 @@ class ThreadController extends OCSController {
|
||||
private ThreadEnrichmentService $threadEnrichmentService,
|
||||
private UserPreferencesService $userPreferencesService,
|
||||
private UserService $userService,
|
||||
private PermissionService $permissionService,
|
||||
private IUserSession $userSession,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
@@ -318,15 +320,45 @@ class ThreadController extends OCSController {
|
||||
* 200: Thread updated
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
#[RequirePermission('canModerate', resourceType: 'category', resourceIdFromThreadId: 'id')]
|
||||
#[RequirePermission('canView', resourceType: 'category', resourceIdFromThreadId: 'id')]
|
||||
#[ApiRoute(verb: 'PUT', url: '/api/threads/{id}')]
|
||||
public function update(int $id, ?string $title = null, ?bool $isLocked = null, ?bool $isPinned = null, ?bool $isHidden = null): DataResponse {
|
||||
try {
|
||||
$user = $this->userSession->getUser();
|
||||
|
||||
if (!$user) {
|
||||
return new DataResponse(['error' => 'User not authenticated'], Http::STATUS_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
$thread = $this->threadMapper->find($id);
|
||||
|
||||
// Check if user is the author or has moderation permission
|
||||
$isAuthor = $thread->getAuthorId() === $user->getUID();
|
||||
$canModerate = $this->permissionService->hasCategoryPermission(
|
||||
$user->getUID(),
|
||||
$thread->getCategoryId(),
|
||||
'canModerate'
|
||||
);
|
||||
|
||||
// Title can be updated by author or moderator
|
||||
if ($title !== null) {
|
||||
if (!$isAuthor && !$canModerate) {
|
||||
return new DataResponse(
|
||||
['error' => 'You do not have permission to edit this thread title'],
|
||||
Http::STATUS_FORBIDDEN
|
||||
);
|
||||
}
|
||||
$thread->setTitle($title);
|
||||
}
|
||||
|
||||
// Lock, pin, and hidden status can only be updated by moderators
|
||||
if (($isLocked !== null || $isPinned !== null || $isHidden !== null) && !$canModerate) {
|
||||
return new DataResponse(
|
||||
['error' => 'You do not have permission to modify thread status'],
|
||||
Http::STATUS_FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
if ($isLocked !== null) {
|
||||
$thread->setIsLocked($isLocked);
|
||||
}
|
||||
@@ -340,7 +372,7 @@ class ThreadController extends OCSController {
|
||||
|
||||
/** @var \OCA\Forum\Db\Thread */
|
||||
$updatedThread = $this->threadMapper->update($thread);
|
||||
return new DataResponse($updatedThread->jsonSerialize());
|
||||
return new DataResponse($this->threadEnrichmentService->enrichThread($updatedThread));
|
||||
} catch (DoesNotExistException $e) {
|
||||
return new DataResponse(['error' => 'Thread not found'], Http::STATUS_NOT_FOUND);
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -105,15 +105,51 @@
|
||||
<!-- Thread Header -->
|
||||
<div v-else-if="thread" class="thread-header mt-16">
|
||||
<div class="thread-title-section">
|
||||
<h2 class="thread-title">
|
||||
<span v-if="thread.isPinned" class="badge badge-pinned" :title="strings.pinned">
|
||||
<PinIcon :size="20" />
|
||||
</span>
|
||||
<span v-if="thread.isLocked" class="badge badge-locked" :title="strings.locked">
|
||||
<LockIcon :size="20" />
|
||||
</span>
|
||||
{{ thread.title }}
|
||||
</h2>
|
||||
<div class="thread-title-row">
|
||||
<h2 v-if="!isEditingTitle" class="thread-title">
|
||||
<span v-if="thread.isPinned" class="badge badge-pinned" :title="strings.pinned">
|
||||
<PinIcon :size="20" />
|
||||
</span>
|
||||
<span v-if="thread.isLocked" class="badge badge-locked" :title="strings.locked">
|
||||
<LockIcon :size="20" />
|
||||
</span>
|
||||
{{ thread.title }}
|
||||
</h2>
|
||||
<NcTextField
|
||||
v-else
|
||||
v-model="editedTitle"
|
||||
class="thread-title-input"
|
||||
@keydown.enter="handleSaveTitle"
|
||||
@keydown.esc="handleCancelEditTitle"
|
||||
ref="titleInput"
|
||||
:disabled="isSavingTitle"
|
||||
/>
|
||||
<NcButton
|
||||
v-if="!isEditingTitle && canEditTitle"
|
||||
@click="handleStartEditTitle"
|
||||
type="tertiary"
|
||||
:aria-label="strings.editTitle"
|
||||
:title="strings.editTitle"
|
||||
class="edit-title-button"
|
||||
>
|
||||
<template #icon>
|
||||
<PencilIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton
|
||||
v-if="isEditingTitle"
|
||||
@click="handleSaveTitle"
|
||||
:disabled="isSavingTitle || !editedTitle.trim()"
|
||||
type="primary"
|
||||
:aria-label="strings.saveTitle"
|
||||
:title="strings.saveTitle"
|
||||
class="save-title-button"
|
||||
>
|
||||
<template #icon>
|
||||
<CheckIcon :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
<div class="thread-meta">
|
||||
<span class="meta-item">
|
||||
<span class="meta-label">{{ strings.by }}</span>
|
||||
@@ -216,6 +252,7 @@ import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
|
||||
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
import AppToolbar from '@/components/AppToolbar.vue'
|
||||
import PageWrapper from '@/components/PageWrapper.vue'
|
||||
import PostCard from '@/components/PostCard.vue'
|
||||
@@ -230,6 +267,8 @@ import BellIcon from '@icons/Bell.vue'
|
||||
import ArrowLeftIcon from '@icons/ArrowLeft.vue'
|
||||
import RefreshIcon from '@icons/Refresh.vue'
|
||||
import ReplyIcon from '@icons/Reply.vue'
|
||||
import PencilIcon from '@icons/Pencil.vue'
|
||||
import CheckIcon from '@icons/Check.vue'
|
||||
import type { Post } from '@/types'
|
||||
import { ocs } from '@/axios'
|
||||
import { t, n } from '@nextcloud/l10n'
|
||||
@@ -248,6 +287,7 @@ export default defineComponent({
|
||||
NcLoadingIcon,
|
||||
NcDateTime,
|
||||
NcNoteCard,
|
||||
NcTextField,
|
||||
AppToolbar,
|
||||
PageWrapper,
|
||||
PostCard,
|
||||
@@ -262,6 +302,8 @@ export default defineComponent({
|
||||
ArrowLeftIcon,
|
||||
RefreshIcon,
|
||||
ReplyIcon,
|
||||
PencilIcon,
|
||||
CheckIcon,
|
||||
},
|
||||
setup() {
|
||||
const { currentThread: thread, fetchThread } = useCurrentThread()
|
||||
@@ -285,6 +327,9 @@ export default defineComponent({
|
||||
offset: 0,
|
||||
postCardRefs: new Map<number, any>(),
|
||||
canModerate: false,
|
||||
isEditingTitle: false,
|
||||
editedTitle: '',
|
||||
isSavingTitle: false,
|
||||
|
||||
strings: {
|
||||
back: t('forum', 'Back'),
|
||||
@@ -317,6 +362,9 @@ export default defineComponent({
|
||||
subscribed: t('forum', 'Subscribed'),
|
||||
threadSubscribed: t('forum', 'Subscribed to thread'),
|
||||
threadUnsubscribed: t('forum', 'Unsubscribed from thread'),
|
||||
editTitle: t('forum', 'Edit title'),
|
||||
saveTitle: t('forum', 'Save title'),
|
||||
titleUpdated: t('forum', 'Thread title updated'),
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -327,6 +375,10 @@ export default defineComponent({
|
||||
threadSlug(): string | null {
|
||||
return (this.$route.params.slug as string) || null
|
||||
},
|
||||
canEditTitle(): boolean {
|
||||
// Allow if user is the author, or has moderation permissions
|
||||
return this.thread?.authorId === this.userId || this.canModerate
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$route.hash'(newHash: string) {
|
||||
@@ -775,6 +827,61 @@ export default defineComponent({
|
||||
this.$router.push('/')
|
||||
}
|
||||
},
|
||||
|
||||
handleStartEditTitle(): void {
|
||||
if (!this.thread) return
|
||||
|
||||
this.editedTitle = this.thread.title
|
||||
this.isEditingTitle = true
|
||||
|
||||
// Focus the input after it's rendered
|
||||
this.$nextTick(() => {
|
||||
const textFieldComponent = this.$refs.titleInput as any
|
||||
if (textFieldComponent && textFieldComponent.$el) {
|
||||
const input = textFieldComponent.$el.querySelector('input')
|
||||
if (input) {
|
||||
input.focus()
|
||||
input.select()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
handleCancelEditTitle(): void {
|
||||
this.isEditingTitle = false
|
||||
this.editedTitle = ''
|
||||
},
|
||||
|
||||
async handleSaveTitle(): Promise<void> {
|
||||
if (!this.thread || !this.editedTitle.trim() || this.isSavingTitle) return
|
||||
|
||||
// Don't save if title hasn't changed
|
||||
if (this.editedTitle.trim() === this.thread.title) {
|
||||
this.handleCancelEditTitle()
|
||||
return
|
||||
}
|
||||
|
||||
this.isSavingTitle = true
|
||||
|
||||
try {
|
||||
const response = await ocs.put(`/threads/${this.thread.id}/title`, {
|
||||
title: this.editedTitle.trim(),
|
||||
})
|
||||
|
||||
if (response.data) {
|
||||
// Update local thread state
|
||||
this.thread.title = this.editedTitle.trim()
|
||||
showSuccess(this.strings.titleUpdated)
|
||||
this.isEditingTitle = false
|
||||
this.editedTitle = ''
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to update thread title', e)
|
||||
showError(t('forum', 'Failed to update thread title'))
|
||||
} finally {
|
||||
this.isSavingTitle = false
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -835,6 +942,12 @@ export default defineComponent({
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.thread-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.thread-title {
|
||||
margin: 0;
|
||||
font-size: 1.75rem;
|
||||
@@ -846,6 +959,22 @@ export default defineComponent({
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.thread-title-input {
|
||||
flex: 1;
|
||||
|
||||
:deep(input) {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-main-text);
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-title-button,
|
||||
.save-title-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 1.2rem;
|
||||
display: inline-flex;
|
||||
|
||||
@@ -15,6 +15,7 @@ use OCA\Forum\Db\ThreadMapper;
|
||||
use OCA\Forum\Db\ThreadSubscriptionMapper;
|
||||
use OCA\Forum\Db\UserStats;
|
||||
use OCA\Forum\Db\UserStatsMapper;
|
||||
use OCA\Forum\Service\PermissionService;
|
||||
use OCA\Forum\Service\ThreadEnrichmentService;
|
||||
use OCA\Forum\Service\UserPreferencesService;
|
||||
use OCA\Forum\Service\UserService;
|
||||
@@ -36,6 +37,7 @@ class ThreadControllerTest extends TestCase {
|
||||
private ThreadEnrichmentService $threadEnrichmentService;
|
||||
private UserPreferencesService $userPreferencesService;
|
||||
private UserService $userService;
|
||||
private PermissionService $permissionService;
|
||||
private IUserSession $userSession;
|
||||
private LoggerInterface $logger;
|
||||
private IRequest $request;
|
||||
@@ -50,6 +52,7 @@ class ThreadControllerTest extends TestCase {
|
||||
$this->threadEnrichmentService = $this->createMock(ThreadEnrichmentService::class);
|
||||
$this->userPreferencesService = $this->createMock(UserPreferencesService::class);
|
||||
$this->userService = $this->createMock(UserService::class);
|
||||
$this->permissionService = $this->createMock(PermissionService::class);
|
||||
$this->userSession = $this->createMock(IUserSession::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
|
||||
@@ -75,6 +78,7 @@ class ThreadControllerTest extends TestCase {
|
||||
$this->threadEnrichmentService,
|
||||
$this->userPreferencesService,
|
||||
$this->userService,
|
||||
$this->permissionService,
|
||||
$this->userSession,
|
||||
$this->logger
|
||||
);
|
||||
@@ -348,16 +352,27 @@ class ThreadControllerTest extends TestCase {
|
||||
$this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testUpdateThreadSuccessfully(): void {
|
||||
public function testUpdateThreadTitleSuccessfullyAsAuthor(): void {
|
||||
$threadId = 1;
|
||||
$categoryId = 1;
|
||||
$authorId = 'user1';
|
||||
$newTitle = 'Updated Title';
|
||||
$thread = $this->createMockThread($threadId, 1, 'user1', 'Original Title');
|
||||
$thread = $this->createMockThread($threadId, $categoryId, $authorId, 'Original Title');
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')->willReturn($authorId);
|
||||
$this->userSession->method('getUser')->willReturn($user);
|
||||
|
||||
$this->threadMapper->expects($this->once())
|
||||
->method('find')
|
||||
->with($threadId)
|
||||
->willReturn($thread);
|
||||
|
||||
$this->permissionService->expects($this->once())
|
||||
->method('hasCategoryPermission')
|
||||
->with($authorId, $categoryId, 'canModerate')
|
||||
->willReturn(false);
|
||||
|
||||
$this->threadMapper->expects($this->once())
|
||||
->method('update')
|
||||
->willReturnCallback(function ($updatedThread) use ($newTitle) {
|
||||
@@ -372,15 +387,93 @@ class ThreadControllerTest extends TestCase {
|
||||
$this->assertEquals($threadId, $data['id']);
|
||||
}
|
||||
|
||||
public function testUpdateThreadLockedStatus(): void {
|
||||
public function testUpdateThreadTitleSuccessfullyAsModerator(): void {
|
||||
$threadId = 1;
|
||||
$thread = $this->createMockThread($threadId, 1, 'user1', 'Test Thread');
|
||||
$categoryId = 1;
|
||||
$authorId = 'user1';
|
||||
$moderatorId = 'moderator1';
|
||||
$newTitle = 'Updated Title';
|
||||
$thread = $this->createMockThread($threadId, $categoryId, $authorId, 'Original Title');
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')->willReturn($moderatorId);
|
||||
$this->userSession->method('getUser')->willReturn($user);
|
||||
|
||||
$this->threadMapper->expects($this->once())
|
||||
->method('find')
|
||||
->with($threadId)
|
||||
->willReturn($thread);
|
||||
|
||||
$this->permissionService->expects($this->once())
|
||||
->method('hasCategoryPermission')
|
||||
->with($moderatorId, $categoryId, 'canModerate')
|
||||
->willReturn(true);
|
||||
|
||||
$this->threadMapper->expects($this->once())
|
||||
->method('update')
|
||||
->willReturnCallback(function ($updatedThread) use ($newTitle) {
|
||||
$this->assertEquals($newTitle, $updatedThread->getTitle());
|
||||
return $updatedThread;
|
||||
});
|
||||
|
||||
$response = $this->controller->update($threadId, $newTitle);
|
||||
|
||||
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
||||
$data = $response->getData();
|
||||
$this->assertEquals($threadId, $data['id']);
|
||||
}
|
||||
|
||||
public function testUpdateThreadTitleReturnsForbiddenWhenUserIsNeitherAuthorNorModerator(): void {
|
||||
$threadId = 1;
|
||||
$categoryId = 1;
|
||||
$authorId = 'user1';
|
||||
$otherUserId = 'user2';
|
||||
$newTitle = 'Updated Title';
|
||||
$thread = $this->createMockThread($threadId, $categoryId, $authorId, 'Original Title');
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')->willReturn($otherUserId);
|
||||
$this->userSession->method('getUser')->willReturn($user);
|
||||
|
||||
$this->threadMapper->expects($this->once())
|
||||
->method('find')
|
||||
->with($threadId)
|
||||
->willReturn($thread);
|
||||
|
||||
$this->permissionService->expects($this->once())
|
||||
->method('hasCategoryPermission')
|
||||
->with($otherUserId, $categoryId, 'canModerate')
|
||||
->willReturn(false);
|
||||
|
||||
$this->threadMapper->expects($this->never())
|
||||
->method('update');
|
||||
|
||||
$response = $this->controller->update($threadId, $newTitle);
|
||||
|
||||
$this->assertEquals(Http::STATUS_FORBIDDEN, $response->getStatus());
|
||||
$this->assertEquals(['error' => 'You do not have permission to edit this thread title'], $response->getData());
|
||||
}
|
||||
|
||||
public function testUpdateThreadLockedStatus(): void {
|
||||
$threadId = 1;
|
||||
$categoryId = 1;
|
||||
$moderatorId = 'moderator1';
|
||||
$thread = $this->createMockThread($threadId, $categoryId, 'user1', 'Test Thread');
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')->willReturn($moderatorId);
|
||||
$this->userSession->method('getUser')->willReturn($user);
|
||||
|
||||
$this->threadMapper->expects($this->once())
|
||||
->method('find')
|
||||
->with($threadId)
|
||||
->willReturn($thread);
|
||||
|
||||
$this->permissionService->expects($this->once())
|
||||
->method('hasCategoryPermission')
|
||||
->with($moderatorId, $categoryId, 'canModerate')
|
||||
->willReturn(true);
|
||||
|
||||
$this->threadMapper->expects($this->once())
|
||||
->method('update')
|
||||
->willReturnCallback(function ($updatedThread) {
|
||||
@@ -395,13 +488,24 @@ class ThreadControllerTest extends TestCase {
|
||||
|
||||
public function testUpdateThreadPinnedStatus(): void {
|
||||
$threadId = 1;
|
||||
$thread = $this->createMockThread($threadId, 1, 'user1', 'Test Thread');
|
||||
$categoryId = 1;
|
||||
$moderatorId = 'moderator1';
|
||||
$thread = $this->createMockThread($threadId, $categoryId, 'user1', 'Test Thread');
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')->willReturn($moderatorId);
|
||||
$this->userSession->method('getUser')->willReturn($user);
|
||||
|
||||
$this->threadMapper->expects($this->once())
|
||||
->method('find')
|
||||
->with($threadId)
|
||||
->willReturn($thread);
|
||||
|
||||
$this->permissionService->expects($this->once())
|
||||
->method('hasCategoryPermission')
|
||||
->with($moderatorId, $categoryId, 'canModerate')
|
||||
->willReturn(true);
|
||||
|
||||
$this->threadMapper->expects($this->once())
|
||||
->method('update')
|
||||
->willReturnCallback(function ($updatedThread) {
|
||||
@@ -416,13 +520,24 @@ class ThreadControllerTest extends TestCase {
|
||||
|
||||
public function testUpdateThreadHiddenStatus(): void {
|
||||
$threadId = 1;
|
||||
$thread = $this->createMockThread($threadId, 1, 'user1', 'Test Thread');
|
||||
$categoryId = 1;
|
||||
$moderatorId = 'moderator1';
|
||||
$thread = $this->createMockThread($threadId, $categoryId, 'user1', 'Test Thread');
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')->willReturn($moderatorId);
|
||||
$this->userSession->method('getUser')->willReturn($user);
|
||||
|
||||
$this->threadMapper->expects($this->once())
|
||||
->method('find')
|
||||
->with($threadId)
|
||||
->willReturn($thread);
|
||||
|
||||
$this->permissionService->expects($this->once())
|
||||
->method('hasCategoryPermission')
|
||||
->with($moderatorId, $categoryId, 'canModerate')
|
||||
->willReturn(true);
|
||||
|
||||
$this->threadMapper->expects($this->once())
|
||||
->method('update')
|
||||
->willReturnCallback(function ($updatedThread) {
|
||||
@@ -435,8 +550,28 @@ class ThreadControllerTest extends TestCase {
|
||||
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
||||
}
|
||||
|
||||
public function testUpdateThreadReturnsUnauthorizedWhenUserNotAuthenticated(): void {
|
||||
$threadId = 1;
|
||||
$newTitle = 'New Title';
|
||||
|
||||
$this->userSession->method('getUser')->willReturn(null);
|
||||
|
||||
$this->threadMapper->expects($this->never())
|
||||
->method('find');
|
||||
|
||||
$response = $this->controller->update($threadId, $newTitle);
|
||||
|
||||
$this->assertEquals(Http::STATUS_UNAUTHORIZED, $response->getStatus());
|
||||
$this->assertEquals(['error' => 'User not authenticated'], $response->getData());
|
||||
}
|
||||
|
||||
public function testUpdateThreadReturnsNotFoundWhenThreadDoesNotExist(): void {
|
||||
$threadId = 999;
|
||||
$userId = 'user1';
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')->willReturn($userId);
|
||||
$this->userSession->method('getUser')->willReturn($user);
|
||||
|
||||
$this->threadMapper->expects($this->once())
|
||||
->method('find')
|
||||
@@ -449,6 +584,35 @@ class ThreadControllerTest extends TestCase {
|
||||
$this->assertEquals(['error' => 'Thread not found'], $response->getData());
|
||||
}
|
||||
|
||||
public function testUpdateThreadStatusReturnsForbiddenWhenUserLacksModeratePermission(): void {
|
||||
$threadId = 1;
|
||||
$categoryId = 1;
|
||||
$userId = 'user1';
|
||||
$thread = $this->createMockThread($threadId, $categoryId, $userId, 'Test Thread');
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')->willReturn($userId);
|
||||
$this->userSession->method('getUser')->willReturn($user);
|
||||
|
||||
$this->threadMapper->expects($this->once())
|
||||
->method('find')
|
||||
->with($threadId)
|
||||
->willReturn($thread);
|
||||
|
||||
$this->permissionService->expects($this->once())
|
||||
->method('hasCategoryPermission')
|
||||
->with($userId, $categoryId, 'canModerate')
|
||||
->willReturn(false);
|
||||
|
||||
$this->threadMapper->expects($this->never())
|
||||
->method('update');
|
||||
|
||||
$response = $this->controller->update($threadId, null, true);
|
||||
|
||||
$this->assertEquals(Http::STATUS_FORBIDDEN, $response->getStatus());
|
||||
$this->assertEquals(['error' => 'You do not have permission to modify thread status'], $response->getData());
|
||||
}
|
||||
|
||||
public function testDestroyThreadSuccessfully(): void {
|
||||
$threadId = 1;
|
||||
$categoryId = 1;
|
||||
|
||||
Reference in New Issue
Block a user