5 use BookStack\Entities\Models\Chapter;
6 use BookStack\Entities\Models\Page;
8 use Illuminate\Support\Facades\DB;
11 class PagesApiTest extends TestCase
15 protected string $baseEndpoint = '/api/pages';
17 public function test_index_endpoint_returns_expected_page()
19 $this->actingAsApiEditor();
20 $firstPage = Page::query()->orderBy('id', 'asc')->first();
22 $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
23 $resp->assertJson(['data' => [
25 'id' => $firstPage->id,
26 'name' => $firstPage->name,
27 'slug' => $firstPage->slug,
28 'book_id' => $firstPage->book->id,
29 'priority' => $firstPage->priority,
30 'owned_by' => $firstPage->owned_by,
31 'created_by' => $firstPage->created_by,
32 'updated_by' => $firstPage->updated_by,
33 'revision_count' => $firstPage->revision_count,
38 public function test_create_endpoint()
40 $this->actingAsApiEditor();
41 $book = $this->entities->book();
43 'name' => 'My API page',
44 'book_id' => $book->id,
45 'html' => '<p>My new page content</p>',
49 'value' => 'tagvalue',
55 $resp = $this->postJson($this->baseEndpoint, $details);
56 unset($details['html']);
57 $resp->assertStatus(200);
58 $newItem = Page::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
59 $resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug]));
60 $this->assertDatabaseHas('tags', [
61 'entity_id' => $newItem->id,
62 'entity_type' => $newItem->getMorphClass(),
64 'value' => 'tagvalue',
66 $resp->assertSeeText('My new page content');
67 $resp->assertJsonMissing(['book' => []]);
68 $this->assertActivityExists('page_create', $newItem);
71 public function test_page_name_needed_to_create()
73 $this->actingAsApiEditor();
74 $book = $this->entities->book();
76 'book_id' => $book->id,
77 'html' => '<p>A page created via the API</p>',
80 $resp = $this->postJson($this->baseEndpoint, $details);
81 $resp->assertStatus(422);
82 $resp->assertJson($this->validationResponse([
83 'name' => ['The name field is required.'],
87 public function test_book_id_or_chapter_id_needed_to_create()
89 $this->actingAsApiEditor();
91 'name' => 'My api page',
92 'html' => '<p>A page created via the API</p>',
95 $resp = $this->postJson($this->baseEndpoint, $details);
96 $resp->assertStatus(422);
97 $resp->assertJson($this->validationResponse([
98 'book_id' => ['The book id field is required when chapter id is not present.'],
99 'chapter_id' => ['The chapter id field is required when book id is not present.'],
102 $chapter = $this->entities->chapter();
103 $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['chapter_id' => $chapter->id]));
104 $resp->assertStatus(200);
106 $book = $this->entities->book();
107 $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['book_id' => $book->id]));
108 $resp->assertStatus(200);
111 public function test_markdown_can_be_provided_for_create()
113 $this->actingAsApiEditor();
114 $book = $this->entities->book();
116 'book_id' => $book->id,
117 'name' => 'My api page',
118 'markdown' => "# A new API page \n[link](https://example.com)",
121 $resp = $this->postJson($this->baseEndpoint, $details);
122 $resp->assertJson(['markdown' => $details['markdown']]);
124 $respHtml = $resp->json('html');
125 $this->assertStringContainsString('new API page</h1>', $respHtml);
126 $this->assertStringContainsString('link</a>', $respHtml);
127 $this->assertStringContainsString('href="https://example.com"', $respHtml);
130 public function test_read_endpoint()
132 $this->actingAsApiEditor();
133 $page = $this->entities->page();
135 $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
136 $resp->assertStatus(200);
139 'slug' => $page->slug,
141 'name' => $page->createdBy->name,
143 'book_id' => $page->book_id,
145 'name' => $page->createdBy->name,
148 'name' => $page->ownedBy->name,
153 public function test_read_endpoint_provides_rendered_html()
155 $this->actingAsApiEditor();
156 $page = $this->entities->page();
157 $page->html = "<p>testing</p><script>alert('danger')</script><h1>Hello</h1>";
160 $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
161 $html = $resp->json('html');
162 $this->assertStringNotContainsString('script', $html);
163 $this->assertStringContainsString('Hello', $html);
164 $this->assertStringContainsString('testing', $html);
167 public function test_read_endpoint_provides_raw_html()
169 $html = "<p>testing</p><script>alert('danger')</script><h1>Hello</h1>";
171 $this->actingAsApiEditor();
172 $page = $this->entities->page();
176 $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
177 $this->assertEquals($html, $resp->json('raw_html'));
178 $this->assertNotEquals($html, $resp->json('html'));
181 public function test_read_endpoint_returns_not_found()
183 $this->actingAsApiEditor();
184 // get an id that is not used
185 $id = Page::orderBy('id', 'desc')->first()->id + 1;
186 $this->assertNull(Page::find($id));
188 $resp = $this->getJson($this->baseEndpoint . "/$id");
190 $resp->assertNotFound();
191 $this->assertNull($resp->json('id'));
192 $resp->assertJsonIsObject('error');
193 $resp->assertJsonStructure([
199 $this->assertSame(404, $resp->json('error')['code']);
202 public function test_update_endpoint()
204 $this->actingAsApiEditor();
205 $page = $this->entities->page();
207 'name' => 'My updated API page',
208 'html' => '<p>A page created via the API</p>',
211 'name' => 'freshtag',
212 'value' => 'freshtagval',
218 $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
221 $resp->assertStatus(200);
222 unset($details['html']);
223 $resp->assertJson(array_merge($details, [
224 'id' => $page->id, 'slug' => $page->slug, 'book_id' => $page->book_id,
226 $this->assertActivityExists('page_update', $page);
229 public function test_providing_new_chapter_id_on_update_will_move_page()
231 $this->actingAsApiEditor();
232 $page = $this->entities->page();
233 $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
235 'name' => 'My updated API page',
236 'chapter_id' => $chapter->id,
237 'html' => '<p>A page created via the API</p>',
240 $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
241 $resp->assertStatus(200);
243 'chapter_id' => $chapter->id,
244 'book_id' => $chapter->book_id,
248 public function test_providing_move_via_update_requires_page_create_permission_on_new_parent()
250 $this->actingAsApiEditor();
251 $page = $this->entities->page();
252 $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
253 $this->permissions->setEntityPermissions($chapter, ['view'], [$this->users->editor()->roles()->first()]);
255 'name' => 'My updated API page',
256 'chapter_id' => $chapter->id,
257 'html' => '<p>A page created via the API</p>',
260 $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
261 $resp->assertStatus(403);
264 public function test_update_endpoint_does_not_wipe_content_if_no_html_or_md_provided()
266 $this->actingAsApiEditor();
267 $page = $this->entities->page();
268 $originalContent = $page->html;
270 'name' => 'My updated API page',
273 'name' => 'freshtag',
274 'value' => 'freshtagval',
279 $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
282 $this->assertEquals($originalContent, $page->html);
285 public function test_update_increments_updated_date_if_only_tags_are_sent()
287 $this->actingAsApiEditor();
288 $page = $this->entities->page();
289 DB::table('pages')->where('id', '=', $page->id)->update(['updated_at' => Carbon::now()->subWeek()]);
292 'tags' => [['name' => 'Category', 'value' => 'Testing']],
295 $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
299 $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $page->updated_at->unix());
302 public function test_delete_endpoint()
304 $this->actingAsApiEditor();
305 $page = $this->entities->page();
306 $resp = $this->deleteJson($this->baseEndpoint . "/{$page->id}");
308 $resp->assertStatus(204);
309 $this->assertActivityExists('page_delete', $page);
312 public function test_export_html_endpoint()
314 $this->actingAsApiEditor();
315 $page = $this->entities->page();
317 $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/html");
318 $resp->assertStatus(200);
319 $resp->assertSee($page->name);
320 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html"');
323 public function test_export_plain_text_endpoint()
325 $this->actingAsApiEditor();
326 $page = $this->entities->page();
328 $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/plaintext");
329 $resp->assertStatus(200);
330 $resp->assertSee($page->name);
331 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt"');
334 public function test_export_pdf_endpoint()
336 $this->actingAsApiEditor();
337 $page = $this->entities->page();
339 $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/pdf");
340 $resp->assertStatus(200);
341 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"');
344 public function test_export_markdown_endpoint()
346 $this->actingAsApiEditor();
347 $page = $this->entities->page();
349 $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/markdown");
350 $resp->assertStatus(200);
351 $resp->assertSee('# ' . $page->name);
352 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.md"');
355 public function test_cant_export_when_not_have_permission()
357 $types = ['html', 'plaintext', 'pdf', 'markdown'];
358 $this->actingAsApiEditor();
359 $this->permissions->removeUserRolePermissions($this->users->editor(), ['content-export']);
361 $page = $this->entities->page();
362 foreach ($types as $type) {
363 $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/{$type}");
364 $this->assertPermissionError($resp);