3 namespace Tests\Settings;
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;
15 class RecycleBinTest extends TestCase
17 public function test_recycle_bin_routes_permissions()
19 $page = Page::query()->first();
20 $editor = $this->getEditor();
21 $this->actingAs($editor)->delete($page->getUrl());
22 $deletion = Deletion::query()->firstOrFail();
25 'GET:/settings/recycle-bin',
26 'POST:/settings/recycle-bin/empty',
27 "GET:/settings/recycle-bin/{$deletion->id}/destroy",
28 "GET:/settings/recycle-bin/{$deletion->id}/restore",
29 "POST:/settings/recycle-bin/{$deletion->id}/restore",
30 "DELETE:/settings/recycle-bin/{$deletion->id}",
33 foreach ($routes as $route) {
34 [$method, $url] = explode(':', $route);
35 $resp = $this->call($method, $url);
36 $this->assertPermissionError($resp);
39 $this->giveUserPermissions($editor, ['restrictions-manage-all']);
41 foreach ($routes as $route) {
42 [$method, $url] = explode(':', $route);
43 $resp = $this->call($method, $url);
44 $this->assertPermissionError($resp);
47 $this->giveUserPermissions($editor, ['settings-manage']);
49 foreach ($routes as $route) {
50 DB::beginTransaction();
51 [$method, $url] = explode(':', $route);
52 $resp = $this->call($method, $url);
53 $this->assertNotPermissionError($resp);
58 public function test_recycle_bin_view()
60 $page = Page::query()->first();
61 $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first();
62 $editor = $this->getEditor();
63 $this->actingAs($editor)->delete($page->getUrl());
64 $this->actingAs($editor)->delete($book->getUrl());
66 $viewReq = $this->asAdmin()->get('/settings/recycle-bin');
67 $html = $this->withHtml($viewReq);
68 $html->assertElementContains('table.table', $page->name);
69 $html->assertElementContains('table.table', $editor->name);
70 $html->assertElementContains('table.table', $book->name);
71 $html->assertElementContains('table.table', $book->pages_count . ' Pages');
72 $html->assertElementContains('table.table', $book->chapters_count . ' Chapters');
75 public function test_recycle_bin_empty()
77 $page = Page::query()->first();
78 $book = Book::query()->where('id', '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
79 $editor = $this->getEditor();
80 $this->actingAs($editor)->delete($page->getUrl());
81 $this->actingAs($editor)->delete($book->getUrl());
83 $this->assertTrue(Deletion::query()->count() === 2);
84 $emptyReq = $this->asAdmin()->post('/settings/recycle-bin/empty');
85 $emptyReq->assertRedirect('/settings/recycle-bin');
87 $this->assertTrue(Deletion::query()->count() === 0);
88 $this->assertDatabaseMissing('books', ['id' => $book->id]);
89 $this->assertDatabaseMissing('pages', ['id' => $page->id]);
90 $this->assertDatabaseMissing('pages', ['id' => $book->pages->first()->id]);
91 $this->assertDatabaseMissing('chapters', ['id' => $book->chapters->first()->id]);
93 $itemCount = 2 + $book->pages->count() + $book->chapters->count();
94 $redirectReq = $this->get('/settings/recycle-bin');
95 $this->assertNotificationContains($redirectReq, 'Deleted ' . $itemCount . ' total items from the recycle bin');
98 public function test_entity_restore()
100 $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
101 $this->asEditor()->delete($book->getUrl());
102 $deletion = Deletion::query()->firstOrFail();
104 $this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
105 $this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
107 $restoreReq = $this->asAdmin()->post("/settings/recycle-bin/{$deletion->id}/restore");
108 $restoreReq->assertRedirect('/settings/recycle-bin');
109 $this->assertTrue(Deletion::query()->count() === 0);
111 $this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
112 $this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
114 $itemCount = 1 + $book->pages->count() + $book->chapters->count();
115 $redirectReq = $this->get('/settings/recycle-bin');
116 $this->assertNotificationContains($redirectReq, 'Restored ' . $itemCount . ' total items from the recycle bin');
119 public function test_permanent_delete()
121 $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
122 $this->asEditor()->delete($book->getUrl());
123 $deletion = Deletion::query()->firstOrFail();
125 $deleteReq = $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
126 $deleteReq->assertRedirect('/settings/recycle-bin');
127 $this->assertTrue(Deletion::query()->count() === 0);
129 $this->assertDatabaseMissing('books', ['id' => $book->id]);
130 $this->assertDatabaseMissing('pages', ['id' => $book->pages->first()->id]);
131 $this->assertDatabaseMissing('chapters', ['id' => $book->chapters->first()->id]);
133 $itemCount = 1 + $book->pages->count() + $book->chapters->count();
134 $redirectReq = $this->get('/settings/recycle-bin');
135 $this->assertNotificationContains($redirectReq, 'Deleted ' . $itemCount . ' total items from the recycle bin');
138 public function test_permanent_delete_for_each_type()
140 /** @var Entity $entity */
141 foreach ([new Bookshelf(), new Book(), new Chapter(), new Page()] as $entity) {
142 $entity = $entity->newQuery()->first();
143 $this->asEditor()->delete($entity->getUrl());
144 $deletion = Deletion::query()->orderBy('id', 'desc')->firstOrFail();
146 $deleteReq = $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
147 $deleteReq->assertRedirect('/settings/recycle-bin');
148 $this->assertDatabaseMissing('deletions', ['id' => $deletion->id]);
149 $this->assertDatabaseMissing($entity->getTable(), ['id' => $entity->id]);
153 public function test_permanent_entity_delete_updates_existing_activity_with_entity_name()
155 $page = Page::query()->firstOrFail();
156 $this->asEditor()->delete($page->getUrl());
157 $deletion = $page->deletions()->firstOrFail();
159 $this->assertDatabaseHas('activities', [
160 'type' => 'page_delete',
161 'entity_id' => $page->id,
162 'entity_type' => $page->getMorphClass(),
165 $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
167 $this->assertDatabaseMissing('activities', [
168 'type' => 'page_delete',
169 'entity_id' => $page->id,
170 'entity_type' => $page->getMorphClass(),
173 $this->assertDatabaseHas('activities', [
174 'type' => 'page_delete',
176 'entity_type' => null,
177 'detail' => $page->name,
181 public function test_auto_clear_functionality_works()
183 config()->set('app.recycle_bin_lifetime', 5);
184 $page = Page::query()->firstOrFail();
185 $otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail();
187 $this->asEditor()->delete($page->getUrl());
188 $this->assertDatabaseHas('pages', ['id' => $page->id]);
189 $this->assertEquals(1, Deletion::query()->count());
191 Carbon::setTestNow(Carbon::now()->addDays(6));
192 $this->asEditor()->delete($otherPage->getUrl());
193 $this->assertEquals(1, Deletion::query()->count());
195 $this->assertDatabaseMissing('pages', ['id' => $page->id]);
198 public function test_auto_clear_functionality_with_negative_time_keeps_forever()
200 config()->set('app.recycle_bin_lifetime', -1);
201 $page = Page::query()->firstOrFail();
202 $otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail();
204 $this->asEditor()->delete($page->getUrl());
205 $this->assertEquals(1, Deletion::query()->count());
207 Carbon::setTestNow(Carbon::now()->addDays(6000));
208 $this->asEditor()->delete($otherPage->getUrl());
209 $this->assertEquals(2, Deletion::query()->count());
211 $this->assertDatabaseHas('pages', ['id' => $page->id]);
214 public function test_auto_clear_functionality_with_zero_time_deletes_instantly()
216 config()->set('app.recycle_bin_lifetime', 0);
217 $page = Page::query()->firstOrFail();
219 $this->asEditor()->delete($page->getUrl());
220 $this->assertDatabaseMissing('pages', ['id' => $page->id]);
221 $this->assertEquals(0, Deletion::query()->count());
224 public function test_restore_flow_when_restoring_nested_delete_first()
226 $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
227 $chapter = $book->chapters->first();
228 $this->asEditor()->delete($chapter->getUrl());
229 $this->asEditor()->delete($book->getUrl());
231 $bookDeletion = $book->deletions()->first();
232 $chapterDeletion = $chapter->deletions()->first();
234 $chapterRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$chapterDeletion->id}/restore");
235 $chapterRestoreView->assertStatus(200);
236 $chapterRestoreView->assertSeeText($chapter->name);
238 $chapterRestore = $this->post("/settings/recycle-bin/{$chapterDeletion->id}/restore");
239 $chapterRestore->assertRedirect('/settings/recycle-bin');
240 $this->assertDatabaseMissing('deletions', ['id' => $chapterDeletion->id]);
243 $this->assertNotNull($chapter->deleted_at);
245 $bookRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$bookDeletion->id}/restore");
246 $bookRestoreView->assertStatus(200);
247 $bookRestoreView->assertSeeText($chapter->name);
249 $this->post("/settings/recycle-bin/{$bookDeletion->id}/restore");
251 $this->assertNull($chapter->deleted_at);
254 public function test_restore_page_shows_link_to_parent_restore_if_parent_also_deleted()
256 /** @var Book $book */
257 $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
258 $chapter = $book->chapters->first();
259 /** @var Page $page */
260 $page = $chapter->pages->first();
261 $this->asEditor()->delete($page->getUrl());
262 $this->asEditor()->delete($book->getUrl());
264 $bookDeletion = $book->deletions()->first();
265 $pageDeletion = $page->deletions()->first();
267 $pageRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$pageDeletion->id}/restore");
268 $pageRestoreView->assertSee('The parent of this item has also been deleted.');
269 $this->withHtml($pageRestoreView)->assertElementContains('a[href$="/settings/recycle-bin/' . $bookDeletion->id . '/restore"]', 'Restore Parent');