]> BookStack Code Mirror - bookstack/blob - tests/Entity/BookTest.php
f124a1690e4fae57f554a35ff74349ee93a80c0c
[bookstack] / tests / Entity / BookTest.php
1 <?php
2
3 namespace Tests\Entity;
4
5 use BookStack\Entities\Models\Book;
6 use BookStack\Entities\Models\BookChild;
7 use BookStack\Entities\Models\Bookshelf;
8 use BookStack\Entities\Repos\BookRepo;
9 use Tests\TestCase;
10 use Tests\Uploads\UsesImages;
11
12 class BookTest extends TestCase
13 {
14     use UsesImages;
15
16     public function test_create()
17     {
18         $book = Book::factory()->make([
19             'name' => 'My First Book',
20         ]);
21
22         $resp = $this->asEditor()->get('/books');
23         $this->withHtml($resp)->assertElementContains('a[href="' . url('/create-book') . '"]', 'Create New Book');
24
25         $resp = $this->get('/create-book');
26         $this->withHtml($resp)->assertElementContains('form[action="' . url('/books') . '"][method="POST"]', 'Save Book');
27
28         $resp = $this->post('/books', $book->only('name', 'description'));
29         $resp->assertRedirect('/books/my-first-book');
30
31         $resp = $this->get('/books/my-first-book');
32         $resp->assertSee($book->name);
33         $resp->assertSee($book->description);
34     }
35
36     public function test_create_uses_different_slugs_when_name_reused()
37     {
38         $book = Book::factory()->make([
39             'name' => 'My First Book',
40         ]);
41
42         $this->asEditor()->post('/books', $book->only('name', 'description'));
43         $this->asEditor()->post('/books', $book->only('name', 'description'));
44
45         $books = Book::query()->where('name', '=', $book->name)
46             ->orderBy('id', 'desc')
47             ->take(2)
48             ->get();
49
50         $this->assertMatchesRegularExpression('/my-first-book-[0-9a-zA-Z]{3}/', $books[0]->slug);
51         $this->assertEquals('my-first-book', $books[1]->slug);
52     }
53
54     public function test_create_sets_tags()
55     {
56         // Cheeky initial update to refresh slug
57         $this->asEditor()->post('books', [
58             'name'        => 'My book with tags',
59             'description' => 'A book with tags',
60             'tags'        => [
61                 [
62                     'name'  => 'Category',
63                     'value' => 'Donkey Content',
64                 ],
65                 [
66                     'name'  => 'Level',
67                     'value' => '5',
68                 ],
69             ],
70         ]);
71
72         /** @var Book $book */
73         $book = Book::query()->where('name', '=', 'My book with tags')->firstOrFail();
74         $tags = $book->tags()->get();
75
76         $this->assertEquals(2, $tags->count());
77         $this->assertEquals('Donkey Content', $tags[0]->value);
78         $this->assertEquals('Level', $tags[1]->name);
79     }
80
81     public function test_update()
82     {
83         $book = $this->entities->book();
84         // Cheeky initial update to refresh slug
85         $this->asEditor()->put($book->getUrl(), ['name' => $book->name . '5', 'description' => $book->description]);
86         $book->refresh();
87
88         $newName = $book->name . ' Updated';
89         $newDesc = $book->description . ' with more content';
90
91         $resp = $this->get($book->getUrl('/edit'));
92         $resp->assertSee($book->name);
93         $resp->assertSee($book->description);
94         $this->withHtml($resp)->assertElementContains('form[action="' . $book->getUrl() . '"]', 'Save Book');
95
96         $resp = $this->put($book->getUrl(), ['name' => $newName, 'description' => $newDesc]);
97         $resp->assertRedirect($book->getUrl() . '-updated');
98
99         $resp = $this->get($book->getUrl() . '-updated');
100         $resp->assertSee($newName);
101         $resp->assertSee($newDesc);
102     }
103
104     public function test_update_sets_tags()
105     {
106         $book = $this->entities->book();
107
108         $this->assertEquals(0, $book->tags()->count());
109
110         // Cheeky initial update to refresh slug
111         $this->asEditor()->put($book->getUrl(), [
112             'name' => $book->name,
113             'tags' => [
114                 [
115                     'name'  => 'Category',
116                     'value' => 'Dolphin Content',
117                 ],
118                 [
119                     'name'  => 'Level',
120                     'value' => '5',
121                 ],
122             ],
123         ]);
124
125         $book->refresh();
126         $tags = $book->tags()->get();
127
128         $this->assertEquals(2, $tags->count());
129         $this->assertEquals('Dolphin Content', $tags[0]->value);
130         $this->assertEquals('Level', $tags[1]->name);
131     }
132
133     public function test_delete()
134     {
135         $book = Book::query()->whereHas('pages')->whereHas('chapters')->first();
136         $this->assertNull($book->deleted_at);
137         $pageCount = $book->pages()->count();
138         $chapterCount = $book->chapters()->count();
139
140         $deleteViewReq = $this->asEditor()->get($book->getUrl('/delete'));
141         $deleteViewReq->assertSeeText('Are you sure you want to delete this book?');
142
143         $deleteReq = $this->delete($book->getUrl());
144         $deleteReq->assertRedirect(url('/books'));
145         $this->assertActivityExists('book_delete', $book);
146
147         $book->refresh();
148         $this->assertNotNull($book->deleted_at);
149
150         $this->assertTrue($book->pages()->count() === 0);
151         $this->assertTrue($book->chapters()->count() === 0);
152         $this->assertTrue($book->pages()->withTrashed()->count() === $pageCount);
153         $this->assertTrue($book->chapters()->withTrashed()->count() === $chapterCount);
154         $this->assertTrue($book->deletions()->count() === 1);
155
156         $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
157         $this->assertNotificationContains($redirectReq, 'Book Successfully Deleted');
158     }
159
160     public function test_cancel_on_create_page_leads_back_to_books_listing()
161     {
162         $resp = $this->asEditor()->get('/create-book');
163         $this->withHtml($resp)->assertElementContains('form a[href="' . url('/books') . '"]', 'Cancel');
164     }
165
166     public function test_cancel_on_edit_book_page_leads_back_to_book()
167     {
168         $book = $this->entities->book();
169         $resp = $this->asEditor()->get($book->getUrl('/edit'));
170         $this->withHtml($resp)->assertElementContains('form a[href="' . $book->getUrl() . '"]', 'Cancel');
171     }
172
173     public function test_next_previous_navigation_controls_show_within_book_content()
174     {
175         $book = $this->entities->book();
176         $chapter = $book->chapters->first();
177
178         $resp = $this->asEditor()->get($chapter->getUrl());
179         $this->withHtml($resp)->assertElementContains('#sibling-navigation', 'Next');
180         $this->withHtml($resp)->assertElementContains('#sibling-navigation', substr($chapter->pages[0]->name, 0, 20));
181
182         $resp = $this->get($chapter->pages[0]->getUrl());
183         $this->withHtml($resp)->assertElementContains('#sibling-navigation', substr($chapter->pages[1]->name, 0, 20));
184         $this->withHtml($resp)->assertElementContains('#sibling-navigation', 'Previous');
185         $this->withHtml($resp)->assertElementContains('#sibling-navigation', substr($chapter->name, 0, 20));
186     }
187
188     public function test_recently_viewed_books_updates_as_expected()
189     {
190         $books = Book::all()->take(2);
191
192         $resp = $this->asAdmin()->get('/books');
193         $this->withHtml($resp)->assertElementNotContains('#recents', $books[0]->name)
194             ->assertElementNotContains('#recents', $books[1]->name);
195
196         $this->get($books[0]->getUrl());
197         $this->get($books[1]->getUrl());
198
199         $resp = $this->get('/books');
200         $this->withHtml($resp)->assertElementContains('#recents', $books[0]->name)
201             ->assertElementContains('#recents', $books[1]->name);
202     }
203
204     public function test_popular_books_updates_upon_visits()
205     {
206         $books = Book::all()->take(2);
207
208         $resp = $this->asAdmin()->get('/books');
209         $this->withHtml($resp)->assertElementNotContains('#popular', $books[0]->name)
210             ->assertElementNotContains('#popular', $books[1]->name);
211
212         $this->get($books[0]->getUrl());
213         $this->get($books[1]->getUrl());
214         $this->get($books[0]->getUrl());
215
216         $resp = $this->get('/books');
217         $this->withHtml($resp)->assertElementContains('#popular .book:nth-child(1)', $books[0]->name)
218             ->assertElementContains('#popular .book:nth-child(2)', $books[1]->name);
219     }
220
221     public function test_books_view_shows_view_toggle_option()
222     {
223         /** @var Book $book */
224         $editor = $this->users->editor();
225         setting()->putUser($editor, 'books_view_type', 'list');
226
227         $resp = $this->actingAs($editor)->get('/books');
228         $this->withHtml($resp)->assertElementContains('form[action$="/preferences/change-view/books"]', 'Grid View');
229         $this->withHtml($resp)->assertElementExists('button[name="view"][value="grid"]');
230
231         $resp = $this->patch("/preferences/change-view/books", ['view' => 'grid']);
232         $resp->assertRedirect();
233         $this->assertEquals('grid', setting()->getUser($editor, 'books_view_type'));
234
235         $resp = $this->actingAs($editor)->get('/books');
236         $this->withHtml($resp)->assertElementContains('form[action$="/preferences/change-view/books"]', 'List View');
237         $this->withHtml($resp)->assertElementExists('button[name="view"][value="list"]');
238
239         $resp = $this->patch("/preferences/change-view/books", ['view_type' => 'list']);
240         $resp->assertRedirect();
241         $this->assertEquals('list', setting()->getUser($editor, 'books_view_type'));
242     }
243
244     public function test_slug_multi_byte_url_safe()
245     {
246         $book = $this->entities->newBook([
247             'name' => 'информация',
248         ]);
249
250         $this->assertEquals('informaciia', $book->slug);
251
252         $book = $this->entities->newBook([
253             'name' => '¿Qué?',
254         ]);
255
256         $this->assertEquals('que', $book->slug);
257     }
258
259     public function test_slug_format()
260     {
261         $book = $this->entities->newBook([
262             'name' => 'PartA / PartB / PartC',
263         ]);
264
265         $this->assertEquals('parta-partb-partc', $book->slug);
266     }
267
268     public function test_show_view_has_copy_button()
269     {
270         $book = $this->entities->book();
271         $resp = $this->asEditor()->get($book->getUrl());
272
273         $this->withHtml($resp)->assertElementContains("a[href=\"{$book->getUrl('/copy')}\"]", 'Copy');
274     }
275
276     public function test_copy_view()
277     {
278         $book = $this->entities->book();
279         $resp = $this->asEditor()->get($book->getUrl('/copy'));
280
281         $resp->assertOk();
282         $resp->assertSee('Copy Book');
283         $this->withHtml($resp)->assertElementExists("input[name=\"name\"][value=\"{$book->name}\"]");
284     }
285
286     public function test_copy()
287     {
288         /** @var Book $book */
289         $book = Book::query()->whereHas('chapters')->whereHas('pages')->first();
290         $resp = $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
291
292         /** @var Book $copy */
293         $copy = Book::query()->where('name', '=', 'My copy book')->first();
294
295         $resp->assertRedirect($copy->getUrl());
296         $this->assertEquals($book->getDirectChildren()->count(), $copy->getDirectChildren()->count());
297     }
298
299     public function test_copy_does_not_copy_non_visible_content()
300     {
301         /** @var Book $book */
302         $book = Book::query()->whereHas('chapters')->whereHas('pages')->first();
303
304         // Hide child content
305         /** @var BookChild $page */
306         foreach ($book->getDirectChildren() as $child) {
307             $this->permissions->setEntityPermissions($child, [], []);
308         }
309
310         $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
311         /** @var Book $copy */
312         $copy = Book::query()->where('name', '=', 'My copy book')->first();
313
314         $this->assertEquals(0, $copy->getDirectChildren()->count());
315     }
316
317     public function test_copy_does_not_copy_pages_or_chapters_if_user_cant_create()
318     {
319         /** @var Book $book */
320         $book = Book::query()->whereHas('chapters')->whereHas('directPages')->whereHas('chapters')->first();
321         $viewer = $this->users->viewer();
322         $this->permissions->grantUserRolePermissions($viewer, ['book-create-all']);
323
324         $this->actingAs($viewer)->post($book->getUrl('/copy'), ['name' => 'My copy book']);
325         /** @var Book $copy */
326         $copy = Book::query()->where('name', '=', 'My copy book')->first();
327
328         $this->assertEquals(0, $copy->pages()->count());
329         $this->assertEquals(0, $copy->chapters()->count());
330     }
331
332     public function test_copy_clones_cover_image_if_existing()
333     {
334         $book = $this->entities->book();
335         $bookRepo = $this->app->make(BookRepo::class);
336         $coverImageFile = $this->getTestImage('cover.png');
337         $bookRepo->updateCoverImage($book, $coverImageFile);
338
339         $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
340         /** @var Book $copy */
341         $copy = Book::query()->where('name', '=', 'My copy book')->first();
342
343         $this->assertNotNull($copy->cover);
344         $this->assertNotEquals($book->cover->id, $copy->cover->id);
345     }
346
347     public function test_copy_adds_book_to_shelves_if_edit_permissions_allows()
348     {
349         /** @var Bookshelf $shelfA */
350         /** @var Bookshelf $shelfB */
351         [$shelfA, $shelfB] = Bookshelf::query()->take(2)->get();
352         $book = $this->entities->book();
353
354         $shelfA->appendBook($book);
355         $shelfB->appendBook($book);
356
357         $viewer = $this->users->viewer();
358         $this->permissions->grantUserRolePermissions($viewer, ['book-update-all', 'book-create-all', 'bookshelf-update-all']);
359         $this->permissions->setEntityPermissions($shelfB);
360
361
362         $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
363         /** @var Book $copy */
364         $copy = Book::query()->where('name', '=', 'My copy book')->first();
365
366         $this->assertTrue($copy->shelves()->where('id', '=', $shelfA->id)->exists());
367         $this->assertFalse($copy->shelves()->where('id', '=', $shelfB->id)->exists());
368     }
369 }