5 use BookStack\Entities\Models\Book;
6 use BookStack\Entities\Repos\BaseRepo;
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,
35 public function test_index_endpoint_includes_cover_if_set()
37 $this->actingAsApiEditor();
38 $book = $this->entities->book();
40 $baseRepo = $this->app->make(BaseRepo::class);
41 $image = $this->files->uploadedImage('book_cover');
42 $baseRepo->updateCoverImage($book, $image);
44 $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $book->id);
45 $resp->assertJson(['data' => [
49 'id' => $book->coverInfo()->getImage()->id,
50 'url' => $book->coverInfo()->getImage()->url,
56 public function test_create_endpoint()
58 $this->actingAsApiEditor();
59 $templatePage = $this->entities->templatePage();
61 'name' => 'My API book',
62 'description' => 'A book created via the API',
63 'default_template_id' => $templatePage->id,
66 $resp = $this->postJson($this->baseEndpoint, $details);
67 $resp->assertStatus(200);
69 $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
70 $resp->assertJson(array_merge($details, [
72 'slug' => $newItem->slug,
73 'description_html' => '<p>A book created via the API</p>',
75 $this->assertActivityExists('book_create', $newItem);
78 public function test_create_endpoint_with_html()
80 $this->actingAsApiEditor();
82 'name' => 'My API book',
83 'description_html' => '<p>A book <em>created</em> <strong>via</strong> the API</p>',
86 $resp = $this->postJson($this->baseEndpoint, $details);
87 $resp->assertStatus(200);
89 $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
90 $expectedDetails = array_merge($details, [
92 'description' => 'A book created via the API',
95 $resp->assertJson($expectedDetails);
96 $this->assertDatabaseHasEntityData('book', $expectedDetails);
99 public function test_book_name_needed_to_create()
101 $this->actingAsApiEditor();
103 'description' => 'A book created via the API',
106 $resp = $this->postJson($this->baseEndpoint, $details);
107 $resp->assertStatus(422);
110 'message' => 'The given data was invalid.',
112 'name' => ['The name field is required.'],
119 public function test_read_endpoint()
121 $this->actingAsApiEditor();
122 $book = $this->entities->book();
124 $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
126 $resp->assertStatus(200);
129 'slug' => $book->slug,
131 'name' => $book->createdBy->name,
134 'name' => $book->createdBy->name,
137 'name' => $book->ownedBy->name,
139 'default_template_id' => null,
143 public function test_read_endpoint_includes_chapter_and_page_contents()
145 $this->actingAsApiEditor();
146 $book = $this->entities->bookHasChaptersAndPages();
147 $chapter = $book->chapters()->first();
148 $chapterPage = $chapter->pages()->first();
150 $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
152 $directChildCount = $book->directPages()->count() + $book->chapters()->count();
153 $resp->assertStatus(200);
154 $resp->assertJsonCount($directChildCount, 'contents');
156 $contents = $resp->json('contents');
157 $respChapter = array_values(array_filter($contents, fn ($item) => ($item['id'] === $chapter->id && $item['type'] === 'chapter')))[0];
158 $this->assertArrayMapIncludes([
159 'id' => $chapter->id,
161 'name' => $chapter->name,
162 'slug' => $chapter->slug,
165 $respPage = array_values(array_filter($respChapter['pages'], fn ($item) => ($item['id'] === $chapterPage->id)))[0];
167 $this->assertArrayMapIncludes([
168 'id' => $chapterPage->id,
169 'name' => $chapterPage->name,
170 'slug' => $chapterPage->slug,
174 public function test_read_endpoint_contents_nested_pages_has_permissions_applied()
176 $this->actingAsApiEditor();
178 $book = $this->entities->bookHasChaptersAndPages();
179 $chapter = $book->chapters()->first();
180 $chapterPage = $chapter->pages()->first();
181 $customName = 'MyNonVisiblePageWithinAChapter';
182 $chapterPage->name = $customName;
183 $chapterPage->save();
185 $this->permissions->disableEntityInheritedPermissions($chapterPage);
187 $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
188 $resp->assertJsonMissing(['name' => $customName]);
191 public function test_update_endpoint()
193 $this->actingAsApiEditor();
194 $book = $this->entities->book();
195 $templatePage = $this->entities->templatePage();
197 'name' => 'My updated API book',
198 'description' => 'A book updated via the API',
199 'default_template_id' => $templatePage->id,
202 $resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
205 $resp->assertStatus(200);
206 $resp->assertJson(array_merge($details, [
208 'slug' => $book->slug,
209 'description_html' => '<p>A book updated via the API</p>',
211 $this->assertActivityExists('book_update', $book);
214 public function test_update_endpoint_with_html()
216 $this->actingAsApiEditor();
217 $book = $this->entities->book();
219 'name' => 'My updated API book',
220 'description_html' => '<p>A book <strong>updated</strong> via the API</p>',
223 $resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
224 $resp->assertStatus(200);
226 $this->assertDatabaseHasEntityData('book', array_merge($details, ['id' => $book->id, 'description' => 'A book updated via the API']));
229 public function test_update_increments_updated_date_if_only_tags_are_sent()
231 $this->actingAsApiEditor();
232 $book = $this->entities->book();
233 Book::query()->where('id', '=', $book->id)->update(['updated_at' => Carbon::now()->subWeek()]);
236 'tags' => [['name' => 'Category', 'value' => 'Testing']],
239 $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
241 $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $book->updated_at->unix());
244 public function test_update_cover_image_control()
246 $this->actingAsApiEditor();
247 /** @var Book $book */
248 $book = $this->entities->book();
249 $this->assertNull($book->coverInfo()->getImage());
250 $file = $this->files->uploadedImage('image.png');
252 // Ensure cover image can be set via API
253 $resp = $this->call('PUT', $this->baseEndpoint . "/{$book->id}", [
254 'name' => 'My updated API book with image',
255 ], [], ['image' => $file]);
258 $resp->assertStatus(200);
259 $this->assertNotNull($book->coverInfo()->getImage());
261 // Ensure further updates without image do not clear cover image
262 $resp = $this->put($this->baseEndpoint . "/{$book->id}", [
263 'name' => 'My updated book again',
267 $resp->assertStatus(200);
268 $this->assertNotNull($book->coverInfo()->getImage());
270 // Ensure update with null image property clears image
271 $resp = $this->put($this->baseEndpoint . "/{$book->id}", [
276 $resp->assertStatus(200);
277 $this->assertNull($book->coverInfo()->getImage());
280 public function test_delete_endpoint()
282 $this->actingAsApiEditor();
283 $book = $this->entities->book();
284 $resp = $this->deleteJson($this->baseEndpoint . "/{$book->id}");
286 $resp->assertStatus(204);
287 $this->assertActivityExists('book_delete');