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,
34 public function test_create_endpoint()
36 $this->actingAsApiEditor();
37 $book = $this->entities->book();
39 'name' => 'My API page',
40 'book_id' => $book->id,
41 'html' => '<p>My new page content</p>',
45 '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 = $this->entities->book();
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 = $this->entities->chapter();
99 $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['chapter_id' => $chapter->id]));
100 $resp->assertStatus(200);
102 $book = $this->entities->book();
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 = $this->entities->book();
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 = $this->entities->page();
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 = $this->entities->page();
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_read_endpoint_provides_raw_html()
165 $html = "<p>testing</p><script>alert('danger')</script><h1>Hello</h1>";
167 $this->actingAsApiEditor();
168 $page = $this->entities->page();
172 $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
173 $this->assertEquals($html, $resp->json('raw_html'));
174 $this->assertNotEquals($html, $resp->json('html'));
177 public function test_read_endpoint_returns_not_found()
179 $this->actingAsApiEditor();
180 // get an id that is not used
181 $id = Page::orderBy('id', 'desc')->first()->id + 1;
182 $this->assertNull(Page::find($id));
184 $resp = $this->getJson($this->baseEndpoint . "/$id");
186 $resp->assertNotFound();
187 $this->assertNull($resp->json('id'));
188 $resp->assertJsonIsObject('error');
189 $resp->assertJsonStructure([
195 $this->assertSame(404, $resp->json('error')['code']);
198 public function test_update_endpoint()
200 $this->actingAsApiEditor();
201 $page = $this->entities->page();
203 'name' => 'My updated API page',
204 'html' => '<p>A page created via the API</p>',
207 'name' => 'freshtag',
208 'value' => 'freshtagval',
214 $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
217 $resp->assertStatus(200);
218 unset($details['html']);
219 $resp->assertJson(array_merge($details, [
220 'id' => $page->id, 'slug' => $page->slug, 'book_id' => $page->book_id,
222 $this->assertActivityExists('page_update', $page);
225 public function test_providing_new_chapter_id_on_update_will_move_page()
227 $this->actingAsApiEditor();
228 $page = $this->entities->page();
229 $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
231 'name' => 'My updated API page',
232 'chapter_id' => $chapter->id,
233 'html' => '<p>A page created via the API</p>',
236 $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
237 $resp->assertStatus(200);
239 'chapter_id' => $chapter->id,
240 'book_id' => $chapter->book_id,
244 public function test_providing_move_via_update_requires_page_create_permission_on_new_parent()
246 $this->actingAsApiEditor();
247 $page = $this->entities->page();
248 $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
249 $this->permissions->setEntityPermissions($chapter, ['view'], [$this->users->editor()->roles()->first()]);
251 'name' => 'My updated API page',
252 'chapter_id' => $chapter->id,
253 'html' => '<p>A page created via the API</p>',
256 $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
257 $resp->assertStatus(403);
260 public function test_update_endpoint_does_not_wipe_content_if_no_html_or_md_provided()
262 $this->actingAsApiEditor();
263 $page = $this->entities->page();
264 $originalContent = $page->html;
266 'name' => 'My updated API page',
269 'name' => 'freshtag',
270 'value' => 'freshtagval',
275 $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
278 $this->assertEquals($originalContent, $page->html);
281 public function test_update_increments_updated_date_if_only_tags_are_sent()
283 $this->actingAsApiEditor();
284 $page = $this->entities->page();
285 DB::table('pages')->where('id', '=', $page->id)->update(['updated_at' => Carbon::now()->subWeek()]);
288 'tags' => [['name' => 'Category', 'value' => 'Testing']],
291 $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
295 $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $page->updated_at->unix());
298 public function test_delete_endpoint()
300 $this->actingAsApiEditor();
301 $page = $this->entities->page();
302 $resp = $this->deleteJson($this->baseEndpoint . "/{$page->id}");
304 $resp->assertStatus(204);
305 $this->assertActivityExists('page_delete', $page);
308 public function test_export_html_endpoint()
310 $this->actingAsApiEditor();
311 $page = $this->entities->page();
313 $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/html");
314 $resp->assertStatus(200);
315 $resp->assertSee($page->name);
316 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html"');
319 public function test_export_plain_text_endpoint()
321 $this->actingAsApiEditor();
322 $page = $this->entities->page();
324 $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/plaintext");
325 $resp->assertStatus(200);
326 $resp->assertSee($page->name);
327 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt"');
330 public function test_export_pdf_endpoint()
332 $this->actingAsApiEditor();
333 $page = $this->entities->page();
335 $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/pdf");
336 $resp->assertStatus(200);
337 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"');
340 public function test_export_markdown_endpoint()
342 $this->actingAsApiEditor();
343 $page = $this->entities->page();
345 $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/markdown");
346 $resp->assertStatus(200);
347 $resp->assertSee('# ' . $page->name);
348 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.md"');
351 public function test_cant_export_when_not_have_permission()
353 $types = ['html', 'plaintext', 'pdf', 'markdown'];
354 $this->actingAsApiEditor();
355 $this->permissions->removeUserRolePermissions($this->users->editor(), ['content-export']);
357 $page = $this->entities->page();
358 foreach ($types as $type) {
359 $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/{$type}");
360 $this->assertPermissionError($resp);