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