]> BookStack Code Mirror - bookstack/blob - tests/Settings/RecycleBinTest.php
Create additional test helper classes
[bookstack] / tests / Settings / RecycleBinTest.php
1 <?php
2
3 namespace Tests\Settings;
4
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;
10 use Tests\TestCase;
11
12 class RecycleBinTest extends TestCase
13 {
14     public function test_recycle_bin_routes_permissions()
15     {
16         $page = $this->entities->page();
17         $editor = $this->users->editor();
18         $this->actingAs($editor)->delete($page->getUrl());
19         $deletion = Deletion::query()->firstOrFail();
20
21         $routes = [
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}",
28         ];
29
30         foreach ($routes as $route) {
31             [$method, $url] = explode(':', $route);
32             $resp = $this->call($method, $url);
33             $this->assertPermissionError($resp);
34         }
35
36         $this->permissions->grantUserRolePermissions($editor, ['restrictions-manage-all']);
37
38         foreach ($routes as $route) {
39             [$method, $url] = explode(':', $route);
40             $resp = $this->call($method, $url);
41             $this->assertPermissionError($resp);
42         }
43
44         $this->permissions->grantUserRolePermissions($editor, ['settings-manage']);
45
46         foreach ($routes as $route) {
47             DB::beginTransaction();
48             [$method, $url] = explode(':', $route);
49             $resp = $this->call($method, $url);
50             $this->assertNotPermissionError($resp);
51             DB::rollBack();
52         }
53     }
54
55     public function test_recycle_bin_view()
56     {
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());
62
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');
70     }
71
72     public function test_recycle_bin_empty()
73     {
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());
79
80         $this->assertTrue(Deletion::query()->count() === 2);
81         $emptyReq = $this->asAdmin()->post('/settings/recycle-bin/empty');
82         $emptyReq->assertRedirect('/settings/recycle-bin');
83
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]);
89
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');
93     }
94
95     public function test_entity_restore()
96     {
97         $book = $this->entities->bookHasChaptersAndPages();
98         $this->asEditor()->delete($book->getUrl());
99         $deletion = Deletion::query()->firstOrFail();
100
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());
103
104         $restoreReq = $this->asAdmin()->post("/settings/recycle-bin/{$deletion->id}/restore");
105         $restoreReq->assertRedirect('/settings/recycle-bin');
106         $this->assertTrue(Deletion::query()->count() === 0);
107
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());
110
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');
114     }
115
116     public function test_permanent_delete()
117     {
118         $book = $this->entities->bookHasChaptersAndPages();
119         $this->asEditor()->delete($book->getUrl());
120         $deletion = Deletion::query()->firstOrFail();
121
122         $deleteReq = $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
123         $deleteReq->assertRedirect('/settings/recycle-bin');
124         $this->assertTrue(Deletion::query()->count() === 0);
125
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]);
129
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');
133     }
134
135     public function test_permanent_delete_for_each_type()
136     {
137         foreach ($this->entities->all() as $type => $entity) {
138             $this->asEditor()->delete($entity->getUrl());
139             $deletion = Deletion::query()->orderBy('id', 'desc')->firstOrFail();
140
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]);
145         }
146     }
147
148     public function test_permanent_entity_delete_updates_existing_activity_with_entity_name()
149     {
150         $page = $this->entities->page();
151         $this->asEditor()->delete($page->getUrl());
152         $deletion = $page->deletions()->firstOrFail();
153
154         $this->assertDatabaseHas('activities', [
155             'type'        => 'page_delete',
156             'entity_id'   => $page->id,
157             'entity_type' => $page->getMorphClass(),
158         ]);
159
160         $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
161
162         $this->assertDatabaseMissing('activities', [
163             'type'        => 'page_delete',
164             'entity_id'   => $page->id,
165             'entity_type' => $page->getMorphClass(),
166         ]);
167
168         $this->assertDatabaseHas('activities', [
169             'type'        => 'page_delete',
170             'entity_id'   => null,
171             'entity_type' => null,
172             'detail'      => $page->name,
173         ]);
174     }
175
176     public function test_auto_clear_functionality_works()
177     {
178         config()->set('app.recycle_bin_lifetime', 5);
179         $page = $this->entities->page();
180         $otherPage = $this->entities->page();
181
182         $this->asEditor()->delete($page->getUrl());
183         $this->assertDatabaseHas('pages', ['id' => $page->id]);
184         $this->assertEquals(1, Deletion::query()->count());
185
186         Carbon::setTestNow(Carbon::now()->addDays(6));
187         $this->asEditor()->delete($otherPage->getUrl());
188         $this->assertEquals(1, Deletion::query()->count());
189
190         $this->assertDatabaseMissing('pages', ['id' => $page->id]);
191     }
192
193     public function test_auto_clear_functionality_with_negative_time_keeps_forever()
194     {
195         config()->set('app.recycle_bin_lifetime', -1);
196         $page = $this->entities->page();
197         $otherPage = $this->entities->page();
198
199         $this->asEditor()->delete($page->getUrl());
200         $this->assertEquals(1, Deletion::query()->count());
201
202         Carbon::setTestNow(Carbon::now()->addDays(6000));
203         $this->asEditor()->delete($otherPage->getUrl());
204         $this->assertEquals(2, Deletion::query()->count());
205
206         $this->assertDatabaseHas('pages', ['id' => $page->id]);
207     }
208
209     public function test_auto_clear_functionality_with_zero_time_deletes_instantly()
210     {
211         config()->set('app.recycle_bin_lifetime', 0);
212         $page = $this->entities->page();
213
214         $this->asEditor()->delete($page->getUrl());
215         $this->assertDatabaseMissing('pages', ['id' => $page->id]);
216         $this->assertEquals(0, Deletion::query()->count());
217     }
218
219     public function test_restore_flow_when_restoring_nested_delete_first()
220     {
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());
225
226         $bookDeletion = $book->deletions()->first();
227         $chapterDeletion = $chapter->deletions()->first();
228
229         $chapterRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$chapterDeletion->id}/restore");
230         $chapterRestoreView->assertStatus(200);
231         $chapterRestoreView->assertSeeText($chapter->name);
232
233         $chapterRestore = $this->post("/settings/recycle-bin/{$chapterDeletion->id}/restore");
234         $chapterRestore->assertRedirect('/settings/recycle-bin');
235         $this->assertDatabaseMissing('deletions', ['id' => $chapterDeletion->id]);
236
237         $chapter->refresh();
238         $this->assertNotNull($chapter->deleted_at);
239
240         $bookRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$bookDeletion->id}/restore");
241         $bookRestoreView->assertStatus(200);
242         $bookRestoreView->assertSeeText($chapter->name);
243
244         $this->post("/settings/recycle-bin/{$bookDeletion->id}/restore");
245         $chapter->refresh();
246         $this->assertNull($chapter->deleted_at);
247     }
248
249     public function test_restore_page_shows_link_to_parent_restore_if_parent_also_deleted()
250     {
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());
257
258         $bookDeletion = $book->deletions()->first();
259         $pageDeletion = $page->deletions()->first();
260
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');
264     }
265 }