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,
31 public function test_create_endpoint()
33 $this->actingAsApiEditor();
34 $templatePage = $this->entities->templatePage();
36 'name' => 'My API book',
37 'description' => 'A book created via the API',
38 'default_template_id' => $templatePage->id,
41 $resp = $this->postJson($this->baseEndpoint, $details);
42 $resp->assertStatus(200);
44 $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
45 $resp->assertJson(array_merge($details, [
47 'slug' => $newItem->slug,
48 'description_html' => '<p>A book created via the API</p>',
50 $this->assertActivityExists('book_create', $newItem);
53 public function test_create_endpoint_with_html()
55 $this->actingAsApiEditor();
57 'name' => 'My API book',
58 'description_html' => '<p>A book <em>created</em> <strong>via</strong> the API</p>',
61 $resp = $this->postJson($this->baseEndpoint, $details);
62 $resp->assertStatus(200);
64 $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
65 $expectedDetails = array_merge($details, [
67 'description' => 'A book created via the API',
70 $resp->assertJson($expectedDetails);
71 $this->assertDatabaseHas('books', $expectedDetails);
74 public function test_book_name_needed_to_create()
76 $this->actingAsApiEditor();
78 'description' => 'A book created via the API',
81 $resp = $this->postJson($this->baseEndpoint, $details);
82 $resp->assertStatus(422);
85 'message' => 'The given data was invalid.',
87 'name' => ['The name field is required.'],
94 public function test_read_endpoint()
96 $this->actingAsApiEditor();
97 $book = $this->entities->book();
99 $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
101 $resp->assertStatus(200);
104 'slug' => $book->slug,
106 'name' => $book->createdBy->name,
109 'name' => $book->createdBy->name,
112 'name' => $book->ownedBy->name,
114 'default_template_id' => null,
118 public function test_read_endpoint_includes_chapter_and_page_contents()
120 $this->actingAsApiEditor();
121 $book = $this->entities->bookHasChaptersAndPages();
122 $chapter = $book->chapters()->first();
123 $chapterPage = $chapter->pages()->first();
125 $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
127 $directChildCount = $book->directPages()->count() + $book->chapters()->count();
128 $resp->assertStatus(200);
129 $resp->assertJsonCount($directChildCount, 'contents');
134 'id' => $chapter->id,
135 'name' => $chapter->name,
136 'slug' => $chapter->slug,
139 'id' => $chapterPage->id,
140 'name' => $chapterPage->name,
141 'slug' => $chapterPage->slug,
149 public function test_update_endpoint()
151 $this->actingAsApiEditor();
152 $book = $this->entities->book();
153 $templatePage = $this->entities->templatePage();
155 'name' => 'My updated API book',
156 'description' => 'A book updated via the API',
157 'default_template_id' => $templatePage->id,
160 $resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
163 $resp->assertStatus(200);
164 $resp->assertJson(array_merge($details, [
166 'slug' => $book->slug,
167 'description_html' => '<p>A book updated via the API</p>',
169 $this->assertActivityExists('book_update', $book);
172 public function test_update_endpoint_with_html()
174 $this->actingAsApiEditor();
175 $book = $this->entities->book();
177 'name' => 'My updated API book',
178 'description_html' => '<p>A book <strong>updated</strong> via the API</p>',
181 $resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
182 $resp->assertStatus(200);
184 $this->assertDatabaseHas('books', array_merge($details, ['id' => $book->id, 'description' => 'A book updated via the API']));
187 public function test_update_increments_updated_date_if_only_tags_are_sent()
189 $this->actingAsApiEditor();
190 $book = $this->entities->book();
191 DB::table('books')->where('id', '=', $book->id)->update(['updated_at' => Carbon::now()->subWeek()]);
194 'tags' => [['name' => 'Category', 'value' => 'Testing']],
197 $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
199 $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $book->updated_at->unix());
202 public function test_update_cover_image_control()
204 $this->actingAsApiEditor();
205 /** @var Book $book */
206 $book = $this->entities->book();
207 $this->assertNull($book->cover);
208 $file = $this->files->uploadedImage('image.png');
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]);
216 $resp->assertStatus(200);
217 $this->assertNotNull($book->cover);
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',
225 $resp->assertStatus(200);
226 $this->assertNotNull($book->cover);
228 // Ensure update with null image property clears image
229 $resp = $this->put($this->baseEndpoint . "/{$book->id}", [
234 $resp->assertStatus(200);
235 $this->assertNull($book->cover);
238 public function test_delete_endpoint()
240 $this->actingAsApiEditor();
241 $book = $this->entities->book();
242 $resp = $this->deleteJson($this->baseEndpoint . "/{$book->id}");
244 $resp->assertStatus(204);
245 $this->assertActivityExists('book_delete');
248 public function test_export_html_endpoint()
250 $this->actingAsApiEditor();
251 $book = $this->entities->book();
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"');
259 public function test_export_plain_text_endpoint()
261 $this->actingAsApiEditor();
262 $book = $this->entities->book();
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"');
270 public function test_export_pdf_endpoint()
272 $this->actingAsApiEditor();
273 $book = $this->entities->book();
275 $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/pdf");
276 $resp->assertStatus(200);
277 $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf"');
280 public function test_export_markdown_endpoint()
282 $this->actingAsApiEditor();
283 $book = Book::visible()->has('pages')->has('chapters')->first();
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);
293 public function test_cant_export_when_not_have_permission()
295 $types = ['html', 'plaintext', 'pdf', 'markdown'];
296 $this->actingAsApiEditor();
297 $this->permissions->removeUserRolePermissions($this->users->editor(), ['content-export']);
299 $book = $this->entities->book();
300 foreach ($types as $type) {
301 $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/{$type}");
302 $this->assertPermissionError($resp);