]> BookStack Code Mirror - bookstack/blob - tests/Api/PagesApiTest.php
Merge pull request #5917 from BookStackApp/copy_references
[bookstack] / tests / Api / PagesApiTest.php
1 <?php
2
3 namespace Tests\Api;
4
5 use BookStack\Activity\Models\Comment;
6 use BookStack\Entities\Models\Chapter;
7 use BookStack\Entities\Models\Page;
8 use Carbon\Carbon;
9 use Illuminate\Support\Facades\DB;
10 use Tests\TestCase;
11
12 class PagesApiTest extends TestCase
13 {
14     use TestsApi;
15
16     protected string $baseEndpoint = '/api/pages';
17
18     public function test_index_endpoint_returns_expected_page()
19     {
20         $this->actingAsApiEditor();
21         $firstPage = Page::query()->orderBy('id', 'asc')->first();
22
23         $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
24         $resp->assertJson(['data' => [
25             [
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,
35             ],
36         ]]);
37     }
38
39     public function test_create_endpoint()
40     {
41         $this->actingAsApiEditor();
42         $book = $this->entities->book();
43         $details = [
44             'name'    => 'My API page',
45             'book_id' => $book->id,
46             'html'    => '<p>My new page content</p>',
47             'tags'    => [
48                 [
49                     'name'  => 'tagname',
50                     'value' => 'tagvalue',
51                 ],
52             ],
53             'priority' => 15,
54         ];
55
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(),
64             'name'        => 'tagname',
65             'value'       => 'tagvalue',
66         ]);
67         $resp->assertSeeText('My new page content');
68         $resp->assertJsonMissing(['book' => []]);
69         $this->assertActivityExists('page_create', $newItem);
70     }
71
72     public function test_page_name_needed_to_create()
73     {
74         $this->actingAsApiEditor();
75         $book = $this->entities->book();
76         $details = [
77             'book_id' => $book->id,
78             'html'    => '<p>A page created via the API</p>',
79         ];
80
81         $resp = $this->postJson($this->baseEndpoint, $details);
82         $resp->assertStatus(422);
83         $resp->assertJson($this->validationResponse([
84             'name' => ['The name field is required.'],
85         ]));
86     }
87
88     public function test_book_id_or_chapter_id_needed_to_create()
89     {
90         $this->actingAsApiEditor();
91         $details = [
92             'name' => 'My api page',
93             'html' => '<p>A page created via the API</p>',
94         ];
95
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.'],
101         ]));
102
103         $chapter = $this->entities->chapter();
104         $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['chapter_id' => $chapter->id]));
105         $resp->assertStatus(200);
106
107         $book = $this->entities->book();
108         $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['book_id' => $book->id]));
109         $resp->assertStatus(200);
110     }
111
112     public function test_markdown_can_be_provided_for_create()
113     {
114         $this->actingAsApiEditor();
115         $book = $this->entities->book();
116         $details = [
117             'book_id'  => $book->id,
118             'name'     => 'My api page',
119             'markdown' => "# A new API page \n[link](https://example.com)",
120         ];
121
122         $resp = $this->postJson($this->baseEndpoint, $details);
123         $resp->assertJson(['markdown' => $details['markdown']]);
124
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);
129     }
130
131     public function test_read_endpoint()
132     {
133         $this->actingAsApiEditor();
134         $page = $this->entities->page();
135
136         $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
137         $resp->assertStatus(200);
138         $resp->assertJson([
139             'id'         => $page->id,
140             'slug'       => $page->slug,
141             'created_by' => [
142                 'name' => $page->createdBy->name,
143             ],
144             'book_id'    => $page->book_id,
145             'updated_by' => [
146                 'name' => $page->createdBy->name,
147             ],
148             'owned_by' => [
149                 'name' => $page->ownedBy->name,
150             ],
151         ]);
152     }
153
154     public function test_read_endpoint_provides_rendered_html()
155     {
156         $this->actingAsApiEditor();
157         $page = $this->entities->page();
158         $page->html = "<p>testing</p><script>alert('danger')</script><h1>Hello</h1>";
159         $page->save();
160
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);
166     }
167
168     public function test_read_endpoint_provides_raw_html()
169     {
170         $html = "<p>testing</p><script>alert('danger')</script><h1>Hello</h1>";
171
172         $this->actingAsApiEditor();
173         $page = $this->entities->page();
174         $page->html = $html;
175         $page->save();
176
177         $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
178         $this->assertEquals($html, $resp->json('raw_html'));
179         $this->assertNotEquals($html, $resp->json('html'));
180     }
181
182     public function test_read_endpoint_returns_not_found()
183     {
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));
188
189         $resp = $this->getJson($this->baseEndpoint . "/$id");
190
191         $resp->assertNotFound();
192         $this->assertNull($resp->json('id'));
193         $resp->assertJsonIsObject('error');
194         $resp->assertJsonStructure([
195             'error' => [
196                 'code',
197                 'message',
198             ],
199         ]);
200         $this->assertSame(404, $resp->json('error')['code']);
201     }
202
203     public function test_read_endpoint_includes_page_comments_tree_structure()
204     {
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]);
212
213         $resp = $this->getJson("{$this->baseEndpoint}/{$page->id}");
214         $resp->assertOk();
215
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');
220
221         $resp->assertJsonFragment([
222             'id' => $active->id,
223             'local_id' => $active->local_id,
224             'html' => '<p>My active comment</p>',
225         ]);
226     }
227
228     public function test_update_endpoint()
229     {
230         $this->actingAsApiEditor();
231         $page = $this->entities->page();
232         $details = [
233             'name' => 'My updated API page',
234             'html' => '<p>A page created via the API</p>',
235             'tags' => [
236                 [
237                     'name'  => 'freshtag',
238                     'value' => 'freshtagval',
239                 ],
240             ],
241             'priority' => 15,
242         ];
243
244         $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
245         $page->refresh();
246
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,
251         ]));
252         $this->assertActivityExists('page_update', $page);
253     }
254
255     public function test_providing_new_chapter_id_on_update_will_move_page()
256     {
257         $this->actingAsApiEditor();
258         $page = $this->entities->page();
259         $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
260         $details = [
261             'name'       => 'My updated API page',
262             'chapter_id' => $chapter->id,
263             'html'       => '<p>A page created via the API</p>',
264         ];
265
266         $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
267         $resp->assertStatus(200);
268         $resp->assertJson([
269             'chapter_id' => $chapter->id,
270             'book_id'    => $chapter->book_id,
271         ]);
272     }
273
274     public function test_providing_move_via_update_requires_page_create_permission_on_new_parent()
275     {
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()]);
280         $details = [
281             'name'       => 'My updated API page',
282             'chapter_id' => $chapter->id,
283             'html'       => '<p>A page created via the API</p>',
284         ];
285
286         $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
287         $resp->assertStatus(403);
288     }
289
290     public function test_update_endpoint_does_not_wipe_content_if_no_html_or_md_provided()
291     {
292         $this->actingAsApiEditor();
293         $page = $this->entities->page();
294         $originalContent = $page->html;
295         $details = [
296             'name' => 'My updated API page',
297             'tags' => [
298                 [
299                     'name'  => 'freshtag',
300                     'value' => 'freshtagval',
301                 ],
302             ],
303         ];
304
305         $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
306         $page->refresh();
307
308         $this->assertEquals($originalContent, $page->html);
309     }
310
311     public function test_update_increments_updated_date_if_only_tags_are_sent()
312     {
313         $this->actingAsApiEditor();
314         $page = $this->entities->page();
315         $page->newQuery()->where('id', '=', $page->id)->update(['updated_at' => Carbon::now()->subWeek()]);
316
317         $details = [
318             'tags' => [['name' => 'Category', 'value' => 'Testing']],
319         ];
320
321         $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
322         $resp->assertOk();
323
324         $page->refresh();
325         $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $page->updated_at->unix());
326     }
327
328     public function test_delete_endpoint()
329     {
330         $this->actingAsApiEditor();
331         $page = $this->entities->page();
332         $resp = $this->deleteJson($this->baseEndpoint . "/{$page->id}");
333
334         $resp->assertStatus(204);
335         $this->assertActivityExists('page_delete', $page);
336     }
337 }