]> BookStack Code Mirror - bookstack/commitdiff
API: Built out tests for comment API endpoints
authorDan Brown <redacted>
Thu, 23 Oct 2025 15:52:29 +0000 (16:52 +0100)
committerDan Brown <redacted>
Thu, 23 Oct 2025 15:52:29 +0000 (16:52 +0100)
app/Activity/CommentRepo.php
app/Activity/Controllers/CommentApiController.php
app/Activity/Models/Comment.php
tests/Activity/CommentsApiTest.php
tests/TestCase.php

index ba12f4d33cb1195d5ef6b036fcfb0d9058230fc0..20513bcd4f59c1549ee81ef452cc9f695732dbc6 100644 (file)
@@ -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) {
index 7ba9b5b645d86e57e54fe7fa222fc3d195eb1998..92551bf36a1f6f3c2b48a8dd0f3926dabd236273 100644 (file)
@@ -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);
     }
index 91c91e7a8ae1160d925bb636b8c31fbe33c15f45..4d6c7fa41c6c908640505cd084058ea0c35aa0b3 100644 (file)
@@ -41,7 +41,7 @@ class Comment extends Model implements Loggable, OwnableInterface
      */
     public function entity(): MorphTo
     {
-        return $this->morphTo('entity');
+        return $this->morphTo('commentable');
     }
 
     /**
index 29769a2605b9e9905eb02898d6b61209097f09cb..ec4ddba995c422d56c91d56dd9c44c3ed4a8f6ef 100644 (file)
@@ -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' => '<p>My wonderful comment</p>',
+            'content_ref' => 'test-content-ref',
+        ]);
+        $resp->assertOk();
+        $id = $resp->json('id');
+
+        $this->assertDatabaseHas('comments', [
+            'id' => $id,
+            'commentable_id' => $page->id,
+            'commentable_type' => 'page',
+            'html' => '<p>My wonderful comment</p>',
+        ]);
+
+        $comment = Comment::query()->findOrFail($id);
+        $this->assertIsInt($comment->local_id);
+
+        $reply = $this->actingAsApiEditor()->postJson('/api/comments', [
+            'page_id' => $page->id,
+            'html' => '<p>My wonderful reply</p>',
+            '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' => '<p>My wonderful reply</p>',
+            'parent_id' => $comment->local_id,
+        ]);
     }
 
     public function test_read()
     {
-        // TODO
+        $page = $this->entities->page();
+        $user = $this->users->viewer();
+        $comment = Comment::factory()->make([
+            'html' => '<p>A lovely comment <script>hello</script></p>',
+            'created_by' => $user->id,
+            'updated_by' => $user->id,
+        ]);
+        $page->comments()->save($comment);
+        $comment->refresh();
+        $reply = Comment::factory()->make([
+            'parent_id' => $comment->local_id,
+            'html' => '<p>A lovely<script>angry</script>reply</p>',
+        ]);
+        $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' => '<p>A lovely comment </p>',
+            'archived' => false,
+            'created_by' => [
+                'id' => $user->id,
+                'name' => $user->name,
+            ],
+            'updated_by' => [
+                'id' => $user->id,
+                'name' => $user->name,
+            ],
+            'replies' => [
+                [
+                    'id' => $reply->id,
+                    'html' => '<p>A lovelyreply</p>'
+                ]
+            ]
+        ]);
     }
 
     public function test_update()
     {
-        // TODO
+        $page = $this->entities->page();
+        $user = $this->users->editor();
+        $this->permissions->grantUserRolePermissions($user, [Permission::CommentUpdateAll]);
+        $comment = Comment::factory()->make([
+            'html' => '<p>A lovely comment</p>',
+            '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' => '<p>A lovely updated comment</p>',
+        ])->assertOk();
+
+        $this->assertDatabaseHas('comments', [
+            'id' => $comment->id,
+            'html' => '<p>A lovely updated comment</p>',
+            'archived' => 0,
+        ]);
+
+        $this->putJson("/api/comments/{$comment->id}", [
+            'archived' => true,
+        ]);
+
+        $this->assertDatabaseHas('comments', [
+            'id' => $comment->id,
+            'html' => '<p>A lovely updated comment</p>',
+            'archived' => 1,
+        ]);
+
+        $this->putJson("/api/comments/{$comment->id}", [
+            'archived' => false,
+            'html' => '<p>A lovely updated again comment</p>',
+        ]);
+
+        $this->assertDatabaseHas('comments', [
+            'id' => $comment->id,
+            'html' => '<p>A lovely updated again comment</p>',
+            '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' => '<p>A lovely comment</p>',
+            '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' => '<p>A lovely comment</p>',
+        ]);
+        $page->comments()->save($comment);
+
+        $this->actingAsForApi($user)->deleteJson("/api/comments/{$comment->id}")->assertStatus(204);
+        $this->assertDatabaseMissing('comments', [
+            'id' => $comment->id,
+        ]);
     }
 }
index 2395317482eb856ae389d91eaeac587b7949b133..f69f20d4c5590c7a482712bbd780a6d3dbb222cd 100644 (file)
@@ -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