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;
14 class RecycleBinTest extends TestCase
16 public function test_recycle_bin_routes_permissions()
18 $page = Page::query()->first();
19 $editor = $this->getEditor();
20 $this->actingAs($editor)->delete($page->getUrl());
21 $deletion = Deletion::query()->firstOrFail();
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}",
32 foreach ($routes as $route) {
33 [$method, $url] = explode(':', $route);
34 $resp = $this->call($method, $url);
35 $this->assertPermissionError($resp);
38 $this->giveUserPermissions($editor, ['restrictions-manage-all']);
40 foreach ($routes as $route) {
41 [$method, $url] = explode(':', $route);
42 $resp = $this->call($method, $url);
43 $this->assertPermissionError($resp);
46 $this->giveUserPermissions($editor, ['settings-manage']);
48 foreach ($routes as $route) {
49 DB::beginTransaction();
50 [$method, $url] = explode(':', $route);
51 $resp = $this->call($method, $url);
52 $this->assertNotPermissionError($resp);
57 public function test_recycle_bin_view()
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());
65 $viewReq = $this->asAdmin()->get('/settings/recycle-bin');
66 $viewReq->assertElementContains('table.table', $page->name);
67 $viewReq->assertElementContains('table.table', $editor->name);
68 $viewReq->assertElementContains('table.table', $book->name);
69 $viewReq->assertElementContains('table.table', $book->pages_count . ' Pages');
70 $viewReq->assertElementContains('table.table', $book->chapters_count . ' Chapters');
73 public function test_recycle_bin_empty()
75 $page = Page::query()->first();
76 $book = Book::query()->where('id', '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
77 $editor = $this->getEditor();
78 $this->actingAs($editor)->delete($page->getUrl());
79 $this->actingAs($editor)->delete($book->getUrl());
81 $this->assertTrue(Deletion::query()->count() === 2);
82 $emptyReq = $this->asAdmin()->post('/settings/recycle-bin/empty');
83 $emptyReq->assertRedirect('/settings/recycle-bin');
85 $this->assertTrue(Deletion::query()->count() === 0);
86 $this->assertDatabaseMissing('books', ['id' => $book->id]);
87 $this->assertDatabaseMissing('pages', ['id' => $page->id]);
88 $this->assertDatabaseMissing('pages', ['id' => $book->pages->first()->id]);
89 $this->assertDatabaseMissing('chapters', ['id' => $book->chapters->first()->id]);
91 $itemCount = 2 + $book->pages->count() + $book->chapters->count();
92 $redirectReq = $this->get('/settings/recycle-bin');
93 $redirectReq->assertNotificationContains('Deleted ' . $itemCount . ' total items from the recycle bin');
96 public function test_entity_restore()
98 $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
99 $this->asEditor()->delete($book->getUrl());
100 $deletion = Deletion::query()->firstOrFail();
102 $this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
103 $this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
105 $restoreReq = $this->asAdmin()->post("/settings/recycle-bin/{$deletion->id}/restore");
106 $restoreReq->assertRedirect('/settings/recycle-bin');
107 $this->assertTrue(Deletion::query()->count() === 0);
109 $this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
110 $this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
112 $itemCount = 1 + $book->pages->count() + $book->chapters->count();
113 $redirectReq = $this->get('/settings/recycle-bin');
114 $redirectReq->assertNotificationContains('Restored ' . $itemCount . ' total items from the recycle bin');
117 public function test_permanent_delete()
119 $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
120 $this->asEditor()->delete($book->getUrl());
121 $deletion = Deletion::query()->firstOrFail();
123 $deleteReq = $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
124 $deleteReq->assertRedirect('/settings/recycle-bin');
125 $this->assertTrue(Deletion::query()->count() === 0);
127 $this->assertDatabaseMissing('books', ['id' => $book->id]);
128 $this->assertDatabaseMissing('pages', ['id' => $book->pages->first()->id]);
129 $this->assertDatabaseMissing('chapters', ['id' => $book->chapters->first()->id]);
131 $itemCount = 1 + $book->pages->count() + $book->chapters->count();
132 $redirectReq = $this->get('/settings/recycle-bin');
133 $redirectReq->assertNotificationContains('Deleted ' . $itemCount . ' total items from the recycle bin');
136 public function test_permanent_delete_for_each_type()
138 /** @var Entity $entity */
139 foreach ([new Bookshelf(), new Book(), new Chapter(), new Page()] as $entity) {
140 $entity = $entity->newQuery()->first();
141 $this->asEditor()->delete($entity->getUrl());
142 $deletion = Deletion::query()->orderBy('id', 'desc')->firstOrFail();
144 $deleteReq = $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
145 $deleteReq->assertRedirect('/settings/recycle-bin');
146 $this->assertDatabaseMissing('deletions', ['id' => $deletion->id]);
147 $this->assertDatabaseMissing($entity->getTable(), ['id' => $entity->id]);
151 public function test_permanent_entity_delete_updates_existing_activity_with_entity_name()
153 $page = Page::query()->firstOrFail();
154 $this->asEditor()->delete($page->getUrl());
155 $deletion = $page->deletions()->firstOrFail();
157 $this->assertDatabaseHas('activities', [
158 'type' => 'page_delete',
159 'entity_id' => $page->id,
160 'entity_type' => $page->getMorphClass(),
163 $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
165 $this->assertDatabaseMissing('activities', [
166 'type' => 'page_delete',
167 'entity_id' => $page->id,
168 'entity_type' => $page->getMorphClass(),
171 $this->assertDatabaseHas('activities', [
172 'type' => 'page_delete',
174 'entity_type' => null,
175 'detail' => $page->name,
179 public function test_auto_clear_functionality_works()
181 config()->set('app.recycle_bin_lifetime', 5);
182 $page = Page::query()->firstOrFail();
183 $otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail();
185 $this->asEditor()->delete($page->getUrl());
186 $this->assertDatabaseHas('pages', ['id' => $page->id]);
187 $this->assertEquals(1, Deletion::query()->count());
189 Carbon::setTestNow(Carbon::now()->addDays(6));
190 $this->asEditor()->delete($otherPage->getUrl());
191 $this->assertEquals(1, Deletion::query()->count());
193 $this->assertDatabaseMissing('pages', ['id' => $page->id]);
196 public function test_auto_clear_functionality_with_negative_time_keeps_forever()
198 config()->set('app.recycle_bin_lifetime', -1);
199 $page = Page::query()->firstOrFail();
200 $otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail();
202 $this->asEditor()->delete($page->getUrl());
203 $this->assertEquals(1, Deletion::query()->count());
205 Carbon::setTestNow(Carbon::now()->addDays(6000));
206 $this->asEditor()->delete($otherPage->getUrl());
207 $this->assertEquals(2, Deletion::query()->count());
209 $this->assertDatabaseHas('pages', ['id' => $page->id]);
212 public function test_auto_clear_functionality_with_zero_time_deletes_instantly()
214 config()->set('app.recycle_bin_lifetime', 0);
215 $page = Page::query()->firstOrFail();
217 $this->asEditor()->delete($page->getUrl());
218 $this->assertDatabaseMissing('pages', ['id' => $page->id]);
219 $this->assertEquals(0, Deletion::query()->count());
222 public function test_restore_flow_when_restoring_nested_delete_first()
224 $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
225 $chapter = $book->chapters->first();
226 $this->asEditor()->delete($chapter->getUrl());
227 $this->asEditor()->delete($book->getUrl());
229 $bookDeletion = $book->deletions()->first();
230 $chapterDeletion = $chapter->deletions()->first();
232 $chapterRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$chapterDeletion->id}/restore");
233 $chapterRestoreView->assertStatus(200);
234 $chapterRestoreView->assertSeeText($chapter->name);
236 $chapterRestore = $this->post("/settings/recycle-bin/{$chapterDeletion->id}/restore");
237 $chapterRestore->assertRedirect('/settings/recycle-bin');
238 $this->assertDatabaseMissing('deletions', ['id' => $chapterDeletion->id]);
241 $this->assertNotNull($chapter->deleted_at);
243 $bookRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$bookDeletion->id}/restore");
244 $bookRestoreView->assertStatus(200);
245 $bookRestoreView->assertSeeText($chapter->name);
247 $this->post("/settings/recycle-bin/{$bookDeletion->id}/restore");
249 $this->assertNull($chapter->deleted_at);
252 public function test_restore_page_shows_link_to_parent_restore_if_parent_also_deleted()
254 /** @var Book $book */
255 $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
256 $chapter = $book->chapters->first();
257 /** @var Page $page */
258 $page = $chapter->pages->first();
259 $this->asEditor()->delete($page->getUrl());
260 $this->asEditor()->delete($book->getUrl());
262 $bookDeletion = $book->deletions()->first();
263 $pageDeletion = $page->deletions()->first();
265 $pageRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$pageDeletion->id}/restore");
266 $pageRestoreView->assertSee('The parent of this item has also been deleted.');
267 $pageRestoreView->assertElementContains('a[href$="/settings/recycle-bin/' . $bookDeletion->id . '/restore"]', 'Restore Parent');