diff --git a/app/Activity/CommentRepo.php b/app/Activity/CommentRepo.php index ba12f4d33..20513bcd4 100644 --- a/app/Activity/CommentRepo.php +++ b/app/Activity/CommentRepo.php @@ -50,8 +50,8 @@ class CommentRepo // Validate parent ID if ($parentId !== null) { $parentCommentExists = Comment::query() - ->where('entity_id', '=', $entity->id) - ->where('entity_type', '=', $entity->getMorphClass()) + ->where('commentable_id', '=', $entity->id) + ->where('commentable_type', '=', $entity->getMorphClass()) ->where('local_id', '=', $parentId) ->exists(); if (!$parentCommentExists) { diff --git a/app/Activity/Controllers/CommentApiController.php b/app/Activity/Controllers/CommentApiController.php index 7ba9b5b64..92551bf36 100644 --- a/app/Activity/Controllers/CommentApiController.php +++ b/app/Activity/Controllers/CommentApiController.php @@ -23,9 +23,6 @@ class CommentApiController extends ApiController { // TODO - Add tree-style comment listing to page-show responses. - // TODO - Test visibility controls - // TODO - Test permissions of each action - protected array $rules = [ 'create' => [ 'page_id' => ['required', 'integer'], @@ -34,7 +31,7 @@ class CommentApiController extends ApiController 'content_ref' => ['string'], ], 'update' => [ - 'html' => ['required', 'string'], + 'html' => ['string'], 'archived' => ['boolean'], ] ]; @@ -85,6 +82,7 @@ class CommentApiController extends ApiController public function read(string $id): JsonResponse { $comment = $this->commentRepo->getVisibleById(intval($id)); + $comment->load('createdBy', 'updatedBy'); $replies = $this->commentRepo->getQueryForVisible() ->where('parent_id', '=', $comment->local_id) @@ -117,17 +115,19 @@ class CommentApiController extends ApiController $this->checkOwnablePermission(Permission::CommentUpdate, $comment); $input = $this->validate($request, $this->rules()['update']); + $hasHtml = isset($input['html']); if (isset($input['archived'])) { - $archived = $input['archived']; - if ($archived) { - $this->commentRepo->archive($comment, false); + if ($input['archived']) { + $this->commentRepo->archive($comment, !$hasHtml); } else { - $this->commentRepo->unarchive($comment, false); + $this->commentRepo->unarchive($comment, !$hasHtml); } } - $comment = $this->commentRepo->update($comment, $input['html']); + if ($hasHtml) { + $comment = $this->commentRepo->update($comment, $input['html']); + } return response()->json($comment); } diff --git a/app/Activity/Models/Comment.php b/app/Activity/Models/Comment.php index 91c91e7a8..4d6c7fa41 100644 --- a/app/Activity/Models/Comment.php +++ b/app/Activity/Models/Comment.php @@ -41,7 +41,7 @@ class Comment extends Model implements Loggable, OwnableInterface */ public function entity(): MorphTo { - return $this->morphTo('entity'); + return $this->morphTo('commentable'); } /** diff --git a/tests/Activity/CommentsApiTest.php b/tests/Activity/CommentsApiTest.php index 29769a260..ec4ddba99 100644 --- a/tests/Activity/CommentsApiTest.php +++ b/tests/Activity/CommentsApiTest.php @@ -2,8 +2,8 @@ namespace Activity; -use BookStack\Activity\ActivityType; -use BookStack\Facades\Activity; +use BookStack\Activity\Models\Comment; +use BookStack\Permissions\Permission; use Tests\Api\TestsApi; use Tests\TestCase; @@ -13,31 +13,238 @@ class CommentsApiTest extends TestCase public function test_endpoint_permission_controls() { - // TODO + $user = $this->users->editor(); + $this->permissions->grantUserRolePermissions($user, [Permission::CommentDeleteAll, Permission::CommentUpdateAll]); + + $page = $this->entities->page(); + $comment = Comment::factory()->make(); + $page->comments()->save($comment); + $this->actingAsForApi($user); + + $actions = [ + ['GET', '/api/comments'], + ['GET', "/api/comments/{$comment->id}"], + ['POST', "/api/comments"], + ['PUT', "/api/comments/{$comment->id}"], + ['DELETE', "/api/comments/{$comment->id}"], + ]; + + foreach ($actions as [$method, $endpoint]) { + $resp = $this->call($method, $endpoint); + $this->assertNotPermissionError($resp); + } + + $comment = Comment::factory()->make(); + $page->comments()->save($comment); + $this->getJson("/api/comments")->assertSee(['id' => $comment->id]); + + $this->permissions->removeUserRolePermissions($user, [ + Permission::CommentDeleteAll, Permission::CommentDeleteOwn, + Permission::CommentUpdateAll, Permission::CommentUpdateOwn, + Permission::CommentCreateAll + ]); + + $this->assertPermissionError($this->json('delete', "/api/comments/{$comment->id}")); + $this->assertPermissionError($this->json('put', "/api/comments/{$comment->id}")); + $this->assertPermissionError($this->json('post', "/api/comments")); + $this->assertNotPermissionError($this->json('get', "/api/comments/{$comment->id}")); + + $this->permissions->disableEntityInheritedPermissions($page); + $this->json('get', "/api/comments/{$comment->id}")->assertStatus(404); + $this->getJson("/api/comments")->assertDontSee(['id' => $comment->id]); } public function test_index() { - // TODO + $page = $this->entities->page(); + Comment::query()->delete(); + + $comments = Comment::factory()->count(10)->make(); + $page->comments()->saveMany($comments); + + $firstComment = $comments->first(); + $resp = $this->actingAsApiEditor()->getJson('/api/comments'); + $resp->assertJson([ + 'data' => [ + [ + 'id' => $firstComment->id, + 'commentable_id' => $page->id, + 'commentable_type' => 'page', + 'parent_id' => null, + 'local_id' => $firstComment->local_id, + ], + ], + ]); + $resp->assertJsonCount(10, 'data'); + $resp->assertJson(['total' => 10]); + + $filtered = $this->getJson("/api/comments?filter[id]={$firstComment->id}"); + $filtered->assertJsonCount(1, 'data'); + $filtered->assertJson(['total' => 1]); } public function test_create() { - // TODO + $page = $this->entities->page(); + + $resp = $this->actingAsApiEditor()->postJson('/api/comments', [ + 'page_id' => $page->id, + 'html' => '

My wonderful comment

', + 'content_ref' => 'test-content-ref', + ]); + $resp->assertOk(); + $id = $resp->json('id'); + + $this->assertDatabaseHas('comments', [ + 'id' => $id, + 'commentable_id' => $page->id, + 'commentable_type' => 'page', + 'html' => '

My wonderful comment

', + ]); + + $comment = Comment::query()->findOrFail($id); + $this->assertIsInt($comment->local_id); + + $reply = $this->actingAsApiEditor()->postJson('/api/comments', [ + 'page_id' => $page->id, + 'html' => '

My wonderful reply

', + 'content_ref' => 'test-content-ref', + 'reply_to' => $comment->local_id, + ]); + $reply->assertOk(); + + $this->assertDatabaseHas('comments', [ + 'id' => $reply->json('id'), + 'commentable_id' => $page->id, + 'commentable_type' => 'page', + 'html' => '

My wonderful reply

', + 'parent_id' => $comment->local_id, + ]); } public function test_read() { - // TODO + $page = $this->entities->page(); + $user = $this->users->viewer(); + $comment = Comment::factory()->make([ + 'html' => '

A lovely comment

', + 'created_by' => $user->id, + 'updated_by' => $user->id, + ]); + $page->comments()->save($comment); + $comment->refresh(); + $reply = Comment::factory()->make([ + 'parent_id' => $comment->local_id, + 'html' => '

A lovelyreply

', + ]); + $page->comments()->save($reply); + + $resp = $this->actingAsApiEditor()->getJson("/api/comments/{$comment->id}"); + $resp->assertJson([ + 'id' => $comment->id, + 'commentable_id' => $page->id, + 'commentable_type' => 'page', + 'html' => '

A lovely comment

', + 'archived' => false, + 'created_by' => [ + 'id' => $user->id, + 'name' => $user->name, + ], + 'updated_by' => [ + 'id' => $user->id, + 'name' => $user->name, + ], + 'replies' => [ + [ + 'id' => $reply->id, + 'html' => '

A lovelyreply

' + ] + ] + ]); } public function test_update() { - // TODO + $page = $this->entities->page(); + $user = $this->users->editor(); + $this->permissions->grantUserRolePermissions($user, [Permission::CommentUpdateAll]); + $comment = Comment::factory()->make([ + 'html' => '

A lovely comment

', + 'created_by' => $this->users->viewer()->id, + 'updated_by' => $this->users->viewer()->id, + 'parent_id' => null, + ]); + $page->comments()->save($comment); + + $this->actingAsForApi($user)->putJson("/api/comments/{$comment->id}", [ + 'html' => '

A lovely updated comment

', + ])->assertOk(); + + $this->assertDatabaseHas('comments', [ + 'id' => $comment->id, + 'html' => '

A lovely updated comment

', + 'archived' => 0, + ]); + + $this->putJson("/api/comments/{$comment->id}", [ + 'archived' => true, + ]); + + $this->assertDatabaseHas('comments', [ + 'id' => $comment->id, + 'html' => '

A lovely updated comment

', + 'archived' => 1, + ]); + + $this->putJson("/api/comments/{$comment->id}", [ + 'archived' => false, + 'html' => '

A lovely updated again comment

', + ]); + + $this->assertDatabaseHas('comments', [ + 'id' => $comment->id, + 'html' => '

A lovely updated again comment

', + 'archived' => 0, + ]); + } + + public function test_update_cannot_archive_replies() + { + $page = $this->entities->page(); + $user = $this->users->editor(); + $this->permissions->grantUserRolePermissions($user, [Permission::CommentUpdateAll]); + $comment = Comment::factory()->make([ + 'html' => '

A lovely comment

', + 'created_by' => $this->users->viewer()->id, + 'updated_by' => $this->users->viewer()->id, + 'parent_id' => 90, + ]); + $page->comments()->save($comment); + + $resp = $this->actingAsForApi($user)->putJson("/api/comments/{$comment->id}", [ + 'archived' => true, + ]); + + $this->assertEquals($this->errorResponse('Only top-level comments can be archived.', 400), $resp->json()); + $this->assertDatabaseHas('comments', [ + 'id' => $comment->id, + 'archived' => 0, + ]); } public function test_destroy() { - // TODO + $page = $this->entities->page(); + $user = $this->users->editor(); + $this->permissions->grantUserRolePermissions($user, [Permission::CommentDeleteAll]); + $comment = Comment::factory()->make([ + 'html' => '

A lovely comment

', + ]); + $page->comments()->save($comment); + + $this->actingAsForApi($user)->deleteJson("/api/comments/{$comment->id}")->assertStatus(204); + $this->assertDatabaseMissing('comments', [ + 'id' => $comment->id, + ]); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 239531748..f69f20d4c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -199,7 +199,7 @@ abstract class TestCase extends BaseTestCase { if ($response->status() === 403 && $response instanceof JsonResponse) { $errMessage = $response->getData(true)['error']['message'] ?? ''; - return $errMessage === 'You do not have permission to perform the requested action.'; + return str_contains($errMessage, 'do not have permission'); } return $response->status() === 302