5 use BookStack\Entities\Models\Book;
7 use Illuminate\Support\Facades\DB;
10 class BooksApiTest extends TestCase
14 protected string $baseEndpoint = '/api/books';
16 public function test_index_endpoint_returns_expected_book()
18 $this->actingAsApiEditor();
19 $firstBook = Book::query()->orderBy('id', 'asc')->first();
21 $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
22 $resp->assertJson(['data' => [
24 'id' => $firstBook->id,
25 'name' => $firstBook->name,
26 'slug' => $firstBook->slug,
27 'owned_by' => $firstBook->owned_by,
28 'created_by' => $firstBook->created_by,
29 'updated_by' => $firstBook->updated_by,
34 public function test_create_endpoint()
36 $this->actingAsApiEditor();
37 $templatePage = $this->entities->templatePage();
39 'name' => 'My API book',
40 'description' => 'A book created via the API',
41 'default_template_id' => $templatePage->id,
44 $resp = $this->postJson($this->baseEndpoint, $details);
45 $resp->assertStatus(200);
47 $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
48 $resp->assertJson(array_merge($details, [
50 'slug' => $newItem->slug,
51 'description_html' => '<p>A book created via the API</p>',
53 $this->assertActivityExists('book_create', $newItem);
56 public function test_create_endpoint_with_html()
58 $this->actingAsApiEditor();
60 'name' => 'My API book',
61 'description_html' => '<p>A book <em>created</em> <strong>via</strong> the API</p>',
64 $resp = $this->postJson($this->baseEndpoint, $details);
65 $resp->assertStatus(200);
67 $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
68 $expectedDetails = array_merge($details, [
70 'description' => 'A book created via the API',
73 $resp->assertJson($expectedDetails);
74 $this->assertDatabaseHas('books', $expectedDetails);
77 public function test_book_name_needed_to_create()
79 $this->actingAsApiEditor();
81 'description' => 'A book created via the API',
84 $resp = $this->postJson($this->baseEndpoint, $details);
85 $resp->assertStatus(422);
88 'message' => 'The given data was invalid.',
90 'name' => ['The name field is required.'],
97 public function test_read_endpoint()
99 $this->actingAsApiEditor();
100 $book = $this->entities->book();
102 $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
104 $resp->assertStatus(200);
107 'slug' => $book->slug,
109 'name' => $book->createdBy->name,
112 'name' => $book->createdBy->name,
115 'name' => $book->ownedBy->name,
117 'default_template_id' => null,
121 public function test_read_endpoint_includes_chapter_and_page_contents()
123 $this->actingAsApiEditor();
124 $book = $this->entities->bookHasChaptersAndPages();
125 $chapter = $book->chapters()->first();
126 $chapterPage = $chapter->pages()->first();
128 $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
130 $directChildCount = $book->directPages()->count() + $book->chapters()->count();
131 $resp->assertStatus(200);
132 $resp->assertJsonCount($directChildCount, 'contents');
137 'id' => $chapter->id,
138 'name' => $chapter->name,
139 'slug' => $chapter->slug,
142 'id' => $chapterPage->id,
143 'name' => $chapterPage->name,
144 'slug' => $chapterPage->slug,
152 public function test_read_endpoint_contents_nested_pages_has_permissions_applied()
154 $this->actingAsApiEditor();
156 $book = $this->entities->bookHasChaptersAndPages();
157 $chapter = $book->chapters()->first();
158 $chapterPage = $chapter->pages()->first();
159 $customName = 'MyNonVisiblePageWithinAChapter';
160 $chapterPage->name = $customName;
161 $chapterPage->save();
163 $this->permissions->disableEntityInheritedPermissions($chapterPage);
165 $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
166 $resp->assertJsonMissing(['name' => $customName]);
169 public function test_update_endpoint()
171 $this->actingAsApiEditor();
172 $book = $this->entities->book();
173 $templatePage = $this->entities->templatePage();
175 'name' => 'My updated API book',
176 'description' => 'A book updated via the API',
177 'default_template_id' => $templatePage->id,
180 $resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
183 $resp->assertStatus(200);
184 $resp->assertJson(array_merge($details, [
186 'slug' => $book->slug,
187 'description_html' => '<p>A book updated via the API</p>',
189 $this->assertActivityExists('book_update', $book);
192 public function test_update_endpoint_with_html()
194 $this->actingAsApiEditor();
195 $book = $this->entities->book();
197 'name' => 'My updated API book',
198 'description_html' => '<p>A book <strong>updated</strong> via the API</p>',
201 $resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
202 $resp->assertStatus(200);
204 $this->assertDatabaseHas('books', array_merge($details, ['id' => $book->id, 'description' => 'A book updated via the API']));
207 public function test_update_increments_updated_date_if_only_tags_are_sent()
209 $this->actingAsApiEditor();
210 $book = $this->entities->book();
211 DB::table('books')->where('id', '=', $book->id)->update(['updated_at' => Carbon::now()->subWeek()]);
214 'tags' => [['name' => 'Category', 'value' => 'Testing']],
217 $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
219 $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $book->updated_at->unix());
222 public function test_update_cover_image_control()
224 $this->actingAsApiEditor();
225 /** @var Book $book */
226 $book = $this->entities->book();
227 $this->assertNull($book->cover);
228 $file = $this->files->uploadedImage('image.png');
230 // Ensure cover image can be set via API
231 $resp = $this->call('PUT', $this->baseEndpoint . "/{$book->id}", [
232 'name' => 'My updated API book with image',
233 ], [], ['image' => $file]);
236 $resp->assertStatus(200);
237 $this->assertNotNull($book->cover);
239 // Ensure further updates without image do not clear cover image
240 $resp = $this->put($this->baseEndpoint . "/{$book->id}", [
241 'name' => 'My updated book again',
245 $resp->assertStatus(200);
246 $this->assertNotNull($book->cover);
248 // Ensure update with null image property clears image
249 $resp = $this->put($this->baseEndpoint . "/{$book->id}", [
254 $resp->assertStatus(200);
255 $this->assertNull($book->cover);
258 public function test_delete_endpoint()
260 $this->actingAsApiEditor();
261 $book = $this->entities->book();
262 $resp = $this->deleteJson($this->baseEndpoint . "/{$book->id}");
264 $resp->assertStatus(204);
265 $this->assertActivityExists('book_delete');
268 public function test_export_html_endpoint()
270 $this->actingAsApiEditor();
271 $book = $this->entities->book();
273 $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/html");
274 $resp->assertStatus(200);
275 $resp->assertSee($book->name);
276 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html"');
279 public function test_export_plain_text_endpoint()
281 $this->actingAsApiEditor();
282 $book = $this->entities->book();
284 $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/plaintext");
285 $resp->assertStatus(200);
286 $resp->assertSee($book->name);
287 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.txt"');
290 public function test_export_pdf_endpoint()
292 $this->actingAsApiEditor();
293 $book = $this->entities->book();
295 $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/pdf");
296 $resp->assertStatus(200);
297 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf"');
300 public function test_export_markdown_endpoint()
302 $this->actingAsApiEditor();
303 $book = Book::visible()->has('pages')->has('chapters')->first();
305 $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/markdown");
306 $resp->assertStatus(200);
307 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.md"');
308 $resp->assertSee('# ' . $book->name);
309 $resp->assertSee('# ' . $book->pages()->first()->name);
310 $resp->assertSee('# ' . $book->chapters()->first()->name);
313 public function test_cant_export_when_not_have_permission()
315 $types = ['html', 'plaintext', 'pdf', 'markdown'];
316 $this->actingAsApiEditor();
317 $this->permissions->removeUserRolePermissions($this->users->editor(), ['content-export']);
319 $book = $this->entities->book();
320 foreach ($types as $type) {
321 $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/{$type}");
322 $this->assertPermissionError($resp);