]> BookStack Code Mirror - bookstack/blob - tests/RecycleBinTest.php
Added 'Sort Book' action to chapters
[bookstack] / tests / RecycleBinTest.php
1 <?php
2
3 namespace Tests;
4
5 use BookStack\Entities\Models\Book;
6 use BookStack\Entities\Models\Bookshelf;
7 use BookStack\Entities\Models\Chapter;
8 use BookStack\Entities\Models\Deletion;
9 use BookStack\Entities\Models\Entity;
10 use BookStack\Entities\Models\Page;
11 use Illuminate\Support\Carbon;
12 use Illuminate\Support\Facades\DB;
13
14 class RecycleBinTest extends TestCase
15 {
16     public function test_recycle_bin_routes_permissions()
17     {
18         $page = Page::query()->first();
19         $editor = $this->getEditor();
20         $this->actingAs($editor)->delete($page->getUrl());
21         $deletion = Deletion::query()->firstOrFail();
22
23         $routes = [
24             'GET:/settings/recycle-bin',
25             'POST:/settings/recycle-bin/empty',
26             "GET:/settings/recycle-bin/{$deletion->id}/destroy",
27             "GET:/settings/recycle-bin/{$deletion->id}/restore",
28             "POST:/settings/recycle-bin/{$deletion->id}/restore",
29             "DELETE:/settings/recycle-bin/{$deletion->id}",
30         ];
31
32         foreach ($routes as $route) {
33             [$method, $url] = explode(':', $route);
34             $resp = $this->call($method, $url);
35             $this->assertPermissionError($resp);
36         }
37
38         $this->giveUserPermissions($editor, ['restrictions-manage-all']);
39
40         foreach ($routes as $route) {
41             [$method, $url] = explode(':', $route);
42             $resp = $this->call($method, $url);
43             $this->assertPermissionError($resp);
44         }
45
46         $this->giveUserPermissions($editor, ['settings-manage']);
47
48         foreach ($routes as $route) {
49             DB::beginTransaction();
50             [$method, $url] = explode(':', $route);
51             $resp = $this->call($method, $url);
52             $this->assertNotPermissionError($resp);
53             DB::rollBack();
54         }
55     }
56
57     public function test_recycle_bin_view()
58     {
59         $page = Page::query()->first();
60         $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first();
61         $editor = $this->getEditor();
62         $this->actingAs($editor)->delete($page->getUrl());
63         $this->actingAs($editor)->delete($book->getUrl());
64
65         $viewReq = $this->asAdmin()->get('/settings/recycle-bin');
66         $html = $this->withHtml($viewReq);
67         $html->assertElementContains('table.table', $page->name);
68         $html->assertElementContains('table.table', $editor->name);
69         $html->assertElementContains('table.table', $book->name);
70         $html->assertElementContains('table.table', $book->pages_count . ' Pages');
71         $html->assertElementContains('table.table', $book->chapters_count . ' Chapters');
72     }
73
74     public function test_recycle_bin_empty()
75     {
76         $page = Page::query()->first();
77         $book = Book::query()->where('id', '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
78         $editor = $this->getEditor();
79         $this->actingAs($editor)->delete($page->getUrl());
80         $this->actingAs($editor)->delete($book->getUrl());
81
82         $this->assertTrue(Deletion::query()->count() === 2);
83         $emptyReq = $this->asAdmin()->post('/settings/recycle-bin/empty');
84         $emptyReq->assertRedirect('/settings/recycle-bin');
85
86         $this->assertTrue(Deletion::query()->count() === 0);
87         $this->assertDatabaseMissing('books', ['id' => $book->id]);
88         $this->assertDatabaseMissing('pages', ['id' => $page->id]);
89         $this->assertDatabaseMissing('pages', ['id' => $book->pages->first()->id]);
90         $this->assertDatabaseMissing('chapters', ['id' => $book->chapters->first()->id]);
91
92         $itemCount = 2 + $book->pages->count() + $book->chapters->count();
93         $redirectReq = $this->get('/settings/recycle-bin');
94         $this->assertNotificationContains($redirectReq, 'Deleted ' . $itemCount . ' total items from the recycle bin');
95     }
96
97     public function test_entity_restore()
98     {
99         $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
100         $this->asEditor()->delete($book->getUrl());
101         $deletion = Deletion::query()->firstOrFail();
102
103         $this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
104         $this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
105
106         $restoreReq = $this->asAdmin()->post("/settings/recycle-bin/{$deletion->id}/restore");
107         $restoreReq->assertRedirect('/settings/recycle-bin');
108         $this->assertTrue(Deletion::query()->count() === 0);
109
110         $this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
111         $this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
112
113         $itemCount = 1 + $book->pages->count() + $book->chapters->count();
114         $redirectReq = $this->get('/settings/recycle-bin');
115         $this->assertNotificationContains($redirectReq, 'Restored ' . $itemCount . ' total items from the recycle bin');
116     }
117
118     public function test_permanent_delete()
119     {
120         $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
121         $this->asEditor()->delete($book->getUrl());
122         $deletion = Deletion::query()->firstOrFail();
123
124         $deleteReq = $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
125         $deleteReq->assertRedirect('/settings/recycle-bin');
126         $this->assertTrue(Deletion::query()->count() === 0);
127
128         $this->assertDatabaseMissing('books', ['id' => $book->id]);
129         $this->assertDatabaseMissing('pages', ['id' => $book->pages->first()->id]);
130         $this->assertDatabaseMissing('chapters', ['id' => $book->chapters->first()->id]);
131
132         $itemCount = 1 + $book->pages->count() + $book->chapters->count();
133         $redirectReq = $this->get('/settings/recycle-bin');
134         $this->assertNotificationContains($redirectReq, 'Deleted ' . $itemCount . ' total items from the recycle bin');
135     }
136
137     public function test_permanent_delete_for_each_type()
138     {
139         /** @var Entity $entity */
140         foreach ([new Bookshelf(), new Book(), new Chapter(), new Page()] as $entity) {
141             $entity = $entity->newQuery()->first();
142             $this->asEditor()->delete($entity->getUrl());
143             $deletion = Deletion::query()->orderBy('id', 'desc')->firstOrFail();
144
145             $deleteReq = $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
146             $deleteReq->assertRedirect('/settings/recycle-bin');
147             $this->assertDatabaseMissing('deletions', ['id' => $deletion->id]);
148             $this->assertDatabaseMissing($entity->getTable(), ['id' => $entity->id]);
149         }
150     }
151
152     public function test_permanent_entity_delete_updates_existing_activity_with_entity_name()
153     {
154         $page = Page::query()->firstOrFail();
155         $this->asEditor()->delete($page->getUrl());
156         $deletion = $page->deletions()->firstOrFail();
157
158         $this->assertDatabaseHas('activities', [
159             'type'        => 'page_delete',
160             'entity_id'   => $page->id,
161             'entity_type' => $page->getMorphClass(),
162         ]);
163
164         $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
165
166         $this->assertDatabaseMissing('activities', [
167             'type'        => 'page_delete',
168             'entity_id'   => $page->id,
169             'entity_type' => $page->getMorphClass(),
170         ]);
171
172         $this->assertDatabaseHas('activities', [
173             'type'        => 'page_delete',
174             'entity_id'   => null,
175             'entity_type' => null,
176             'detail'      => $page->name,
177         ]);
178     }
179
180     public function test_auto_clear_functionality_works()
181     {
182         config()->set('app.recycle_bin_lifetime', 5);
183         $page = Page::query()->firstOrFail();
184         $otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail();
185
186         $this->asEditor()->delete($page->getUrl());
187         $this->assertDatabaseHas('pages', ['id' => $page->id]);
188         $this->assertEquals(1, Deletion::query()->count());
189
190         Carbon::setTestNow(Carbon::now()->addDays(6));
191         $this->asEditor()->delete($otherPage->getUrl());
192         $this->assertEquals(1, Deletion::query()->count());
193
194         $this->assertDatabaseMissing('pages', ['id' => $page->id]);
195     }
196
197     public function test_auto_clear_functionality_with_negative_time_keeps_forever()
198     {
199         config()->set('app.recycle_bin_lifetime', -1);
200         $page = Page::query()->firstOrFail();
201         $otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail();
202
203         $this->asEditor()->delete($page->getUrl());
204         $this->assertEquals(1, Deletion::query()->count());
205
206         Carbon::setTestNow(Carbon::now()->addDays(6000));
207         $this->asEditor()->delete($otherPage->getUrl());
208         $this->assertEquals(2, Deletion::query()->count());
209
210         $this->assertDatabaseHas('pages', ['id' => $page->id]);
211     }
212
213     public function test_auto_clear_functionality_with_zero_time_deletes_instantly()
214     {
215         config()->set('app.recycle_bin_lifetime', 0);
216         $page = Page::query()->firstOrFail();
217
218         $this->asEditor()->delete($page->getUrl());
219         $this->assertDatabaseMissing('pages', ['id' => $page->id]);
220         $this->assertEquals(0, Deletion::query()->count());
221     }
222
223     public function test_restore_flow_when_restoring_nested_delete_first()
224     {
225         $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
226         $chapter = $book->chapters->first();
227         $this->asEditor()->delete($chapter->getUrl());
228         $this->asEditor()->delete($book->getUrl());
229
230         $bookDeletion = $book->deletions()->first();
231         $chapterDeletion = $chapter->deletions()->first();
232
233         $chapterRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$chapterDeletion->id}/restore");
234         $chapterRestoreView->assertStatus(200);
235         $chapterRestoreView->assertSeeText($chapter->name);
236
237         $chapterRestore = $this->post("/settings/recycle-bin/{$chapterDeletion->id}/restore");
238         $chapterRestore->assertRedirect('/settings/recycle-bin');
239         $this->assertDatabaseMissing('deletions', ['id' => $chapterDeletion->id]);
240
241         $chapter->refresh();
242         $this->assertNotNull($chapter->deleted_at);
243
244         $bookRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$bookDeletion->id}/restore");
245         $bookRestoreView->assertStatus(200);
246         $bookRestoreView->assertSeeText($chapter->name);
247
248         $this->post("/settings/recycle-bin/{$bookDeletion->id}/restore");
249         $chapter->refresh();
250         $this->assertNull($chapter->deleted_at);
251     }
252
253     public function test_restore_page_shows_link_to_parent_restore_if_parent_also_deleted()
254     {
255         /** @var Book $book */
256         $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
257         $chapter = $book->chapters->first();
258         /** @var Page $page */
259         $page = $chapter->pages->first();
260         $this->asEditor()->delete($page->getUrl());
261         $this->asEditor()->delete($book->getUrl());
262
263         $bookDeletion = $book->deletions()->first();
264         $pageDeletion = $page->deletions()->first();
265
266         $pageRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$pageDeletion->id}/restore");
267         $pageRestoreView->assertSee('The parent of this item has also been deleted.');
268         $this->withHtml($pageRestoreView)->assertElementContains('a[href$="/settings/recycle-bin/' . $bookDeletion->id . '/restore"]', 'Restore Parent');
269     }
270 }