2021-06-26 11:23:15 -04:00
|
|
|
<?php
|
|
|
|
|
2022-08-29 12:30:26 -04:00
|
|
|
namespace Tests\Settings;
|
2020-11-06 07:54:39 -05:00
|
|
|
|
2020-11-21 19:17:45 -05:00
|
|
|
use BookStack\Entities\Models\Book;
|
|
|
|
use BookStack\Entities\Models\Deletion;
|
|
|
|
use BookStack\Entities\Models\Page;
|
2020-11-07 08:58:23 -05:00
|
|
|
use Illuminate\Support\Carbon;
|
2021-09-26 10:48:22 -04:00
|
|
|
use Illuminate\Support\Facades\DB;
|
2022-08-29 12:30:26 -04:00
|
|
|
use Tests\TestCase;
|
2020-11-06 07:54:39 -05:00
|
|
|
|
|
|
|
class RecycleBinTest extends TestCase
|
|
|
|
{
|
|
|
|
public function test_recycle_bin_routes_permissions()
|
|
|
|
{
|
2022-09-29 12:31:38 -04:00
|
|
|
$page = $this->entities->page();
|
2023-01-21 06:08:34 -05:00
|
|
|
$editor = $this->users->editor();
|
2020-11-06 07:54:39 -05:00
|
|
|
$this->actingAs($editor)->delete($page->getUrl());
|
|
|
|
$deletion = Deletion::query()->firstOrFail();
|
|
|
|
|
|
|
|
$routes = [
|
|
|
|
'GET:/settings/recycle-bin',
|
|
|
|
'POST:/settings/recycle-bin/empty',
|
|
|
|
"GET:/settings/recycle-bin/{$deletion->id}/destroy",
|
|
|
|
"GET:/settings/recycle-bin/{$deletion->id}/restore",
|
|
|
|
"POST:/settings/recycle-bin/{$deletion->id}/restore",
|
|
|
|
"DELETE:/settings/recycle-bin/{$deletion->id}",
|
|
|
|
];
|
|
|
|
|
2021-06-26 11:23:15 -04:00
|
|
|
foreach ($routes as $route) {
|
2020-11-06 07:54:39 -05:00
|
|
|
[$method, $url] = explode(':', $route);
|
|
|
|
$resp = $this->call($method, $url);
|
|
|
|
$this->assertPermissionError($resp);
|
|
|
|
}
|
|
|
|
|
2023-01-21 06:08:34 -05:00
|
|
|
$this->permissions->grantUserRolePermissions($editor, ['restrictions-manage-all']);
|
2020-11-06 07:54:39 -05:00
|
|
|
|
2021-06-26 11:23:15 -04:00
|
|
|
foreach ($routes as $route) {
|
2020-11-06 07:54:39 -05:00
|
|
|
[$method, $url] = explode(':', $route);
|
|
|
|
$resp = $this->call($method, $url);
|
|
|
|
$this->assertPermissionError($resp);
|
|
|
|
}
|
|
|
|
|
2023-01-21 06:08:34 -05:00
|
|
|
$this->permissions->grantUserRolePermissions($editor, ['settings-manage']);
|
2020-11-06 07:54:39 -05:00
|
|
|
|
2021-06-26 11:23:15 -04:00
|
|
|
foreach ($routes as $route) {
|
2020-11-07 08:58:23 -05:00
|
|
|
DB::beginTransaction();
|
2020-11-06 07:54:39 -05:00
|
|
|
[$method, $url] = explode(':', $route);
|
|
|
|
$resp = $this->call($method, $url);
|
|
|
|
$this->assertNotPermissionError($resp);
|
2020-11-07 08:58:23 -05:00
|
|
|
DB::rollBack();
|
2020-11-06 07:54:39 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function test_recycle_bin_view()
|
|
|
|
{
|
2022-09-29 12:31:38 -04:00
|
|
|
$page = $this->entities->page();
|
2020-11-06 07:54:39 -05:00
|
|
|
$book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first();
|
2023-01-21 06:08:34 -05:00
|
|
|
$editor = $this->users->editor();
|
2020-11-06 07:54:39 -05:00
|
|
|
$this->actingAs($editor)->delete($page->getUrl());
|
|
|
|
$this->actingAs($editor)->delete($book->getUrl());
|
|
|
|
|
|
|
|
$viewReq = $this->asAdmin()->get('/settings/recycle-bin');
|
2022-07-23 10:10:18 -04:00
|
|
|
$html = $this->withHtml($viewReq);
|
2022-11-01 10:53:36 -04:00
|
|
|
$html->assertElementContains('.item-list-row', $page->name);
|
|
|
|
$html->assertElementContains('.item-list-row', $editor->name);
|
|
|
|
$html->assertElementContains('.item-list-row', $book->name);
|
|
|
|
$html->assertElementContains('.item-list-row', $book->pages_count . ' Pages');
|
|
|
|
$html->assertElementContains('.item-list-row', $book->chapters_count . ' Chapters');
|
2020-11-06 07:54:39 -05:00
|
|
|
}
|
2020-11-07 08:19:23 -05:00
|
|
|
|
|
|
|
public function test_recycle_bin_empty()
|
|
|
|
{
|
2022-09-29 12:31:38 -04:00
|
|
|
$page = $this->entities->page();
|
2021-06-26 11:23:15 -04:00
|
|
|
$book = Book::query()->where('id', '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
|
2023-01-21 06:08:34 -05:00
|
|
|
$editor = $this->users->editor();
|
2020-11-07 08:19:23 -05:00
|
|
|
$this->actingAs($editor)->delete($page->getUrl());
|
|
|
|
$this->actingAs($editor)->delete($book->getUrl());
|
|
|
|
|
|
|
|
$this->assertTrue(Deletion::query()->count() === 2);
|
|
|
|
$emptyReq = $this->asAdmin()->post('/settings/recycle-bin/empty');
|
|
|
|
$emptyReq->assertRedirect('/settings/recycle-bin');
|
|
|
|
|
|
|
|
$this->assertTrue(Deletion::query()->count() === 0);
|
|
|
|
$this->assertDatabaseMissing('books', ['id' => $book->id]);
|
|
|
|
$this->assertDatabaseMissing('pages', ['id' => $page->id]);
|
|
|
|
$this->assertDatabaseMissing('pages', ['id' => $book->pages->first()->id]);
|
|
|
|
$this->assertDatabaseMissing('chapters', ['id' => $book->chapters->first()->id]);
|
|
|
|
|
|
|
|
$itemCount = 2 + $book->pages->count() + $book->chapters->count();
|
|
|
|
$redirectReq = $this->get('/settings/recycle-bin');
|
2022-07-23 10:10:18 -04:00
|
|
|
$this->assertNotificationContains($redirectReq, 'Deleted ' . $itemCount . ' total items from the recycle bin');
|
2020-11-07 08:19:23 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
public function test_entity_restore()
|
|
|
|
{
|
2022-09-29 17:11:16 -04:00
|
|
|
$book = $this->entities->bookHasChaptersAndPages();
|
2020-11-07 08:19:23 -05:00
|
|
|
$this->asEditor()->delete($book->getUrl());
|
|
|
|
$deletion = Deletion::query()->firstOrFail();
|
|
|
|
|
2020-11-07 08:58:23 -05:00
|
|
|
$this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
|
|
|
|
$this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
|
2020-11-07 08:19:23 -05:00
|
|
|
|
|
|
|
$restoreReq = $this->asAdmin()->post("/settings/recycle-bin/{$deletion->id}/restore");
|
|
|
|
$restoreReq->assertRedirect('/settings/recycle-bin');
|
|
|
|
$this->assertTrue(Deletion::query()->count() === 0);
|
|
|
|
|
2020-11-07 08:58:23 -05:00
|
|
|
$this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
|
|
|
|
$this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
|
2020-11-07 08:19:23 -05:00
|
|
|
|
|
|
|
$itemCount = 1 + $book->pages->count() + $book->chapters->count();
|
|
|
|
$redirectReq = $this->get('/settings/recycle-bin');
|
2022-07-23 10:10:18 -04:00
|
|
|
$this->assertNotificationContains($redirectReq, 'Restored ' . $itemCount . ' total items from the recycle bin');
|
2020-11-07 08:19:23 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
public function test_permanent_delete()
|
|
|
|
{
|
2022-09-29 17:11:16 -04:00
|
|
|
$book = $this->entities->bookHasChaptersAndPages();
|
2020-11-07 08:19:23 -05:00
|
|
|
$this->asEditor()->delete($book->getUrl());
|
|
|
|
$deletion = Deletion::query()->firstOrFail();
|
|
|
|
|
|
|
|
$deleteReq = $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
|
|
|
|
$deleteReq->assertRedirect('/settings/recycle-bin');
|
|
|
|
$this->assertTrue(Deletion::query()->count() === 0);
|
|
|
|
|
|
|
|
$this->assertDatabaseMissing('books', ['id' => $book->id]);
|
|
|
|
$this->assertDatabaseMissing('pages', ['id' => $book->pages->first()->id]);
|
|
|
|
$this->assertDatabaseMissing('chapters', ['id' => $book->chapters->first()->id]);
|
|
|
|
|
|
|
|
$itemCount = 1 + $book->pages->count() + $book->chapters->count();
|
|
|
|
$redirectReq = $this->get('/settings/recycle-bin');
|
2022-07-23 10:10:18 -04:00
|
|
|
$this->assertNotificationContains($redirectReq, 'Deleted ' . $itemCount . ' total items from the recycle bin');
|
2020-11-07 08:19:23 -05:00
|
|
|
}
|
|
|
|
|
2021-02-06 08:22:31 -05:00
|
|
|
public function test_permanent_delete_for_each_type()
|
|
|
|
{
|
2022-09-29 17:11:16 -04:00
|
|
|
foreach ($this->entities->all() as $type => $entity) {
|
2021-02-06 08:22:31 -05:00
|
|
|
$this->asEditor()->delete($entity->getUrl());
|
|
|
|
$deletion = Deletion::query()->orderBy('id', 'desc')->firstOrFail();
|
|
|
|
|
|
|
|
$deleteReq = $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
|
|
|
|
$deleteReq->assertRedirect('/settings/recycle-bin');
|
|
|
|
$this->assertDatabaseMissing('deletions', ['id' => $deletion->id]);
|
|
|
|
$this->assertDatabaseMissing($entity->getTable(), ['id' => $entity->id]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-07 08:19:23 -05:00
|
|
|
public function test_permanent_entity_delete_updates_existing_activity_with_entity_name()
|
|
|
|
{
|
2022-09-29 17:11:16 -04:00
|
|
|
$page = $this->entities->page();
|
2020-11-07 08:19:23 -05:00
|
|
|
$this->asEditor()->delete($page->getUrl());
|
|
|
|
$deletion = $page->deletions()->firstOrFail();
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('activities', [
|
2021-06-26 11:23:15 -04:00
|
|
|
'type' => 'page_delete',
|
2024-05-05 10:44:58 -04:00
|
|
|
'loggable_id' => $page->id,
|
|
|
|
'loggable_type' => $page->getMorphClass(),
|
2020-11-07 08:19:23 -05:00
|
|
|
]);
|
|
|
|
|
|
|
|
$this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
|
|
|
|
|
|
|
|
$this->assertDatabaseMissing('activities', [
|
2021-06-26 11:23:15 -04:00
|
|
|
'type' => 'page_delete',
|
2024-05-05 10:44:58 -04:00
|
|
|
'loggable_id' => $page->id,
|
|
|
|
'loggable_type' => $page->getMorphClass(),
|
2020-11-07 08:19:23 -05:00
|
|
|
]);
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('activities', [
|
2021-06-26 11:23:15 -04:00
|
|
|
'type' => 'page_delete',
|
2024-05-05 10:44:58 -04:00
|
|
|
'loggable_id' => null,
|
|
|
|
'loggable_type' => null,
|
2021-06-26 11:23:15 -04:00
|
|
|
'detail' => $page->name,
|
2020-11-07 08:19:23 -05:00
|
|
|
]);
|
|
|
|
}
|
2020-11-07 08:58:23 -05:00
|
|
|
|
|
|
|
public function test_auto_clear_functionality_works()
|
|
|
|
{
|
|
|
|
config()->set('app.recycle_bin_lifetime', 5);
|
2022-09-29 17:11:16 -04:00
|
|
|
$page = $this->entities->page();
|
|
|
|
$otherPage = $this->entities->page();
|
2020-11-07 08:58:23 -05:00
|
|
|
|
|
|
|
$this->asEditor()->delete($page->getUrl());
|
|
|
|
$this->assertDatabaseHas('pages', ['id' => $page->id]);
|
|
|
|
$this->assertEquals(1, Deletion::query()->count());
|
|
|
|
|
|
|
|
Carbon::setTestNow(Carbon::now()->addDays(6));
|
|
|
|
$this->asEditor()->delete($otherPage->getUrl());
|
|
|
|
$this->assertEquals(1, Deletion::query()->count());
|
|
|
|
|
|
|
|
$this->assertDatabaseMissing('pages', ['id' => $page->id]);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function test_auto_clear_functionality_with_negative_time_keeps_forever()
|
|
|
|
{
|
|
|
|
config()->set('app.recycle_bin_lifetime', -1);
|
2022-09-29 17:11:16 -04:00
|
|
|
$page = $this->entities->page();
|
|
|
|
$otherPage = $this->entities->page();
|
2020-11-07 08:58:23 -05:00
|
|
|
|
|
|
|
$this->asEditor()->delete($page->getUrl());
|
|
|
|
$this->assertEquals(1, Deletion::query()->count());
|
|
|
|
|
|
|
|
Carbon::setTestNow(Carbon::now()->addDays(6000));
|
|
|
|
$this->asEditor()->delete($otherPage->getUrl());
|
|
|
|
$this->assertEquals(2, Deletion::query()->count());
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('pages', ['id' => $page->id]);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function test_auto_clear_functionality_with_zero_time_deletes_instantly()
|
|
|
|
{
|
|
|
|
config()->set('app.recycle_bin_lifetime', 0);
|
2022-09-29 17:11:16 -04:00
|
|
|
$page = $this->entities->page();
|
2020-11-07 08:58:23 -05:00
|
|
|
|
|
|
|
$this->asEditor()->delete($page->getUrl());
|
|
|
|
$this->assertDatabaseMissing('pages', ['id' => $page->id]);
|
|
|
|
$this->assertEquals(0, Deletion::query()->count());
|
|
|
|
}
|
2020-11-07 10:05:13 -05:00
|
|
|
|
|
|
|
public function test_restore_flow_when_restoring_nested_delete_first()
|
|
|
|
{
|
|
|
|
$book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
|
|
|
|
$chapter = $book->chapters->first();
|
|
|
|
$this->asEditor()->delete($chapter->getUrl());
|
|
|
|
$this->asEditor()->delete($book->getUrl());
|
|
|
|
|
|
|
|
$bookDeletion = $book->deletions()->first();
|
|
|
|
$chapterDeletion = $chapter->deletions()->first();
|
|
|
|
|
|
|
|
$chapterRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$chapterDeletion->id}/restore");
|
|
|
|
$chapterRestoreView->assertStatus(200);
|
|
|
|
$chapterRestoreView->assertSeeText($chapter->name);
|
|
|
|
|
|
|
|
$chapterRestore = $this->post("/settings/recycle-bin/{$chapterDeletion->id}/restore");
|
2021-06-26 11:23:15 -04:00
|
|
|
$chapterRestore->assertRedirect('/settings/recycle-bin');
|
|
|
|
$this->assertDatabaseMissing('deletions', ['id' => $chapterDeletion->id]);
|
2020-11-07 10:05:13 -05:00
|
|
|
|
|
|
|
$chapter->refresh();
|
|
|
|
$this->assertNotNull($chapter->deleted_at);
|
|
|
|
|
|
|
|
$bookRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$bookDeletion->id}/restore");
|
|
|
|
$bookRestoreView->assertStatus(200);
|
|
|
|
$bookRestoreView->assertSeeText($chapter->name);
|
|
|
|
|
|
|
|
$this->post("/settings/recycle-bin/{$bookDeletion->id}/restore");
|
|
|
|
$chapter->refresh();
|
|
|
|
$this->assertNull($chapter->deleted_at);
|
|
|
|
}
|
2021-06-26 07:12:11 -04:00
|
|
|
|
|
|
|
public function test_restore_page_shows_link_to_parent_restore_if_parent_also_deleted()
|
|
|
|
{
|
2022-09-29 17:11:16 -04:00
|
|
|
$book = $this->entities->bookHasChaptersAndPages();
|
2021-06-26 07:12:11 -04:00
|
|
|
$chapter = $book->chapters->first();
|
|
|
|
/** @var Page $page */
|
|
|
|
$page = $chapter->pages->first();
|
|
|
|
$this->asEditor()->delete($page->getUrl());
|
|
|
|
$this->asEditor()->delete($book->getUrl());
|
|
|
|
|
|
|
|
$bookDeletion = $book->deletions()->first();
|
|
|
|
$pageDeletion = $page->deletions()->first();
|
|
|
|
|
|
|
|
$pageRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$pageDeletion->id}/restore");
|
|
|
|
$pageRestoreView->assertSee('The parent of this item has also been deleted.');
|
2022-07-23 10:10:18 -04:00
|
|
|
$this->withHtml($pageRestoreView)->assertElementContains('a[href$="/settings/recycle-bin/' . $bookDeletion->id . '/restore"]', 'Restore Parent');
|
2021-06-26 07:12:11 -04:00
|
|
|
}
|
2021-06-26 11:23:15 -04:00
|
|
|
}
|