diff --git a/.env.example.complete b/.env.example.complete index 45b1e1321..e3dbdb857 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -255,6 +255,14 @@ APP_VIEWS_BOOKSHELVES=grid # If set to 'false' a limit will not be enforced. REVISION_LIMIT=50 +# Recycle Bin Lifetime +# The number of days that content will remain in the recycle bin before +# being considered for auto-removal. It is not a guarantee that content will +# be removed after this time. +# Set to 0 for no recycle bin functionality. +# Set to -1 for unlimited recycle bin lifetime. +RECYCLE_BIN_LIFETIME=30 + # Allow

Hello

"; + $page->save(); + + $resp = $this->getJson($this->baseEndpoint . "/{$page->id}"); + $html = $resp->json('html'); + $this->assertStringNotContainsString('script', $html); + $this->assertStringContainsString('Hello', $html); + $this->assertStringContainsString('testing', $html); + } + + public function test_update_endpoint() + { + $this->actingAsApiEditor(); + $page = Page::visible()->first(); + $details = [ + 'name' => 'My updated API page', + 'html' => '

A page created via the API

', + 'tags' => [ + [ + 'name' => 'freshtag', + 'value' => 'freshtagval', + ] + ], + ]; + + $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details); + $page->refresh(); + + $resp->assertStatus(200); + unset($details['html']); + $resp->assertJson(array_merge($details, [ + 'id' => $page->id, 'slug' => $page->slug, 'book_id' => $page->book_id + ])); + $this->assertActivityExists('page_update', $page); + } + + public function test_providing_new_chapter_id_on_update_will_move_page() + { + $this->actingAsApiEditor(); + $page = Page::visible()->first(); + $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first(); + $details = [ + 'name' => 'My updated API page', + 'chapter_id' => $chapter->id, + 'html' => '

A page created via the API

', + ]; + + $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details); + $resp->assertStatus(200); + $resp->assertJson([ + 'chapter_id' => $chapter->id, + 'book_id' => $chapter->book_id, + ]); + } + + public function test_providing_move_via_update_requires_page_create_permission_on_new_parent() + { + $this->actingAsApiEditor(); + $page = Page::visible()->first(); + $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first(); + $this->setEntityRestrictions($chapter, ['view'], [$this->getEditor()->roles()->first()]); + $details = [ + 'name' => 'My updated API page', + 'chapter_id' => $chapter->id, + 'html' => '

A page created via the API

', + ]; + + $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details); + $resp->assertStatus(403); + } + + public function test_delete_endpoint() + { + $this->actingAsApiEditor(); + $page = Page::visible()->first(); + $resp = $this->deleteJson($this->baseEndpoint . "/{$page->id}"); + + $resp->assertStatus(204); + $this->assertActivityExists('page_delete', $page); + } + + public function test_export_html_endpoint() + { + $this->actingAsApiEditor(); + $page = Page::visible()->first(); + + $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/html"); + $resp->assertStatus(200); + $resp->assertSee($page->name); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html"'); + } + + public function test_export_plain_text_endpoint() + { + $this->actingAsApiEditor(); + $page = Page::visible()->first(); + + $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/plaintext"); + $resp->assertStatus(200); + $resp->assertSee($page->name); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt"'); + } + + public function test_export_pdf_endpoint() + { + $this->actingAsApiEditor(); + $page = Page::visible()->first(); + + $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/pdf"); + $resp->assertStatus(200); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"'); + } +} \ No newline at end of file diff --git a/tests/Api/ShelvesApiTest.php b/tests/Api/ShelvesApiTest.php index 13e44d97d..4c5600d15 100644 --- a/tests/Api/ShelvesApiTest.php +++ b/tests/Api/ShelvesApiTest.php @@ -1,7 +1,7 @@ activityService = app(ActivityService::class); + } public function test_only_accessible_with_right_permissions() { @@ -33,14 +43,14 @@ class AuditLogTest extends TestCase $admin = $this->getAdmin(); $this->actingAs($admin); $page = Page::query()->first(); - app(ActivityService::class)->add($page, 'page_create', $page->book->id); + $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE); $activity = Activity::query()->orderBy('id', 'desc')->first(); $resp = $this->get('settings/audit'); $resp->assertSeeText($page->name); $resp->assertSeeText('page_create'); $resp->assertSeeText($activity->created_at->toDateTimeString()); - $resp->assertElementContains('.audit-log-user', $admin->name); + $resp->assertElementContains('.table-user-item', $admin->name); } public function test_shows_name_for_deleted_items() @@ -48,9 +58,10 @@ class AuditLogTest extends TestCase $this->actingAs( $this->getAdmin()); $page = Page::query()->first(); $pageName = $page->name; - app(ActivityService::class)->add($page, 'page_create', $page->book->id); + $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE); app(PageRepo::class)->destroy($page); + app(TrashCan::class)->empty(); $resp = $this->get('settings/audit'); $resp->assertSeeText('Deleted Item'); @@ -62,7 +73,7 @@ class AuditLogTest extends TestCase $viewer = $this->getViewer(); $this->actingAs($viewer); $page = Page::query()->first(); - app(ActivityService::class)->add($page, 'page_create', $page->book->id); + $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE); $this->actingAs($this->getAdmin()); app(UserRepo::class)->destroy($viewer); @@ -75,7 +86,7 @@ class AuditLogTest extends TestCase { $this->actingAs($this->getAdmin()); $page = Page::query()->first(); - app(ActivityService::class)->add($page, 'page_create', $page->book->id); + $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE); $resp = $this->get('settings/audit'); $resp->assertSeeText($page->name); @@ -88,7 +99,7 @@ class AuditLogTest extends TestCase { $this->actingAs($this->getAdmin()); $page = Page::query()->first(); - app(ActivityService::class)->add($page, 'page_create', $page->book->id); + $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE); $yesterday = (Carbon::now()->subDay()->format('Y-m-d')); $tomorrow = (Carbon::now()->addDay()->format('Y-m-d')); diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php index e2b1e0cd6..a0de7f803 100644 --- a/tests/Auth/AuthTest.php +++ b/tests/Auth/AuthTest.php @@ -2,7 +2,7 @@ use BookStack\Auth\Role; use BookStack\Auth\User; -use BookStack\Entities\Page; +use BookStack\Entities\Models\Page; use BookStack\Notifications\ConfirmEmail; use BookStack\Notifications\ResetPassword; use BookStack\Settings\SettingService; diff --git a/tests/BrowserKitTest.php b/tests/BrowserKitTest.php index b81afe311..6c332a984 100644 --- a/tests/BrowserKitTest.php +++ b/tests/BrowserKitTest.php @@ -1,10 +1,16 @@ get()->last(); + return User::where('system_name', '=', null)->get()->last(); } /** @@ -64,23 +70,21 @@ abstract class BrowserKitTest extends TestCase /** * Create a group of entities that belong to a specific user. - * @param $creatorUser - * @param $updaterUser - * @return array */ - protected function createEntityChainBelongingToUser($creatorUser, $updaterUser = false) + protected function createEntityChainBelongingToUser(User $creatorUser, ?User $updaterUser = null): array { - if ($updaterUser === false) $updaterUser = $creatorUser; - $book = factory(\BookStack\Entities\Book::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]); - $chapter = factory(\BookStack\Entities\Chapter::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id]); - $page = factory(\BookStack\Entities\Page::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id, 'chapter_id' => $chapter->id]); + if (empty($updaterUser)) { + $updaterUser = $creatorUser; + } + + $userAttrs = ['created_by' => $creatorUser->id, 'owned_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]; + $book = factory(Book::class)->create($userAttrs); + $chapter = factory(Chapter::class)->create(array_merge(['book_id' => $book->id], $userAttrs)); + $page = factory(Page::class)->create(array_merge(['book_id' => $book->id, 'chapter_id' => $chapter->id], $userAttrs)); $restrictionService = $this->app[PermissionService::class]; $restrictionService->buildJointPermissionsForEntity($book); - return [ - 'book' => $book, - 'chapter' => $chapter, - 'page' => $page - ]; + + return compact('book', 'chapter', 'page'); } /** @@ -101,7 +105,7 @@ abstract class BrowserKitTest extends TestCase */ protected function getNewBlankUser($attributes = []) { - $user = factory(\BookStack\Auth\User::class)->create($attributes); + $user = factory(User::class)->create($attributes); return $user; } diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index bfc0ac0eb..8c6ea84bf 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -1,10 +1,11 @@ asEditor(); $page = Page::first(); - \Activity::add($page, 'page_update', $page->book->id); + \Activity::addForEntity($page, ActivityType::PAGE_UPDATE); $this->assertDatabaseHas('activities', [ - 'key' => 'page_update', + 'type' => 'page_update', 'entity_id' => $page->id, 'user_id' => $this->getEditor()->id ]); @@ -50,7 +51,7 @@ class CommandsTest extends TestCase $this->assertDatabaseMissing('activities', [ - 'key' => 'page_update' + 'type' => 'page_update' ]); } diff --git a/tests/Entity/BookShelfTest.php b/tests/Entity/BookShelfTest.php index cb3acfb1e..9b3290370 100644 --- a/tests/Entity/BookShelfTest.php +++ b/tests/Entity/BookShelfTest.php @@ -1,8 +1,8 @@ asEditor()->get($shelf->getUrl('/delete')); - $resp->assertSeeText('Delete Bookshelf'); - $resp->assertSee("action=\"{$shelf->getUrl()}\""); + $shelf = Bookshelf::query()->whereHas('books')->first(); + $this->assertNull($shelf->deleted_at); + $bookCount = $shelf->books()->count(); - $resp = $this->delete($shelf->getUrl()); - $resp->assertRedirect('/shelves'); - $this->assertDatabaseMissing('bookshelves', ['id' => $shelf->id]); - $this->assertDatabaseMissing('bookshelves_books', ['bookshelf_id' => $shelf->id]); - $this->assertSessionHas('success'); + $deleteViewReq = $this->asEditor()->get($shelf->getUrl('/delete')); + $deleteViewReq->assertSeeText('Are you sure you want to delete this bookshelf?'); + + $deleteReq = $this->delete($shelf->getUrl()); + $deleteReq->assertRedirect(url('/shelves')); + $this->assertActivityExists('bookshelf_delete', $shelf); + + $shelf->refresh(); + $this->assertNotNull($shelf->deleted_at); + + $this->assertTrue($shelf->books()->count() === $bookCount); + $this->assertTrue($shelf->deletions()->count() === 1); + + $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location')); + $redirectReq->assertNotificationContains('Bookshelf Successfully Deleted'); } public function test_shelf_copy_permissions() diff --git a/tests/Entity/BookTest.php b/tests/Entity/BookTest.php new file mode 100644 index 000000000..6c2cf30d4 --- /dev/null +++ b/tests/Entity/BookTest.php @@ -0,0 +1,34 @@ +whereHas('pages')->whereHas('chapters')->first(); + $this->assertNull($book->deleted_at); + $pageCount = $book->pages()->count(); + $chapterCount = $book->chapters()->count(); + + $deleteViewReq = $this->asEditor()->get($book->getUrl('/delete')); + $deleteViewReq->assertSeeText('Are you sure you want to delete this book?'); + + $deleteReq = $this->delete($book->getUrl()); + $deleteReq->assertRedirect(url('/books')); + $this->assertActivityExists('book_delete', $book); + + $book->refresh(); + $this->assertNotNull($book->deleted_at); + + $this->assertTrue($book->pages()->count() === 0); + $this->assertTrue($book->chapters()->count() === 0); + $this->assertTrue($book->pages()->withTrashed()->count() === $pageCount); + $this->assertTrue($book->chapters()->withTrashed()->count() === $chapterCount); + $this->assertTrue($book->deletions()->count() === 1); + + $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location')); + $redirectReq->assertNotificationContains('Book Successfully Deleted'); + } +} \ No newline at end of file diff --git a/tests/Entity/ChapterTest.php b/tests/Entity/ChapterTest.php new file mode 100644 index 000000000..e9350a32b --- /dev/null +++ b/tests/Entity/ChapterTest.php @@ -0,0 +1,31 @@ +whereHas('pages')->first(); + $this->assertNull($chapter->deleted_at); + $pageCount = $chapter->pages()->count(); + + $deleteViewReq = $this->asEditor()->get($chapter->getUrl('/delete')); + $deleteViewReq->assertSeeText('Are you sure you want to delete this chapter?'); + + $deleteReq = $this->delete($chapter->getUrl()); + $deleteReq->assertRedirect($chapter->getParent()->getUrl()); + $this->assertActivityExists('chapter_delete', $chapter); + + $chapter->refresh(); + $this->assertNotNull($chapter->deleted_at); + + $this->assertTrue($chapter->pages()->count() === 0); + $this->assertTrue($chapter->pages()->withTrashed()->count() === $pageCount); + $this->assertTrue($chapter->deletions()->count() === 1); + + $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location')); + $redirectReq->assertNotificationContains('Chapter Successfully Deleted'); + } +} \ No newline at end of file diff --git a/tests/Entity/CommentSettingTest.php b/tests/Entity/CommentSettingTest.php index 3c8cae68c..49ceede9f 100644 --- a/tests/Entity/CommentSettingTest.php +++ b/tests/Entity/CommentSettingTest.php @@ -1,6 +1,6 @@ bookCreation(); $chapter = $this->chapterCreation($book); - $page = $this->pageCreation($chapter); + $this->pageCreation($chapter); // Test Updating - $book = $this->bookUpdate($book); - - // Test Deletion - $this->bookDelete($book); - } - - public function bookDelete(Book $book) - { - $this->asAdmin() - ->visit($book->getUrl()) - // Check link works correctly - ->click('Delete') - ->seePageIs($book->getUrl() . '/delete') - // Ensure the book name is show to user - ->see($book->name) - ->press('Confirm') - ->seePageIs('/books') - ->notSeeInDatabase('books', ['id' => $book->id]); + $this->bookUpdate($book); } public function bookUpdate(Book $book) @@ -332,34 +314,4 @@ class EntityTest extends BrowserKitTest ->seePageIs($chapter->getUrl()); } - public function test_page_delete_removes_entity_from_its_activity() - { - $page = Page::query()->first(); - - $this->asEditor()->put($page->getUrl(), [ - 'name' => 'My updated page', - 'html' => '

updated content

', - ]); - $page->refresh(); - - $this->seeInDatabase('activities', [ - 'entity_id' => $page->id, - 'entity_type' => $page->getMorphClass(), - ]); - - $resp = $this->delete($page->getUrl()); - $resp->assertResponseStatus(302); - - $this->dontSeeInDatabase('activities', [ - 'entity_id' => $page->id, - 'entity_type' => $page->getMorphClass(), - ]); - - $this->seeInDatabase('activities', [ - 'extra' => 'My updated page', - 'entity_id' => 0, - 'entity_type' => '', - ]); - } - } diff --git a/tests/Entity/ExportTest.php b/tests/Entity/ExportTest.php index b1e6eb5fb..1e44f015a 100644 --- a/tests/Entity/ExportTest.php +++ b/tests/Entity/ExportTest.php @@ -1,7 +1,8 @@ assertSee(''; + $page->save(); + + $resp = $this->asEditor()->get($page->getUrl('/export/html')); + Storage::disk('local')->delete('uploads/images/gallery/svg_test.svg'); + Storage::disk('local')->delete('uploads/images/gallery/svg_test2.svg'); + + $resp->assertDontSee('http://localhost/uploads/images/gallery/svg_test'); + } + public function test_page_export_contained_html_image_fetches_only_run_when_url_points_to_image_upload_folder() { $page = Page::first(); $page->html = '' - ."\n".'' - ."\n".''; + .'' + .''; $storageDisk = Storage::disk('local'); $storageDisk->makeDirectory('uploads/images/gallery'); $storageDisk->put('uploads/images/gallery/svg_test.svg', 'good'); @@ -188,4 +205,4 @@ class ExportTest extends TestCase $resp->assertSee('src="/uploads/svg_test.svg"'); } -} \ No newline at end of file +} diff --git a/tests/Entity/MarkdownTest.php b/tests/Entity/MarkdownTest.php index 452b4c07f..5e5fa8a0c 100644 --- a/tests/Entity/MarkdownTest.php +++ b/tests/Entity/MarkdownTest.php @@ -9,7 +9,7 @@ class MarkdownTest extends BrowserKitTest public function setUp(): void { parent::setUp(); - $this->page = \BookStack\Entities\Page::first(); + $this->page = \BookStack\Entities\Models\Page::first(); } protected function setMarkdownEditor() diff --git a/tests/Entity/PageContentTest.php b/tests/Entity/PageContentTest.php index e97df2c7e..51a8568bf 100644 --- a/tests/Entity/PageContentTest.php +++ b/tests/Entity/PageContentTest.php @@ -1,7 +1,7 @@ page = \BookStack\Entities\Page::first(); + $this->page = \BookStack\Entities\Models\Page::first(); $this->pageRepo = app(PageRepo::class); } @@ -56,7 +56,7 @@ class PageDraftTest extends BrowserKitTest public function test_alert_message_shows_if_someone_else_editing() { - $nonEditedPage = \BookStack\Entities\Page::take(10)->get()->last(); + $nonEditedPage = \BookStack\Entities\Models\Page::take(10)->get()->last(); $addedContent = '

test message content

'; $this->asAdmin()->visit($this->page->getUrl('/edit')) ->dontSeeInField('html', $addedContent); @@ -75,7 +75,7 @@ class PageDraftTest extends BrowserKitTest public function test_draft_pages_show_on_homepage() { - $book = \BookStack\Entities\Book::first(); + $book = \BookStack\Entities\Models\Book::first(); $this->asAdmin()->visit('/') ->dontSeeInElement('#recent-drafts', 'New Page') ->visit($book->getUrl() . '/create-page') @@ -85,7 +85,7 @@ class PageDraftTest extends BrowserKitTest public function test_draft_pages_not_visible_by_others() { - $book = \BookStack\Entities\Book::first(); + $book = \BookStack\Entities\Models\Book::first(); $chapter = $book->chapters->first(); $newUser = $this->getEditor(); diff --git a/tests/Entity/PageRevisionTest.php b/tests/Entity/PageRevisionTest.php index 1e9dbd626..68a8f01a9 100644 --- a/tests/Entity/PageRevisionTest.php +++ b/tests/Entity/PageRevisionTest.php @@ -1,6 +1,6 @@ assertSee('def456'); } + public function test_page_revision_restore_sets_new_revision_with_summary() + { + $this->asEditor(); + + $pageRepo = app(PageRepo::class); + $page = Page::first(); + $pageRepo->update($page, ['name' => 'updated page abc123', 'html' => '

new contente def456

', 'summary' => 'My first update']); + $pageRepo->update($page, ['name' => 'updated page again', 'html' => '

new content

', 'summary' => '']); + $page->refresh(); + + $revToRestore = $page->revisions()->where('name', 'like', '%abc123')->first(); + $this->put($page->getUrl() . '/revisions/' . $revToRestore->id . '/restore'); + $page->refresh(); + + $this->assertDatabaseHas('page_revisions', [ + 'page_id' => $page->id, + 'text' => 'new contente def456', + 'type' => 'version', + 'summary' => "Restored from #{$revToRestore->id}; My first update", + ]); + } + public function test_page_revision_count_increments_on_update() { $page = Page::first(); diff --git a/tests/Entity/PageTemplateTest.php b/tests/Entity/PageTemplateTest.php index 8eba13557..a5594e8b8 100644 --- a/tests/Entity/PageTemplateTest.php +++ b/tests/Entity/PageTemplateTest.php @@ -1,6 +1,6 @@ first(); + $this->assertNull($page->deleted_at); + + $deleteViewReq = $this->asEditor()->get($page->getUrl('/delete')); + $deleteViewReq->assertSeeText('Are you sure you want to delete this page?'); + + $deleteReq = $this->delete($page->getUrl()); + $deleteReq->assertRedirect($page->getParent()->getUrl()); + $this->assertActivityExists('page_delete', $page); + + $page->refresh(); + $this->assertNotNull($page->deleted_at); + $this->assertTrue($page->deletions()->count() === 1); + + $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location')); + $redirectReq->assertNotificationContains('Page Successfully Deleted'); + } +} \ No newline at end of file diff --git a/tests/Entity/SearchOptionsTest.php b/tests/Entity/SearchOptionsTest.php index 727db5533..c9e116523 100644 --- a/tests/Entity/SearchOptionsTest.php +++ b/tests/Entity/SearchOptionsTest.php @@ -1,6 +1,6 @@ actingAs($this->getEditor())->put($page->getUrl('/move'), [ 'entity_selection' => 'book:' . $newBook->id ]); - $page = Page::find($page->id); + $page->refresh(); $movePageResp->assertRedirect($page->getUrl()); $this->assertTrue($page->book->id == $newBook->id, 'Page parent is now the new book'); @@ -287,7 +287,7 @@ class SortTest extends TestCase $resp = $this->actingAs($viewer)->get($page->getUrl()); $resp->assertDontSee($page->getUrl('/copy')); - $newBook->created_by = $viewer->id; + $newBook->owned_by = $viewer->id; $newBook->save(); $this->giveUserPermissions($viewer, ['page-create-own']); $this->regenEntityPermissions($newBook); diff --git a/tests/Entity/TagTest.php b/tests/Entity/TagTest.php index e8a99cf78..3ad10641e 100644 --- a/tests/Entity/TagTest.php +++ b/tests/Entity/TagTest.php @@ -1,10 +1,10 @@ getEditor(); setting()->putUser($editor, 'bookshelves_view_type', 'grid'); + $shelf = Bookshelf::query()->firstOrFail(); $this->setSettings(['app-homepage-type' => 'bookshelves']); $this->asEditor(); $homeVisit = $this->get('/'); $homeVisit->assertSee('Shelves'); - $homeVisit->assertSee('bookshelf-grid-item grid-card'); $homeVisit->assertSee('grid-card-content'); - $homeVisit->assertSee('grid-card-footer'); $homeVisit->assertSee('featured-image-container'); + $homeVisit->assertElementContains('.grid-card', $shelf->name); $this->setSettings(['app-homepage-type' => false]); $this->test_default_homepage_visible(); diff --git a/tests/Permissions/EntityOwnerChangeTest.php b/tests/Permissions/EntityOwnerChangeTest.php new file mode 100644 index 000000000..2f06bff2e --- /dev/null +++ b/tests/Permissions/EntityOwnerChangeTest.php @@ -0,0 +1,50 @@ +first(); + $user = User::query()->where('id', '!=', $page->owned_by)->first(); + + $this->asAdmin()->put($page->getUrl('permissions'), ['owned_by' => $user->id]); + $this->assertDatabaseHas('pages', ['owned_by' => $user->id, 'id' => $page->id]); + } + + public function test_changing_chapter_owner() + { + $chapter = Chapter::query()->first(); + $user = User::query()->where('id', '!=', $chapter->owned_by)->first(); + + $this->asAdmin()->put($chapter->getUrl('permissions'), ['owned_by' => $user->id]); + $this->assertDatabaseHas('chapters', ['owned_by' => $user->id, 'id' => $chapter->id]); + } + + public function test_changing_book_owner() + { + $book = Book::query()->first(); + $user = User::query()->where('id', '!=', $book->owned_by)->first(); + + $this->asAdmin()->put($book->getUrl('permissions'), ['owned_by' => $user->id]); + $this->assertDatabaseHas('books', ['owned_by' => $user->id, 'id' => $book->id]); + } + + public function test_changing_shelf_owner() + { + $shelf = Bookshelf::query()->first(); + $user = User::query()->where('id', '!=', $shelf->owned_by)->first(); + + $this->asAdmin()->put($shelf->getUrl('permissions'), ['owned_by' => $user->id]); + $this->assertDatabaseHas('bookshelves', ['owned_by' => $user->id, 'id' => $shelf->id]); + } + +} \ No newline at end of file diff --git a/tests/Permissions/RestrictionsTest.php b/tests/Permissions/EntityPermissionsTest.php similarity index 98% rename from tests/Permissions/RestrictionsTest.php rename to tests/Permissions/EntityPermissionsTest.php index 2dcc0ea69..1e6d1cc32 100644 --- a/tests/Permissions/RestrictionsTest.php +++ b/tests/Permissions/EntityPermissionsTest.php @@ -1,14 +1,15 @@ actingAs($this->user) ->visit($shelf->getUrl('/edit')) @@ -642,11 +643,15 @@ class RestrictionsTest extends BrowserKitTest public function test_page_visible_if_has_permissions_when_book_not_visible() { $book = Book::first(); - - $this->setEntityRestrictions($book, []); - $bookChapter = $book->chapters->first(); $bookPage = $bookChapter->pages->first(); + + foreach ([$book, $bookChapter, $bookPage] as $entity) { + $entity->name = Str::random(24); + $entity->save(); + } + + $this->setEntityRestrictions($book, []); $this->setEntityRestrictions($bookPage, ['view']); $this->actingAs($this->viewer); diff --git a/tests/Permissions/ExportPermissionsTest.php b/tests/Permissions/ExportPermissionsTest.php index 32ee9e7d6..e5a1146a5 100644 --- a/tests/Permissions/ExportPermissionsTest.php +++ b/tests/Permissions/ExportPermissionsTest.php @@ -1,7 +1,7 @@ id; $this->asAdmin()->visit($deletePageUrl) ->press('Confirm') @@ -195,7 +200,7 @@ class RolesTest extends BrowserKitTest public function test_restrictions_manage_all_permission() { - $page = \BookStack\Entities\Page::take(1)->get()->first(); + $page = Page::take(1)->get()->first(); $this->actingAs($this->user)->visit($page->getUrl()) ->dontSee('Permissions') ->visit($page->getUrl() . '/permissions') @@ -209,7 +214,7 @@ class RolesTest extends BrowserKitTest public function test_restrictions_manage_own_permission() { - $otherUsersPage = \BookStack\Entities\Page::first(); + $otherUsersPage = Page::first(); $content = $this->createEntityChainBelongingToUser($this->user); // Check can't restrict other's content $this->actingAs($this->user)->visit($otherUsersPage->getUrl()) @@ -284,7 +289,7 @@ class RolesTest extends BrowserKitTest { $otherShelf = Bookshelf::first(); $ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']); - $ownShelf->forceFill(['created_by' => $this->user->id, 'updated_by' => $this->user->id])->save(); + $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save(); $this->regenEntityPermissions($ownShelf); $this->checkAccessPermission('bookshelf-update-own', [ @@ -301,7 +306,7 @@ class RolesTest extends BrowserKitTest public function test_bookshelves_edit_all_permission() { - $otherShelf = \BookStack\Entities\Bookshelf::first(); + $otherShelf = Bookshelf::first(); $this->checkAccessPermission('bookshelf-update-all', [ $otherShelf->getUrl('/edit') ], [ @@ -312,9 +317,9 @@ class RolesTest extends BrowserKitTest public function test_bookshelves_delete_own_permission() { $this->giveUserPermissions($this->user, ['bookshelf-update-all']); - $otherShelf = \BookStack\Entities\Bookshelf::first(); + $otherShelf = Bookshelf::first(); $ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']); - $ownShelf->forceFill(['created_by' => $this->user->id, 'updated_by' => $this->user->id])->save(); + $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save(); $this->regenEntityPermissions($ownShelf); $this->checkAccessPermission('bookshelf-delete-own', [ @@ -336,7 +341,7 @@ class RolesTest extends BrowserKitTest public function test_bookshelves_delete_all_permission() { $this->giveUserPermissions($this->user, ['bookshelf-update-all']); - $otherShelf = \BookStack\Entities\Bookshelf::first(); + $otherShelf = Bookshelf::first(); $this->checkAccessPermission('bookshelf-delete-all', [ $otherShelf->getUrl('/delete') ], [ @@ -366,7 +371,7 @@ class RolesTest extends BrowserKitTest public function test_books_edit_own_permission() { - $otherBook = \BookStack\Entities\Book::take(1)->get()->first(); + $otherBook = Book::take(1)->get()->first(); $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; $this->checkAccessPermission('book-update-own', [ $ownBook->getUrl() . '/edit' @@ -382,7 +387,7 @@ class RolesTest extends BrowserKitTest public function test_books_edit_all_permission() { - $otherBook = \BookStack\Entities\Book::take(1)->get()->first(); + $otherBook = Book::take(1)->get()->first(); $this->checkAccessPermission('book-update-all', [ $otherBook->getUrl() . '/edit' ], [ @@ -393,7 +398,7 @@ class RolesTest extends BrowserKitTest public function test_books_delete_own_permission() { $this->giveUserPermissions($this->user, ['book-update-all']); - $otherBook = \BookStack\Entities\Book::take(1)->get()->first(); + $otherBook = Book::take(1)->get()->first(); $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; $this->checkAccessPermission('book-delete-own', [ $ownBook->getUrl() . '/delete' @@ -414,7 +419,7 @@ class RolesTest extends BrowserKitTest public function test_books_delete_all_permission() { $this->giveUserPermissions($this->user, ['book-update-all']); - $otherBook = \BookStack\Entities\Book::take(1)->get()->first(); + $otherBook = Book::take(1)->get()->first(); $this->checkAccessPermission('book-delete-all', [ $otherBook->getUrl() . '/delete' ], [ @@ -429,7 +434,7 @@ class RolesTest extends BrowserKitTest public function test_chapter_create_own_permissions() { - $book = \BookStack\Entities\Book::take(1)->get()->first(); + $book = Book::take(1)->get()->first(); $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; $this->checkAccessPermission('chapter-create-own', [ $ownBook->getUrl('/create-chapter') @@ -451,7 +456,7 @@ class RolesTest extends BrowserKitTest public function test_chapter_create_all_permissions() { - $book = \BookStack\Entities\Book::take(1)->get()->first(); + $book = Book::take(1)->get()->first(); $this->checkAccessPermission('chapter-create-all', [ $book->getUrl('/create-chapter') ], [ @@ -467,7 +472,7 @@ class RolesTest extends BrowserKitTest public function test_chapter_edit_own_permission() { - $otherChapter = \BookStack\Entities\Chapter::take(1)->get()->first(); + $otherChapter = Chapter::take(1)->get()->first(); $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter']; $this->checkAccessPermission('chapter-update-own', [ $ownChapter->getUrl() . '/edit' @@ -483,7 +488,7 @@ class RolesTest extends BrowserKitTest public function test_chapter_edit_all_permission() { - $otherChapter = \BookStack\Entities\Chapter::take(1)->get()->first(); + $otherChapter = Chapter::take(1)->get()->first(); $this->checkAccessPermission('chapter-update-all', [ $otherChapter->getUrl() . '/edit' ], [ @@ -494,7 +499,7 @@ class RolesTest extends BrowserKitTest public function test_chapter_delete_own_permission() { $this->giveUserPermissions($this->user, ['chapter-update-all']); - $otherChapter = \BookStack\Entities\Chapter::take(1)->get()->first(); + $otherChapter = Chapter::take(1)->get()->first(); $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter']; $this->checkAccessPermission('chapter-delete-own', [ $ownChapter->getUrl() . '/delete' @@ -516,7 +521,7 @@ class RolesTest extends BrowserKitTest public function test_chapter_delete_all_permission() { $this->giveUserPermissions($this->user, ['chapter-update-all']); - $otherChapter = \BookStack\Entities\Chapter::take(1)->get()->first(); + $otherChapter = Chapter::take(1)->get()->first(); $this->checkAccessPermission('chapter-delete-all', [ $otherChapter->getUrl() . '/delete' ], [ @@ -532,8 +537,8 @@ class RolesTest extends BrowserKitTest public function test_page_create_own_permissions() { - $book = \BookStack\Entities\Book::first(); - $chapter = \BookStack\Entities\Chapter::first(); + $book = Book::first(); + $chapter = Chapter::first(); $entities = $this->createEntityChainBelongingToUser($this->user); $ownBook = $entities['book']; @@ -557,7 +562,7 @@ class RolesTest extends BrowserKitTest foreach ($accessUrls as $index => $url) { $this->actingAs($this->user)->visit($url); - $expectedUrl = \BookStack\Entities\Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl(); + $expectedUrl = Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl(); $this->seePageIs($expectedUrl); } @@ -579,8 +584,8 @@ class RolesTest extends BrowserKitTest public function test_page_create_all_permissions() { - $book = \BookStack\Entities\Book::take(1)->get()->first(); - $chapter = \BookStack\Entities\Chapter::take(1)->get()->first(); + $book = Book::take(1)->get()->first(); + $chapter = Chapter::take(1)->get()->first(); $baseUrl = $book->getUrl() . '/page'; $createUrl = $book->getUrl('/create-page'); @@ -601,7 +606,7 @@ class RolesTest extends BrowserKitTest foreach ($accessUrls as $index => $url) { $this->actingAs($this->user)->visit($url); - $expectedUrl = \BookStack\Entities\Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl(); + $expectedUrl = Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl(); $this->seePageIs($expectedUrl); } @@ -620,7 +625,7 @@ class RolesTest extends BrowserKitTest public function test_page_edit_own_permission() { - $otherPage = \BookStack\Entities\Page::take(1)->get()->first(); + $otherPage = Page::take(1)->get()->first(); $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $this->checkAccessPermission('page-update-own', [ $ownPage->getUrl() . '/edit' @@ -636,7 +641,7 @@ class RolesTest extends BrowserKitTest public function test_page_edit_all_permission() { - $otherPage = \BookStack\Entities\Page::take(1)->get()->first(); + $otherPage = Page::take(1)->get()->first(); $this->checkAccessPermission('page-update-all', [ $otherPage->getUrl() . '/edit' ], [ @@ -647,7 +652,7 @@ class RolesTest extends BrowserKitTest public function test_page_delete_own_permission() { $this->giveUserPermissions($this->user, ['page-update-all']); - $otherPage = \BookStack\Entities\Page::take(1)->get()->first(); + $otherPage = Page::take(1)->get()->first(); $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $this->checkAccessPermission('page-delete-own', [ $ownPage->getUrl() . '/delete' @@ -669,7 +674,7 @@ class RolesTest extends BrowserKitTest public function test_page_delete_all_permission() { $this->giveUserPermissions($this->user, ['page-update-all']); - $otherPage = \BookStack\Entities\Page::take(1)->get()->first(); + $otherPage = Page::take(1)->get()->first(); $this->checkAccessPermission('page-delete-all', [ $otherPage->getUrl() . '/delete' ], [ @@ -685,7 +690,7 @@ class RolesTest extends BrowserKitTest public function test_public_role_visible_in_user_edit_screen() { - $user = \BookStack\Auth\User::first(); + $user = User::first(); $adminRole = Role::getSystemRole('admin'); $publicRole = Role::getSystemRole('public'); $this->asAdmin()->visit('/settings/users/' . $user->id) @@ -721,8 +726,8 @@ class RolesTest extends BrowserKitTest public function test_image_delete_own_permission() { $this->giveUserPermissions($this->user, ['image-update-all']); - $page = \BookStack\Entities\Page::first(); - $image = factory(\BookStack\Uploads\Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $this->user->id, 'updated_by' => $this->user->id]); + $page = Page::first(); + $image = factory(Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $this->user->id, 'updated_by' => $this->user->id]); $this->actingAs($this->user)->json('delete', '/images/' . $image->id) ->seeStatusCode(403); @@ -738,8 +743,8 @@ class RolesTest extends BrowserKitTest { $this->giveUserPermissions($this->user, ['image-update-all']); $admin = $this->getAdmin(); - $page = \BookStack\Entities\Page::first(); - $image = factory(\BookStack\Uploads\Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $admin->id, 'updated_by' => $admin->id]); + $page = Page::first(); + $image = factory(Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $admin->id, 'updated_by' => $admin->id]); $this->actingAs($this->user)->json('delete', '/images/' . $image->id) ->seeStatusCode(403); @@ -760,7 +765,7 @@ class RolesTest extends BrowserKitTest { // To cover issue fixed in f99c8ff99aee9beb8c692f36d4b84dc6e651e50a. $page = Page::first(); - $viewerRole = \BookStack\Auth\Role::getRole('viewer'); + $viewerRole = Role::getRole('viewer'); $viewer = $this->getViewer(); $this->actingAs($viewer)->visit($page->getUrl())->assertResponseStatus(200); @@ -778,14 +783,14 @@ class RolesTest extends BrowserKitTest { $admin = $this->getAdmin(); // Book links - $book = factory(\BookStack\Entities\Book::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id]); + $book = factory(Book::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id]); $this->updateEntityPermissions($book); $this->actingAs($this->getViewer())->visit($book->getUrl()) ->dontSee('Create a new page') ->dontSee('Add a chapter'); // Chapter links - $chapter = factory(\BookStack\Entities\Chapter::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id, 'book_id' => $book->id]); + $chapter = factory(Chapter::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id, 'book_id' => $book->id]); $this->updateEntityPermissions($chapter); $this->actingAs($this->getViewer())->visit($chapter->getUrl()) ->dontSee('Create a new page') @@ -869,7 +874,7 @@ class RolesTest extends BrowserKitTest } private function addComment($page) { - $comment = factory(\BookStack\Actions\Comment::class)->make(); + $comment = factory(Comment::class)->make(); $url = "/comment/$page->id"; $request = [ 'text' => $comment->text, @@ -882,7 +887,7 @@ class RolesTest extends BrowserKitTest } private function updateComment($commentId) { - $comment = factory(\BookStack\Actions\Comment::class)->make(); + $comment = factory(Comment::class)->make(); $url = "/comment/$commentId"; $request = [ 'text' => $comment->text, diff --git a/tests/PublicActionTest.php b/tests/PublicActionTest.php index 3670df87d..194190124 100644 --- a/tests/PublicActionTest.php +++ b/tests/PublicActionTest.php @@ -5,9 +5,9 @@ use BookStack\Auth\Permissions\PermissionService; use BookStack\Auth\Permissions\RolePermission; use BookStack\Auth\Role; use BookStack\Auth\User; -use BookStack\Entities\Book; -use BookStack\Entities\Chapter; -use BookStack\Entities\Page; +use BookStack\Entities\Models\Book; +use BookStack\Entities\Models\Chapter; +use BookStack\Entities\Models\Page; class PublicActionTest extends BrowserKitTest { diff --git a/tests/RecycleBinTest.php b/tests/RecycleBinTest.php new file mode 100644 index 000000000..60f06cfc4 --- /dev/null +++ b/tests/RecycleBinTest.php @@ -0,0 +1,232 @@ +first(); + $editor = $this->getEditor(); + $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}", + ]; + + foreach($routes as $route) { + [$method, $url] = explode(':', $route); + $resp = $this->call($method, $url); + $this->assertPermissionError($resp); + } + + $this->giveUserPermissions($editor, ['restrictions-manage-all']); + + foreach($routes as $route) { + [$method, $url] = explode(':', $route); + $resp = $this->call($method, $url); + $this->assertPermissionError($resp); + } + + $this->giveUserPermissions($editor, ['settings-manage']); + + foreach($routes as $route) { + DB::beginTransaction(); + [$method, $url] = explode(':', $route); + $resp = $this->call($method, $url); + $this->assertNotPermissionError($resp); + DB::rollBack(); + } + + } + + public function test_recycle_bin_view() + { + $page = Page::query()->first(); + $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first(); + $editor = $this->getEditor(); + $this->actingAs($editor)->delete($page->getUrl()); + $this->actingAs($editor)->delete($book->getUrl()); + + $viewReq = $this->asAdmin()->get('/settings/recycle-bin'); + $viewReq->assertElementContains('table.table', $page->name); + $viewReq->assertElementContains('table.table', $editor->name); + $viewReq->assertElementContains('table.table', $book->name); + $viewReq->assertElementContains('table.table', $book->pages_count . ' Pages'); + $viewReq->assertElementContains('table.table', $book->chapters_count . ' Chapters'); + } + + public function test_recycle_bin_empty() + { + $page = Page::query()->first(); + $book = Book::query()->where('id' , '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail(); + $editor = $this->getEditor(); + $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'); + $redirectReq->assertNotificationContains('Deleted '.$itemCount.' total items from the recycle bin'); + } + + public function test_entity_restore() + { + $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail(); + $this->asEditor()->delete($book->getUrl()); + $deletion = Deletion::query()->firstOrFail(); + + $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()); + + $restoreReq = $this->asAdmin()->post("/settings/recycle-bin/{$deletion->id}/restore"); + $restoreReq->assertRedirect('/settings/recycle-bin'); + $this->assertTrue(Deletion::query()->count() === 0); + + $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()); + + $itemCount = 1 + $book->pages->count() + $book->chapters->count(); + $redirectReq = $this->get('/settings/recycle-bin'); + $redirectReq->assertNotificationContains('Restored '.$itemCount.' total items from the recycle bin'); + } + + public function test_permanent_delete() + { + $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail(); + $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'); + $redirectReq->assertNotificationContains('Deleted '.$itemCount.' total items from the recycle bin'); + } + + public function test_permanent_entity_delete_updates_existing_activity_with_entity_name() + { + $page = Page::query()->firstOrFail(); + $this->asEditor()->delete($page->getUrl()); + $deletion = $page->deletions()->firstOrFail(); + + $this->assertDatabaseHas('activities', [ + 'type' => 'page_delete', + 'entity_id' => $page->id, + 'entity_type' => $page->getMorphClass(), + ]); + + $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}"); + + $this->assertDatabaseMissing('activities', [ + 'type' => 'page_delete', + 'entity_id' => $page->id, + 'entity_type' => $page->getMorphClass(), + ]); + + $this->assertDatabaseHas('activities', [ + 'type' => 'page_delete', + 'entity_id' => null, + 'entity_type' => null, + 'detail' => $page->name, + ]); + } + + public function test_auto_clear_functionality_works() + { + config()->set('app.recycle_bin_lifetime', 5); + $page = Page::query()->firstOrFail(); + $otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail(); + + $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); + $page = Page::query()->firstOrFail(); + $otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail(); + + $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); + $page = Page::query()->firstOrFail(); + + $this->asEditor()->delete($page->getUrl()); + $this->assertDatabaseMissing('pages', ['id' => $page->id]); + $this->assertEquals(0, Deletion::query()->count()); + } + + 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"); + $chapterRestore->assertRedirect("/settings/recycle-bin"); + $this->assertDatabaseMissing("deletions", ["id" => $chapterDeletion->id]); + + $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); + } +} \ No newline at end of file diff --git a/tests/SecurityHeaderTest.php b/tests/SecurityHeaderTest.php new file mode 100644 index 000000000..db095ff70 --- /dev/null +++ b/tests/SecurityHeaderTest.php @@ -0,0 +1,71 @@ +get("/"); + foreach ($resp->headers->getCookies() as $cookie) { + $this->assertEquals("lax", $cookie->getSameSite()); + } + } + + public function test_cookies_samesite_none_when_iframe_hosts_set() + { + $this->runWithEnv("ALLOWED_IFRAME_HOSTS", "http://example.com", function() { + $resp = $this->get("/"); + foreach ($resp->headers->getCookies() as $cookie) { + $this->assertEquals("none", $cookie->getSameSite()); + } + }); + } + + public function test_secure_cookies_controlled_by_app_url() + { + $this->runWithEnv("APP_URL", "http://example.com", function() { + $resp = $this->get("/"); + foreach ($resp->headers->getCookies() as $cookie) { + $this->assertFalse($cookie->isSecure()); + } + }); + + $this->runWithEnv("APP_URL", "https://example.com", function() { + $resp = $this->get("/"); + foreach ($resp->headers->getCookies() as $cookie) { + $this->assertTrue($cookie->isSecure()); + } + }); + } + + public function test_iframe_csp_self_only_by_default() + { + $resp = $this->get("/"); + $cspHeaders = collect($resp->headers->get('Content-Security-Policy')); + $frameHeaders = $cspHeaders->filter(function ($val) { + return Str::startsWith($val, 'frame-ancestors'); + }); + + $this->assertTrue($frameHeaders->count() === 1); + $this->assertEquals('frame-ancestors \'self\'', $frameHeaders->first()); + } + + public function test_iframe_csp_includes_extra_hosts_if_configured() + { + $this->runWithEnv("ALLOWED_IFRAME_HOSTS", "https://a.example.com https://b.example.com", function() { + $resp = $this->get("/"); + $cspHeaders = collect($resp->headers->get('Content-Security-Policy')); + $frameHeaders = $cspHeaders->filter(function($val) { + return Str::startsWith($val, 'frame-ancestors'); + }); + + $this->assertTrue($frameHeaders->count() === 1); + $this->assertEquals('frame-ancestors \'self\' https://a.example.com https://b.example.com', $frameHeaders->first()); + }); + + } + +} \ No newline at end of file diff --git a/tests/SharedTestHelpers.php b/tests/SharedTestHelpers.php index c7659a02d..02f7caae1 100644 --- a/tests/SharedTestHelpers.php +++ b/tests/SharedTestHelpers.php @@ -1,11 +1,11 @@ createNewRole($permissions); $user->attachRole($newRole); $user->load('roles'); - $user->permissions(false); + $user->clearPermissionCache(); } /** @@ -270,14 +270,25 @@ trait SharedTestHelpers */ protected function assertPermissionError($response) { - if ($response instanceof BrowserKitTest) { - $response = \Illuminate\Foundation\Testing\TestResponse::fromBaseResponse($response->response); - } + PHPUnit::assertTrue($this->isPermissionError($response->baseResponse ?? $response->response), "Failed asserting the response contains a permission error."); + } - $response->assertRedirect('/'); - $this->assertSessionHas('error'); - $error = session()->pull('error'); - $this->assertStringStartsWith('You do not have permission to access', $error); + /** + * Assert a permission error has occurred. + */ + protected function assertNotPermissionError($response) + { + PHPUnit::assertFalse($this->isPermissionError($response->baseResponse ?? $response->response), "Failed asserting the response does not contain a permission error."); + } + + /** + * Check if the given response is a permission error. + */ + private function isPermissionError($response): bool + { + return $response->status() === 302 + && $response->headers->get('Location') === url('/') + && strpos(session()->pull('error', ''), 'You do not have permission to access') === 0; } /** diff --git a/tests/TestCase.php b/tests/TestCase.php index 1f1d5ece7..2c901981a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,6 +1,6 @@ $key]; + $detailsToCheck = ['type' => $type]; if ($entity) { $detailsToCheck['entity_type'] = $entity->getMorphClass(); diff --git a/tests/TestResponse.php b/tests/TestResponse.php index a68a5783f..9c6b78782 100644 --- a/tests/TestResponse.php +++ b/tests/TestResponse.php @@ -15,9 +15,8 @@ class TestResponse extends BaseTestResponse { /** * Get the DOM Crawler for the response content. - * @return Crawler */ - protected function crawler() + protected function crawler(): Crawler { if (!is_object($this->crawlerInstance)) { $this->crawlerInstance = new Crawler($this->getContent()); @@ -27,7 +26,6 @@ class TestResponse extends BaseTestResponse { /** * Assert the response contains the specified element. - * @param string $selector * @return $this */ public function assertElementExists(string $selector) @@ -45,7 +43,6 @@ class TestResponse extends BaseTestResponse { /** * Assert the response does not contain the specified element. - * @param string $selector * @return $this */ public function assertElementNotExists(string $selector) @@ -63,8 +60,6 @@ class TestResponse extends BaseTestResponse { /** * Assert the response includes a specific element containing the given text. - * @param string $selector - * @param string $text * @return $this */ public function assertElementContains(string $selector, string $text) @@ -95,8 +90,6 @@ class TestResponse extends BaseTestResponse { /** * Assert the response does not include a specific element containing the given text. - * @param string $selector - * @param string $text * @return $this */ public function assertElementNotContains(string $selector, string $text) @@ -125,12 +118,20 @@ class TestResponse extends BaseTestResponse { return $this; } + /** + * Assert there's a notification within the view containing the given text. + * @return $this + */ + public function assertNotificationContains(string $text) + { + return $this->assertElementContains('[notification]', $text); + } + /** * Get the escaped text pattern for the constraint. - * @param string $text * @return string */ - protected function getEscapedPattern($text) + protected function getEscapedPattern(string $text) { $rawPattern = preg_quote($text, '/'); $escapedPattern = preg_quote(e($text), '/'); diff --git a/tests/Uploads/AttachmentTest.php b/tests/Uploads/AttachmentTest.php index 5838b019e..1ca9ea23b 100644 --- a/tests/Uploads/AttachmentTest.php +++ b/tests/Uploads/AttachmentTest.php @@ -1,7 +1,9 @@ $fileName ]); - $this->call('DELETE', $page->getUrl()); + app(PageRepo::class)->destroy($page); + app(TrashCan::class)->empty(); $this->assertDatabaseMissing('attachments', [ 'name' => $fileName diff --git a/tests/Uploads/DrawioTest.php b/tests/Uploads/DrawioTest.php index 3fc009c8a..d134135aa 100644 --- a/tests/Uploads/DrawioTest.php +++ b/tests/Uploads/DrawioTest.php @@ -1,6 +1,6 @@ assertTrue(strlen($secret) === 32); $this->assertSessionHas('success'); + $this->assertActivityExists(ActivityType::API_TOKEN_CREATE); } public function test_create_with_no_expiry_sets_expiry_hundred_years_away() @@ -124,6 +126,7 @@ class UserApiTokenTest extends TestCase $this->assertDatabaseHas('api_tokens', array_merge($updateData, ['id' => $token->id])); $this->assertSessionHas('success'); + $this->assertActivityExists(ActivityType::API_TOKEN_UPDATE); } public function test_token_update_with_blank_expiry_sets_to_hundred_years_away() @@ -162,6 +165,7 @@ class UserApiTokenTest extends TestCase $resp = $this->delete($tokenUrl); $resp->assertRedirect($editor->getEditUrl('#api_tokens')); $this->assertDatabaseMissing('api_tokens', ['id' => $token->id]); + $this->assertActivityExists(ActivityType::API_TOKEN_DELETE); } public function test_user_manage_can_delete_token_without_api_permission_themselves() diff --git a/tests/User/UserManagementTest.php b/tests/User/UserManagementTest.php new file mode 100644 index 000000000..d99d61401 --- /dev/null +++ b/tests/User/UserManagementTest.php @@ -0,0 +1,44 @@ +getEditor(); + $resp = $this->asAdmin()->delete("settings/users/{$editor->id}"); + $resp->assertRedirect("/settings/users"); + $resp = $this->followRedirects($resp); + + $resp->assertSee("User successfully removed"); + $this->assertActivityExists(ActivityType::USER_DELETE); + + $this->assertDatabaseMissing('users', ['id' => $editor->id]); + } + + public function test_delete_offers_migrate_option() + { + $editor = $this->getEditor(); + $resp = $this->asAdmin()->get("settings/users/{$editor->id}/delete"); + $resp->assertSee("Migrate Ownership"); + $resp->assertSee("new_owner_id"); + } + + public function test_delete_with_new_owner_id_changes_ownership() + { + $page = Page::query()->first(); + $owner = $page->ownedBy; + $newOwner = User::query()->where('id', '!=' , $owner->id)->first(); + + $this->asAdmin()->delete("settings/users/{$owner->id}", ['new_owner_id' => $newOwner->id]); + $this->assertDatabaseHas('pages', [ + 'id' => $page->id, + 'owned_by' => $newOwner->id, + ]); + } +} \ No newline at end of file diff --git a/tests/User/UserPreferencesTest.php b/tests/User/UserPreferencesTest.php index 0db4f803a..7ffc8f9db 100644 --- a/tests/User/UserPreferencesTest.php +++ b/tests/User/UserPreferencesTest.php @@ -1,4 +1,4 @@ -getNewBlankUser(); $this->actingAs($newUser); $entities = $this->createEntityChainBelongingToUser($newUser, $newUser); - Activity::add($entities['book'], 'book_update', $entities['book']->id); - Activity::add($entities['page'], 'page_create', $entities['book']->id); + Activity::addForEntity($entities['book'], ActivityType::BOOK_UPDATE); + Activity::addForEntity($entities['page'], ActivityType::PAGE_CREATE); $this->asAdmin()->visit('/user/' . $newUser->id) ->seeInElement('#recent-user-activity', 'updated book') @@ -74,8 +75,8 @@ class UserProfileTest extends BrowserKitTest $newUser = $this->getNewBlankUser(); $this->actingAs($newUser); $entities = $this->createEntityChainBelongingToUser($newUser, $newUser); - Activity::add($entities['book'], 'book_update', $entities['book']->id); - Activity::add($entities['page'], 'page_create', $entities['book']->id); + Activity::addForEntity($entities['book'], ActivityType::BOOK_UPDATE); + Activity::addForEntity($entities['page'], ActivityType::PAGE_CREATE); $this->asAdmin()->visit('/')->clickInElement('#recent-activity', $newUser->name) ->seePageIs('/user/' . $newUser->id)