5 use BookStack\Activity\Models\Comment;
6 use BookStack\Entities\Models\Chapter;
7 use BookStack\Entities\Models\Page;
9 use Illuminate\Support\Facades\DB;
12 class PagesApiTest extends TestCase
16 protected string $baseEndpoint = '/api/pages';
18 public function test_index_endpoint_returns_expected_page()
20 $this->actingAsApiEditor();
21 $firstPage = Page::query()->orderBy('id', 'asc')->first();
23 $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
24 $resp->assertJson(['data' => [
26 'id' => $firstPage->id,
27 'name' => $firstPage->name,
28 'slug' => $firstPage->slug,
29 'book_id' => $firstPage->book->id,
30 'priority' => $firstPage->priority,
31 'owned_by' => $firstPage->owned_by,
32 'created_by' => $firstPage->created_by,
33 'updated_by' => $firstPage->updated_by,
34 'revision_count' => $firstPage->revision_count,
39 public function test_create_endpoint()
41 $this->actingAsApiEditor();
42 $book = $this->entities->book();
44 'name' => 'My API page',
45 'book_id' => $book->id,
46 'html' => '<p>My new page content</p>',
50 'value' => 'tagvalue',
56 $resp = $this->postJson($this->baseEndpoint, $details);
57 unset($details['html']);
58 $resp->assertStatus(200);
59 $newItem = Page::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
60 $resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug]));
61 $this->assertDatabaseHas('tags', [
62 'entity_id' => $newItem->id,
63 'entity_type' => $newItem->getMorphClass(),
65 'value' => 'tagvalue',
67 $resp->assertSeeText('My new page content');
68 $resp->assertJsonMissing(['book' => []]);
69 $this->assertActivityExists('page_create', $newItem);
72 public function test_page_name_needed_to_create()
74 $this->actingAsApiEditor();
75 $book = $this->entities->book();
77 'book_id' => $book->id,
78 'html' => '<p>A page created via the API</p>',
81 $resp = $this->postJson($this->baseEndpoint, $details);
82 $resp->assertStatus(422);
83 $resp->assertJson($this->validationResponse([
84 'name' => ['The name field is required.'],
88 public function test_book_id_or_chapter_id_needed_to_create()
90 $this->actingAsApiEditor();
92 'name' => 'My api page',
93 'html' => '<p>A page created via the API</p>',
96 $resp = $this->postJson($this->baseEndpoint, $details);
97 $resp->assertStatus(422);
98 $resp->assertJson($this->validationResponse([
99 'book_id' => ['The book id field is required when chapter id is not present.'],
100 'chapter_id' => ['The chapter id field is required when book id is not present.'],
103 $chapter = $this->entities->chapter();
104 $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['chapter_id' => $chapter->id]));
105 $resp->assertStatus(200);
107 $book = $this->entities->book();
108 $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['book_id' => $book->id]));
109 $resp->assertStatus(200);
112 public function test_markdown_can_be_provided_for_create()
114 $this->actingAsApiEditor();
115 $book = $this->entities->book();
117 'book_id' => $book->id,
118 'name' => 'My api page',
119 'markdown' => "# A new API page \n[link](https://example.com)",
122 $resp = $this->postJson($this->baseEndpoint, $details);
123 $resp->assertJson(['markdown' => $details['markdown']]);
125 $respHtml = $resp->json('html');
126 $this->assertStringContainsString('new API page</h1>', $respHtml);
127 $this->assertStringContainsString('link</a>', $respHtml);
128 $this->assertStringContainsString('href="https://example.com"', $respHtml);
131 public function test_read_endpoint()
133 $this->actingAsApiEditor();
134 $page = $this->entities->page();
136 $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
137 $resp->assertStatus(200);
140 'slug' => $page->slug,
142 'name' => $page->createdBy->name,
144 'book_id' => $page->book_id,
146 'name' => $page->createdBy->name,
149 'name' => $page->ownedBy->name,
154 public function test_read_endpoint_provides_rendered_html()
156 $this->actingAsApiEditor();
157 $page = $this->entities->page();
158 $page->html = "<p>testing</p><script>alert('danger')</script><h1>Hello</h1>";
161 $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
162 $html = $resp->json('html');
163 $this->assertStringNotContainsString('script', $html);
164 $this->assertStringContainsString('Hello', $html);
165 $this->assertStringContainsString('testing', $html);
168 public function test_read_endpoint_provides_raw_html()
170 $html = "<p>testing</p><script>alert('danger')</script><h1>Hello</h1>";
172 $this->actingAsApiEditor();
173 $page = $this->entities->page();
177 $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
178 $this->assertEquals($html, $resp->json('raw_html'));
179 $this->assertNotEquals($html, $resp->json('html'));
182 public function test_read_endpoint_returns_not_found()
184 $this->actingAsApiEditor();
185 // get an id that is not used
186 $id = Page::orderBy('id', 'desc')->first()->id + 1;
187 $this->assertNull(Page::find($id));
189 $resp = $this->getJson($this->baseEndpoint . "/$id");
191 $resp->assertNotFound();
192 $this->assertNull($resp->json('id'));
193 $resp->assertJsonIsObject('error');
194 $resp->assertJsonStructure([
200 $this->assertSame(404, $resp->json('error')['code']);
203 public function test_read_endpoint_includes_page_comments_tree_structure()
205 $this->actingAsApiEditor();
206 $page = $this->entities->page();
207 $relation = ['commentable_type' => 'page', 'commentable_id' => $page->id];
208 $active = Comment::factory()->create([...$relation, 'html' => '<p>My active<script>cat</script> comment</p>']);
209 Comment::factory()->count(5)->create([...$relation, 'parent_id' => $active->local_id]);
210 $archived = Comment::factory()->create([...$relation, 'archived' => true]);
211 Comment::factory()->count(2)->create([...$relation, 'parent_id' => $archived->local_id]);
213 $resp = $this->getJson("{$this->baseEndpoint}/{$page->id}");
216 $resp->assertJsonCount(1, 'comments.active');
217 $resp->assertJsonCount(1, 'comments.archived');
218 $resp->assertJsonCount(5, 'comments.active.0.children');
219 $resp->assertJsonCount(2, 'comments.archived.0.children');
221 $resp->assertJsonFragment([
223 'local_id' => $active->local_id,
224 'html' => '<p>My active comment</p>',
228 public function test_update_endpoint()
230 $this->actingAsApiEditor();
231 $page = $this->entities->page();
233 'name' => 'My updated API page',
234 'html' => '<p>A page created via the API</p>',
237 'name' => 'freshtag',
238 'value' => 'freshtagval',
244 $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
247 $resp->assertStatus(200);
248 unset($details['html']);
249 $resp->assertJson(array_merge($details, [
250 'id' => $page->id, 'slug' => $page->slug, 'book_id' => $page->book_id,
252 $this->assertActivityExists('page_update', $page);
255 public function test_providing_new_chapter_id_on_update_will_move_page()
257 $this->actingAsApiEditor();
258 $page = $this->entities->page();
259 $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
261 'name' => 'My updated API page',
262 'chapter_id' => $chapter->id,
263 'html' => '<p>A page created via the API</p>',
266 $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
267 $resp->assertStatus(200);
269 'chapter_id' => $chapter->id,
270 'book_id' => $chapter->book_id,
274 public function test_providing_move_via_update_requires_page_create_permission_on_new_parent()
276 $this->actingAsApiEditor();
277 $page = $this->entities->page();
278 $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
279 $this->permissions->setEntityPermissions($chapter, ['view'], [$this->users->editor()->roles()->first()]);
281 'name' => 'My updated API page',
282 'chapter_id' => $chapter->id,
283 'html' => '<p>A page created via the API</p>',
286 $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
287 $resp->assertStatus(403);
290 public function test_update_endpoint_does_not_wipe_content_if_no_html_or_md_provided()
292 $this->actingAsApiEditor();
293 $page = $this->entities->page();
294 $originalContent = $page->html;
296 'name' => 'My updated API page',
299 'name' => 'freshtag',
300 'value' => 'freshtagval',
305 $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
308 $this->assertEquals($originalContent, $page->html);
311 public function test_update_increments_updated_date_if_only_tags_are_sent()
313 $this->actingAsApiEditor();
314 $page = $this->entities->page();
315 $page->newQuery()->where('id', '=', $page->id)->update(['updated_at' => Carbon::now()->subWeek()]);
318 'tags' => [['name' => 'Category', 'value' => 'Testing']],
321 $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
325 $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $page->updated_at->unix());
328 public function test_delete_endpoint()
330 $this->actingAsApiEditor();
331 $page = $this->entities->page();
332 $resp = $this->deleteJson($this->baseEndpoint . "/{$page->id}");
334 $resp->assertStatus(204);
335 $this->assertActivityExists('page_delete', $page);