]> BookStack Code Mirror - bookstack/blob - tests/Api/PagesApiTest.php
Deps & Tests: Updated PHP deps, fixed test namespaces
[bookstack] / tests / Api / PagesApiTest.php
1 <?php
2
3 namespace Tests\Api;
4
5 use BookStack\Entities\Models\Chapter;
6 use BookStack\Entities\Models\Page;
7 use Carbon\Carbon;
8 use Illuminate\Support\Facades\DB;
9 use Tests\TestCase;
10
11 class PagesApiTest extends TestCase
12 {
13     use TestsApi;
14
15     protected string $baseEndpoint = '/api/pages';
16
17     public function test_index_endpoint_returns_expected_page()
18     {
19         $this->actingAsApiEditor();
20         $firstPage = Page::query()->orderBy('id', 'asc')->first();
21
22         $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
23         $resp->assertJson(['data' => [
24             [
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,
34             ],
35         ]]);
36     }
37
38     public function test_create_endpoint()
39     {
40         $this->actingAsApiEditor();
41         $book = $this->entities->book();
42         $details = [
43             'name'    => 'My API page',
44             'book_id' => $book->id,
45             'html'    => '<p>My new page content</p>',
46             'tags'    => [
47                 [
48                     'name'  => 'tagname',
49                     'value' => 'tagvalue',
50                 ],
51             ],
52             'priority' => 15,
53         ];
54
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(),
63             'name'        => 'tagname',
64             'value'       => 'tagvalue',
65         ]);
66         $resp->assertSeeText('My new page content');
67         $resp->assertJsonMissing(['book' => []]);
68         $this->assertActivityExists('page_create', $newItem);
69     }
70
71     public function test_page_name_needed_to_create()
72     {
73         $this->actingAsApiEditor();
74         $book = $this->entities->book();
75         $details = [
76             'book_id' => $book->id,
77             'html'    => '<p>A page created via the API</p>',
78         ];
79
80         $resp = $this->postJson($this->baseEndpoint, $details);
81         $resp->assertStatus(422);
82         $resp->assertJson($this->validationResponse([
83             'name' => ['The name field is required.'],
84         ]));
85     }
86
87     public function test_book_id_or_chapter_id_needed_to_create()
88     {
89         $this->actingAsApiEditor();
90         $details = [
91             'name' => 'My api page',
92             'html' => '<p>A page created via the API</p>',
93         ];
94
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.'],
100         ]));
101
102         $chapter = $this->entities->chapter();
103         $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['chapter_id' => $chapter->id]));
104         $resp->assertStatus(200);
105
106         $book = $this->entities->book();
107         $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['book_id' => $book->id]));
108         $resp->assertStatus(200);
109     }
110
111     public function test_markdown_can_be_provided_for_create()
112     {
113         $this->actingAsApiEditor();
114         $book = $this->entities->book();
115         $details = [
116             'book_id'  => $book->id,
117             'name'     => 'My api page',
118             'markdown' => "# A new API page \n[link](https://example.com)",
119         ];
120
121         $resp = $this->postJson($this->baseEndpoint, $details);
122         $resp->assertJson(['markdown' => $details['markdown']]);
123
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);
128     }
129
130     public function test_read_endpoint()
131     {
132         $this->actingAsApiEditor();
133         $page = $this->entities->page();
134
135         $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
136         $resp->assertStatus(200);
137         $resp->assertJson([
138             'id'         => $page->id,
139             'slug'       => $page->slug,
140             'created_by' => [
141                 'name' => $page->createdBy->name,
142             ],
143             'book_id'    => $page->book_id,
144             'updated_by' => [
145                 'name' => $page->createdBy->name,
146             ],
147             'owned_by' => [
148                 'name' => $page->ownedBy->name,
149             ],
150         ]);
151     }
152
153     public function test_read_endpoint_provides_rendered_html()
154     {
155         $this->actingAsApiEditor();
156         $page = $this->entities->page();
157         $page->html = "<p>testing</p><script>alert('danger')</script><h1>Hello</h1>";
158         $page->save();
159
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);
165     }
166
167     public function test_read_endpoint_provides_raw_html()
168     {
169         $html = "<p>testing</p><script>alert('danger')</script><h1>Hello</h1>";
170
171         $this->actingAsApiEditor();
172         $page = $this->entities->page();
173         $page->html = $html;
174         $page->save();
175
176         $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
177         $this->assertEquals($html, $resp->json('raw_html'));
178         $this->assertNotEquals($html, $resp->json('html'));
179     }
180
181     public function test_read_endpoint_returns_not_found()
182     {
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));
187
188         $resp = $this->getJson($this->baseEndpoint . "/$id");
189
190         $resp->assertNotFound();
191         $this->assertNull($resp->json('id'));
192         $resp->assertJsonIsObject('error');
193         $resp->assertJsonStructure([
194             'error' => [
195                 'code',
196                 'message',
197             ],
198         ]);
199         $this->assertSame(404, $resp->json('error')['code']);
200     }
201
202     public function test_update_endpoint()
203     {
204         $this->actingAsApiEditor();
205         $page = $this->entities->page();
206         $details = [
207             'name' => 'My updated API page',
208             'html' => '<p>A page created via the API</p>',
209             'tags' => [
210                 [
211                     'name'  => 'freshtag',
212                     'value' => 'freshtagval',
213                 ],
214             ],
215             'priority' => 15,
216         ];
217
218         $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
219         $page->refresh();
220
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,
225         ]));
226         $this->assertActivityExists('page_update', $page);
227     }
228
229     public function test_providing_new_chapter_id_on_update_will_move_page()
230     {
231         $this->actingAsApiEditor();
232         $page = $this->entities->page();
233         $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
234         $details = [
235             'name'       => 'My updated API page',
236             'chapter_id' => $chapter->id,
237             'html'       => '<p>A page created via the API</p>',
238         ];
239
240         $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
241         $resp->assertStatus(200);
242         $resp->assertJson([
243             'chapter_id' => $chapter->id,
244             'book_id'    => $chapter->book_id,
245         ]);
246     }
247
248     public function test_providing_move_via_update_requires_page_create_permission_on_new_parent()
249     {
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()]);
254         $details = [
255             'name'       => 'My updated API page',
256             'chapter_id' => $chapter->id,
257             'html'       => '<p>A page created via the API</p>',
258         ];
259
260         $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
261         $resp->assertStatus(403);
262     }
263
264     public function test_update_endpoint_does_not_wipe_content_if_no_html_or_md_provided()
265     {
266         $this->actingAsApiEditor();
267         $page = $this->entities->page();
268         $originalContent = $page->html;
269         $details = [
270             'name' => 'My updated API page',
271             'tags' => [
272                 [
273                     'name'  => 'freshtag',
274                     'value' => 'freshtagval',
275                 ],
276             ],
277         ];
278
279         $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
280         $page->refresh();
281
282         $this->assertEquals($originalContent, $page->html);
283     }
284
285     public function test_update_increments_updated_date_if_only_tags_are_sent()
286     {
287         $this->actingAsApiEditor();
288         $page = $this->entities->page();
289         DB::table('pages')->where('id', '=', $page->id)->update(['updated_at' => Carbon::now()->subWeek()]);
290
291         $details = [
292             'tags' => [['name' => 'Category', 'value' => 'Testing']],
293         ];
294
295         $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
296         $resp->assertOk();
297
298         $page->refresh();
299         $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $page->updated_at->unix());
300     }
301
302     public function test_delete_endpoint()
303     {
304         $this->actingAsApiEditor();
305         $page = $this->entities->page();
306         $resp = $this->deleteJson($this->baseEndpoint . "/{$page->id}");
307
308         $resp->assertStatus(204);
309         $this->assertActivityExists('page_delete', $page);
310     }
311
312     public function test_export_html_endpoint()
313     {
314         $this->actingAsApiEditor();
315         $page = $this->entities->page();
316
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"');
321     }
322
323     public function test_export_plain_text_endpoint()
324     {
325         $this->actingAsApiEditor();
326         $page = $this->entities->page();
327
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"');
332     }
333
334     public function test_export_pdf_endpoint()
335     {
336         $this->actingAsApiEditor();
337         $page = $this->entities->page();
338
339         $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/pdf");
340         $resp->assertStatus(200);
341         $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"');
342     }
343
344     public function test_export_markdown_endpoint()
345     {
346         $this->actingAsApiEditor();
347         $page = $this->entities->page();
348
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"');
353     }
354
355     public function test_cant_export_when_not_have_permission()
356     {
357         $types = ['html', 'plaintext', 'pdf', 'markdown'];
358         $this->actingAsApiEditor();
359         $this->permissions->removeUserRolePermissions($this->users->editor(), ['content-export']);
360
361         $page = $this->entities->page();
362         foreach ($types as $type) {
363             $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/{$type}");
364             $this->assertPermissionError($resp);
365         }
366     }
367 }