]> BookStack Code Mirror - bookstack/blob - tests/Entity/BookTest.php
Updated book copy to copy shelf relations
[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         /** @var Book $book */
84         $book = Book::query()->first();
85         // Cheeky initial update to refresh slug
86         $this->asEditor()->put($book->getUrl(), ['name' => $book->name . '5', 'description' => $book->description]);
87         $book->refresh();
88
89         $newName = $book->name . ' Updated';
90         $newDesc = $book->description . ' with more content';
91
92         $resp = $this->get($book->getUrl('/edit'));
93         $resp->assertSee($book->name);
94         $resp->assertSee($book->description);
95         $this->withHtml($resp)->assertElementContains('form[action="' . $book->getUrl() . '"]', 'Save Book');
96
97         $resp = $this->put($book->getUrl(), ['name' => $newName, 'description' => $newDesc]);
98         $resp->assertRedirect($book->getUrl() . '-updated');
99
100         $resp = $this->get($book->getUrl() . '-updated');
101         $resp->assertSee($newName);
102         $resp->assertSee($newDesc);
103     }
104
105     public function test_update_sets_tags()
106     {
107         /** @var Book $book */
108         $book = Book::query()->first();
109
110         $this->assertEquals(0, $book->tags()->count());
111
112         // Cheeky initial update to refresh slug
113         $this->asEditor()->put($book->getUrl(), [
114             'name' => $book->name,
115             'tags' => [
116                 [
117                     'name'  => 'Category',
118                     'value' => 'Dolphin Content',
119                 ],
120                 [
121                     'name'  => 'Level',
122                     'value' => '5',
123                 ],
124             ],
125         ]);
126
127         $book->refresh();
128         $tags = $book->tags()->get();
129
130         $this->assertEquals(2, $tags->count());
131         $this->assertEquals('Dolphin Content', $tags[0]->value);
132         $this->assertEquals('Level', $tags[1]->name);
133     }
134
135     public function test_delete()
136     {
137         $book = Book::query()->whereHas('pages')->whereHas('chapters')->first();
138         $this->assertNull($book->deleted_at);
139         $pageCount = $book->pages()->count();
140         $chapterCount = $book->chapters()->count();
141
142         $deleteViewReq = $this->asEditor()->get($book->getUrl('/delete'));
143         $deleteViewReq->assertSeeText('Are you sure you want to delete this book?');
144
145         $deleteReq = $this->delete($book->getUrl());
146         $deleteReq->assertRedirect(url('/books'));
147         $this->assertActivityExists('book_delete', $book);
148
149         $book->refresh();
150         $this->assertNotNull($book->deleted_at);
151
152         $this->assertTrue($book->pages()->count() === 0);
153         $this->assertTrue($book->chapters()->count() === 0);
154         $this->assertTrue($book->pages()->withTrashed()->count() === $pageCount);
155         $this->assertTrue($book->chapters()->withTrashed()->count() === $chapterCount);
156         $this->assertTrue($book->deletions()->count() === 1);
157
158         $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
159         $this->assertNotificationContains($redirectReq, 'Book Successfully Deleted');
160     }
161
162     public function test_cancel_on_create_page_leads_back_to_books_listing()
163     {
164         $resp = $this->asEditor()->get('/create-book');
165         $this->withHtml($resp)->assertElementContains('form a[href="' . url('/books') . '"]', 'Cancel');
166     }
167
168     public function test_cancel_on_edit_book_page_leads_back_to_book()
169     {
170         /** @var Book $book */
171         $book = Book::query()->first();
172         $resp = $this->asEditor()->get($book->getUrl('/edit'));
173         $this->withHtml($resp)->assertElementContains('form a[href="' . $book->getUrl() . '"]', 'Cancel');
174     }
175
176     public function test_next_previous_navigation_controls_show_within_book_content()
177     {
178         $book = Book::query()->first();
179         $chapter = $book->chapters->first();
180
181         $resp = $this->asEditor()->get($chapter->getUrl());
182         $this->withHtml($resp)->assertElementContains('#sibling-navigation', 'Next');
183         $this->withHtml($resp)->assertElementContains('#sibling-navigation', substr($chapter->pages[0]->name, 0, 20));
184
185         $resp = $this->get($chapter->pages[0]->getUrl());
186         $this->withHtml($resp)->assertElementContains('#sibling-navigation', substr($chapter->pages[1]->name, 0, 20));
187         $this->withHtml($resp)->assertElementContains('#sibling-navigation', 'Previous');
188         $this->withHtml($resp)->assertElementContains('#sibling-navigation', substr($chapter->name, 0, 20));
189     }
190
191     public function test_recently_viewed_books_updates_as_expected()
192     {
193         $books = Book::all()->take(2);
194
195         $resp = $this->asAdmin()->get('/books');
196         $this->withHtml($resp)->assertElementNotContains('#recents', $books[0]->name)
197             ->assertElementNotContains('#recents', $books[1]->name);
198
199         $this->get($books[0]->getUrl());
200         $this->get($books[1]->getUrl());
201
202         $resp = $this->get('/books');
203         $this->withHtml($resp)->assertElementContains('#recents', $books[0]->name)
204             ->assertElementContains('#recents', $books[1]->name);
205     }
206
207     public function test_popular_books_updates_upon_visits()
208     {
209         $books = Book::all()->take(2);
210
211         $resp = $this->asAdmin()->get('/books');
212         $this->withHtml($resp)->assertElementNotContains('#popular', $books[0]->name)
213             ->assertElementNotContains('#popular', $books[1]->name);
214
215         $this->get($books[0]->getUrl());
216         $this->get($books[1]->getUrl());
217         $this->get($books[0]->getUrl());
218
219         $resp = $this->get('/books');
220         $this->withHtml($resp)->assertElementContains('#popular .book:nth-child(1)', $books[0]->name)
221             ->assertElementContains('#popular .book:nth-child(2)', $books[1]->name);
222     }
223
224     public function test_books_view_shows_view_toggle_option()
225     {
226         /** @var Book $book */
227         $editor = $this->getEditor();
228         setting()->putUser($editor, 'books_view_type', 'list');
229
230         $resp = $this->actingAs($editor)->get('/books');
231         $this->withHtml($resp)->assertElementContains('form[action$="/settings/users/' . $editor->id . '/switch-books-view"]', 'Grid View');
232         $this->withHtml($resp)->assertElementExists('input[name="view_type"][value="grid"]');
233
234         $resp = $this->patch("/settings/users/{$editor->id}/switch-books-view", ['view_type' => 'grid']);
235         $resp->assertRedirect();
236         $this->assertEquals('grid', setting()->getUser($editor, 'books_view_type'));
237
238         $resp = $this->actingAs($editor)->get('/books');
239         $this->withHtml($resp)->assertElementContains('form[action$="/settings/users/' . $editor->id . '/switch-books-view"]', 'List View');
240         $this->withHtml($resp)->assertElementExists('input[name="view_type"][value="list"]');
241
242         $resp = $this->patch("/settings/users/{$editor->id}/switch-books-view", ['view_type' => 'list']);
243         $resp->assertRedirect();
244         $this->assertEquals('list', setting()->getUser($editor, 'books_view_type'));
245     }
246
247     public function test_slug_multi_byte_url_safe()
248     {
249         $book = $this->newBook([
250             'name' => 'информация',
251         ]);
252
253         $this->assertEquals('informaciya', $book->slug);
254
255         $book = $this->newBook([
256             'name' => '¿Qué?',
257         ]);
258
259         $this->assertEquals('que', $book->slug);
260     }
261
262     public function test_slug_format()
263     {
264         $book = $this->newBook([
265             'name' => 'PartA / PartB / PartC',
266         ]);
267
268         $this->assertEquals('parta-partb-partc', $book->slug);
269     }
270
271     public function test_show_view_has_copy_button()
272     {
273         /** @var Book $book */
274         $book = Book::query()->first();
275         $resp = $this->asEditor()->get($book->getUrl());
276
277         $this->withHtml($resp)->assertElementContains("a[href=\"{$book->getUrl('/copy')}\"]", 'Copy');
278     }
279
280     public function test_copy_view()
281     {
282         /** @var Book $book */
283         $book = Book::query()->first();
284         $resp = $this->asEditor()->get($book->getUrl('/copy'));
285
286         $resp->assertOk();
287         $resp->assertSee('Copy Book');
288         $this->withHtml($resp)->assertElementExists("input[name=\"name\"][value=\"{$book->name}\"]");
289     }
290
291     public function test_copy()
292     {
293         /** @var Book $book */
294         $book = Book::query()->whereHas('chapters')->whereHas('pages')->first();
295         $resp = $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
296
297         /** @var Book $copy */
298         $copy = Book::query()->where('name', '=', 'My copy book')->first();
299
300         $resp->assertRedirect($copy->getUrl());
301         $this->assertEquals($book->getDirectChildren()->count(), $copy->getDirectChildren()->count());
302     }
303
304     public function test_copy_does_not_copy_non_visible_content()
305     {
306         /** @var Book $book */
307         $book = Book::query()->whereHas('chapters')->whereHas('pages')->first();
308
309         // Hide child content
310         /** @var BookChild $page */
311         foreach ($book->getDirectChildren() as $child) {
312             $child->restricted = true;
313             $child->save();
314             $this->regenEntityPermissions($child);
315         }
316
317         $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
318         /** @var Book $copy */
319         $copy = Book::query()->where('name', '=', 'My copy book')->first();
320
321         $this->assertEquals(0, $copy->getDirectChildren()->count());
322     }
323
324     public function test_copy_does_not_copy_pages_or_chapters_if_user_cant_create()
325     {
326         /** @var Book $book */
327         $book = Book::query()->whereHas('chapters')->whereHas('directPages')->whereHas('chapters')->first();
328         $viewer = $this->getViewer();
329         $this->giveUserPermissions($viewer, ['book-create-all']);
330
331         $this->actingAs($viewer)->post($book->getUrl('/copy'), ['name' => 'My copy book']);
332         /** @var Book $copy */
333         $copy = Book::query()->where('name', '=', 'My copy book')->first();
334
335         $this->assertEquals(0, $copy->pages()->count());
336         $this->assertEquals(0, $copy->chapters()->count());
337     }
338
339     public function test_copy_clones_cover_image_if_existing()
340     {
341         /** @var Book $book */
342         $book = Book::query()->first();
343         $bookRepo = $this->app->make(BookRepo::class);
344         $coverImageFile = $this->getTestImage('cover.png');
345         $bookRepo->updateCoverImage($book, $coverImageFile);
346
347         $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
348         /** @var Book $copy */
349         $copy = Book::query()->where('name', '=', 'My copy book')->first();
350
351         $this->assertNotNull($copy->cover);
352         $this->assertNotEquals($book->cover->id, $copy->cover->id);
353     }
354
355     public function test_copy_adds_book_to_shelves_if_edit_permissions_allows()
356     {
357         /** @var Bookshelf $shelfA */
358         /** @var Bookshelf $shelfB */
359         [$shelfA, $shelfB] = Bookshelf::query()->take(2)->get();
360         /** @var Book $book */
361         $book = Book::query()->first();
362
363         $shelfA->appendBook($book);
364         $shelfB->appendBook($book);
365
366         $viewer = $this->getViewer();
367         $this->giveUserPermissions($viewer, ['book-update-all', 'book-create-all', 'bookshelf-update-all']);
368         $this->setEntityRestrictions($shelfB);
369
370
371         $this->asEditor()->post($book->getUrl('/copy'), ['name' => 'My copy book']);
372         /** @var Book $copy */
373         $copy = Book::query()->where('name', '=', 'My copy book')->first();
374
375         $this->assertTrue($copy->shelves()->where('id', '=', $shelfA->id)->exists());
376         $this->assertFalse($copy->shelves()->where('id', '=', $shelfB->id)->exists());
377     }
378 }