5 use BookStack\Entities\Models\Book;
6 use BookStack\Entities\Repos\BaseRepo;
8 use Illuminate\Support\Facades\DB;
11 class BooksApiTest extends TestCase
15 protected string $baseEndpoint = '/api/books';
17 public function test_index_endpoint_returns_expected_book()
19 $this->actingAsApiEditor();
20 $firstBook = Book::query()->orderBy('id', 'asc')->first();
22 $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
23 $resp->assertJson(['data' => [
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,
36 public function test_index_endpoint_includes_cover_if_set()
38 $this->actingAsApiEditor();
39 $book = $this->entities->book();
41 $baseRepo = $this->app->make(BaseRepo::class);
42 $image = $this->files->uploadedImage('book_cover');
43 $baseRepo->updateCoverImage($book, $image);
45 $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $book->id);
46 $resp->assertJson(['data' => [
50 'id' => $book->cover->id,
51 'url' => $book->cover->url,
57 public function test_create_endpoint()
59 $this->actingAsApiEditor();
60 $templatePage = $this->entities->templatePage();
62 'name' => 'My API book',
63 'description' => 'A book created via the API',
64 'default_template_id' => $templatePage->id,
67 $resp = $this->postJson($this->baseEndpoint, $details);
68 $resp->assertStatus(200);
70 $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
71 $resp->assertJson(array_merge($details, [
73 'slug' => $newItem->slug,
74 'description_html' => '<p>A book created via the API</p>',
76 $this->assertActivityExists('book_create', $newItem);
79 public function test_create_endpoint_with_html()
81 $this->actingAsApiEditor();
83 'name' => 'My API book',
84 'description_html' => '<p>A book <em>created</em> <strong>via</strong> the API</p>',
87 $resp = $this->postJson($this->baseEndpoint, $details);
88 $resp->assertStatus(200);
90 $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
91 $expectedDetails = array_merge($details, [
93 'description' => 'A book created via the API',
96 $resp->assertJson($expectedDetails);
97 $this->assertDatabaseHas('books', $expectedDetails);
100 public function test_book_name_needed_to_create()
102 $this->actingAsApiEditor();
104 'description' => 'A book created via the API',
107 $resp = $this->postJson($this->baseEndpoint, $details);
108 $resp->assertStatus(422);
111 'message' => 'The given data was invalid.',
113 'name' => ['The name field is required.'],
120 public function test_read_endpoint()
122 $this->actingAsApiEditor();
123 $book = $this->entities->book();
125 $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
127 $resp->assertStatus(200);
130 'slug' => $book->slug,
132 'name' => $book->createdBy->name,
135 'name' => $book->createdBy->name,
138 'name' => $book->ownedBy->name,
140 'default_template_id' => null,
144 public function test_read_endpoint_includes_chapter_and_page_contents()
146 $this->actingAsApiEditor();
147 $book = $this->entities->bookHasChaptersAndPages();
148 $chapter = $book->chapters()->first();
149 $chapterPage = $chapter->pages()->first();
151 $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
153 $directChildCount = $book->directPages()->count() + $book->chapters()->count();
154 $resp->assertStatus(200);
155 $resp->assertJsonCount($directChildCount, 'contents');
160 'id' => $chapter->id,
161 'name' => $chapter->name,
162 'slug' => $chapter->slug,
165 'id' => $chapterPage->id,
166 'name' => $chapterPage->name,
167 'slug' => $chapterPage->slug,
175 public function test_read_endpoint_contents_nested_pages_has_permissions_applied()
177 $this->actingAsApiEditor();
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();
186 $this->permissions->disableEntityInheritedPermissions($chapterPage);
188 $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
189 $resp->assertJsonMissing(['name' => $customName]);
192 public function test_update_endpoint()
194 $this->actingAsApiEditor();
195 $book = $this->entities->book();
196 $templatePage = $this->entities->templatePage();
198 'name' => 'My updated API book',
199 'description' => 'A book updated via the API',
200 'default_template_id' => $templatePage->id,
203 $resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
206 $resp->assertStatus(200);
207 $resp->assertJson(array_merge($details, [
209 'slug' => $book->slug,
210 'description_html' => '<p>A book updated via the API</p>',
212 $this->assertActivityExists('book_update', $book);
215 public function test_update_endpoint_with_html()
217 $this->actingAsApiEditor();
218 $book = $this->entities->book();
220 'name' => 'My updated API book',
221 'description_html' => '<p>A book <strong>updated</strong> via the API</p>',
224 $resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
225 $resp->assertStatus(200);
227 $this->assertDatabaseHas('books', array_merge($details, ['id' => $book->id, 'description' => 'A book updated via the API']));
230 public function test_update_increments_updated_date_if_only_tags_are_sent()
232 $this->actingAsApiEditor();
233 $book = $this->entities->book();
234 DB::table('books')->where('id', '=', $book->id)->update(['updated_at' => Carbon::now()->subWeek()]);
237 'tags' => [['name' => 'Category', 'value' => 'Testing']],
240 $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
242 $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $book->updated_at->unix());
245 public function test_update_cover_image_control()
247 $this->actingAsApiEditor();
248 /** @var Book $book */
249 $book = $this->entities->book();
250 $this->assertNull($book->cover);
251 $file = $this->files->uploadedImage('image.png');
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]);
259 $resp->assertStatus(200);
260 $this->assertNotNull($book->cover);
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',
268 $resp->assertStatus(200);
269 $this->assertNotNull($book->cover);
271 // Ensure update with null image property clears image
272 $resp = $this->put($this->baseEndpoint . "/{$book->id}", [
277 $resp->assertStatus(200);
278 $this->assertNull($book->cover);
281 public function test_delete_endpoint()
283 $this->actingAsApiEditor();
284 $book = $this->entities->book();
285 $resp = $this->deleteJson($this->baseEndpoint . "/{$book->id}");
287 $resp->assertStatus(204);
288 $this->assertActivityExists('book_delete');
291 public function test_export_html_endpoint()
293 $this->actingAsApiEditor();
294 $book = $this->entities->book();
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"');
302 public function test_export_plain_text_endpoint()
304 $this->actingAsApiEditor();
305 $book = $this->entities->book();
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"');
313 public function test_export_pdf_endpoint()
315 $this->actingAsApiEditor();
316 $book = $this->entities->book();
318 $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/pdf");
319 $resp->assertStatus(200);
320 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf"');
323 public function test_export_markdown_endpoint()
325 $this->actingAsApiEditor();
326 $book = Book::visible()->has('pages')->has('chapters')->first();
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);
336 public function test_cant_export_when_not_have_permission()
338 $types = ['html', 'plaintext', 'pdf', 'markdown'];
339 $this->actingAsApiEditor();
340 $this->permissions->removeUserRolePermissions($this->users->editor(), ['content-export']);
342 $book = $this->entities->book();
343 foreach ($types as $type) {
344 $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/{$type}");
345 $this->assertPermissionError($resp);