]> BookStack Code Mirror - bookstack/blob - tests/Api/BooksApiTest.php
Merge pull request #5917 from BookStackApp/copy_references
[bookstack] / tests / Api / BooksApiTest.php
1 <?php
2
3 namespace Tests\Api;
4
5 use BookStack\Entities\Models\Book;
6 use BookStack\Entities\Repos\BaseRepo;
7 use Carbon\Carbon;
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                 'owned_by' => $firstBook->owned_by,
28                 'created_by' => $firstBook->created_by,
29                 'updated_by' => $firstBook->updated_by,
30                 'cover' => null,
31             ],
32         ]]);
33     }
34
35     public function test_index_endpoint_includes_cover_if_set()
36     {
37         $this->actingAsApiEditor();
38         $book = $this->entities->book();
39
40         $baseRepo = $this->app->make(BaseRepo::class);
41         $image = $this->files->uploadedImage('book_cover');
42         $baseRepo->updateCoverImage($book, $image);
43
44         $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $book->id);
45         $resp->assertJson(['data' => [
46             [
47                 'id'   => $book->id,
48                 'cover' => [
49                     'id' => $book->coverInfo()->getImage()->id,
50                     'url' => $book->coverInfo()->getImage()->url,
51                 ],
52             ],
53         ]]);
54     }
55
56     public function test_create_endpoint()
57     {
58         $this->actingAsApiEditor();
59         $templatePage = $this->entities->templatePage();
60         $details = [
61             'name'                => 'My API book',
62             'description'         => 'A book created via the API',
63             'default_template_id' => $templatePage->id,
64         ];
65
66         $resp = $this->postJson($this->baseEndpoint, $details);
67         $resp->assertStatus(200);
68
69         $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
70         $resp->assertJson(array_merge($details, [
71             'id' => $newItem->id,
72             'slug' => $newItem->slug,
73             'description_html' => '<p>A book created via the API</p>',
74         ]));
75         $this->assertActivityExists('book_create', $newItem);
76     }
77
78     public function test_create_endpoint_with_html()
79     {
80         $this->actingAsApiEditor();
81         $details = [
82             'name'             => 'My API book',
83             'description_html' => '<p>A book <em>created</em> <strong>via</strong> the API</p>',
84         ];
85
86         $resp = $this->postJson($this->baseEndpoint, $details);
87         $resp->assertStatus(200);
88
89         $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
90         $expectedDetails = array_merge($details, [
91             'id'          => $newItem->id,
92             'description' => 'A book created via the API',
93         ]);
94
95         $resp->assertJson($expectedDetails);
96         $this->assertDatabaseHasEntityData('book', $expectedDetails);
97     }
98
99     public function test_book_name_needed_to_create()
100     {
101         $this->actingAsApiEditor();
102         $details = [
103             'description' => 'A book created via the API',
104         ];
105
106         $resp = $this->postJson($this->baseEndpoint, $details);
107         $resp->assertStatus(422);
108         $resp->assertJson([
109             'error' => [
110                 'message'    => 'The given data was invalid.',
111                 'validation' => [
112                     'name' => ['The name field is required.'],
113                 ],
114                 'code'       => 422,
115             ],
116         ]);
117     }
118
119     public function test_read_endpoint()
120     {
121         $this->actingAsApiEditor();
122         $book = $this->entities->book();
123
124         $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
125
126         $resp->assertStatus(200);
127         $resp->assertJson([
128             'id'         => $book->id,
129             'slug'       => $book->slug,
130             'created_by' => [
131                 'name' => $book->createdBy->name,
132             ],
133             'updated_by' => [
134                 'name' => $book->createdBy->name,
135             ],
136             'owned_by' => [
137                 'name' => $book->ownedBy->name,
138             ],
139             'default_template_id' => null,
140         ]);
141     }
142
143     public function test_read_endpoint_includes_chapter_and_page_contents()
144     {
145         $this->actingAsApiEditor();
146         $book = $this->entities->bookHasChaptersAndPages();
147         $chapter = $book->chapters()->first();
148         $chapterPage = $chapter->pages()->first();
149
150         $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
151
152         $directChildCount = $book->directPages()->count() + $book->chapters()->count();
153         $resp->assertStatus(200);
154         $resp->assertJsonCount($directChildCount, 'contents');
155
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,
160             'type' => 'chapter',
161             'name' => $chapter->name,
162             'slug' => $chapter->slug,
163         ], $respChapter);
164
165         $respPage = array_values(array_filter($respChapter['pages'], fn ($item) =>  ($item['id'] === $chapterPage->id)))[0];
166
167         $this->assertArrayMapIncludes([
168             'id' => $chapterPage->id,
169             'name' => $chapterPage->name,
170             'slug' => $chapterPage->slug,
171         ], $respPage);
172     }
173
174     public function test_read_endpoint_contents_nested_pages_has_permissions_applied()
175     {
176         $this->actingAsApiEditor();
177
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();
184
185         $this->permissions->disableEntityInheritedPermissions($chapterPage);
186
187         $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
188         $resp->assertJsonMissing(['name' => $customName]);
189     }
190
191     public function test_update_endpoint()
192     {
193         $this->actingAsApiEditor();
194         $book = $this->entities->book();
195         $templatePage = $this->entities->templatePage();
196         $details = [
197             'name'        => 'My updated API book',
198             'description' => 'A book updated via the API',
199             'default_template_id' => $templatePage->id,
200         ];
201
202         $resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
203         $book->refresh();
204
205         $resp->assertStatus(200);
206         $resp->assertJson(array_merge($details, [
207             'id' => $book->id,
208             'slug' => $book->slug,
209             'description_html' => '<p>A book updated via the API</p>',
210         ]));
211         $this->assertActivityExists('book_update', $book);
212     }
213
214     public function test_update_endpoint_with_html()
215     {
216         $this->actingAsApiEditor();
217         $book = $this->entities->book();
218         $details = [
219             'name'             => 'My updated API book',
220             'description_html' => '<p>A book <strong>updated</strong> via the API</p>',
221         ];
222
223         $resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
224         $resp->assertStatus(200);
225
226         $this->assertDatabaseHasEntityData('book', array_merge($details, ['id' => $book->id, 'description' => 'A book updated via the API']));
227     }
228
229     public function test_update_increments_updated_date_if_only_tags_are_sent()
230     {
231         $this->actingAsApiEditor();
232         $book = $this->entities->book();
233         Book::query()->where('id', '=', $book->id)->update(['updated_at' => Carbon::now()->subWeek()]);
234
235         $details = [
236             'tags' => [['name' => 'Category', 'value' => 'Testing']],
237         ];
238
239         $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
240         $book->refresh();
241         $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $book->updated_at->unix());
242     }
243
244     public function test_update_cover_image_control()
245     {
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');
251
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]);
256         $book->refresh();
257
258         $resp->assertStatus(200);
259         $this->assertNotNull($book->coverInfo()->getImage());
260
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',
264         ]);
265         $book->refresh();
266
267         $resp->assertStatus(200);
268         $this->assertNotNull($book->coverInfo()->getImage());
269
270         // Ensure update with null image property clears image
271         $resp = $this->put($this->baseEndpoint . "/{$book->id}", [
272             'image' => null,
273         ]);
274         $book->refresh();
275
276         $resp->assertStatus(200);
277         $this->assertNull($book->coverInfo()->getImage());
278     }
279
280     public function test_delete_endpoint()
281     {
282         $this->actingAsApiEditor();
283         $book = $this->entities->book();
284         $resp = $this->deleteJson($this->baseEndpoint . "/{$book->id}");
285
286         $resp->assertStatus(204);
287         $this->assertActivityExists('book_delete');
288     }
289 }