]> BookStack Code Mirror - bookstack/blob - tests/Api/BooksApiTest.php
Lexical: Fixed tiny image resizer on image insert
[bookstack] / tests / Api / BooksApiTest.php
1 <?php
2
3 namespace Tests\Api;
4
5 use BookStack\Entities\Models\Book;
6 use BookStack\Entities\Repos\BaseRepo;
7 use Carbon\Carbon;
8 use Illuminate\Support\Facades\DB;
9 use Tests\TestCase;
10
11 class BooksApiTest extends TestCase
12 {
13     use TestsApi;
14
15     protected string $baseEndpoint = '/api/books';
16
17     public function test_index_endpoint_returns_expected_book()
18     {
19         $this->actingAsApiEditor();
20         $firstBook = Book::query()->orderBy('id', 'asc')->first();
21
22         $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
23         $resp->assertJson(['data' => [
24             [
25                 'id'   => $firstBook->id,
26                 'name' => $firstBook->name,
27                 'slug' => $firstBook->slug,
28                 'owned_by' => $firstBook->owned_by,
29                 'created_by' => $firstBook->created_by,
30                 'updated_by' => $firstBook->updated_by,
31                 'cover' => null,
32             ],
33         ]]);
34     }
35
36     public function test_index_endpoint_includes_cover_if_set()
37     {
38         $this->actingAsApiEditor();
39         $book = $this->entities->book();
40
41         $baseRepo = $this->app->make(BaseRepo::class);
42         $image = $this->files->uploadedImage('book_cover');
43         $baseRepo->updateCoverImage($book, $image);
44
45         $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $book->id);
46         $resp->assertJson(['data' => [
47             [
48                 'id'   => $book->id,
49                 'cover' => [
50                     'id' => $book->cover->id,
51                     'url' => $book->cover->url,
52                 ],
53             ],
54         ]]);
55     }
56
57     public function test_create_endpoint()
58     {
59         $this->actingAsApiEditor();
60         $templatePage = $this->entities->templatePage();
61         $details = [
62             'name'                => 'My API book',
63             'description'         => 'A book created via the API',
64             'default_template_id' => $templatePage->id,
65         ];
66
67         $resp = $this->postJson($this->baseEndpoint, $details);
68         $resp->assertStatus(200);
69
70         $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
71         $resp->assertJson(array_merge($details, [
72             'id' => $newItem->id,
73             'slug' => $newItem->slug,
74             'description_html' => '<p>A book created via the API</p>',
75         ]));
76         $this->assertActivityExists('book_create', $newItem);
77     }
78
79     public function test_create_endpoint_with_html()
80     {
81         $this->actingAsApiEditor();
82         $details = [
83             'name'             => 'My API book',
84             'description_html' => '<p>A book <em>created</em> <strong>via</strong> the API</p>',
85         ];
86
87         $resp = $this->postJson($this->baseEndpoint, $details);
88         $resp->assertStatus(200);
89
90         $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
91         $expectedDetails = array_merge($details, [
92             'id'          => $newItem->id,
93             'description' => 'A book created via the API',
94         ]);
95
96         $resp->assertJson($expectedDetails);
97         $this->assertDatabaseHas('books', $expectedDetails);
98     }
99
100     public function test_book_name_needed_to_create()
101     {
102         $this->actingAsApiEditor();
103         $details = [
104             'description' => 'A book created via the API',
105         ];
106
107         $resp = $this->postJson($this->baseEndpoint, $details);
108         $resp->assertStatus(422);
109         $resp->assertJson([
110             'error' => [
111                 'message'    => 'The given data was invalid.',
112                 'validation' => [
113                     'name' => ['The name field is required.'],
114                 ],
115                 'code'       => 422,
116             ],
117         ]);
118     }
119
120     public function test_read_endpoint()
121     {
122         $this->actingAsApiEditor();
123         $book = $this->entities->book();
124
125         $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
126
127         $resp->assertStatus(200);
128         $resp->assertJson([
129             'id'         => $book->id,
130             'slug'       => $book->slug,
131             'created_by' => [
132                 'name' => $book->createdBy->name,
133             ],
134             'updated_by' => [
135                 'name' => $book->createdBy->name,
136             ],
137             'owned_by' => [
138                 'name' => $book->ownedBy->name,
139             ],
140             'default_template_id' => null,
141         ]);
142     }
143
144     public function test_read_endpoint_includes_chapter_and_page_contents()
145     {
146         $this->actingAsApiEditor();
147         $book = $this->entities->bookHasChaptersAndPages();
148         $chapter = $book->chapters()->first();
149         $chapterPage = $chapter->pages()->first();
150
151         $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
152
153         $directChildCount = $book->directPages()->count() + $book->chapters()->count();
154         $resp->assertStatus(200);
155         $resp->assertJsonCount($directChildCount, 'contents');
156         $resp->assertJson([
157             'contents' => [
158                 [
159                     'type' => 'chapter',
160                     'id' => $chapter->id,
161                     'name' => $chapter->name,
162                     'slug' => $chapter->slug,
163                     'pages' => [
164                         [
165                             'id' => $chapterPage->id,
166                             'name' => $chapterPage->name,
167                             'slug' => $chapterPage->slug,
168                         ]
169                     ]
170                 ]
171             ]
172         ]);
173     }
174
175     public function test_read_endpoint_contents_nested_pages_has_permissions_applied()
176     {
177         $this->actingAsApiEditor();
178
179         $book = $this->entities->bookHasChaptersAndPages();
180         $chapter = $book->chapters()->first();
181         $chapterPage = $chapter->pages()->first();
182         $customName = 'MyNonVisiblePageWithinAChapter';
183         $chapterPage->name = $customName;
184         $chapterPage->save();
185
186         $this->permissions->disableEntityInheritedPermissions($chapterPage);
187
188         $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
189         $resp->assertJsonMissing(['name' => $customName]);
190     }
191
192     public function test_update_endpoint()
193     {
194         $this->actingAsApiEditor();
195         $book = $this->entities->book();
196         $templatePage = $this->entities->templatePage();
197         $details = [
198             'name'        => 'My updated API book',
199             'description' => 'A book updated via the API',
200             'default_template_id' => $templatePage->id,
201         ];
202
203         $resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
204         $book->refresh();
205
206         $resp->assertStatus(200);
207         $resp->assertJson(array_merge($details, [
208             'id' => $book->id,
209             'slug' => $book->slug,
210             'description_html' => '<p>A book updated via the API</p>',
211         ]));
212         $this->assertActivityExists('book_update', $book);
213     }
214
215     public function test_update_endpoint_with_html()
216     {
217         $this->actingAsApiEditor();
218         $book = $this->entities->book();
219         $details = [
220             'name'             => 'My updated API book',
221             'description_html' => '<p>A book <strong>updated</strong> via the API</p>',
222         ];
223
224         $resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
225         $resp->assertStatus(200);
226
227         $this->assertDatabaseHas('books', array_merge($details, ['id' => $book->id, 'description' => 'A book updated via the API']));
228     }
229
230     public function test_update_increments_updated_date_if_only_tags_are_sent()
231     {
232         $this->actingAsApiEditor();
233         $book = $this->entities->book();
234         DB::table('books')->where('id', '=', $book->id)->update(['updated_at' => Carbon::now()->subWeek()]);
235
236         $details = [
237             'tags' => [['name' => 'Category', 'value' => 'Testing']],
238         ];
239
240         $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
241         $book->refresh();
242         $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $book->updated_at->unix());
243     }
244
245     public function test_update_cover_image_control()
246     {
247         $this->actingAsApiEditor();
248         /** @var Book $book */
249         $book = $this->entities->book();
250         $this->assertNull($book->cover);
251         $file = $this->files->uploadedImage('image.png');
252
253         // Ensure cover image can be set via API
254         $resp = $this->call('PUT', $this->baseEndpoint . "/{$book->id}", [
255             'name'  => 'My updated API book with image',
256         ], [], ['image' => $file]);
257         $book->refresh();
258
259         $resp->assertStatus(200);
260         $this->assertNotNull($book->cover);
261
262         // Ensure further updates without image do not clear cover image
263         $resp = $this->put($this->baseEndpoint . "/{$book->id}", [
264             'name' => 'My updated book again',
265         ]);
266         $book->refresh();
267
268         $resp->assertStatus(200);
269         $this->assertNotNull($book->cover);
270
271         // Ensure update with null image property clears image
272         $resp = $this->put($this->baseEndpoint . "/{$book->id}", [
273             'image' => null,
274         ]);
275         $book->refresh();
276
277         $resp->assertStatus(200);
278         $this->assertNull($book->cover);
279     }
280
281     public function test_delete_endpoint()
282     {
283         $this->actingAsApiEditor();
284         $book = $this->entities->book();
285         $resp = $this->deleteJson($this->baseEndpoint . "/{$book->id}");
286
287         $resp->assertStatus(204);
288         $this->assertActivityExists('book_delete');
289     }
290
291     public function test_export_html_endpoint()
292     {
293         $this->actingAsApiEditor();
294         $book = $this->entities->book();
295
296         $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/html");
297         $resp->assertStatus(200);
298         $resp->assertSee($book->name);
299         $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html"');
300     }
301
302     public function test_export_plain_text_endpoint()
303     {
304         $this->actingAsApiEditor();
305         $book = $this->entities->book();
306
307         $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/plaintext");
308         $resp->assertStatus(200);
309         $resp->assertSee($book->name);
310         $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.txt"');
311     }
312
313     public function test_export_pdf_endpoint()
314     {
315         $this->actingAsApiEditor();
316         $book = $this->entities->book();
317
318         $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/pdf");
319         $resp->assertStatus(200);
320         $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf"');
321     }
322
323     public function test_export_markdown_endpoint()
324     {
325         $this->actingAsApiEditor();
326         $book = Book::visible()->has('pages')->has('chapters')->first();
327
328         $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/markdown");
329         $resp->assertStatus(200);
330         $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.md"');
331         $resp->assertSee('# ' . $book->name);
332         $resp->assertSee('# ' . $book->pages()->first()->name);
333         $resp->assertSee('# ' . $book->chapters()->first()->name);
334     }
335
336     public function test_cant_export_when_not_have_permission()
337     {
338         $types = ['html', 'plaintext', 'pdf', 'markdown'];
339         $this->actingAsApiEditor();
340         $this->permissions->removeUserRolePermissions($this->users->editor(), ['content-export']);
341
342         $book = $this->entities->book();
343         foreach ($types as $type) {
344             $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/{$type}");
345             $this->assertPermissionError($resp);
346         }
347     }
348 }