diff --git a/.env.example b/.env.example index a0a1b72e683..4dee3b33441 100644 --- a/.env.example +++ b/.env.example @@ -37,8 +37,10 @@ MAIL_FROM=bookstack@example.com # SMTP mail options # These settings can be checked using the "Send a Test Email" # feature found in the "Settings > Maintenance" area of the system. +# For more detailed documentation on mail options, refer to: +# https://www.bookstackapp.com/docs/admin/email-webhooks/#email-configuration MAIL_HOST=localhost -MAIL_PORT=1025 +MAIL_PORT=587 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null diff --git a/.env.example.complete b/.env.example.complete index f81bccae470..96a3b448ff4 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -3,6 +3,10 @@ # Each option is shown with it's default value. # Do not copy this whole file to use as your '.env' file. +# The details here only serve as a quick reference. +# Please refer to the BookStack documentation for full details: +# https://www.bookstackapp.com/docs/ + # Application environment # Can be 'production', 'development', 'testing' or 'demo' APP_ENV=production @@ -65,22 +69,19 @@ DB_PASSWORD=database_user_password # certificate itself (Common Name or Subject Alternative Name). MYSQL_ATTR_SSL_CA="/path/to/ca.pem" -# Mail system to use -# Can be 'smtp' or 'sendmail' +# Mail configuration +# Refer to https://www.bookstackapp.com/docs/admin/email-webhooks/#email-configuration MAIL_DRIVER=smtp - -# Mail sending options MAIL_FROM=mail@bookstackapp.com MAIL_FROM_NAME=BookStack -# SMTP mail options MAIL_HOST=localhost -MAIL_PORT=1025 +MAIL_PORT=587 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null +MAIL_VERIFY_SSL=true -# Command to use when email is sent via sendmail MAIL_SENDMAIL_COMMAND="/usr/sbin/sendmail -bs" # Cache & Session driver to use @@ -322,6 +323,13 @@ FILE_UPLOAD_SIZE_LIMIT=50 # Can be 'a4' or 'letter'. EXPORT_PAGE_SIZE=a4 +# Set path to wkhtmltopdf binary for PDF generation. +# Can be 'false' or a path path like: '/home/bins/wkhtmltopdf' +# When false, BookStack will attempt to find a wkhtmltopdf in the application +# root folder then fall back to the default dompdf renderer if no binary exists. +# Only used if 'ALLOW_UNTRUSTED_SERVER_FETCHING=true' which disables security protections. +WKHTMLTOPDF=false + # Allow
More info is at the path \\\\cat\\dog\\badger
+ ']); + $page->tags()->save(new Tag(['name' => '\\Category', 'value' => '\\animals\\fluffy'])); + + $search = $this->asEditor()->get('/search?term=' . urlencode('\\\\cat\\dog')); + $search->assertSee($page->getUrl(), false); + + $search = $this->asEditor()->get('/search?term=' . urlencode('"\\dog\\"')); + $search->assertSee($page->getUrl(), false); + + $search = $this->asEditor()->get('/search?term=' . urlencode('"\\badger\\"')); + $search->assertDontSee($page->getUrl(), false); + + $search = $this->asEditor()->get('/search?term=' . urlencode('[\\Categorylike%\\fluffy]')); + $search->assertSee($page->getUrl(), false); + } + public function test_searches_with_user_filters_adds_them_into_advanced_search_form() { $resp = $this->asEditor()->get('/search?term=' . urlencode('test {updated_by:dan} {created_by:dan}')); diff --git a/tests/Entity/ExportTest.php b/tests/Entity/ExportTest.php index 6ae8dcde313..2b5244bf010 100644 --- a/tests/Entity/ExportTest.php +++ b/tests/Entity/ExportTest.php @@ -2,7 +2,6 @@ namespace Tests\Entity; -use BookStack\Auth\Role; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; diff --git a/tests/Entity/PageContentTest.php b/tests/Entity/PageContentTest.php index 6d6224abf4b..d8845fe1276 100644 --- a/tests/Entity/PageContentTest.php +++ b/tests/Entity/PageContentTest.php @@ -108,6 +108,18 @@ public function test_page_includes_rendered_on_book_export() $htmlContent->assertSee('my cat is awesome and scratchy'); } + public function test_page_includes_can_be_nested_up_to_three_times() + { + $page = $this->entities->page(); + $tag = "{{@{$page->id}#bkmrk-test}}"; + $page->html = 'Hello Barry ' . $tag . '
'; + $page->save(); + + $pageResp = $this->asEditor()->get($page->getUrl()); + $this->withHtml($pageResp)->assertElementContains('#bkmrk-test', 'Hello Barry Hello Barry Hello Barry Hello Barry ' . $tag); + $this->withHtml($pageResp)->assertElementNotContains('#bkmrk-test', 'Hello Barry Hello Barry Hello Barry Hello Barry Hello Barry ' . $tag); + } + public function test_page_content_scripts_removed_by_default() { $this->asEditor(); diff --git a/tests/Entity/PageDraftTest.php b/tests/Entity/PageDraftTest.php index 75b1933ea0e..e99ba9b8189 100644 --- a/tests/Entity/PageDraftTest.php +++ b/tests/Entity/PageDraftTest.php @@ -166,6 +166,30 @@ public function test_page_html_in_ajax_fetch_response() ]); } + public function test_user_draft_removed_on_user_drafts_delete_call() + { + $editor = $this->users->editor(); + $page = $this->entities->page(); + + $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [ + 'name' => $page->name, + 'html' => 'updated draft again
', + ]); + + $revisionData = [ + 'type' => 'update_draft', + 'created_by' => $editor->id, + 'page_id' => $page->id, + ]; + + $this->assertDatabaseHas('page_revisions', $revisionData); + + $resp = $this->delete("/page-revisions/user-drafts/{$page->id}"); + + $resp->assertOk(); + $this->assertDatabaseMissing('page_revisions', $revisionData); + } + public function test_updating_page_draft_with_markdown_retains_markdown_content() { $book = $this->entities->book(); diff --git a/tests/Entity/PageRevisionTest.php b/tests/Entity/PageRevisionTest.php index 0df37728e19..97d5a6664da 100644 --- a/tests/Entity/PageRevisionTest.php +++ b/tests/Entity/PageRevisionTest.php @@ -2,7 +2,7 @@ namespace Tests\Entity; -use BookStack\Actions\ActivityType; +use BookStack\Activity\ActivityType; use BookStack\Entities\Models\Page; use Tests\TestCase; diff --git a/tests/Entity/PageTest.php b/tests/Entity/PageTest.php index 370c4381c63..daad82e76dc 100644 --- a/tests/Entity/PageTest.php +++ b/tests/Entity/PageTest.php @@ -50,6 +50,13 @@ public function test_page_view_when_creator_is_deleted_but_owner_exists() $resp->assertSeeText('Owned by ' . $owner->name); } + public function test_page_show_includes_pointer_section_select_mode_button() + { + $page = $this->entities->page(); + $resp = $this->asEditor()->get($page->getUrl()); + $this->withHtml($resp)->assertElementContains('.content-wrap button.screen-reader-only', 'Enter section select mode'); + } + public function test_page_creation_with_markdown_content() { $this->setSettings(['app-editor' => 'markdown']); diff --git a/tests/Entity/TagTest.php b/tests/Entity/TagTest.php index 7e667495908..c1240e95538 100644 --- a/tests/Entity/TagTest.php +++ b/tests/Entity/TagTest.php @@ -2,7 +2,7 @@ namespace Tests\Entity; -use BookStack\Actions\Tag; +use BookStack\Activity\Models\Tag; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; use Tests\TestCase; diff --git a/tests/FavouriteTest.php b/tests/FavouriteTest.php index 7778aa8e935..0e30cbd58ca 100644 --- a/tests/FavouriteTest.php +++ b/tests/FavouriteTest.php @@ -2,8 +2,8 @@ namespace Tests; -use BookStack\Actions\Favourite; -use BookStack\Auth\User; +use BookStack\Activity\Models\Favourite; +use BookStack\Users\Models\User; class FavouriteTest extends TestCase { diff --git a/tests/Helpers/EntityProvider.php b/tests/Helpers/EntityProvider.php index 8b045db54a2..ddc854290b1 100644 --- a/tests/Helpers/EntityProvider.php +++ b/tests/Helpers/EntityProvider.php @@ -2,7 +2,6 @@ namespace Tests\Helpers; -use BookStack\Auth\User; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; @@ -12,6 +11,7 @@ use BookStack\Entities\Repos\BookshelfRepo; use BookStack\Entities\Repos\ChapterRepo; use BookStack\Entities\Repos\PageRepo; +use BookStack\Users\Models\User; use Illuminate\Database\Eloquent\Builder; /** diff --git a/tests/Helpers/PermissionsProvider.php b/tests/Helpers/PermissionsProvider.php index b93c45e25e0..512f43fb6cc 100644 --- a/tests/Helpers/PermissionsProvider.php +++ b/tests/Helpers/PermissionsProvider.php @@ -2,11 +2,11 @@ namespace Tests\Helpers; -use BookStack\Auth\Permissions\EntityPermission; -use BookStack\Auth\Permissions\RolePermission; -use BookStack\Auth\Role; -use BookStack\Auth\User; use BookStack\Entities\Models\Entity; +use BookStack\Permissions\Models\EntityPermission; +use BookStack\Permissions\Models\RolePermission; +use BookStack\Users\Models\Role; +use BookStack\Users\Models\User; class PermissionsProvider { diff --git a/tests/Helpers/UserRoleProvider.php b/tests/Helpers/UserRoleProvider.php index 8c2718bc3e2..b86e9039407 100644 --- a/tests/Helpers/UserRoleProvider.php +++ b/tests/Helpers/UserRoleProvider.php @@ -2,9 +2,9 @@ namespace Tests\Helpers; -use BookStack\Auth\Permissions\PermissionsRepo; -use BookStack\Auth\Role; -use BookStack\Auth\User; +use BookStack\Permissions\PermissionsRepo; +use BookStack\Users\Models\Role; +use BookStack\Users\Models\User; class UserRoleProvider { diff --git a/tests/HomepageTest.php b/tests/HomepageTest.php index c7e8b69bb2c..eb552b2e209 100644 --- a/tests/HomepageTest.php +++ b/tests/HomepageTest.php @@ -2,8 +2,8 @@ namespace Tests; -use BookStack\Auth\Role; -use BookStack\Auth\User; +use BookStack\Users\Models\Role; +use BookStack\Users\Models\User; class HomepageTest extends TestCase { diff --git a/tests/LanguageTest.php b/tests/LanguageTest.php index b65227dd826..a66227ff2e8 100644 --- a/tests/LanguageTest.php +++ b/tests/LanguageTest.php @@ -2,6 +2,8 @@ namespace Tests; +use BookStack\Activity\ActivityType; + class LanguageTest extends TestCase { protected array $langs; @@ -90,4 +92,12 @@ public function test_unknown_lang_does_not_break_app() $loginReq->assertOk(); $loginReq->assertSee('Log In'); } + + public function test_all_activity_types_have_activity_text() + { + foreach (ActivityType::all() as $activityType) { + $langKey = 'activities.' . $activityType; + $this->assertNotEquals($langKey, trans($langKey, [], 'en')); + } + } } diff --git a/tests/Permissions/EntityOwnerChangeTest.php b/tests/Permissions/EntityOwnerChangeTest.php index e94759760ce..f002549220b 100644 --- a/tests/Permissions/EntityOwnerChangeTest.php +++ b/tests/Permissions/EntityOwnerChangeTest.php @@ -2,7 +2,7 @@ namespace Tests\Permissions; -use BookStack\Auth\User; +use BookStack\Users\Models\User; use Tests\TestCase; class EntityOwnerChangeTest extends TestCase diff --git a/tests/Permissions/EntityPermissionsTest.php b/tests/Permissions/EntityPermissionsTest.php index 99a8bd88c1d..6ea0257b81e 100644 --- a/tests/Permissions/EntityPermissionsTest.php +++ b/tests/Permissions/EntityPermissionsTest.php @@ -2,13 +2,13 @@ namespace Tests\Permissions; -use BookStack\Auth\Role; -use BookStack\Auth\User; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; +use BookStack\Users\Models\Role; +use BookStack\Users\Models\User; use Exception; use Illuminate\Support\Str; use Tests\TestCase; @@ -413,6 +413,16 @@ public function test_page_restriction_form() $this->entityRestrictionFormTest(Page::class, 'Page Permissions', 'delete', '2'); } + public function test_shelf_create_permission_visible_with_notice() + { + $shelf = $this->entities->shelf(); + + $resp = $this->asAdmin()->get($shelf->getUrl('/permissions')); + $html = $this->withHtml($resp); + $html->assertElementExists('input[name$="[create]"]'); + $resp->assertSee('Shelf create permissions are only used for copying permissions to child books using the action below.'); + } + public function test_restricted_pages_not_visible_in_book_navigation_on_pages() { $chapter = $this->entities->chapter(); diff --git a/tests/Permissions/RolesTest.php b/tests/Permissions/RolePermissionsTest.php similarity index 73% rename from tests/Permissions/RolesTest.php rename to tests/Permissions/RolePermissionsTest.php index 971479e28ae..0b2e1668655 100644 --- a/tests/Permissions/RolesTest.php +++ b/tests/Permissions/RolePermissionsTest.php @@ -2,20 +2,20 @@ namespace Tests\Permissions; -use BookStack\Actions\ActivityType; -use BookStack\Actions\Comment; -use BookStack\Auth\Role; -use BookStack\Auth\User; +use BookStack\Activity\ActivityType; +use BookStack\Activity\Models\Comment; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; use BookStack\Uploads\Image; +use BookStack\Users\Models\Role; +use BookStack\Users\Models\User; use Illuminate\Testing\TestResponse; use Tests\TestCase; -class RolesTest extends TestCase +class RolePermissionsTest extends TestCase { protected User $user; @@ -25,208 +25,6 @@ protected function setUp(): void $this->user = $this->users->viewer(); } - public function test_admin_can_see_settings() - { - $this->asAdmin()->get('/settings/features')->assertSee('Settings'); - } - - public function test_cannot_delete_admin_role() - { - $adminRole = Role::getRole('admin'); - $deletePageUrl = '/settings/roles/delete/' . $adminRole->id; - - $this->asAdmin()->get($deletePageUrl); - $this->delete($deletePageUrl)->assertRedirect($deletePageUrl); - $this->get($deletePageUrl)->assertSee('cannot be deleted'); - } - - public function test_role_cannot_be_deleted_if_default() - { - $newRole = $this->users->createRole(); - $this->setSettings(['registration-role' => $newRole->id]); - - $deletePageUrl = '/settings/roles/delete/' . $newRole->id; - $this->asAdmin()->get($deletePageUrl); - $this->delete($deletePageUrl)->assertRedirect($deletePageUrl); - $this->get($deletePageUrl)->assertSee('cannot be deleted'); - } - - public function test_role_create_update_delete_flow() - { - $testRoleName = 'Test Role'; - $testRoleDesc = 'a little test description'; - $testRoleUpdateName = 'An Super Updated role'; - - // Creation - $resp = $this->asAdmin()->get('/settings/features'); - $this->withHtml($resp)->assertElementContains('a[href="' . url('/settings/roles') . '"]', 'Roles'); - - $resp = $this->get('/settings/roles'); - $this->withHtml($resp)->assertElementContains('a[href="' . url('/settings/roles/new') . '"]', 'Create New Role'); - - $resp = $this->get('/settings/roles/new'); - $this->withHtml($resp)->assertElementContains('form[action="' . url('/settings/roles/new') . '"]', 'Save Role'); - - $resp = $this->post('/settings/roles/new', [ - 'display_name' => $testRoleName, - 'description' => $testRoleDesc, - ]); - $resp->assertRedirect('/settings/roles'); - - $resp = $this->get('/settings/roles'); - $resp->assertSee($testRoleName); - $resp->assertSee($testRoleDesc); - $this->assertDatabaseHas('roles', [ - 'display_name' => $testRoleName, - 'description' => $testRoleDesc, - 'mfa_enforced' => false, - ]); - - /** @var Role $role */ - $role = Role::query()->where('display_name', '=', $testRoleName)->first(); - - // Updating - $resp = $this->get('/settings/roles/' . $role->id); - $resp->assertSee($testRoleName); - $resp->assertSee($testRoleDesc); - $this->withHtml($resp)->assertElementContains('form[action="' . url('/settings/roles/' . $role->id) . '"]', 'Save Role'); - - $resp = $this->put('/settings/roles/' . $role->id, [ - 'display_name' => $testRoleUpdateName, - 'description' => $testRoleDesc, - 'mfa_enforced' => 'true', - ]); - $resp->assertRedirect('/settings/roles'); - $this->assertDatabaseHas('roles', [ - 'display_name' => $testRoleUpdateName, - 'description' => $testRoleDesc, - 'mfa_enforced' => true, - ]); - - // Deleting - $resp = $this->get('/settings/roles/' . $role->id); - $this->withHtml($resp)->assertElementContains('a[href="' . url("/settings/roles/delete/$role->id") . '"]', 'Delete Role'); - - $resp = $this->get("/settings/roles/delete/$role->id"); - $resp->assertSee($testRoleUpdateName); - $this->withHtml($resp)->assertElementContains('form[action="' . url("/settings/roles/delete/$role->id") . '"]', 'Confirm'); - - $resp = $this->delete("/settings/roles/delete/$role->id"); - $resp->assertRedirect('/settings/roles'); - $this->get('/settings/roles')->assertSee('Role successfully deleted'); - $this->assertActivityExists(ActivityType::ROLE_DELETE); - } - - public function test_admin_role_cannot_be_removed_if_user_last_admin() - { - /** @var Role $adminRole */ - $adminRole = Role::query()->where('system_name', '=', 'admin')->first(); - $adminUser = $this->users->admin(); - $adminRole->users()->where('id', '!=', $adminUser->id)->delete(); - $this->assertEquals(1, $adminRole->users()->count()); - - $viewerRole = $this->users->viewer()->roles()->first(); - - $editUrl = '/settings/users/' . $adminUser->id; - $resp = $this->actingAs($adminUser)->put($editUrl, [ - 'name' => $adminUser->name, - 'email' => $adminUser->email, - 'roles' => [ - 'viewer' => strval($viewerRole->id), - ], - ]); - - $resp->assertRedirect($editUrl); - - $resp = $this->get($editUrl); - $resp->assertSee('This user is the only user assigned to the administrator role'); - } - - public function test_migrate_users_on_delete_works() - { - /** @var Role $roleA */ - $roleA = Role::query()->create(['display_name' => 'Delete Test A']); - /** @var Role $roleB */ - $roleB = Role::query()->create(['display_name' => 'Delete Test B']); - $this->user->attachRole($roleB); - - $this->assertCount(0, $roleA->users()->get()); - $this->assertCount(1, $roleB->users()->get()); - - $deletePage = $this->asAdmin()->get("/settings/roles/delete/$roleB->id"); - $this->withHtml($deletePage)->assertElementExists('select[name=migrate_role_id]'); - $this->asAdmin()->delete("/settings/roles/delete/$roleB->id", [ - 'migrate_role_id' => $roleA->id, - ]); - - $this->assertCount(1, $roleA->users()->get()); - $this->assertEquals($this->user->id, $roleA->users()->first()->id); - } - - public function test_delete_with_empty_migrate_option_works() - { - $role = $this->users->attachNewRole($this->user); - - $this->assertCount(1, $role->users()->get()); - - $deletePage = $this->asAdmin()->get("/settings/roles/delete/$role->id"); - $this->withHtml($deletePage)->assertElementExists('select[name=migrate_role_id]'); - $resp = $this->asAdmin()->delete("/settings/roles/delete/$role->id", [ - 'migrate_role_id' => '', - ]); - - $resp->assertRedirect('/settings/roles'); - $this->assertDatabaseMissing('roles', ['id' => $role->id]); - } - - public function test_entity_permissions_are_removed_on_delete() - { - /** @var Role $roleA */ - $roleA = Role::query()->create(['display_name' => 'Entity Permissions Delete Test']); - $page = $this->entities->page(); - - $this->permissions->setEntityPermissions($page, ['view'], [$roleA]); - - $this->assertDatabaseHas('entity_permissions', [ - 'role_id' => $roleA->id, - 'entity_id' => $page->id, - 'entity_type' => $page->getMorphClass(), - ]); - - $this->asAdmin()->delete("/settings/roles/delete/$roleA->id"); - - $this->assertDatabaseMissing('entity_permissions', [ - 'role_id' => $roleA->id, - 'entity_id' => $page->id, - 'entity_type' => $page->getMorphClass(), - ]); - } - - public function test_image_view_notice_shown_on_role_form() - { - /** @var Role $role */ - $role = Role::query()->first(); - $this->asAdmin()->get("/settings/roles/{$role->id}") - ->assertSee('Actual access of uploaded image files will be dependant upon system image storage option'); - } - - public function test_copy_role_button_shown() - { - /** @var Role $role */ - $role = Role::query()->first(); - $resp = $this->asAdmin()->get("/settings/roles/{$role->id}"); - $this->withHtml($resp)->assertElementContains('a[href$="/roles/new?copy_from=' . $role->id . '"]', 'Copy'); - } - - public function test_copy_from_param_on_create_prefills_with_other_role_data() - { - /** @var Role $role */ - $role = Role::query()->first(); - $resp = $this->asAdmin()->get("/settings/roles/new?copy_from={$role->id}"); - $resp->assertOk(); - $this->withHtml($resp)->assertElementExists('input[name="display_name"][value="' . ($role->display_name . ' (Copy)') . '"]'); - } - public function test_manage_user_permission() { $this->actingAs($this->user)->get('/settings/users')->assertRedirect('/'); @@ -301,12 +99,12 @@ public function test_settings_manage_permission() $resp = $this->post('/settings/features', []); $resp->assertRedirect('/settings/features'); $resp = $this->get('/settings/features'); - $resp->assertSee('Settings saved'); + $resp->assertSee('Settings successfully updated'); } public function test_restrictions_manage_all_permission() { - $page = Page::query()->get()->first(); + $page = $this->entities->page(); $this->actingAs($this->user)->get($page->getUrl())->assertDontSee('Permissions'); $this->get($page->getUrl('/permissions'))->assertRedirect('/'); @@ -322,8 +120,7 @@ public function test_restrictions_manage_all_permission() public function test_restrictions_manage_own_permission() { - /** @var Page $otherUsersPage */ - $otherUsersPage = Page::query()->first(); + $otherUsersPage = $this->entities->page(); $content = $this->entities->createChainBelongingToUser($this->user); // Set a different creator on the page we're checking to ensure @@ -798,44 +595,6 @@ public function test_page_delete_all_permission() $this->get($parent->getUrl())->assertDontSee($otherPage->name); } - public function test_public_role_visible_in_user_edit_screen() - { - /** @var User $user */ - $user = User::query()->first(); - $adminRole = Role::getSystemRole('admin'); - $publicRole = Role::getSystemRole('public'); - $resp = $this->asAdmin()->get('/settings/users/' . $user->id); - $this->withHtml($resp)->assertElementExists('[name="roles[' . $adminRole->id . ']"]') - ->assertElementExists('[name="roles[' . $publicRole->id . ']"]'); - } - - public function test_public_role_visible_in_role_listing() - { - $this->asAdmin()->get('/settings/roles') - ->assertSee('Admin') - ->assertSee('Public'); - } - - public function test_public_role_visible_in_default_role_setting() - { - $resp = $this->asAdmin()->get('/settings/registration'); - $this->withHtml($resp)->assertElementExists('[data-system-role-name="admin"]') - ->assertElementExists('[data-system-role-name="public"]'); - } - - public function test_public_role_not_deletable() - { - /** @var Role $publicRole */ - $publicRole = Role::getSystemRole('public'); - $resp = $this->asAdmin()->delete('/settings/roles/delete/' . $publicRole->id); - $resp->assertRedirect('/'); - - $this->get('/settings/roles/delete/' . $publicRole->id); - $resp = $this->delete('/settings/roles/delete/' . $publicRole->id); - $resp->assertRedirect('/settings/roles/delete/' . $publicRole->id); - $resp = $this->get('/settings/roles/delete/' . $publicRole->id); - $resp->assertSee('This role is a system role and cannot be deleted'); - } public function test_image_delete_own_permission() { @@ -874,23 +633,6 @@ public function test_image_delete_all_permission() $this->assertDatabaseMissing('images', ['id' => $image->id]); } - public function test_role_permission_removal() - { - // To cover issue fixed in f99c8ff99aee9beb8c692f36d4b84dc6e651e50a. - $page = $this->entities->page(); - $viewerRole = Role::getRole('viewer'); - $viewer = $this->users->viewer(); - $this->actingAs($viewer)->get($page->getUrl())->assertOk(); - - $this->asAdmin()->put('/settings/roles/' . $viewerRole->id, [ - 'display_name' => $viewerRole->display_name, - 'description' => $viewerRole->description, - 'permissions' => [], - ])->assertStatus(302); - - $this->actingAs($viewer)->get($page->getUrl())->assertStatus(404); - } - public function test_empty_state_actions_not_visible_without_permission() { $admin = $this->users->admin(); diff --git a/tests/Permissions/Scenarios/PermissionScenarioTestCase.php b/tests/Permissions/Scenarios/PermissionScenarioTestCase.php index 5352f468a9d..2299fe8787b 100644 --- a/tests/Permissions/Scenarios/PermissionScenarioTestCase.php +++ b/tests/Permissions/Scenarios/PermissionScenarioTestCase.php @@ -2,8 +2,8 @@ namespace Tests\Permissions\Scenarios; -use BookStack\Auth\User; use BookStack\Entities\Models\Entity; +use BookStack\Users\Models\User; use Tests\TestCase; // Cases defined in dev/docs/permission-scenario-testing.md diff --git a/tests/PublicActionTest.php b/tests/PublicActionTest.php index e21afdf3322..6f0e2f1d3bb 100644 --- a/tests/PublicActionTest.php +++ b/tests/PublicActionTest.php @@ -2,11 +2,11 @@ namespace Tests; -use BookStack\Auth\Permissions\RolePermission; -use BookStack\Auth\Role; -use BookStack\Auth\User; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; +use BookStack\Permissions\Models\RolePermission; +use BookStack\Users\Models\Role; +use BookStack\Users\Models\User; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\View; @@ -193,4 +193,18 @@ public function test_access_hidden_content_then_login_redirects_to_intended_cont $resp->assertRedirect($book->getUrl()); $this->followRedirects($resp)->assertSee($book->name); } + + public function test_public_view_can_take_on_other_roles() + { + $this->setSettings(['app-public' => 'true']); + $newRole = $this->users->attachNewRole(User::getDefault(), []); + $page = $this->entities->page(); + $this->permissions->disableEntityInheritedPermissions($page); + $this->permissions->addEntityPermission($page, ['view', 'update'], $newRole); + + $resp = $this->get($page->getUrl()); + $resp->assertOk(); + + $this->withHtml($resp)->assertLinkExists($page->getUrl('/edit')); + } } diff --git a/tests/References/ReferencesTest.php b/tests/References/ReferencesTest.php index 4330598baf6..a19e1b90157 100644 --- a/tests/References/ReferencesTest.php +++ b/tests/References/ReferencesTest.php @@ -2,9 +2,9 @@ namespace Tests\References; +use BookStack\App\Model; use BookStack\Entities\Repos\PageRepo; use BookStack\Entities\Tools\TrashCan; -use BookStack\Model; use BookStack\References\Reference; use Tests\TestCase; diff --git a/tests/Settings/RegenerateReferencesTest.php b/tests/Settings/RegenerateReferencesTest.php index 239f50e765f..25832b03e58 100644 --- a/tests/Settings/RegenerateReferencesTest.php +++ b/tests/Settings/RegenerateReferencesTest.php @@ -2,7 +2,7 @@ namespace Tests\Settings; -use BookStack\Actions\ActivityType; +use BookStack\Activity\ActivityType; use BookStack\References\ReferenceStore; use Tests\TestCase; diff --git a/tests/Settings/SettingsTest.php b/tests/Settings/SettingsTest.php index fb952585a20..9d45706e77b 100644 --- a/tests/Settings/SettingsTest.php +++ b/tests/Settings/SettingsTest.php @@ -6,6 +6,11 @@ class SettingsTest extends TestCase { + public function test_admin_can_see_settings() + { + $this->asAdmin()->get('/settings/features')->assertSee('Settings'); + } + public function test_settings_endpoint_redirects_to_settings_view() { $resp = $this->asAdmin()->get('/settings'); diff --git a/tests/TestCase.php b/tests/TestCase.php index abee1d3b342..322ab037032 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -213,18 +213,14 @@ protected function assertNotPermissionError($response) */ private function isPermissionError($response): bool { + if ($response->status() === 403 && $response instanceof JsonResponse) { + $errMessage = $response->getData(true)['error']['message'] ?? ''; + return $errMessage === 'You do not have permission to perform the requested action.'; + } + return $response->status() === 302 - && ( - ( - $response->headers->get('Location') === url('/') - && strpos(session()->pull('error', ''), 'You do not have permission to access') === 0 - ) - || - ( - $response instanceof JsonResponse && - $response->json(['error' => 'You do not have permission to perform the requested action.']) - ) - ); + && $response->headers->get('Location') === url('/') + && str_starts_with(session()->pull('error', ''), 'You do not have permission to access'); } /** diff --git a/tests/ThemeTest.php b/tests/ThemeTest.php index 03ae7b307d4..6976f23847c 100644 --- a/tests/ThemeTest.php +++ b/tests/ThemeTest.php @@ -2,15 +2,15 @@ namespace Tests; -use BookStack\Actions\ActivityType; -use BookStack\Actions\DispatchWebhookJob; -use BookStack\Actions\Webhook; -use BookStack\Auth\User; +use BookStack\Activity\ActivityType; +use BookStack\Activity\DispatchWebhookJob; +use BookStack\Activity\Models\Webhook; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Page; use BookStack\Entities\Tools\PageContent; use BookStack\Facades\Theme; use BookStack\Theming\ThemeEvents; +use BookStack\Users\Models\User; use Illuminate\Console\Command; use Illuminate\Http\Client\Request as HttpClientRequest; use Illuminate\Http\Request; @@ -23,8 +23,8 @@ class ThemeTest extends TestCase { - protected $themeFolderName; - protected $themeFolderPath; + protected string $themeFolderName; + protected string $themeFolderPath; public function test_translation_text_can_be_overridden_via_theme() { diff --git a/tests/Unit/ConfigTest.php b/tests/Unit/ConfigTest.php index 9966a4fb151..2de32c1b8c8 100644 --- a/tests/Unit/ConfigTest.php +++ b/tests/Unit/ConfigTest.php @@ -3,6 +3,8 @@ namespace Tests\Unit; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Mail; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; use Tests\TestCase; /** @@ -96,11 +98,68 @@ public function test_snappy_paper_size_options_are_limited() $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'a4', 'snappy.pdf.options.page-size', 'A4'); } - public function test_sendmail_command_is_configurage() + public function test_sendmail_command_is_configurable() { $this->checkEnvConfigResult('MAIL_SENDMAIL_COMMAND', '/var/sendmail -o', 'mail.mailers.sendmail.path', '/var/sendmail -o'); } + public function test_mail_disable_ssl_verification_alters_mailer() + { + $getStreamOptions = function (): array { + /** @var EsmtpTransport $transport */ + $transport = Mail::mailer('smtp')->getSymfonyTransport(); + return $transport->getStream()->getStreamOptions(); + }; + + $this->assertEmpty($getStreamOptions()); + + + $this->runWithEnv('MAIL_VERIFY_SSL', 'false', function () use ($getStreamOptions) { + $options = $getStreamOptions(); + $this->assertArrayHasKey('ssl', $options); + $this->assertFalse($options['ssl']['verify_peer']); + $this->assertFalse($options['ssl']['verify_peer_name']); + }); + } + + public function test_non_null_mail_encryption_options_enforce_smtp_scheme() + { + $this->checkEnvConfigResult('MAIL_ENCRYPTION', 'tls', 'mail.mailers.smtp.tls_required', true); + $this->checkEnvConfigResult('MAIL_ENCRYPTION', 'ssl', 'mail.mailers.smtp.tls_required', true); + $this->checkEnvConfigResult('MAIL_ENCRYPTION', 'null', 'mail.mailers.smtp.tls_required', false); + } + + public function test_smtp_scheme_and_certain_port_forces_tls_usage() + { + $isMailTlsRequired = function () { + /** @var \BookStack\App\Mail\EsmtpTransport $transport */ + $transport = Mail::mailer('smtp')->getSymfonyTransport(); + Mail::purge('smtp'); + return $transport->getTlsRequirement(); + }; + + config()->set([ + 'mail.mailers.smtp.tls_required' => null, + 'mail.mailers.smtp.port' => 587, + ]); + + $this->assertFalse($isMailTlsRequired()); + + config()->set([ + 'mail.mailers.smtp.tls_required' => 'tls', + 'mail.mailers.smtp.port' => 587, + ]); + + $this->assertTrue($isMailTlsRequired()); + + config()->set([ + 'mail.mailers.smtp.tls_required' => null, + 'mail.mailers.smtp.port' => 465, + ]); + + $this->assertTrue($isMailTlsRequired()); + } + /** * Set an environment variable of the given name and value * then check the given config key to see if it matches the given result. diff --git a/tests/Unit/IpFormatterTest.php b/tests/Unit/IpFormatterTest.php index 928b1ab1009..14878257570 100644 --- a/tests/Unit/IpFormatterTest.php +++ b/tests/Unit/IpFormatterTest.php @@ -2,7 +2,7 @@ namespace Tests\Unit; -use BookStack\Actions\IpFormatter; +use BookStack\Activity\Tools\IpFormatter; use Tests\TestCase; class IpFormatterTest extends TestCase diff --git a/tests/Unit/OidcIdTokenTest.php b/tests/Unit/OidcIdTokenTest.php index ad91eecd8b2..6302f84c75f 100644 --- a/tests/Unit/OidcIdTokenTest.php +++ b/tests/Unit/OidcIdTokenTest.php @@ -2,8 +2,8 @@ namespace Tests\Unit; -use BookStack\Auth\Access\Oidc\OidcIdToken; -use BookStack\Auth\Access\Oidc\OidcInvalidTokenException; +use BookStack\Access\Oidc\OidcIdToken; +use BookStack\Access\Oidc\OidcInvalidTokenException; use Tests\Helpers\OidcJwtHelper; use Tests\TestCase; diff --git a/tests/Uploads/AvatarTest.php b/tests/Uploads/AvatarTest.php index 57f28db4292..363c1fa9542 100644 --- a/tests/Uploads/AvatarTest.php +++ b/tests/Uploads/AvatarTest.php @@ -2,9 +2,10 @@ namespace Tests\Uploads; -use BookStack\Auth\User; use BookStack\Exceptions\HttpFetchException; use BookStack\Uploads\HttpFetcher; +use BookStack\Uploads\UserAvatars; +use BookStack\Users\Models\User; use Tests\TestCase; class AvatarTest extends TestCase @@ -110,4 +111,28 @@ public function test_no_failure_but_error_logged_on_failed_avatar_fetch() $this->createUserRequest($user); $this->assertTrue($logger->hasError('Failed to save user avatar image')); } + + public function test_exception_message_on_failed_fetch() + { + // set wrong url + config()->set([ + 'services.disable_services' => false, + 'services.avatar_url' => 'http_malformed_url/${email}/${hash}/${size}', + ]); + + $user = User::factory()->make(); + $avatar = app()->make(UserAvatars::class); + $url = 'http_malformed_url/' . urlencode(strtolower($user->email)) . '/' . md5(strtolower($user->email)) . '/500'; + $logger = $this->withTestLogger(); + + $avatar->fetchAndAssignToUser($user); + + $this->assertTrue($logger->hasError('Failed to save user avatar image')); + $exception = $logger->getRecords()[0]['context']['exception']; + $this->assertEquals(new HttpFetchException( + 'Cannot get image from ' . $url, + 6, + (new HttpFetchException('Could not resolve host: http_malformed_url', 6)) + ), $exception); + } } diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index 53040ea086b..a9684eef72a 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -92,6 +92,52 @@ public function test_image_edit() ]); } + public function test_image_file_update() + { + $page = $this->entities->page(); + $this->asEditor(); + + $imgDetails = $this->files->uploadGalleryImageToPage($this, $page); + $relPath = $imgDetails['path']; + + $newUpload = $this->files->uploadedImage('updated-image.png', 'compressed.png'); + $this->assertFileEquals($this->files->testFilePath('test-image.png'), public_path($relPath)); + + $imageId = $imgDetails['response']->id; + $image = Image::findOrFail($imageId); + $image->updated_at = now()->subMonth(); + $image->save(); + + $this->call('PUT', "/images/{$imageId}/file", [], [], ['file' => $newUpload]) + ->assertOk(); + + $this->assertFileEquals($this->files->testFilePath('compressed.png'), public_path($relPath)); + + $image->refresh(); + $this->assertTrue($image->updated_at->gt(now()->subMinute())); + + $this->files->deleteAtRelativePath($relPath); + } + + public function test_image_file_update_does_not_allow_change_in_image_extension() + { + $page = $this->entities->page(); + $this->asEditor(); + + $imgDetails = $this->files->uploadGalleryImageToPage($this, $page); + $relPath = $imgDetails['path']; + $newUpload = $this->files->uploadedImage('updated-image.jpg', 'compressed.png'); + + $imageId = $imgDetails['response']->id; + $this->call('PUT', "/images/{$imageId}/file", [], [], ['file' => $newUpload]) + ->assertJson([ + "message" => "Image file replacements must be of the same type", + "status" => "error", + ]); + + $this->files->deleteAtRelativePath($relPath); + } + public function test_gallery_get_list_format() { $this->asEditor(); @@ -163,7 +209,8 @@ public function test_php_files_cannot_be_uploaded() $file = $this->files->imageFromBase64File('bad-php.base64', $fileName); $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []); - $upload->assertStatus(302); + $upload->assertStatus(500); + $this->assertStringContainsString('The file must have a valid & supported image extension', $upload->json('message')); $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped'); @@ -185,7 +232,8 @@ public function test_php_like_files_cannot_be_uploaded() $file = $this->files->imageFromBase64File('bad-phtml.base64', $fileName); $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []); - $upload->assertStatus(302); + $upload->assertStatus(500); + $this->assertStringContainsString('The file must have a valid & supported image extension', $upload->json('message')); $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped'); } @@ -491,15 +539,15 @@ public function test_image_manager_delete_button_only_shows_with_permission() $image = Image::first(); $resp = $this->get("/images/edit/{$image->id}"); - $this->withHtml($resp)->assertElementExists('button#image-manager-delete[title="Delete"]'); + $this->withHtml($resp)->assertElementExists('button#image-manager-delete'); $resp = $this->actingAs($viewer)->get("/images/edit/{$image->id}"); - $this->withHtml($resp)->assertElementNotExists('button#image-manager-delete[title="Delete"]'); + $this->withHtml($resp)->assertElementNotExists('button#image-manager-delete'); $this->permissions->grantUserRolePermissions($viewer, ['image-delete-all']); $resp = $this->actingAs($viewer)->get("/images/edit/{$image->id}"); - $this->withHtml($resp)->assertElementExists('button#image-manager-delete[title="Delete"]'); + $this->withHtml($resp)->assertElementExists('button#image-manager-delete'); $this->files->deleteAtRelativePath($relPath); } diff --git a/tests/User/RoleManagementTest.php b/tests/User/RoleManagementTest.php new file mode 100644 index 00000000000..5722a0b08c8 --- /dev/null +++ b/tests/User/RoleManagementTest.php @@ -0,0 +1,289 @@ +id; + + $this->asAdmin()->get($deletePageUrl); + $this->delete($deletePageUrl)->assertRedirect($deletePageUrl); + $this->get($deletePageUrl)->assertSee('cannot be deleted'); + } + + public function test_role_cannot_be_deleted_if_default() + { + $newRole = $this->users->createRole(); + $this->setSettings(['registration-role' => $newRole->id]); + + $deletePageUrl = '/settings/roles/delete/' . $newRole->id; + $this->asAdmin()->get($deletePageUrl); + $this->delete($deletePageUrl)->assertRedirect($deletePageUrl); + $this->get($deletePageUrl)->assertSee('cannot be deleted'); + } + + public function test_role_create_update_delete_flow() + { + $testRoleName = 'Test Role'; + $testRoleDesc = 'a little test description'; + $testRoleUpdateName = 'An Super Updated role'; + + // Creation + $resp = $this->asAdmin()->get('/settings/features'); + $this->withHtml($resp)->assertElementContains('a[href="' . url('/settings/roles') . '"]', 'Roles'); + + $resp = $this->get('/settings/roles'); + $this->withHtml($resp)->assertElementContains('a[href="' . url('/settings/roles/new') . '"]', 'Create New Role'); + + $resp = $this->get('/settings/roles/new'); + $this->withHtml($resp)->assertElementContains('form[action="' . url('/settings/roles/new') . '"]', 'Save Role'); + + $resp = $this->post('/settings/roles/new', [ + 'display_name' => $testRoleName, + 'description' => $testRoleDesc, + ]); + $resp->assertRedirect('/settings/roles'); + + $resp = $this->get('/settings/roles'); + $resp->assertSee($testRoleName); + $resp->assertSee($testRoleDesc); + $this->assertDatabaseHas('roles', [ + 'display_name' => $testRoleName, + 'description' => $testRoleDesc, + 'mfa_enforced' => false, + ]); + + /** @var Role $role */ + $role = Role::query()->where('display_name', '=', $testRoleName)->first(); + + // Updating + $resp = $this->get('/settings/roles/' . $role->id); + $resp->assertSee($testRoleName); + $resp->assertSee($testRoleDesc); + $this->withHtml($resp)->assertElementContains('form[action="' . url('/settings/roles/' . $role->id) . '"]', 'Save Role'); + + $resp = $this->put('/settings/roles/' . $role->id, [ + 'display_name' => $testRoleUpdateName, + 'description' => $testRoleDesc, + 'mfa_enforced' => 'true', + ]); + $resp->assertRedirect('/settings/roles'); + $this->assertDatabaseHas('roles', [ + 'display_name' => $testRoleUpdateName, + 'description' => $testRoleDesc, + 'mfa_enforced' => true, + ]); + + // Deleting + $resp = $this->get('/settings/roles/' . $role->id); + $this->withHtml($resp)->assertElementContains('a[href="' . url("/settings/roles/delete/$role->id") . '"]', 'Delete Role'); + + $resp = $this->get("/settings/roles/delete/$role->id"); + $resp->assertSee($testRoleUpdateName); + $this->withHtml($resp)->assertElementContains('form[action="' . url("/settings/roles/delete/$role->id") . '"]', 'Confirm'); + + $resp = $this->delete("/settings/roles/delete/$role->id"); + $resp->assertRedirect('/settings/roles'); + $this->get('/settings/roles')->assertSee('Role successfully deleted'); + $this->assertActivityExists(ActivityType::ROLE_DELETE); + } + + public function test_admin_role_cannot_be_removed_if_user_last_admin() + { + /** @var Role $adminRole */ + $adminRole = Role::query()->where('system_name', '=', 'admin')->first(); + $adminUser = $this->users->admin(); + $adminRole->users()->where('id', '!=', $adminUser->id)->delete(); + $this->assertEquals(1, $adminRole->users()->count()); + + $viewerRole = $this->users->viewer()->roles()->first(); + + $editUrl = '/settings/users/' . $adminUser->id; + $resp = $this->actingAs($adminUser)->put($editUrl, [ + 'name' => $adminUser->name, + 'email' => $adminUser->email, + 'roles' => [ + 'viewer' => strval($viewerRole->id), + ], + ]); + + $resp->assertRedirect($editUrl); + + $resp = $this->get($editUrl); + $resp->assertSee('This user is the only user assigned to the administrator role'); + } + + public function test_migrate_users_on_delete_works() + { + $roleA = $this->users->createRole(); + $roleB = $this->users->createRole(); + $user = $this->users->viewer(); + $user->attachRole($roleB); + + $this->assertCount(0, $roleA->users()->get()); + $this->assertCount(1, $roleB->users()->get()); + + $deletePage = $this->asAdmin()->get("/settings/roles/delete/$roleB->id"); + $this->withHtml($deletePage)->assertElementExists('select[name=migrate_role_id]'); + $this->asAdmin()->delete("/settings/roles/delete/$roleB->id", [ + 'migrate_role_id' => $roleA->id, + ]); + + $this->assertCount(1, $roleA->users()->get()); + $this->assertEquals($user->id, $roleA->users()->first()->id); + } + + public function test_delete_with_empty_migrate_option_works() + { + $role = $this->users->attachNewRole($this->users->viewer()); + + $this->assertCount(1, $role->users()->get()); + + $deletePage = $this->asAdmin()->get("/settings/roles/delete/$role->id"); + $this->withHtml($deletePage)->assertElementExists('select[name=migrate_role_id]'); + $resp = $this->asAdmin()->delete("/settings/roles/delete/$role->id", [ + 'migrate_role_id' => '', + ]); + + $resp->assertRedirect('/settings/roles'); + $this->assertDatabaseMissing('roles', ['id' => $role->id]); + } + + public function test_entity_permissions_are_removed_on_delete() + { + /** @var Role $roleA */ + $roleA = Role::query()->create(['display_name' => 'Entity Permissions Delete Test']); + $page = $this->entities->page(); + + $this->permissions->setEntityPermissions($page, ['view'], [$roleA]); + + $this->assertDatabaseHas('entity_permissions', [ + 'role_id' => $roleA->id, + 'entity_id' => $page->id, + 'entity_type' => $page->getMorphClass(), + ]); + + $this->asAdmin()->delete("/settings/roles/delete/$roleA->id"); + + $this->assertDatabaseMissing('entity_permissions', [ + 'role_id' => $roleA->id, + 'entity_id' => $page->id, + 'entity_type' => $page->getMorphClass(), + ]); + } + + public function test_image_view_notice_shown_on_role_form() + { + /** @var Role $role */ + $role = Role::query()->first(); + $this->asAdmin()->get("/settings/roles/{$role->id}") + ->assertSee('Actual access of uploaded image files will be dependant upon system image storage option'); + } + + public function test_copy_role_button_shown() + { + /** @var Role $role */ + $role = Role::query()->first(); + $resp = $this->asAdmin()->get("/settings/roles/{$role->id}"); + $this->withHtml($resp)->assertElementContains('a[href$="/roles/new?copy_from=' . $role->id . '"]', 'Copy'); + } + + public function test_copy_from_param_on_create_prefills_with_other_role_data() + { + /** @var Role $role */ + $role = Role::query()->first(); + $resp = $this->asAdmin()->get("/settings/roles/new?copy_from={$role->id}"); + $resp->assertOk(); + $this->withHtml($resp)->assertElementExists('input[name="display_name"][value="' . ($role->display_name . ' (Copy)') . '"]'); + } + + public function test_public_role_visible_in_user_edit_screen() + { + /** @var User $user */ + $user = User::query()->first(); + $adminRole = Role::getSystemRole('admin'); + $publicRole = Role::getSystemRole('public'); + $resp = $this->asAdmin()->get('/settings/users/' . $user->id); + $this->withHtml($resp)->assertElementExists('[name="roles[' . $adminRole->id . ']"]') + ->assertElementExists('[name="roles[' . $publicRole->id . ']"]'); + } + + public function test_public_role_visible_in_role_listing() + { + $this->asAdmin()->get('/settings/roles') + ->assertSee('Admin') + ->assertSee('Public'); + } + + public function test_public_role_visible_in_default_role_setting() + { + $resp = $this->asAdmin()->get('/settings/registration'); + $this->withHtml($resp)->assertElementExists('[data-system-role-name="admin"]') + ->assertElementExists('[data-system-role-name="public"]'); + } + + public function test_public_role_not_deletable() + { + /** @var Role $publicRole */ + $publicRole = Role::getSystemRole('public'); + $resp = $this->asAdmin()->delete('/settings/roles/delete/' . $publicRole->id); + $resp->assertRedirect('/'); + + $this->get('/settings/roles/delete/' . $publicRole->id); + $resp = $this->delete('/settings/roles/delete/' . $publicRole->id); + $resp->assertRedirect('/settings/roles/delete/' . $publicRole->id); + $resp = $this->get('/settings/roles/delete/' . $publicRole->id); + $resp->assertSee('This role is a system role and cannot be deleted'); + } + + public function test_role_permission_removal() + { + // To cover issue fixed in f99c8ff99aee9beb8c692f36d4b84dc6e651e50a. + $page = $this->entities->page(); + $viewerRole = Role::getRole('viewer'); + $viewer = $this->users->viewer(); + $this->actingAs($viewer)->get($page->getUrl())->assertOk(); + + $this->asAdmin()->put('/settings/roles/' . $viewerRole->id, [ + 'display_name' => $viewerRole->display_name, + 'description' => $viewerRole->description, + 'permissions' => [], + ])->assertStatus(302); + + $this->actingAs($viewer)->get($page->getUrl())->assertStatus(404); + } + + public function test_index_listing_sorting() + { + $this->asAdmin(); + $role = $this->users->createRole(); + $role->display_name = 'zz test role'; + $role->created_at = now()->addDays(1); + $role->save(); + + $runTest = function (string $order, string $direction, bool $expectFirstResult) use ($role) { + setting()->putForCurrentUser('roles_sort', $order); + setting()->putForCurrentUser('roles_sort_order', $direction); + $html = $this->withHtml($this->get('/settings/roles')); + $selector = ".item-list-row:first-child a[href$=\"/roles/{$role->id}\"]"; + if ($expectFirstResult) { + $html->assertElementExists($selector); + } else { + $html->assertElementNotExists($selector); + } + }; + + $runTest('name', 'asc', false); + $runTest('name', 'desc', true); + $runTest('created_at', 'desc', true); + $runTest('created_at', 'asc', false); + } +} diff --git a/tests/User/UserApiTokenTest.php b/tests/User/UserApiTokenTest.php index 93070b71218..75de49aed9a 100644 --- a/tests/User/UserApiTokenTest.php +++ b/tests/User/UserApiTokenTest.php @@ -2,7 +2,7 @@ namespace Tests\User; -use BookStack\Actions\ActivityType; +use BookStack\Activity\ActivityType; use BookStack\Api\ApiToken; use Carbon\Carbon; use Tests\TestCase; diff --git a/tests/User/UserManagementTest.php b/tests/User/UserManagementTest.php index 7ccc5b773a7..df60bede6e1 100644 --- a/tests/User/UserManagementTest.php +++ b/tests/User/UserManagementTest.php @@ -2,11 +2,11 @@ namespace Tests\User; -use BookStack\Actions\ActivityType; -use BookStack\Auth\Access\UserInviteService; -use BookStack\Auth\Role; -use BookStack\Auth\User; +use BookStack\Access\UserInviteService; +use BookStack\Activity\ActivityType; use BookStack\Uploads\Image; +use BookStack\Users\Models\Role; +use BookStack\Users\Models\User; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use Mockery\MockInterface; diff --git a/tests/User/UserProfileTest.php b/tests/User/UserProfileTest.php index c507e8fa63a..4bfb3c87822 100644 --- a/tests/User/UserProfileTest.php +++ b/tests/User/UserProfileTest.php @@ -3,8 +3,8 @@ namespace Tests\User; use Activity; -use BookStack\Actions\ActivityType; -use BookStack\Auth\User; +use BookStack\Activity\ActivityType; +use BookStack\Users\Models\User; use Tests\TestCase; class UserProfileTest extends TestCase diff --git a/tests/User/UserSearchTest.php b/tests/User/UserSearchTest.php index 1b3ca8a3514..1387311ce75 100644 --- a/tests/User/UserSearchTest.php +++ b/tests/User/UserSearchTest.php @@ -2,7 +2,7 @@ namespace Tests\User; -use BookStack\Auth\User; +use BookStack\Users\Models\User; use Tests\TestCase; class UserSearchTest extends TestCase diff --git a/version b/version index 370aac099a8..10f06c6bd76 100644 --- a/version +++ b/version @@ -1 +1 @@ -v23.02.3 +v23.06.2