3 namespace Tests\Settings;
5 use BookStack\Entities\Models\Book;
6 use BookStack\Entities\Models\Deletion;
7 use BookStack\Entities\Models\Page;
8 use Illuminate\Support\Carbon;
9 use Illuminate\Support\Facades\DB;
12 class RecycleBinTest extends TestCase
14 public function test_recycle_bin_routes_permissions()
16 $page = $this->entities->page();
17 $editor = $this->users->editor();
18 $this->actingAs($editor)->delete($page->getUrl());
19 $deletion = Deletion::query()->firstOrFail();
22 'GET:/settings/recycle-bin',
23 'POST:/settings/recycle-bin/empty',
24 "GET:/settings/recycle-bin/{$deletion->id}/destroy",
25 "GET:/settings/recycle-bin/{$deletion->id}/restore",
26 "POST:/settings/recycle-bin/{$deletion->id}/restore",
27 "DELETE:/settings/recycle-bin/{$deletion->id}",
30 foreach ($routes as $route) {
31 [$method, $url] = explode(':', $route);
32 $resp = $this->call($method, $url);
33 $this->assertPermissionError($resp);
36 $this->permissions->grantUserRolePermissions($editor, ['restrictions-manage-all']);
38 foreach ($routes as $route) {
39 [$method, $url] = explode(':', $route);
40 $resp = $this->call($method, $url);
41 $this->assertPermissionError($resp);
44 $this->permissions->grantUserRolePermissions($editor, ['settings-manage']);
46 foreach ($routes as $route) {
47 DB::beginTransaction();
48 [$method, $url] = explode(':', $route);
49 $resp = $this->call($method, $url);
50 $this->assertNotPermissionError($resp);
55 public function test_recycle_bin_view()
57 $page = $this->entities->page();
58 $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first();
59 $editor = $this->users->editor();
60 $this->actingAs($editor)->delete($page->getUrl());
61 $this->actingAs($editor)->delete($book->getUrl());
63 $viewReq = $this->asAdmin()->get('/settings/recycle-bin');
64 $html = $this->withHtml($viewReq);
65 $html->assertElementContains('.item-list-row', $page->name);
66 $html->assertElementContains('.item-list-row', $editor->name);
67 $html->assertElementContains('.item-list-row', $book->name);
68 $html->assertElementContains('.item-list-row', $book->pages_count . ' Pages');
69 $html->assertElementContains('.item-list-row', $book->chapters_count . ' Chapters');
72 public function test_recycle_bin_empty()
74 $page = $this->entities->page();
75 $book = Book::query()->where('id', '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
76 $editor = $this->users->editor();
77 $this->actingAs($editor)->delete($page->getUrl());
78 $this->actingAs($editor)->delete($book->getUrl());
80 $this->assertTrue(Deletion::query()->count() === 2);
81 $emptyReq = $this->asAdmin()->post('/settings/recycle-bin/empty');
82 $emptyReq->assertRedirect('/settings/recycle-bin');
84 $this->assertTrue(Deletion::query()->count() === 0);
85 $this->assertDatabaseMissing('books', ['id' => $book->id]);
86 $this->assertDatabaseMissing('pages', ['id' => $page->id]);
87 $this->assertDatabaseMissing('pages', ['id' => $book->pages->first()->id]);
88 $this->assertDatabaseMissing('chapters', ['id' => $book->chapters->first()->id]);
90 $itemCount = 2 + $book->pages->count() + $book->chapters->count();
91 $redirectReq = $this->get('/settings/recycle-bin');
92 $this->assertNotificationContains($redirectReq, 'Deleted ' . $itemCount . ' total items from the recycle bin');
95 public function test_entity_restore()
97 $book = $this->entities->bookHasChaptersAndPages();
98 $this->asEditor()->delete($book->getUrl());
99 $deletion = Deletion::query()->firstOrFail();
101 $this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
102 $this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
104 $restoreReq = $this->asAdmin()->post("/settings/recycle-bin/{$deletion->id}/restore");
105 $restoreReq->assertRedirect('/settings/recycle-bin');
106 $this->assertTrue(Deletion::query()->count() === 0);
108 $this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
109 $this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
111 $itemCount = 1 + $book->pages->count() + $book->chapters->count();
112 $redirectReq = $this->get('/settings/recycle-bin');
113 $this->assertNotificationContains($redirectReq, 'Restored ' . $itemCount . ' total items from the recycle bin');
116 public function test_permanent_delete()
118 $book = $this->entities->bookHasChaptersAndPages();
119 $this->asEditor()->delete($book->getUrl());
120 $deletion = Deletion::query()->firstOrFail();
122 $deleteReq = $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
123 $deleteReq->assertRedirect('/settings/recycle-bin');
124 $this->assertTrue(Deletion::query()->count() === 0);
126 $this->assertDatabaseMissing('books', ['id' => $book->id]);
127 $this->assertDatabaseMissing('pages', ['id' => $book->pages->first()->id]);
128 $this->assertDatabaseMissing('chapters', ['id' => $book->chapters->first()->id]);
130 $itemCount = 1 + $book->pages->count() + $book->chapters->count();
131 $redirectReq = $this->get('/settings/recycle-bin');
132 $this->assertNotificationContains($redirectReq, 'Deleted ' . $itemCount . ' total items from the recycle bin');
135 public function test_permanent_delete_for_each_type()
137 foreach ($this->entities->all() as $type => $entity) {
138 $this->asEditor()->delete($entity->getUrl());
139 $deletion = Deletion::query()->orderBy('id', 'desc')->firstOrFail();
141 $deleteReq = $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
142 $deleteReq->assertRedirect('/settings/recycle-bin');
143 $this->assertDatabaseMissing('deletions', ['id' => $deletion->id]);
144 $this->assertDatabaseMissing($entity->getTable(), ['id' => $entity->id]);
148 public function test_permanent_entity_delete_updates_existing_activity_with_entity_name()
150 $page = $this->entities->page();
151 $this->asEditor()->delete($page->getUrl());
152 $deletion = $page->deletions()->firstOrFail();
154 $this->assertDatabaseHas('activities', [
155 'type' => 'page_delete',
156 'entity_id' => $page->id,
157 'entity_type' => $page->getMorphClass(),
160 $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
162 $this->assertDatabaseMissing('activities', [
163 'type' => 'page_delete',
164 'entity_id' => $page->id,
165 'entity_type' => $page->getMorphClass(),
168 $this->assertDatabaseHas('activities', [
169 'type' => 'page_delete',
171 'entity_type' => null,
172 'detail' => $page->name,
176 public function test_auto_clear_functionality_works()
178 config()->set('app.recycle_bin_lifetime', 5);
179 $page = $this->entities->page();
180 $otherPage = $this->entities->page();
182 $this->asEditor()->delete($page->getUrl());
183 $this->assertDatabaseHas('pages', ['id' => $page->id]);
184 $this->assertEquals(1, Deletion::query()->count());
186 Carbon::setTestNow(Carbon::now()->addDays(6));
187 $this->asEditor()->delete($otherPage->getUrl());
188 $this->assertEquals(1, Deletion::query()->count());
190 $this->assertDatabaseMissing('pages', ['id' => $page->id]);
193 public function test_auto_clear_functionality_with_negative_time_keeps_forever()
195 config()->set('app.recycle_bin_lifetime', -1);
196 $page = $this->entities->page();
197 $otherPage = $this->entities->page();
199 $this->asEditor()->delete($page->getUrl());
200 $this->assertEquals(1, Deletion::query()->count());
202 Carbon::setTestNow(Carbon::now()->addDays(6000));
203 $this->asEditor()->delete($otherPage->getUrl());
204 $this->assertEquals(2, Deletion::query()->count());
206 $this->assertDatabaseHas('pages', ['id' => $page->id]);
209 public function test_auto_clear_functionality_with_zero_time_deletes_instantly()
211 config()->set('app.recycle_bin_lifetime', 0);
212 $page = $this->entities->page();
214 $this->asEditor()->delete($page->getUrl());
215 $this->assertDatabaseMissing('pages', ['id' => $page->id]);
216 $this->assertEquals(0, Deletion::query()->count());
219 public function test_restore_flow_when_restoring_nested_delete_first()
221 $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
222 $chapter = $book->chapters->first();
223 $this->asEditor()->delete($chapter->getUrl());
224 $this->asEditor()->delete($book->getUrl());
226 $bookDeletion = $book->deletions()->first();
227 $chapterDeletion = $chapter->deletions()->first();
229 $chapterRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$chapterDeletion->id}/restore");
230 $chapterRestoreView->assertStatus(200);
231 $chapterRestoreView->assertSeeText($chapter->name);
233 $chapterRestore = $this->post("/settings/recycle-bin/{$chapterDeletion->id}/restore");
234 $chapterRestore->assertRedirect('/settings/recycle-bin');
235 $this->assertDatabaseMissing('deletions', ['id' => $chapterDeletion->id]);
238 $this->assertNotNull($chapter->deleted_at);
240 $bookRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$bookDeletion->id}/restore");
241 $bookRestoreView->assertStatus(200);
242 $bookRestoreView->assertSeeText($chapter->name);
244 $this->post("/settings/recycle-bin/{$bookDeletion->id}/restore");
246 $this->assertNull($chapter->deleted_at);
249 public function test_restore_page_shows_link_to_parent_restore_if_parent_also_deleted()
251 $book = $this->entities->bookHasChaptersAndPages();
252 $chapter = $book->chapters->first();
253 /** @var Page $page */
254 $page = $chapter->pages->first();
255 $this->asEditor()->delete($page->getUrl());
256 $this->asEditor()->delete($book->getUrl());
258 $bookDeletion = $book->deletions()->first();
259 $pageDeletion = $page->deletions()->first();
261 $pageRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$pageDeletion->id}/restore");
262 $pageRestoreView->assertSee('The parent of this item has also been deleted.');
263 $this->withHtml($pageRestoreView)->assertElementContains('a[href$="/settings/recycle-bin/' . $bookDeletion->id . '/restore"]', 'Restore Parent');