5 use BookStack\Entities\Models\Book;
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,
35 public function test_create_endpoint()
37 $this->actingAsApiEditor();
38 $book = Book::query()->first();
40 'name' => 'My API page',
41 'book_id' => $book->id,
42 'html' => '<p>My new page content</p>',
46 'value' => 'tagvalue',
51 $resp = $this->postJson($this->baseEndpoint, $details);
52 unset($details['html']);
53 $resp->assertStatus(200);
54 $newItem = Page::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
55 $resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug]));
56 $this->assertDatabaseHas('tags', [
57 'entity_id' => $newItem->id,
58 'entity_type' => $newItem->getMorphClass(),
60 'value' => 'tagvalue',
62 $resp->assertSeeText('My new page content');
63 $resp->assertJsonMissing(['book' => []]);
64 $this->assertActivityExists('page_create', $newItem);
67 public function test_page_name_needed_to_create()
69 $this->actingAsApiEditor();
70 $book = Book::query()->first();
72 'book_id' => $book->id,
73 'html' => '<p>A page created via the API</p>',
76 $resp = $this->postJson($this->baseEndpoint, $details);
77 $resp->assertStatus(422);
78 $resp->assertJson($this->validationResponse([
79 'name' => ['The name field is required.'],
83 public function test_book_id_or_chapter_id_needed_to_create()
85 $this->actingAsApiEditor();
87 'name' => 'My api page',
88 'html' => '<p>A page created via the API</p>',
91 $resp = $this->postJson($this->baseEndpoint, $details);
92 $resp->assertStatus(422);
93 $resp->assertJson($this->validationResponse([
94 'book_id' => ['The book id field is required when chapter id is not present.'],
95 'chapter_id' => ['The chapter id field is required when book id is not present.'],
98 $chapter = Chapter::visible()->first();
99 $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['chapter_id' => $chapter->id]));
100 $resp->assertStatus(200);
102 $book = Book::visible()->first();
103 $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['book_id' => $book->id]));
104 $resp->assertStatus(200);
107 public function test_markdown_can_be_provided_for_create()
109 $this->actingAsApiEditor();
110 $book = Book::visible()->first();
112 'book_id' => $book->id,
113 'name' => 'My api page',
114 'markdown' => "# A new API page \n[link](https://example.com)",
117 $resp = $this->postJson($this->baseEndpoint, $details);
118 $resp->assertJson(['markdown' => $details['markdown']]);
120 $respHtml = $resp->json('html');
121 $this->assertStringContainsString('new API page</h1>', $respHtml);
122 $this->assertStringContainsString('link</a>', $respHtml);
123 $this->assertStringContainsString('href="https://example.com"', $respHtml);
126 public function test_read_endpoint()
128 $this->actingAsApiEditor();
129 $page = Page::visible()->first();
131 $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
132 $resp->assertStatus(200);
135 'slug' => $page->slug,
137 'name' => $page->createdBy->name,
139 'book_id' => $page->book_id,
141 'name' => $page->createdBy->name,
144 'name' => $page->ownedBy->name,
149 public function test_read_endpoint_provides_rendered_html()
151 $this->actingAsApiEditor();
152 $page = Page::visible()->first();
153 $page->html = "<p>testing</p><script>alert('danger')</script><h1>Hello</h1>";
156 $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
157 $html = $resp->json('html');
158 $this->assertStringNotContainsString('script', $html);
159 $this->assertStringContainsString('Hello', $html);
160 $this->assertStringContainsString('testing', $html);
163 public function test_update_endpoint()
165 $this->actingAsApiEditor();
166 $page = Page::visible()->first();
168 'name' => 'My updated API page',
169 'html' => '<p>A page created via the API</p>',
172 'name' => 'freshtag',
173 'value' => 'freshtagval',
178 $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
181 $resp->assertStatus(200);
182 unset($details['html']);
183 $resp->assertJson(array_merge($details, [
184 'id' => $page->id, 'slug' => $page->slug, 'book_id' => $page->book_id,
186 $this->assertActivityExists('page_update', $page);
189 public function test_providing_new_chapter_id_on_update_will_move_page()
191 $this->actingAsApiEditor();
192 $page = Page::visible()->first();
193 $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
195 'name' => 'My updated API page',
196 'chapter_id' => $chapter->id,
197 'html' => '<p>A page created via the API</p>',
200 $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
201 $resp->assertStatus(200);
203 'chapter_id' => $chapter->id,
204 'book_id' => $chapter->book_id,
208 public function test_providing_move_via_update_requires_page_create_permission_on_new_parent()
210 $this->actingAsApiEditor();
211 $page = Page::visible()->first();
212 $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
213 $this->setEntityRestrictions($chapter, ['view'], [$this->getEditor()->roles()->first()]);
215 'name' => 'My updated API page',
216 'chapter_id' => $chapter->id,
217 'html' => '<p>A page created via the API</p>',
220 $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
221 $resp->assertStatus(403);
224 public function test_update_endpoint_does_not_wipe_content_if_no_html_or_md_provided()
226 $this->actingAsApiEditor();
227 $page = Page::visible()->first();
228 $originalContent = $page->html;
230 'name' => 'My updated API page',
233 'name' => 'freshtag',
234 'value' => 'freshtagval',
239 $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
242 $this->assertEquals($originalContent, $page->html);
245 public function test_update_increments_updated_date_if_only_tags_are_sent()
247 $this->actingAsApiEditor();
248 $page = Page::visible()->first();
249 DB::table('pages')->where('id', '=', $page->id)->update(['updated_at' => Carbon::now()->subWeek()]);
252 'tags' => [['name' => 'Category', 'value' => 'Testing']]
255 $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
257 $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $page->updated_at->unix());
260 public function test_delete_endpoint()
262 $this->actingAsApiEditor();
263 $page = Page::visible()->first();
264 $resp = $this->deleteJson($this->baseEndpoint . "/{$page->id}");
266 $resp->assertStatus(204);
267 $this->assertActivityExists('page_delete', $page);
270 public function test_export_html_endpoint()
272 $this->actingAsApiEditor();
273 $page = Page::visible()->first();
275 $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/html");
276 $resp->assertStatus(200);
277 $resp->assertSee($page->name);
278 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html"');
281 public function test_export_plain_text_endpoint()
283 $this->actingAsApiEditor();
284 $page = Page::visible()->first();
286 $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/plaintext");
287 $resp->assertStatus(200);
288 $resp->assertSee($page->name);
289 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt"');
292 public function test_export_pdf_endpoint()
294 $this->actingAsApiEditor();
295 $page = Page::visible()->first();
297 $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/pdf");
298 $resp->assertStatus(200);
299 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"');
302 public function test_export_markdown_endpoint()
304 $this->actingAsApiEditor();
305 $page = Page::visible()->first();
307 $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/markdown");
308 $resp->assertStatus(200);
309 $resp->assertSee('# ' . $page->name);
310 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.md"');
313 public function test_cant_export_when_not_have_permission()
315 $types = ['html', 'plaintext', 'pdf', 'markdown'];
316 $this->actingAsApiEditor();
317 $this->removePermissionFromUser($this->getEditor(), 'content-export');
319 $page = Page::visible()->first();
320 foreach ($types as $type) {
321 $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/{$type}");
322 $this->assertPermissionError($resp);