diff --git a/app/Actions/Comment.php b/app/Actions/Comment.php index ef390939e..34fd84709 100644 --- a/app/Actions/Comment.php +++ b/app/Actions/Comment.php @@ -7,10 +7,11 @@ use BookStack\Traits\HasCreatorAndUpdater; use Illuminate\Database\Eloquent\Relations\MorphTo; /** - * @property string text - * @property string html - * @property int|null parent_id - * @property int local_id + * @property int $id + * @property string $text + * @property string $html + * @property int|null $parent_id + * @property int $local_id */ class Comment extends Model { diff --git a/app/Auth/Role.php b/app/Auth/Role.php index dcd960948..46921caeb 100644 --- a/app/Auth/Role.php +++ b/app/Auth/Role.php @@ -13,12 +13,13 @@ use Illuminate\Database\Eloquent\Relations\HasMany; /** * Class Role. * - * @property int $id - * @property string $display_name - * @property string $description - * @property string $external_auth_id - * @property string $system_name - * @property bool $mfa_enforced + * @property int $id + * @property string $display_name + * @property string $description + * @property string $external_auth_id + * @property string $system_name + * @property bool $mfa_enforced + * @property Collection $users */ class Role extends Model implements Loggable { diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index df30c1c71..1e4591bd7 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -12,9 +12,12 @@ use Illuminate\Support\Collection; /** * Class Book. * - * @property string $description - * @property int $image_id - * @property Image|null $cover + * @property string $description + * @property int $image_id + * @property Image|null $cover + * @property \Illuminate\Database\Eloquent\Collection $chapters + * @property \Illuminate\Database\Eloquent\Collection $pages + * @property \Illuminate\Database\Eloquent\Collection $directPages */ class Book extends Entity implements HasCoverImage { diff --git a/app/Exceptions/StoppedAuthenticationException.php b/app/Exceptions/StoppedAuthenticationException.php index ef7f24017..d10a6da5e 100644 --- a/app/Exceptions/StoppedAuthenticationException.php +++ b/app/Exceptions/StoppedAuthenticationException.php @@ -55,7 +55,7 @@ class StoppedAuthenticationException extends \Exception implements Responsable ], 401); } - if (session()->get('sent-email-confirmation') === true) { + if (session()->pull('sent-email-confirmation') === true) { return redirect('/register/confirm'); } diff --git a/composer.json b/composer.json index 7362a085d..31ecbef84 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "facade/ignition": "^1.16.4", "fideloper/proxy": "^4.4.1", "intervention/image": "^2.5.1", - "laravel/framework": "^6.20.16", + "laravel/framework": "^6.20.33", "laravel/socialite": "^5.1", "league/commonmark": "^1.5", "league/flysystem-aws-s3-v3": "^1.0.29", @@ -41,9 +41,9 @@ "barryvdh/laravel-debugbar": "^3.5.1", "barryvdh/laravel-ide-helper": "^2.8.2", "fakerphp/faker": "^1.13.0", - "laravel/browser-kit-testing": "^5.2", "mockery/mockery": "^1.3.3", - "phpunit/phpunit": "^9.5.3" + "phpunit/phpunit": "^9.5.3", + "symfony/dom-crawler": "^5.3" }, "autoload": { "classmap": [ diff --git a/composer.lock b/composer.lock index b2ad6b691..d267d13d6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,23 +4,74 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4d845f3c8b77c8d73bf92c9223ddd805", + "content-hash": "10825887b8f66d1d412b92bcc0ca864f", "packages": [ { - "name": "aws/aws-sdk-php", - "version": "3.191.8", + "name": "aws/aws-crt-php", + "version": "v1.0.2", "source": { "type": "git", - "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "949feb83cc0db46f07b12aa3128d47be3b9cca63" + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "3942776a8c99209908ee0b287746263725685732" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/949feb83cc0db46f07b12aa3128d47be3b9cca63", - "reference": "949feb83cc0db46f07b12aa3128d47be3b9cca63", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/3942776a8c99209908ee0b287746263725685732", + "reference": "3942776a8c99209908ee0b287746263725685732", "shasum": "" }, "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|^5.4.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.0.2" + }, + "time": "2021-09-03T22:57:30+00:00" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.194.1", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "67bdee05acef9e8ad60098090996690b49babd09" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/67bdee05acef9e8ad60098090996690b49babd09", + "reference": "67bdee05acef9e8ad60098090996690b49babd09", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.0.2", "ext-json": "*", "ext-pcre": "*", "ext-simplexml": "*", @@ -92,9 +143,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.191.8" + "source": "https://github.com/aws/aws-sdk-php/tree/3.194.1" }, - "time": "2021-08-31T18:18:02+00:00" + "time": "2021-09-17T18:15:42+00:00" }, { "name": "bacon/bacon-qr-code", @@ -428,16 +479,16 @@ }, { "name": "doctrine/dbal", - "version": "2.13.2", + "version": "2.13.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "8dd39d2ead4409ce652fd4f02621060f009ea5e4" + "reference": "0d7adf4cadfee6f70850e5b163e6cdd706417838" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/8dd39d2ead4409ce652fd4f02621060f009ea5e4", - "reference": "8dd39d2ead4409ce652fd4f02621060f009ea5e4", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/0d7adf4cadfee6f70850e5b163e6cdd706417838", + "reference": "0d7adf4cadfee6f70850e5b163e6cdd706417838", "shasum": "" }, "require": { @@ -449,13 +500,14 @@ }, "require-dev": { "doctrine/coding-standard": "9.0.0", - "jetbrains/phpstorm-stubs": "2020.2", - "phpstan/phpstan": "0.12.81", + "jetbrains/phpstorm-stubs": "2021.1", + "phpstan/phpstan": "0.12.96", "phpunit/phpunit": "^7.5.20|^8.5|9.5.5", + "psalm/plugin-phpunit": "0.16.1", "squizlabs/php_codesniffer": "3.6.0", "symfony/cache": "^4.4", "symfony/console": "^2.0.5|^3.0|^4.0|^5.0", - "vimeo/psalm": "4.6.4" + "vimeo/psalm": "4.10.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -516,7 +568,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/2.13.2" + "source": "https://github.com/doctrine/dbal/tree/2.13.3" }, "funding": [ { @@ -532,7 +584,7 @@ "type": "tidelift" } ], - "time": "2021-06-18T21:48:39+00:00" + "time": "2021-09-12T19:11:48+00:00" }, { "name": "doctrine/deprecations", @@ -1052,16 +1104,16 @@ }, { "name": "facade/flare-client-php", - "version": "1.8.1", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/facade/flare-client-php.git", - "reference": "47b639dc02bcfdfc4ebb83de703856fa01e35f5f" + "reference": "b2adf1512755637d0cef4f7d1b54301325ac78ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/facade/flare-client-php/zipball/47b639dc02bcfdfc4ebb83de703856fa01e35f5f", - "reference": "47b639dc02bcfdfc4ebb83de703856fa01e35f5f", + "url": "https://api.github.com/repos/facade/flare-client-php/zipball/b2adf1512755637d0cef4f7d1b54301325ac78ed", + "reference": "b2adf1512755637d0cef4f7d1b54301325ac78ed", "shasum": "" }, "require": { @@ -1105,7 +1157,7 @@ ], "support": { "issues": "https://github.com/facade/flare-client-php/issues", - "source": "https://github.com/facade/flare-client-php/tree/1.8.1" + "source": "https://github.com/facade/flare-client-php/tree/1.9.1" }, "funding": [ { @@ -1113,7 +1165,7 @@ "type": "github" } ], - "time": "2021-05-31T19:23:29+00:00" + "time": "2021-09-13T12:16:46+00:00" }, { "name": "facade/ignition", @@ -1762,16 +1814,16 @@ }, { "name": "laravel/framework", - "version": "v6.20.33", + "version": "v6.20.34", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "49aa211f2dd1d419bfd9dbbd6f590d57a1dfda58" + "reference": "72a6da88c90cee793513b3fe49cf0fcb368eefa0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/49aa211f2dd1d419bfd9dbbd6f590d57a1dfda58", - "reference": "49aa211f2dd1d419bfd9dbbd6f590d57a1dfda58", + "url": "https://api.github.com/repos/laravel/framework/zipball/72a6da88c90cee793513b3fe49cf0fcb368eefa0", + "reference": "72a6da88c90cee793513b3fe49cf0fcb368eefa0", "shasum": "" }, "require": { @@ -1911,7 +1963,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2021-08-31T13:56:36+00:00" + "time": "2021-09-07T13:28:55+00:00" }, { "name": "laravel/socialite", @@ -2230,16 +2282,16 @@ }, { "name": "league/html-to-markdown", - "version": "5.0.0", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/thephpleague/html-to-markdown.git", - "reference": "c4dbebbebe0fe454b6b38e6c683a977615bd7dc2" + "reference": "e5600a2c5ce7b7571b16732c7086940f56f7abec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/c4dbebbebe0fe454b6b38e6c683a977615bd7dc2", - "reference": "c4dbebbebe0fe454b6b38e6c683a977615bd7dc2", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/e5600a2c5ce7b7571b16732c7086940f56f7abec", + "reference": "e5600a2c5ce7b7571b16732c7086940f56f7abec", "shasum": "" }, "require": { @@ -2295,7 +2347,7 @@ ], "support": { "issues": "https://github.com/thephpleague/html-to-markdown/issues", - "source": "https://github.com/thephpleague/html-to-markdown/tree/5.0.0" + "source": "https://github.com/thephpleague/html-to-markdown/tree/5.0.1" }, "funding": [ { @@ -2311,11 +2363,11 @@ "type": "github" }, { - "url": "https://www.patreon.com/colinodell", - "type": "patreon" + "url": "https://tidelift.com/funding/github/packagist/league/html-to-markdown", + "type": "tidelift" } ], - "time": "2021-03-29T01:29:08+00:00" + "time": "2021-09-17T20:00:27+00:00" }, { "name": "league/mime-type-detection", @@ -2451,24 +2503,24 @@ }, { "name": "monolog/monolog", - "version": "2.3.2", + "version": "2.3.4", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "71312564759a7db5b789296369c1a264efc43aad" + "reference": "437e7a1c50044b92773b361af77620efb76fff59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/71312564759a7db5b789296369c1a264efc43aad", - "reference": "71312564759a7db5b789296369c1a264efc43aad", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/437e7a1c50044b92773b361af77620efb76fff59", + "reference": "437e7a1c50044b92773b361af77620efb76fff59", "shasum": "" }, "require": { "php": ">=7.2", - "psr/log": "^1.0.1" + "psr/log": "^1.0.1 || ^2.0 || ^3.0" }, "provide": { - "psr/log-implementation": "1.0.0" + "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" }, "require-dev": { "aws/aws-sdk-php": "^2.4.9 || ^3.0", @@ -2483,7 +2535,7 @@ "phpunit/phpunit": "^8.5", "predis/predis": "^1.1", "rollbar/rollbar": "^1.3", - "ruflin/elastica": ">=0.90 <7.0.1", + "ruflin/elastica": ">=0.90@dev", "swiftmailer/swiftmailer": "^5.3|^6.0" }, "suggest": { @@ -2491,8 +2543,11 @@ "doctrine/couchdb": "Allow sending log messages to a CouchDB server", "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", "ext-mbstring": "Allow to work properly with unicode symbols", "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", @@ -2531,7 +2586,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/2.3.2" + "source": "https://github.com/Seldaek/monolog/tree/2.3.4" }, "funding": [ { @@ -2543,7 +2598,7 @@ "type": "tidelift" } ], - "time": "2021-07-23T07:42:52+00:00" + "time": "2021-09-15T11:27:21+00:00" }, { "name": "mtdowling/jmespath.php", @@ -2608,16 +2663,16 @@ }, { "name": "nesbot/carbon", - "version": "2.52.0", + "version": "2.53.1", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "369c0e2737c56a0f39c946dd261855255a6fccbe" + "reference": "f4655858a784988f880c1b8c7feabbf02dfdf045" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/369c0e2737c56a0f39c946dd261855255a6fccbe", - "reference": "369c0e2737c56a0f39c946dd261855255a6fccbe", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/f4655858a784988f880c1b8c7feabbf02dfdf045", + "reference": "f4655858a784988f880c1b8c7feabbf02dfdf045", "shasum": "" }, "require": { @@ -2629,7 +2684,7 @@ }, "require-dev": { "doctrine/orm": "^2.7", - "friendsofphp/php-cs-fixer": "^2.14 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.0", "kylekatarnls/multi-tester": "^2.0", "phpmd/phpmd": "^2.9", "phpstan/extension-installer": "^1.0", @@ -2698,7 +2753,7 @@ "type": "tidelift" } ], - "time": "2021-08-14T19:10:52+00:00" + "time": "2021-09-06T09:29:23+00:00" }, { "name": "nunomaduro/collision", @@ -4492,20 +4547,20 @@ }, { "name": "symfony/css-selector", - "version": "v4.4.27", + "version": "v5.3.4", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "5194f18bd80d106f11efa8f7cd0fbdcc3af96ce6" + "reference": "7fb120adc7f600a59027775b224c13a33530dd90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/5194f18bd80d106f11efa8f7cd0fbdcc3af96ce6", - "reference": "5194f18bd80d106f11efa8f7cd0fbdcc3af96ce6", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/7fb120adc7f600a59027775b224c13a33530dd90", + "reference": "7fb120adc7f600a59027775b224c13a33530dd90", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", "symfony/polyfill-php80": "^1.16" }, "type": "library", @@ -4538,7 +4593,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v4.4.27" + "source": "https://github.com/symfony/css-selector/tree/v5.3.4" }, "funding": [ { @@ -4554,7 +4609,7 @@ "type": "tidelift" } ], - "time": "2021-07-21T12:19:41+00:00" + "time": "2021-07-21T12:38:00+00:00" }, { "name": "symfony/debug", @@ -6884,16 +6939,16 @@ }, { "name": "composer/composer", - "version": "2.1.6", + "version": "2.1.8", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "e5cac5f9d2354d08b67f1d21c664ae70d748c603" + "reference": "24d38e9686092de05214cafa187dc282a5d89497" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/e5cac5f9d2354d08b67f1d21c664ae70d748c603", - "reference": "e5cac5f9d2354d08b67f1d21c664ae70d748c603", + "url": "https://api.github.com/repos/composer/composer/zipball/24d38e9686092de05214cafa187dc282a5d89497", + "reference": "24d38e9686092de05214cafa187dc282a5d89497", "shasum": "" }, "require": { @@ -6962,7 +7017,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", - "source": "https://github.com/composer/composer/tree/2.1.6" + "source": "https://github.com/composer/composer/tree/2.1.8" }, "funding": [ { @@ -6978,7 +7033,7 @@ "type": "tidelift" } ], - "time": "2021-08-19T15:11:08+00:00" + "time": "2021-09-15T11:55:15+00:00" }, { "name": "composer/metadata-minifier", @@ -7344,21 +7399,21 @@ }, { "name": "fakerphp/faker", - "version": "v1.15.0", + "version": "v1.16.0", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "89c6201c74db25fa759ff16e78a4d8f32547770e" + "reference": "271d384d216e5e5c468a6b28feedf95d49f83b35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/89c6201c74db25fa759ff16e78a4d8f32547770e", - "reference": "89c6201c74db25fa759ff16e78a4d8f32547770e", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/271d384d216e5e5c468a6b28feedf95d49f83b35", + "reference": "271d384d216e5e5c468a6b28feedf95d49f83b35", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", - "psr/container": "^1.0", + "psr/container": "^1.0 || ^2.0", "symfony/deprecation-contracts": "^2.2" }, "conflict": { @@ -7378,7 +7433,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "v1.15-dev" + "dev-main": "v1.16-dev" } }, "autoload": { @@ -7403,9 +7458,9 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.15.0" + "source": "https://github.com/FakerPHP/Faker/tree/v1.16.0" }, - "time": "2021-07-06T20:39:40+00:00" + "time": "2021-09-06T14:53:37+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -7528,71 +7583,6 @@ }, "time": "2021-07-22T09:24:00+00:00" }, - { - "name": "laravel/browser-kit-testing", - "version": "v5.2.0", - "source": { - "type": "git", - "url": "https://github.com/laravel/browser-kit-testing.git", - "reference": "fa0efb279c009e2a276f934f8aff946caf66edc7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/fa0efb279c009e2a276f934f8aff946caf66edc7", - "reference": "fa0efb279c009e2a276f934f8aff946caf66edc7", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "illuminate/contracts": "~5.7.0|~5.8.0|^6.0", - "illuminate/database": "~5.7.0|~5.8.0|^6.0", - "illuminate/http": "~5.7.0|~5.8.0|^6.0", - "illuminate/support": "~5.7.0|~5.8.0|^6.0", - "mockery/mockery": "^1.0", - "php": "^7.1.3|^8.0", - "phpunit/phpunit": "^7.5|^8.0|^9.3", - "symfony/console": "^4.2", - "symfony/css-selector": "^4.2", - "symfony/dom-crawler": "^4.2", - "symfony/http-foundation": "^4.2", - "symfony/http-kernel": "^4.2" - }, - "require-dev": { - "laravel/framework": "~5.7.0|~5.8.0|^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "Laravel\\BrowserKitTesting\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "Provides backwards compatibility for BrowserKit testing in the latest Laravel release.", - "keywords": [ - "laravel", - "testing" - ], - "support": { - "issues": "https://github.com/laravel/browser-kit-testing/issues", - "source": "https://github.com/laravel/browser-kit-testing/tree/v5.2.0" - }, - "time": "2020-10-30T08:49:09+00:00" - }, { "name": "maximebf/debugbar", "version": "v1.17.1", @@ -7660,16 +7650,16 @@ }, { "name": "mockery/mockery", - "version": "1.4.3", + "version": "1.4.4", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "d1339f64479af1bee0e82a0413813fe5345a54ea" + "reference": "e01123a0e847d52d186c5eb4b9bf58b0c6d00346" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/d1339f64479af1bee0e82a0413813fe5345a54ea", - "reference": "d1339f64479af1bee0e82a0413813fe5345a54ea", + "url": "https://api.github.com/repos/mockery/mockery/zipball/e01123a0e847d52d186c5eb4b9bf58b0c6d00346", + "reference": "e01123a0e847d52d186c5eb4b9bf58b0c6d00346", "shasum": "" }, "require": { @@ -7726,9 +7716,9 @@ ], "support": { "issues": "https://github.com/mockery/mockery/issues", - "source": "https://github.com/mockery/mockery/tree/1.4.3" + "source": "https://github.com/mockery/mockery/tree/1.4.4" }, - "time": "2021-02-24T09:51:49+00:00" + "time": "2021-09-13T15:28:59+00:00" }, { "name": "myclabs/deep-copy", @@ -8066,16 +8056,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.4.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + "reference": "30f38bffc6f24293dadd1823936372dfa9e86e2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/30f38bffc6f24293dadd1823936372dfa9e86e2f", + "reference": "30f38bffc6f24293dadd1823936372dfa9e86e2f", "shasum": "" }, "require": { @@ -8083,7 +8073,8 @@ "phpdocumentor/reflection-common": "^2.0" }, "require-dev": { - "ext-tokenizer": "*" + "ext-tokenizer": "*", + "psalm/phar": "^4.8" }, "type": "library", "extra": { @@ -8109,39 +8100,39 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.5.0" }, - "time": "2020-09-17T18:55:26+00:00" + "time": "2021-09-17T15:28:14+00:00" }, { "name": "phpspec/prophecy", - "version": "1.13.0", + "version": "1.14.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea" + "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e", + "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e", "shasum": "" }, "require": { "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.1", + "php": "^7.2 || ~8.0, <8.2", "phpdocumentor/reflection-docblock": "^5.2", "sebastian/comparator": "^3.0 || ^4.0", "sebastian/recursion-context": "^3.0 || ^4.0" }, "require-dev": { - "phpspec/phpspec": "^6.0", + "phpspec/phpspec": "^6.0 || ^7.0", "phpunit/phpunit": "^8.0 || ^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { @@ -8176,29 +8167,29 @@ ], "support": { "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/1.13.0" + "source": "https://github.com/phpspec/prophecy/tree/1.14.0" }, - "time": "2021-03-17T13:42:18+00:00" + "time": "2021-09-10T09:02:12+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.6", + "version": "9.2.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f6293e1b30a2354e8428e004689671b83871edde" + "reference": "d4c798ed8d51506800b441f7a13ecb0f76f12218" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde", - "reference": "f6293e1b30a2354e8428e004689671b83871edde", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d4c798ed8d51506800b441f7a13ecb0f76f12218", + "reference": "d4c798ed8d51506800b441f7a13ecb0f76f12218", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.10.2", + "nikic/php-parser": "^4.12.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -8247,7 +8238,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.7" }, "funding": [ { @@ -8255,7 +8246,7 @@ "type": "github" } ], - "time": "2021-03-28T07:26:59+00:00" + "time": "2021-09-17T05:39:03+00:00" }, { "name": "phpunit/php-file-iterator", @@ -9728,20 +9719,21 @@ }, { "name": "symfony/dom-crawler", - "version": "v4.4.30", + "version": "v5.3.7", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "4632ae3567746c7e915c33c67a2fb6ab746090c4" + "reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/4632ae3567746c7e915c33c67a2fb6ab746090c4", - "reference": "4632ae3567746c7e915c33c67a2fb6ab746090c4", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/c7eef3a60ccfdd8eafe07f81652e769ac9c7146c", + "reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "^1.16" @@ -9751,7 +9743,7 @@ }, "require-dev": { "masterminds/html5": "^2.6", - "symfony/css-selector": "^3.4|^4.0|^5.0" + "symfony/css-selector": "^4.4|^5.0" }, "suggest": { "symfony/css-selector": "" @@ -9782,7 +9774,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v4.4.30" + "source": "https://github.com/symfony/dom-crawler/tree/v5.3.7" }, "funding": [ { @@ -9798,7 +9790,7 @@ "type": "tidelift" } ], - "time": "2021-08-28T15:40:01+00:00" + "time": "2021-08-29T19:32:13+00:00" }, { "name": "symfony/filesystem", diff --git a/tests/ActivityTrackingTest.php b/tests/ActivityTrackingTest.php deleted file mode 100644 index 494a1f506..000000000 --- a/tests/ActivityTrackingTest.php +++ /dev/null @@ -1,37 +0,0 @@ -take(10); - - $this->asAdmin()->visit('/books') - ->dontSeeInElement('#recents', $books[0]->name) - ->dontSeeInElement('#recents', $books[1]->name) - ->visit($books[0]->getUrl()) - ->visit($books[1]->getUrl()) - ->visit('/books') - ->seeInElement('#recents', $books[0]->name) - ->seeInElement('#recents', $books[1]->name); - } - - public function test_popular_books() - { - $books = Book::all()->take(10); - - $this->asAdmin()->visit('/books') - ->dontSeeInElement('#popular', $books[0]->name) - ->dontSeeInElement('#popular', $books[1]->name) - ->visit($books[0]->getUrl()) - ->visit($books[1]->getUrl()) - ->visit($books[0]->getUrl()) - ->visit('/books') - ->seeInNthElement('#popular .book', 0, $books[0]->name) - ->seeInNthElement('#popular .book', 1, $books[1]->name); - } -} diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php index 2380aad7b..a718b14b6 100644 --- a/tests/Auth/AuthTest.php +++ b/tests/Auth/AuthTest.php @@ -3,49 +3,41 @@ namespace Tests\Auth; use BookStack\Auth\Access\Mfa\MfaSession; -use BookStack\Auth\Role; use BookStack\Auth\User; use BookStack\Entities\Models\Page; use BookStack\Notifications\ConfirmEmail; use BookStack\Notifications\ResetPassword; -use BookStack\Settings\SettingService; use DB; -use Hash; use Illuminate\Support\Facades\Notification; -use Illuminate\Support\Str; -use Tests\BrowserKitTest; +use Tests\TestCase; +use Tests\TestResponse; -class AuthTest extends BrowserKitTest +class AuthTest extends TestCase { public function test_auth_working() { - $this->visit('/') - ->seePageIs('/login'); + $this->get('/')->assertRedirect('/login'); } public function test_login() { - $this->login('admin@admin.com', 'password') - ->seePageIs('/'); + $this->login('admin@admin.com', 'password')->assertRedirect('/'); } public function test_public_viewing() { - $settings = app(SettingService::class); - $settings->put('app-public', 'true'); - $this->visit('/') - ->seePageIs('/') - ->see('Log In'); + $this->setSettings(['app-public' => 'true']); + $this->get('/') + ->assertOk() + ->assertSee('Log in'); } public function test_registration_showing() { // Ensure registration form is showing $this->setSettings(['registration-enabled' => 'true']); - $this->visit('/login') - ->see('Sign up') - ->click('Sign up') - ->seePageIs('/register'); + $this->get('/login') + ->assertElementContains('a[href="' . url('/register') . '"]', 'Sign up'); } public function test_normal_registration() @@ -55,15 +47,17 @@ class AuthTest extends BrowserKitTest $user = factory(User::class)->make(); // Test form and ensure user is created - $this->visit('/register') - ->see('Sign Up') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/') - ->see($user->name) - ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email]); + $this->get('/register') + ->assertSee('Sign Up') + ->assertElementContains('form[action="' . url('/register') . '"]', 'Create Account'); + + $resp = $this->post('/register', $user->only('password', 'name', 'email')); + $resp->assertRedirect('/'); + + $resp = $this->get('/'); + $resp->assertOk(); + $resp->assertSee($user->name); + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email]); } public function test_empty_registration_redirects_back_with_errors() @@ -72,36 +66,33 @@ class AuthTest extends BrowserKitTest $this->setSettings(['registration-enabled' => 'true']); // Test form and ensure user is created - $this->visit('/register') - ->press('Create Account') - ->see('The name field is required') - ->seePageIs('/register'); + $this->get('/register'); + $this->post('/register', [])->assertRedirect('/register'); + $this->get('/register')->assertSee('The name field is required'); } public function test_registration_validation() { $this->setSettings(['registration-enabled' => 'true']); - $this->visit('/register') - ->type('1', '#name') - ->type('1', '#email') - ->type('1', '#password') - ->press('Create Account') - ->see('The name must be at least 2 characters.') - ->see('The email must be a valid email address.') - ->see('The password must be at least 8 characters.') - ->seePageIs('/register'); + $this->get('/register'); + $resp = $this->followingRedirects()->post('/register', [ + 'name' => '1', + 'email' => '1', + 'password' => '1', + ]); + $resp->assertSee('The name must be at least 2 characters.'); + $resp->assertSee('The email must be a valid email address.'); + $resp->assertSee('The password must be at least 8 characters.'); } public function test_sign_up_link_on_login() { - $this->visit('/login') - ->dontSee('Sign up'); + $this->get('/login')->assertDontSee('Sign up'); $this->setSettings(['registration-enabled' => 'true']); - $this->visit('/login') - ->see('Sign up'); + $this->get('/login')->assertSee('Sign up'); } public function test_confirmed_registration() @@ -114,27 +105,24 @@ class AuthTest extends BrowserKitTest $user = factory(User::class)->make(); // Go through registration process - $this->visit('/register') - ->see('Sign Up') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/register/confirm') - ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); + $resp = $this->post('/register', $user->only('name', 'email', 'password')); + $resp->assertRedirect('/register/confirm'); + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); // Ensure notification sent - $dbUser = User::where('email', '=', $user->email)->first(); + /** @var User $dbUser */ + $dbUser = User::query()->where('email', '=', $user->email)->first(); Notification::assertSentTo($dbUser, ConfirmEmail::class); // Test access and resend confirmation email - $this->login($user->email, $user->password) - ->seePageIs('/register/confirm/awaiting') - ->see('Resend') - ->visit('/books') - ->seePageIs('/login') - ->visit('/register/confirm/awaiting') - ->press('Resend Confirmation Email'); + $resp = $this->login($user->email, $user->password); + $resp->assertRedirect('/register/confirm/awaiting'); + + $resp = $this->get('/register/confirm/awaiting'); + $resp->assertElementContains('form[action="' . url('/register/confirm/resend') . '"]', 'Resend'); + + $this->get('/books')->assertRedirect('/login'); + $this->post('/register/confirm/resend', $user->only('email')); // Get confirmation and confirm notification matches $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first(); @@ -143,188 +131,69 @@ class AuthTest extends BrowserKitTest }); // Check confirmation email confirmation activation. - $this->visit('/register/confirm/' . $emailConfirmation->token) - ->seePageIs('/') - ->see($user->name) - ->notSeeInDatabase('email_confirmations', ['token' => $emailConfirmation->token]) - ->seeInDatabase('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]); + $this->get('/register/confirm/' . $emailConfirmation->token)->assertRedirect('/'); + $this->get('/')->assertSee($user->name); + $this->assertDatabaseMissing('email_confirmations', ['token' => $emailConfirmation->token]); + $this->assertDatabaseHas('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]); } public function test_restricted_registration() { $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']); $user = factory(User::class)->make(); + // Go through registration process - $this->visit('/register') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/register') - ->dontSeeInDatabase('users', ['email' => $user->email]) - ->see('That email domain does not have access to this application'); + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register'); + $resp = $this->get('/register'); + $resp->assertSee('That email domain does not have access to this application'); + $this->assertDatabaseMissing('users', $user->only('email')); $user->email = 'barry@example.com'; - $this->visit('/register') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/register/confirm') - ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register/confirm'); + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); $this->assertNull(auth()->user()); - $this->visit('/')->seePageIs('/login') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Log In') - ->seePageIs('/register/confirm/awaiting') - ->seeText('Email Address Not Confirmed'); + $this->get('/')->assertRedirect('/login'); + $resp = $this->followingRedirects()->post('/login', $user->only('email', 'password')); + $resp->assertSee('Email Address Not Confirmed'); + $this->assertNull(auth()->user()); } public function test_restricted_registration_with_confirmation_disabled() { $this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'false', 'registration-restrict' => 'example.com']); $user = factory(User::class)->make(); + // Go through registration process - $this->visit('/register') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/register') - ->dontSeeInDatabase('users', ['email' => $user->email]) - ->see('That email domain does not have access to this application'); + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register'); + $this->assertDatabaseMissing('users', $user->only('email')); + $this->get('/register')->assertSee('That email domain does not have access to this application'); $user->email = 'barry@example.com'; - $this->visit('/register') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Create Account') - ->seePageIs('/register/confirm') - ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); + $this->post('/register', $user->only('name', 'email', 'password')) + ->assertRedirect('/register/confirm'); + $this->assertDatabaseHas('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); $this->assertNull(auth()->user()); - $this->visit('/')->seePageIs('/login') - ->type($user->email, '#email') - ->type($user->password, '#password') - ->press('Log In') - ->seePageIs('/register/confirm/awaiting') - ->seeText('Email Address Not Confirmed'); - } - - public function test_user_creation() - { - /** @var User $user */ - $user = factory(User::class)->make(); - $adminRole = Role::getRole('admin'); - - $this->asAdmin() - ->visit('/settings/users') - ->click('Add New User') - ->type($user->name, '#name') - ->type($user->email, '#email') - ->check("roles[{$adminRole->id}]") - ->type($user->password, '#password') - ->type($user->password, '#password-confirm') - ->press('Save') - ->seePageIs('/settings/users') - ->seeInDatabase('users', $user->only(['name', 'email'])) - ->see($user->name); - - $user->refresh(); - $this->assertStringStartsWith(Str::slug($user->name), $user->slug); - } - - public function test_user_updating() - { - $user = $this->getNormalUser(); - $password = $user->password; - $this->asAdmin() - ->visit('/settings/users') - ->click($user->name) - ->seePageIs('/settings/users/' . $user->id) - ->see($user->email) - ->type('Barry Scott', '#name') - ->press('Save') - ->seePageIs('/settings/users') - ->seeInDatabase('users', ['id' => $user->id, 'name' => 'Barry Scott', 'password' => $password]) - ->notSeeInDatabase('users', ['name' => $user->name]); - - $user->refresh(); - $this->assertStringStartsWith(Str::slug($user->name), $user->slug); - } - - public function test_user_password_update() - { - $user = $this->getNormalUser(); - $userProfilePage = '/settings/users/' . $user->id; - $this->asAdmin() - ->visit($userProfilePage) - ->type('newpassword', '#password') - ->press('Save') - ->seePageIs($userProfilePage) - ->see('Password confirmation required') - - ->type('newpassword', '#password') - ->type('newpassword', '#password-confirm') - ->press('Save') - ->seePageIs('/settings/users'); - - $userPassword = User::find($user->id)->password; - $this->assertTrue(Hash::check('newpassword', $userPassword)); - } - - public function test_user_deletion() - { - $userDetails = factory(User::class)->make(); - $user = $this->getEditor($userDetails->toArray()); - - $this->asAdmin() - ->visit('/settings/users/' . $user->id) - ->click('Delete User') - ->see($user->name) - ->press('Confirm') - ->seePageIs('/settings/users') - ->notSeeInDatabase('users', ['name' => $user->name]); - } - - public function test_user_cannot_be_deleted_if_last_admin() - { - $adminRole = Role::getRole('admin'); - - // Delete all but one admin user if there are more than one - $adminUsers = $adminRole->users; - if (count($adminUsers) > 1) { - foreach ($adminUsers->splice(1) as $user) { - $user->delete(); - } - } - - // Ensure we currently only have 1 admin user - $this->assertEquals(1, $adminRole->users()->count()); - $user = $adminRole->users->first(); - - $this->asAdmin()->visit('/settings/users/' . $user->id) - ->click('Delete User') - ->press('Confirm') - ->seePageIs('/settings/users/' . $user->id) - ->see('You cannot delete the only admin'); + $this->get('/')->assertRedirect('/login'); + $resp = $this->post('/login', $user->only('email', 'password')); + $resp->assertRedirect('/register/confirm/awaiting'); + $this->get('/register/confirm/awaiting')->assertSee('Email Address Not Confirmed'); + $this->assertNull(auth()->user()); } public function test_logout() { - $this->asAdmin() - ->visit('/') - ->seePageIs('/') - ->visit('/logout') - ->visit('/') - ->seePageIs('/login'); + $this->asAdmin()->get('/')->assertOk(); + $this->get('/logout')->assertRedirect('/'); + $this->get('/')->assertRedirect('/login'); } public function test_mfa_session_cleared_on_logout() @@ -335,7 +204,7 @@ class AuthTest extends BrowserKitTest $mfaSession->markVerifiedForUser($user); $this->assertTrue($mfaSession->isVerifiedForUser($user)); - $this->asAdmin()->visit('/logout'); + $this->asAdmin()->get('/logout'); $this->assertFalse($mfaSession->isVerifiedForUser($user)); } @@ -343,69 +212,85 @@ class AuthTest extends BrowserKitTest { Notification::fake(); - $this->visit('/login')->click('Forgot Password?') - ->seePageIs('/password/email') - ->type('admin@admin.com', 'email') - ->press('Send Reset Link') - ->see('A password reset link will be sent to admin@admin.com if that email address is found in the system.'); + $this->get('/login') + ->assertElementContains('a[href="' . url('/password/email') . '"]', 'Forgot Password?'); - $this->seeInDatabase('password_resets', [ + $this->get('/password/email') + ->assertElementContains('form[action="' . url('/password/email') . '"]', 'Send Reset Link'); + + $resp = $this->post('/password/email', [ + 'email' => 'admin@admin.com', + ]); + $resp->assertRedirect('/password/email'); + + $resp = $this->get('/password/email'); + $resp->assertSee('A password reset link will be sent to admin@admin.com if that email address is found in the system.'); + + $this->assertDatabaseHas('password_resets', [ 'email' => 'admin@admin.com', ]); - $user = User::where('email', '=', 'admin@admin.com')->first(); + /** @var User $user */ + $user = User::query()->where('email', '=', 'admin@admin.com')->first(); Notification::assertSentTo($user, ResetPassword::class); $n = Notification::sent($user, ResetPassword::class); - $this->visit('/password/reset/' . $n->first()->token) - ->see('Reset Password') - ->submitForm('Reset Password', [ - 'email' => 'admin@admin.com', - 'password' => 'randompass', - 'password_confirmation' => 'randompass', - ])->seePageIs('/') - ->see('Your password has been successfully reset'); + $this->get('/password/reset/' . $n->first()->token) + ->assertOk() + ->assertSee('Reset Password'); + + $resp = $this->post('/password/reset', [ + 'email' => 'admin@admin.com', + 'password' => 'randompass', + 'password_confirmation' => 'randompass', + 'token' => $n->first()->token, + ]); + $resp->assertRedirect('/'); + + $this->get('/')->assertSee('Your password has been successfully reset'); } public function test_reset_password_flow_shows_success_message_even_if_wrong_password_to_prevent_user_discovery() { - $this->visit('/login')->click('Forgot Password?') - ->seePageIs('/password/email') - ->type('barry@admin.com', 'email') - ->press('Send Reset Link') - ->see('A password reset link will be sent to barry@admin.com if that email address is found in the system.') - ->dontSee('We can\'t find a user'); + $this->get('/password/email'); + $resp = $this->followingRedirects()->post('/password/email', [ + 'email' => 'barry@admin.com', + ]); + $resp->assertSee('A password reset link will be sent to barry@admin.com if that email address is found in the system.'); + $resp->assertDontSee('We can\'t find a user'); - $this->visit('/password/reset/arandometokenvalue') - ->see('Reset Password') - ->submitForm('Reset Password', [ - 'email' => 'barry@admin.com', - 'password' => 'randompass', - 'password_confirmation' => 'randompass', - ])->followRedirects() - ->seePageIs('/password/reset/arandometokenvalue') - ->dontSee('We can\'t find a user') - ->see('The password reset token is invalid for this email address.'); + $this->get('/password/reset/arandometokenvalue')->assertSee('Reset Password'); + $resp = $this->post('/password/reset', [ + 'email' => 'barry@admin.com', + 'password' => 'randompass', + 'password_confirmation' => 'randompass', + 'token' => 'arandometokenvalue', + ]); + $resp->assertRedirect('/password/reset/arandometokenvalue'); + + $this->get('/password/reset/arandometokenvalue') + ->assertDontSee('We can\'t find a user') + ->assertSee('The password reset token is invalid for this email address.'); } public function test_reset_password_page_shows_sign_links() { $this->setSettings(['registration-enabled' => 'true']); - $this->visit('/password/email') - ->seeLink('Log in') - ->seeLink('Sign up'); + $this->get('/password/email') + ->assertElementContains('a', 'Log in') + ->assertElementContains('a', 'Sign up'); } public function test_login_redirects_to_initially_requested_url_correctly() { config()->set('app.url', 'http://localhost'); + /** @var Page $page */ $page = Page::query()->first(); - $this->visit($page->getUrl()) - ->seePageUrlIs(url('/login')); + $this->get($page->getUrl())->assertRedirect(url('/login')); $this->login('admin@admin.com', 'password') - ->seePageUrlIs($page->getUrl()); + ->assertRedirect($page->getUrl()); } public function test_login_intended_redirect_does_not_redirect_to_external_pages() @@ -416,15 +301,15 @@ class AuthTest extends BrowserKitTest $this->get('/login', ['referer' => 'https://example.com']); $login = $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']); - $login->assertRedirectedTo('http://localhost'); + $login->assertRedirect('http://localhost'); } public function test_login_intended_redirect_does_not_factor_mfa_routes() { - $this->get('/books')->assertRedirectedTo('/login'); - $this->get('/mfa/setup')->assertRedirectedTo('/login'); + $this->get('/books')->assertRedirect('/login'); + $this->get('/mfa/setup')->assertRedirect('/login'); $login = $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']); - $login->assertRedirectedTo('/books'); + $login->assertRedirect('/books'); } public function test_login_authenticates_admins_on_all_guards() @@ -469,20 +354,15 @@ class AuthTest extends BrowserKitTest auth()->login($user); $this->assertTrue(auth()->check()); - $this->get('/books'); - $this->assertRedirectedTo('/'); - + $this->get('/books')->assertRedirect('/'); $this->assertFalse(auth()->check()); } /** * Perform a login. */ - protected function login(string $email, string $password): AuthTest + protected function login(string $email, string $password): TestResponse { - return $this->visit('/login') - ->type($email, '#email') - ->type($password, '#password') - ->press('Log In'); + return $this->post('/login', compact('email', 'password')); } } diff --git a/tests/BrowserKitTest.php b/tests/BrowserKitTest.php deleted file mode 100644 index 23eb10887..000000000 --- a/tests/BrowserKitTest.php +++ /dev/null @@ -1,218 +0,0 @@ -make(Kernel::class)->bootstrap(); - - return $app; - } - - /** - * Quickly sets an array of settings. - * - * @param $settingsArray - */ - protected function setSettings($settingsArray) - { - $settings = app(SettingService::class); - foreach ($settingsArray as $key => $value) { - $settings->put($key, $value); - } - } - - /** - * Create a group of entities that belong to a specific user. - */ - protected function createEntityChainBelongingToUser(User $creatorUser, ?User $updaterUser = null): array - { - 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 compact('book', 'chapter', 'page'); - } - - /** - * Helper for updating entity permissions. - * - * @param Entity $entity - */ - protected function updateEntityPermissions(Entity $entity) - { - $restrictionService = $this->app[PermissionService::class]; - $restrictionService->buildJointPermissionsForEntity($entity); - } - - /** - * Quick way to create a new user without any permissions. - * - * @param array $attributes - * - * @return mixed - */ - protected function getNewBlankUser($attributes = []) - { - $user = factory(User::class)->create($attributes); - - return $user; - } - - /** - * Assert that a given string is seen inside an element. - * - * @param bool|string|null $element - * @param int $position - * @param string $text - * @param bool $negate - * - * @return $this - */ - protected function seeInNthElement($element, $position, $text, $negate = false) - { - $method = $negate ? 'assertDoesNotMatchRegularExpression' : 'assertMatchesRegularExpression'; - - $rawPattern = preg_quote($text, '/'); - - $escapedPattern = preg_quote(e($text), '/'); - - $content = $this->crawler->filter($element)->eq($position)->html(); - - $pattern = $rawPattern == $escapedPattern - ? $rawPattern : "({$rawPattern}|{$escapedPattern})"; - - $this->$method("/$pattern/i", $content); - - return $this; - } - - /** - * Assert that the current page matches a given URI. - * - * @param string $uri - * - * @return $this - */ - protected function seePageUrlIs($uri) - { - $this->assertEquals( - $uri, - $this->currentUri, - "Did not land on expected page [{$uri}].\n" - ); - - return $this; - } - - /** - * Do a forced visit that does not error out on exception. - * - * @param string $uri - * @param array $parameters - * @param array $cookies - * @param array $files - * - * @return $this - */ - protected function forceVisit($uri, $parameters = [], $cookies = [], $files = []) - { - $method = 'GET'; - $uri = $this->prepareUrlForRequest($uri); - $this->call($method, $uri, $parameters, $cookies, $files); - $this->clearInputs()->followRedirects(); - $this->currentUri = $this->app->make('request')->fullUrl(); - $this->crawler = new Crawler($this->response->getContent(), $uri); - - return $this; - } - - /** - * Click the text within the selected element. - * - * @param $parentElement - * @param $linkText - * - * @return $this - */ - protected function clickInElement($parentElement, $linkText) - { - $elem = $this->crawler->filter($parentElement); - $link = $elem->selectLink($linkText); - $this->visit($link->link()->getUri()); - - return $this; - } - - /** - * Check if the page contains the given element. - * - * @param string $selector - */ - protected function pageHasElement($selector) - { - $elements = $this->crawler->filter($selector); - $this->assertTrue(count($elements) > 0, 'The page does not contain an element matching ' . $selector); - - return $this; - } - - /** - * Check if the page contains the given element. - * - * @param string $selector - */ - protected function pageNotHasElement($selector) - { - $elements = $this->crawler->filter($selector); - $this->assertFalse(count($elements) > 0, 'The page contains ' . count($elements) . ' elements matching ' . $selector); - - return $this; - } -} diff --git a/tests/Entity/BookShelfTest.php b/tests/Entity/BookShelfTest.php index 480d29002..1780ddee8 100644 --- a/tests/Entity/BookShelfTest.php +++ b/tests/Entity/BookShelfTest.php @@ -369,4 +369,12 @@ class BookShelfTest extends TestCase $resp = $this->asEditor()->get($newBook->getUrl()); $resp->assertDontSee($shelfInfo['name']); } + + public function test_cancel_on_child_book_creation_returns_to_original_shelf() + { + /** @var Bookshelf $shelf */ + $shelf = Bookshelf::query()->first(); + $resp = $this->asEditor()->get($shelf->getUrl('/create-book')); + $resp->assertElementContains('form a[href="' . $shelf->getUrl() . '"]', 'Cancel'); + } } diff --git a/tests/Entity/BookTest.php b/tests/Entity/BookTest.php index b4ba2fa82..fa63c0bf9 100644 --- a/tests/Entity/BookTest.php +++ b/tests/Entity/BookTest.php @@ -7,7 +7,69 @@ use Tests\TestCase; class BookTest extends TestCase { - public function test_book_delete() + public function test_create() + { + $book = factory(Book::class)->make([ + 'name' => 'My First Book', + ]); + + $resp = $this->asEditor()->get('/books'); + $resp->assertElementContains('a[href="' . url('/create-book') . '"]', 'Create New Book'); + + $resp = $this->get('/create-book'); + $resp->assertElementContains('form[action="' . url('/books') . '"][method="POST"]', 'Save Book'); + + $resp = $this->post('/books', $book->only('name', 'description')); + $resp->assertRedirect('/books/my-first-book'); + + $resp = $this->get('/books/my-first-book'); + $resp->assertSee($book->name); + $resp->assertSee($book->description); + } + + public function test_create_uses_different_slugs_when_name_reused() + { + $book = factory(Book::class)->make([ + 'name' => 'My First Book', + ]); + + $this->asEditor()->post('/books', $book->only('name', 'description')); + $this->asEditor()->post('/books', $book->only('name', 'description')); + + $books = Book::query()->where('name', '=', $book->name) + ->orderBy('id', 'desc') + ->take(2) + ->get(); + + $this->assertMatchesRegularExpression('/my-first-book-[0-9a-zA-Z]{3}/', $books[0]->slug); + $this->assertEquals('my-first-book', $books[1]->slug); + } + + public function test_update() + { + /** @var Book $book */ + $book = Book::query()->first(); + // Cheeky initial update to refresh slug + $this->asEditor()->put($book->getUrl(), ['name' => $book->name . '5', 'description' => $book->description]); + $book->refresh(); + + $newName = $book->name . ' Updated'; + $newDesc = $book->description . ' with more content'; + + $resp = $this->get($book->getUrl('/edit')); + $resp->assertSee($book->name); + $resp->assertSee($book->description); + $resp->assertElementContains('form[action="' . $book->getUrl() . '"]', 'Save Book'); + + $resp = $this->put($book->getUrl(), ['name' => $newName, 'description' => $newDesc]); + $resp->assertRedirect($book->getUrl() . '-updated'); + + $resp = $this->get($book->getUrl() . '-updated'); + $resp->assertSee($newName); + $resp->assertSee($newDesc); + } + + public function test_delete() { $book = Book::query()->whereHas('pages')->whereHas('chapters')->first(); $this->assertNull($book->deleted_at); @@ -34,6 +96,20 @@ class BookTest extends TestCase $redirectReq->assertNotificationContains('Book Successfully Deleted'); } + public function test_cancel_on_create_page_leads_back_to_books_listing() + { + $resp = $this->asEditor()->get('/create-book'); + $resp->assertElementContains('form a[href="' . url('/books') . '"]', 'Cancel'); + } + + public function test_cancel_on_edit_book_page_leads_back_to_book() + { + /** @var Book $book */ + $book = Book::query()->first(); + $resp = $this->asEditor()->get($book->getUrl('/edit')); + $resp->assertElementContains('form a[href="' . $book->getUrl() . '"]', 'Cancel'); + } + public function test_next_previous_navigation_controls_show_within_book_content() { $book = Book::query()->first(); @@ -48,4 +124,84 @@ class BookTest extends TestCase $resp->assertElementContains('#sibling-navigation', 'Previous'); $resp->assertElementContains('#sibling-navigation', substr($chapter->name, 0, 20)); } + + public function test_recently_viewed_books_updates_as_expected() + { + $books = Book::all()->take(2); + + $this->asAdmin()->get('/books') + ->assertElementNotContains('#recents', $books[0]->name) + ->assertElementNotContains('#recents', $books[1]->name); + + $this->get($books[0]->getUrl()); + $this->get($books[1]->getUrl()); + + $this->get('/books') + ->assertElementContains('#recents', $books[0]->name) + ->assertElementContains('#recents', $books[1]->name); + } + + public function test_popular_books_updates_upon_visits() + { + $books = Book::all()->take(2); + + $this->asAdmin()->get('/books') + ->assertElementNotContains('#popular', $books[0]->name) + ->assertElementNotContains('#popular', $books[1]->name); + + $this->get($books[0]->getUrl()); + $this->get($books[1]->getUrl()); + $this->get($books[0]->getUrl()); + + $this->get('/books') + ->assertElementContains('#popular .book:nth-child(1)', $books[0]->name) + ->assertElementContains('#popular .book:nth-child(2)', $books[1]->name); + } + + public function test_books_view_shows_view_toggle_option() + { + /** @var Book $book */ + $editor = $this->getEditor(); + setting()->putUser($editor, 'books_view_type', 'list'); + + $resp = $this->actingAs($editor)->get('/books'); + $resp->assertElementContains('form[action$="/settings/users/' . $editor->id . '/switch-books-view"]', 'Grid View'); + $resp->assertElementExists('input[name="view_type"][value="grid"]'); + + $resp = $this->patch("/settings/users/{$editor->id}/switch-books-view", ['view_type' => 'grid']); + $resp->assertRedirect(); + $this->assertEquals('grid', setting()->getUser($editor, 'books_view_type')); + + $resp = $this->actingAs($editor)->get('/books'); + $resp->assertElementContains('form[action$="/settings/users/' . $editor->id . '/switch-books-view"]', 'List View'); + $resp->assertElementExists('input[name="view_type"][value="list"]'); + + $resp = $this->patch("/settings/users/{$editor->id}/switch-books-view", ['view_type' => 'list']); + $resp->assertRedirect(); + $this->assertEquals('list', setting()->getUser($editor, 'books_view_type')); + } + + public function test_slug_multi_byte_url_safe() + { + $book = $this->newBook([ + 'name' => 'информация', + ]); + + $this->assertEquals('informatsiya', $book->slug); + + $book = $this->newBook([ + 'name' => '¿Qué?', + ]); + + $this->assertEquals('que', $book->slug); + } + + public function test_slug_format() + { + $book = $this->newBook([ + 'name' => 'PartA / PartB / PartC', + ]); + + $this->assertEquals('parta-partb-partc', $book->slug); + } } diff --git a/tests/Entity/ChapterTest.php b/tests/Entity/ChapterTest.php index 45c132e89..ea29ece5d 100644 --- a/tests/Entity/ChapterTest.php +++ b/tests/Entity/ChapterTest.php @@ -2,12 +2,36 @@ namespace Tests\Entity; +use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use Tests\TestCase; class ChapterTest extends TestCase { - public function test_chapter_delete() + public function test_create() + { + /** @var Book $book */ + $book = Book::query()->first(); + + $chapter = factory(Chapter::class)->make([ + 'name' => 'My First Chapter', + ]); + + $resp = $this->asEditor()->get($book->getUrl()); + $resp->assertElementContains('a[href="' . $book->getUrl('/create-chapter') . '"]', 'New Chapter'); + + $resp = $this->get($book->getUrl('/create-chapter')); + $resp->assertElementContains('form[action="' . $book->getUrl('/create-chapter') . '"][method="POST"]', 'Save Chapter'); + + $resp = $this->post($book->getUrl('/create-chapter'), $chapter->only('name', 'description')); + $resp->assertRedirect($book->getUrl('/chapter/my-first-chapter')); + + $resp = $this->get($book->getUrl('/chapter/my-first-chapter')); + $resp->assertSee($chapter->name); + $resp->assertSee($chapter->description); + } + + public function test_delete() { $chapter = Chapter::query()->whereHas('pages')->first(); $this->assertNull($chapter->deleted_at); diff --git a/tests/Entity/EntityAccessTest.php b/tests/Entity/EntityAccessTest.php new file mode 100644 index 000000000..f2f244538 --- /dev/null +++ b/tests/Entity/EntityAccessTest.php @@ -0,0 +1,52 @@ +getEditor(); + $updater = $this->getViewer(); + $entities = $this->createEntityChainBelongingToUser($creator, $updater); + app()->make(UserRepo::class)->destroy($creator); + app()->make(PageRepo::class)->update($entities['page'], ['html' => '

hello!

>']); + + $this->checkEntitiesViewable($entities); + } + + public function test_entities_viewable_after_updater_deletion() + { + // Create required assets and revisions + $creator = $this->getViewer(); + $updater = $this->getEditor(); + $entities = $this->createEntityChainBelongingToUser($creator, $updater); + app()->make(UserRepo::class)->destroy($updater); + app()->make(PageRepo::class)->update($entities['page'], ['html' => '

Hello there!

']); + + $this->checkEntitiesViewable($entities); + } + + /** + * @param array $entities + */ + private function checkEntitiesViewable(array $entities) + { + // Check pages and books are visible. + $this->asAdmin(); + foreach ($entities as $entity) { + $this->get($entity->getUrl()) + ->assertStatus(200) + ->assertSee($entity->name); + } + + // Check revision listing shows no errors. + $this->get($entities['page']->getUrl('/revisions'))->assertStatus(200); + } +} diff --git a/tests/Entity/EntityTest.php b/tests/Entity/EntityTest.php deleted file mode 100644 index f8c88b1fe..000000000 --- a/tests/Entity/EntityTest.php +++ /dev/null @@ -1,319 +0,0 @@ -bookCreation(); - $chapter = $this->chapterCreation($book); - $this->pageCreation($chapter); - - // Test Updating - $this->bookUpdate($book); - } - - public function bookUpdate(Book $book) - { - $newName = $book->name . ' Updated'; - $this->asAdmin() - // Go to edit screen - ->visit($book->getUrl() . '/edit') - ->see($book->name) - // Submit new name - ->type($newName, '#name') - ->press('Save Book') - // Check page url and text - ->seePageIs($book->getUrl() . '-updated') - ->see($newName); - - return Book::find($book->id); - } - - public function test_book_sort_page_shows() - { - $books = Book::all(); - $bookToSort = $books[0]; - $this->asAdmin() - ->visit($bookToSort->getUrl()) - ->click('Sort') - ->seePageIs($bookToSort->getUrl() . '/sort') - ->seeStatusCode(200) - ->see($bookToSort->name); - } - - public function test_book_sort_item_returns_book_content() - { - $books = Book::all(); - $bookToSort = $books[0]; - $firstPage = $bookToSort->pages[0]; - $firstChapter = $bookToSort->chapters[0]; - $this->asAdmin() - ->visit($bookToSort->getUrl() . '/sort-item') - // Ensure book details are returned - ->see($bookToSort->name) - ->see($firstPage->name) - ->see($firstChapter->name); - } - - public function test_toggle_book_view() - { - $editor = $this->getEditor(); - setting()->putUser($editor, 'books_view_type', 'grid'); - - $this->actingAs($editor) - ->visit('/books') - ->pageHasElement('.featured-image-container') - ->submitForm('List View') - // Check redirection. - ->seePageIs('/books') - ->pageNotHasElement('.featured-image-container'); - - $this->actingAs($editor) - ->visit('/books') - ->submitForm('Grid View') - ->seePageIs('/books') - ->pageHasElement('.featured-image-container'); - } - - public function pageCreation($chapter) - { - $page = factory(Page::class)->make([ - 'name' => 'My First Page', - ]); - - $this->asAdmin() - // Navigate to page create form - ->visit($chapter->getUrl()) - ->click('New Page'); - - $draftPage = Page::where('draft', '=', true)->orderBy('created_at', 'desc')->first(); - - $this->seePageIs($draftPage->getUrl()) - // Fill out form - ->type($page->name, '#name') - ->type($page->html, '#html') - ->press('Save Page') - // Check redirect and page - ->seePageIs($chapter->book->getUrl() . '/page/my-first-page') - ->see($page->name); - - $page = Page::where('slug', '=', 'my-first-page')->where('chapter_id', '=', $chapter->id)->first(); - - return $page; - } - - public function chapterCreation(Book $book) - { - $chapter = factory(Chapter::class)->make([ - 'name' => 'My First Chapter', - ]); - - $this->asAdmin() - // Navigate to chapter create page - ->visit($book->getUrl()) - ->click('New Chapter') - ->seePageIs($book->getUrl() . '/create-chapter') - // Fill out form - ->type($chapter->name, '#name') - ->type($chapter->description, '#description') - ->press('Save Chapter') - // Check redirect and landing page - ->seePageIs($book->getUrl() . '/chapter/my-first-chapter') - ->see($chapter->name)->see($chapter->description); - - $chapter = Chapter::where('slug', '=', 'my-first-chapter')->where('book_id', '=', $book->id)->first(); - - return $chapter; - } - - public function bookCreation() - { - $book = factory(Book::class)->make([ - 'name' => 'My First Book', - ]); - $this->asAdmin() - ->visit('/books') - // Choose to create a book - ->click('Create New Book') - ->seePageIs('/create-book') - // Fill out form & save - ->type($book->name, '#name') - ->type($book->description, '#description') - ->press('Save Book') - // Check it redirects correctly - ->seePageIs('/books/my-first-book') - ->see($book->name)->see($book->description); - - // Ensure duplicate names are given different slugs - $this->asAdmin() - ->visit('/create-book') - ->type($book->name, '#name') - ->type($book->description, '#description') - ->press('Save Book'); - - $expectedPattern = '/\/books\/my-first-book-[0-9a-zA-Z]{3}/'; - $this->assertMatchesRegularExpression($expectedPattern, $this->currentUri, "Did not land on expected page [$expectedPattern].\n"); - - $book = Book::where('slug', '=', 'my-first-book')->first(); - - return $book; - } - - public function test_entities_viewable_after_creator_deletion() - { - // Create required assets and revisions - $creator = $this->getEditor(); - $updater = $this->getEditor(); - $entities = $this->createEntityChainBelongingToUser($creator, $updater); - $this->actingAs($creator); - app(UserRepo::class)->destroy($creator); - app(PageRepo::class)->update($entities['page'], ['html' => '

hello!

>']); - - $this->checkEntitiesViewable($entities); - } - - public function test_entities_viewable_after_updater_deletion() - { - // Create required assets and revisions - $creator = $this->getEditor(); - $updater = $this->getEditor(); - $entities = $this->createEntityChainBelongingToUser($creator, $updater); - $this->actingAs($updater); - app(UserRepo::class)->destroy($updater); - app(PageRepo::class)->update($entities['page'], ['html' => '

Hello there!

']); - - $this->checkEntitiesViewable($entities); - } - - private function checkEntitiesViewable($entities) - { - // Check pages and books are visible. - $this->asAdmin(); - $this->visit($entities['book']->getUrl())->seeStatusCode(200) - ->visit($entities['chapter']->getUrl())->seeStatusCode(200) - ->visit($entities['page']->getUrl())->seeStatusCode(200); - // Check revision listing shows no errors. - $this->visit($entities['page']->getUrl()) - ->click('Revisions')->seeStatusCode(200); - } - - public function test_recently_updated_pages_view() - { - $user = $this->getEditor(); - $content = $this->createEntityChainBelongingToUser($user); - - $this->asAdmin()->visit('/pages/recently-updated') - ->seeInNthElement('.entity-list .page', 0, $content['page']->name); - } - - public function test_old_page_slugs_redirect_to_new_pages() - { - $page = Page::first(); - $pageUrl = $page->getUrl(); - $newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page'; - // Need to save twice since revisions are not generated in seeder. - $this->asAdmin()->visit($pageUrl) - ->clickInElement('#content', 'Edit') - ->type('super test', '#name') - ->press('Save Page'); - - $page = Page::first(); - $pageUrl = $page->getUrl(); - - // Second Save - $this->visit($pageUrl) - ->clickInElement('#content', 'Edit') - ->type('super test page', '#name') - ->press('Save Page') - // Check redirect - ->seePageIs($newPageUrl); - - $this->visit($pageUrl) - ->seePageIs($newPageUrl); - } - - public function test_recently_updated_pages_on_home() - { - $page = Page::orderBy('updated_at', 'asc')->first(); - Page::where('id', '!=', $page->id)->update([ - 'updated_at' => Carbon::now()->subSecond(1), - ]); - $this->asAdmin()->visit('/') - ->dontSeeInElement('#recently-updated-pages', $page->name); - $this->visit($page->getUrl() . '/edit') - ->press('Save Page') - ->visit('/') - ->seeInElement('#recently-updated-pages', $page->name); - } - - public function test_slug_multi_byte_url_safe() - { - $book = $this->newBook([ - 'name' => 'информация', - ]); - - $this->assertEquals('informatsiya', $book->slug); - - $book = $this->newBook([ - 'name' => '¿Qué?', - ]); - - $this->assertEquals('que', $book->slug); - } - - public function test_slug_format() - { - $book = $this->newBook([ - 'name' => 'PartA / PartB / PartC', - ]); - - $this->assertEquals('parta-partb-partc', $book->slug); - } - - public function test_shelf_cancel_creation_returns_to_correct_place() - { - $shelf = Bookshelf::first(); - - // Cancel button from shelf goes back to shelf - $this->asEditor()->visit($shelf->getUrl('/create-book')) - ->see('Cancel') - ->click('Cancel') - ->seePageIs($shelf->getUrl()); - - // Cancel button from books goes back to books - $this->asEditor()->visit('/create-book') - ->see('Cancel') - ->click('Cancel') - ->seePageIs('/books'); - - // Cancel button from book edit goes back to book - $book = Book::first(); - - $this->asEditor()->visit($book->getUrl('/edit')) - ->see('Cancel') - ->click('Cancel') - ->seePageIs($book->getUrl()); - } - - public function test_page_within_chapter_deletion_returns_to_chapter() - { - $chapter = Chapter::query()->first(); - $page = $chapter->pages()->first(); - - $this->asEditor()->visit($page->getUrl('/delete')) - ->submitForm('Confirm') - ->seePageIs($chapter->getUrl()); - } -} diff --git a/tests/Entity/MarkdownTest.php b/tests/Entity/MarkdownTest.php deleted file mode 100644 index 7de7ea11b..000000000 --- a/tests/Entity/MarkdownTest.php +++ /dev/null @@ -1,53 +0,0 @@ -page = \BookStack\Entities\Models\Page::first(); - } - - protected function setMarkdownEditor() - { - $this->setSettings(['app-editor' => 'markdown']); - } - - public function test_default_editor_is_wysiwyg() - { - $this->assertEquals(setting('app-editor'), 'wysiwyg'); - $this->asAdmin()->visit($this->page->getUrl() . '/edit') - ->pageHasElement('#html-editor'); - } - - public function test_markdown_setting_shows_markdown_editor() - { - $this->setMarkdownEditor(); - $this->asAdmin()->visit($this->page->getUrl() . '/edit') - ->pageNotHasElement('#html-editor') - ->pageHasElement('#markdown-editor'); - } - - public function test_markdown_content_given_to_editor() - { - $this->setMarkdownEditor(); - $mdContent = '# hello. This is a test'; - $this->page->markdown = $mdContent; - $this->page->save(); - $this->asAdmin()->visit($this->page->getUrl() . '/edit') - ->seeInField('markdown', $mdContent); - } - - public function test_html_content_given_to_editor_if_no_markdown() - { - $this->setMarkdownEditor(); - $this->asAdmin()->visit($this->page->getUrl() . '/edit') - ->seeInField('markdown', $this->page->html); - } -} diff --git a/tests/Entity/PageDraftTest.php b/tests/Entity/PageDraftTest.php index 68059af6e..b2fa4bb31 100644 --- a/tests/Entity/PageDraftTest.php +++ b/tests/Entity/PageDraftTest.php @@ -2,12 +2,16 @@ namespace Tests\Entity; +use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\PageRepo; -use Tests\BrowserKitTest; +use Tests\TestCase; -class PageDraftTest extends BrowserKitTest +class PageDraftTest extends TestCase { + /** + * @var Page + */ protected $page; /** @@ -18,99 +22,101 @@ class PageDraftTest extends BrowserKitTest public function setUp(): void { parent::setUp(); - $this->page = \BookStack\Entities\Models\Page::first(); - $this->pageRepo = app(PageRepo::class); + $this->page = Page::query()->first(); + $this->pageRepo = app()->make(PageRepo::class); } public function test_draft_content_shows_if_available() { $addedContent = '

test message content

'; - $this->asAdmin()->visit($this->page->getUrl('/edit')) - ->dontSeeInField('html', $addedContent); + + $this->asAdmin()->get($this->page->getUrl('/edit')) + ->assertElementNotContains('[name="html"]', $addedContent); $newContent = $this->page->html . $addedContent; $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]); - $this->asAdmin()->visit($this->page->getUrl('/edit')) - ->seeInField('html', $newContent); + $this->asAdmin()->get($this->page->getUrl('/edit')) + ->assertElementContains('[name="html"]', $newContent); } public function test_draft_not_visible_by_others() { $addedContent = '

test message content

'; - $this->asAdmin()->visit($this->page->getUrl('/edit')) - ->dontSeeInField('html', $addedContent); + $this->asAdmin()->get($this->page->getUrl('/edit')) + ->assertElementNotContains('[name="html"]', $addedContent); $newContent = $this->page->html . $addedContent; $newUser = $this->getEditor(); $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]); - $this->actingAs($newUser)->visit($this->page->getUrl('/edit')) - ->dontSeeInField('html', $newContent); + $this->actingAs($newUser)->get($this->page->getUrl('/edit')) + ->assertElementNotContains('[name="html"]', $newContent); } public function test_alert_message_shows_if_editing_draft() { $this->asAdmin(); $this->pageRepo->updatePageDraft($this->page, ['html' => 'test content']); - $this->asAdmin()->visit($this->page->getUrl('/edit')) - ->see('You are currently editing a draft'); + $this->asAdmin()->get($this->page->getUrl('/edit')) + ->assertSee('You are currently editing a draft'); } public function test_alert_message_shows_if_someone_else_editing() { - $nonEditedPage = \BookStack\Entities\Models\Page::take(10)->get()->last(); + $nonEditedPage = Page::query()->take(10)->get()->last(); $addedContent = '

test message content

'; - $this->asAdmin()->visit($this->page->getUrl('/edit')) - ->dontSeeInField('html', $addedContent); + $this->asAdmin()->get($this->page->getUrl('/edit')) + ->assertElementNotContains('[name="html"]', $addedContent); $newContent = $this->page->html . $addedContent; $newUser = $this->getEditor(); $this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]); $this->actingAs($newUser) - ->visit($this->page->getUrl('/edit')) - ->see('Admin has started editing this page'); + ->get($this->page->getUrl('/edit')) + ->assertSee('Admin has started editing this page'); $this->flushSession(); - $this->visit($nonEditedPage->getUrl() . '/edit') - ->dontSeeInElement('.notification', 'Admin has started editing this page'); + $this->get($nonEditedPage->getUrl() . '/edit') + ->assertElementNotContains('.notification', 'Admin has started editing this page'); } public function test_draft_pages_show_on_homepage() { - $book = \BookStack\Entities\Models\Book::first(); - $this->asAdmin()->visit('/') - ->dontSeeInElement('#recent-drafts', 'New Page') - ->visit($book->getUrl() . '/create-page') - ->visit('/') - ->seeInElement('#recent-drafts', 'New Page'); + /** @var Book $book */ + $book = Book::query()->first(); + $this->asAdmin()->get('/') + ->assertElementNotContains('#recent-drafts', 'New Page'); + + $this->get($book->getUrl() . '/create-page'); + + $this->get('/')->assertElementContains('#recent-drafts', 'New Page'); } public function test_draft_pages_not_visible_by_others() { - $book = \BookStack\Entities\Models\Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $chapter = $book->chapters->first(); $newUser = $this->getEditor(); - $this->actingAs($newUser)->visit('/') - ->visit($book->getUrl('/create-page')) - ->visit($chapter->getUrl('/create-page')) - ->visit($book->getUrl()) - ->seeInElement('.book-contents', 'New Page'); + $this->actingAs($newUser)->get($book->getUrl('/create-page')); + $this->get($chapter->getUrl('/create-page')); + $this->get($book->getUrl()) + ->assertElementContains('.book-contents', 'New Page'); - $this->asAdmin() - ->visit($book->getUrl()) - ->dontSeeInElement('.book-contents', 'New Page') - ->visit($chapter->getUrl()) - ->dontSeeInElement('.book-contents', 'New Page'); + $this->asAdmin()->get($book->getUrl()) + ->assertElementNotContains('.book-contents', 'New Page'); + $this->get($chapter->getUrl()) + ->assertElementNotContains('.book-contents', 'New Page'); } public function test_page_html_in_ajax_fetch_response() { $this->asAdmin(); + /** @var Page $page */ $page = Page::query()->first(); - $this->getJson('/ajax/page/' . $page->id); - $this->seeJson([ + $this->getJson('/ajax/page/' . $page->id)->assertJson([ 'html' => $page->html, ]); } diff --git a/tests/Entity/PageEditorTest.php b/tests/Entity/PageEditorTest.php new file mode 100644 index 000000000..9b0a8f188 --- /dev/null +++ b/tests/Entity/PageEditorTest.php @@ -0,0 +1,77 @@ +page = Page::query()->first(); + } + + public function test_default_editor_is_wysiwyg() + { + $this->assertEquals('wysiwyg', setting('app-editor')); + $this->asAdmin()->get($this->page->getUrl() . '/edit') + ->assertElementExists('#html-editor'); + } + + public function test_markdown_setting_shows_markdown_editor() + { + $this->setSettings(['app-editor' => 'markdown']); + $this->asAdmin()->get($this->page->getUrl() . '/edit') + ->assertElementNotExists('#html-editor') + ->assertElementExists('#markdown-editor'); + } + + public function test_markdown_content_given_to_editor() + { + $this->setSettings(['app-editor' => 'markdown']); + + $mdContent = '# hello. This is a test'; + $this->page->markdown = $mdContent; + $this->page->save(); + + $this->asAdmin()->get($this->page->getUrl() . '/edit') + ->assertElementContains('[name="markdown"]', $mdContent); + } + + public function test_html_content_given_to_editor_if_no_markdown() + { + $this->setSettings(['app-editor' => 'markdown']); + $this->asAdmin()->get($this->page->getUrl() . '/edit') + ->assertElementContains('[name="markdown"]', $this->page->html); + } + + public function test_empty_markdown_still_saves_without_error() + { + $this->setSettings(['app-editor' => 'markdown']); + /** @var Book $book */ + $book = Book::query()->first(); + + $this->asEditor()->get($book->getUrl('/create-page')); + $draft = Page::query()->where('book_id', '=', $book->id) + ->where('draft', '=', true)->first(); + + $details = [ + 'name' => 'my page', + 'markdown' => '', + ]; + $resp = $this->post($book->getUrl("/draft/{$draft->id}"), $details); + $resp->assertRedirect(); + + $this->assertDatabaseHas('pages', [ + 'markdown' => $details['markdown'], + 'id' => $draft->id, + 'draft' => false, + ]); + } +} diff --git a/tests/Entity/PageTest.php b/tests/Entity/PageTest.php index 2721c225c..313fc77f0 100644 --- a/tests/Entity/PageTest.php +++ b/tests/Entity/PageTest.php @@ -3,11 +3,40 @@ namespace Tests\Entity; use BookStack\Entities\Models\Book; +use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; +use Carbon\Carbon; use Tests\TestCase; class PageTest extends TestCase { + public function test_create() + { + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); + $page = factory(Page::class)->make([ + 'name' => 'My First Page', + ]); + + $resp = $this->asEditor()->get($chapter->getUrl()); + $resp->assertElementContains('a[href="' . $chapter->getUrl('/create-page') . '"]', 'New Page'); + + $resp = $this->get($chapter->getUrl('/create-page')); + /** @var Page $draftPage */ + $draftPage = Page::query() + ->where('draft', '=', true) + ->orderBy('created_at', 'desc') + ->first(); + $resp->assertRedirect($draftPage->getUrl()); + + $resp = $this->get($draftPage->getUrl()); + $resp->assertElementContains('form[action="' . $draftPage->getUrl() . '"][method="POST"]', 'Save Page'); + + $resp = $this->post($draftPage->getUrl(), $draftPage->only('name', 'html')); + $draftPage->refresh(); + $resp->assertRedirect($draftPage->getUrl()); + } + public function test_page_view_when_creator_is_deleted_but_owner_exists() { $page = Page::query()->first(); @@ -190,26 +219,65 @@ class PageTest extends TestCase ]); } - public function test_empty_markdown_still_saves_without_error() + public function test_old_page_slugs_redirect_to_new_pages() { - $this->setSettings(['app-editor' => 'markdown']); - $book = Book::query()->first(); + /** @var Page $page */ + $page = Page::query()->first(); - $this->asEditor()->get($book->getUrl('/create-page')); - $draft = Page::query()->where('book_id', '=', $book->id) - ->where('draft', '=', true)->first(); - - $details = [ - 'name' => 'my page', - 'markdown' => '', - ]; - $resp = $this->post($book->getUrl("/draft/{$draft->id}"), $details); - $resp->assertRedirect(); - - $this->assertDatabaseHas('pages', [ - 'markdown' => $details['markdown'], - 'id' => $draft->id, - 'draft' => false, + // Need to save twice since revisions are not generated in seeder. + $this->asAdmin()->put($page->getUrl(), [ + 'name' => 'super test', + 'html' => '

', ]); + + $page->refresh(); + $pageUrl = $page->getUrl(); + + $this->put($pageUrl, [ + 'name' => 'super test page', + 'html' => '

', + ]); + + $this->get($pageUrl) + ->assertRedirect("/books/{$page->book->slug}/page/super-test-page"); + } + + public function test_page_within_chapter_deletion_returns_to_chapter() + { + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); + $page = $chapter->pages()->first(); + + $this->asEditor()->delete($page->getUrl()) + ->assertRedirect($chapter->getUrl()); + } + + public function test_recently_updated_pages_view() + { + $user = $this->getEditor(); + $content = $this->createEntityChainBelongingToUser($user); + + $this->asAdmin()->get('/pages/recently-updated') + ->assertElementContains('.entity-list .page:nth-child(1)', $content['page']->name); + } + + public function test_recently_updated_pages_on_home() + { + /** @var Page $page */ + $page = Page::query()->orderBy('updated_at', 'asc')->first(); + Page::query()->where('id', '!=', $page->id)->update([ + 'updated_at' => Carbon::now()->subSecond(1), + ]); + + $this->asAdmin()->get('/') + ->assertElementNotContains('#recently-updated-pages', $page->name); + + $this->put($page->getUrl(), [ + 'name' => $page->name, + 'html' => $page->html, + ]); + + $this->get('/') + ->assertElementContains('#recently-updated-pages', $page->name); } } diff --git a/tests/Entity/SortTest.php b/tests/Entity/SortTest.php index e058b39aa..5cfc5c3c5 100644 --- a/tests/Entity/SortTest.php +++ b/tests/Entity/SortTest.php @@ -216,6 +216,19 @@ class SortTest extends TestCase $this->assertEquals($newBook->id, $pageToCheck->book_id); } + public function test_book_sort_page_shows() + { + /** @var Book $bookToSort */ + $bookToSort = Book::query()->first(); + + $resp = $this->asAdmin()->get($bookToSort->getUrl()); + $resp->assertElementExists('a[href="' . $bookToSort->getUrl('/sort') . '"]'); + + $resp = $this->get($bookToSort->getUrl('/sort')); + $resp->assertStatus(200); + $resp->assertSee($bookToSort->name); + } + public function test_book_sort() { $oldBook = Book::query()->first(); @@ -259,6 +272,21 @@ class SortTest extends TestCase $checkResp->assertSee($newBook->name); } + public function test_book_sort_item_returns_book_content() + { + $books = Book::all(); + $bookToSort = $books[0]; + $firstPage = $bookToSort->pages[0]; + $firstChapter = $bookToSort->chapters[0]; + + $resp = $this->asAdmin()->get($bookToSort->getUrl() . '/sort-item'); + + // Ensure book details are returned + $resp->assertSee($bookToSort->name); + $resp->assertSee($firstPage->name); + $resp->assertSee($firstChapter->name); + } + public function test_pages_in_book_show_sorted_by_priority() { /** @var Book $book */ diff --git a/tests/Permissions/EntityPermissionsTest.php b/tests/Permissions/EntityPermissionsTest.php index 77c62fdb5..bb011cfc6 100644 --- a/tests/Permissions/EntityPermissionsTest.php +++ b/tests/Permissions/EntityPermissionsTest.php @@ -9,9 +9,9 @@ use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; use Illuminate\Support\Str; -use Tests\BrowserKitTest; +use Tests\TestCase; -class EntityPermissionsTest extends BrowserKitTest +class EntityPermissionsTest extends TestCase { /** * @var User @@ -41,608 +41,598 @@ class EntityPermissionsTest extends BrowserKitTest public function test_bookshelf_view_restriction() { - $shelf = Bookshelf::first(); + /** @var Bookshelf $shelf */ + $shelf = Bookshelf::query()->first(); $this->actingAs($this->user) - ->visit($shelf->getUrl()) - ->seePageIs($shelf->getUrl()); + ->get($shelf->getUrl()) + ->assertStatus(200); $this->setRestrictionsForTestRoles($shelf, []); - $this->forceVisit($shelf->getUrl()) - ->see('Bookshelf not found'); + $this->followingRedirects()->get($shelf->getUrl()) + ->assertSee('Bookshelf not found'); $this->setRestrictionsForTestRoles($shelf, ['view']); - $this->visit($shelf->getUrl()) - ->see($shelf->name); + $this->get($shelf->getUrl()) + ->assertSee($shelf->name); } public function test_bookshelf_update_restriction() { - $shelf = Bookshelf::first(); + /** @var Bookshelf $shelf */ + $shelf = Bookshelf::query()->first(); $this->actingAs($this->user) - ->visit($shelf->getUrl('/edit')) - ->see('Edit Book'); + ->get($shelf->getUrl('/edit')) + ->assertSee('Edit Book'); $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']); - $this->forceVisit($shelf->getUrl('/edit')) - ->see('You do not have permission')->seePageIs('/'); + $resp = $this->get($shelf->getUrl('/edit')) + ->assertRedirect('/'); + $this->followRedirects($resp)->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($shelf, ['view', 'update']); - $this->visit($shelf->getUrl('/edit')) - ->seePageIs($shelf->getUrl('/edit')); + $this->get($shelf->getUrl('/edit')) + ->assertOk(); } public function test_bookshelf_delete_restriction() { - $shelf = Book::first(); + /** @var Bookshelf $shelf */ + $shelf = Bookshelf::query()->first(); $this->actingAs($this->user) - ->visit($shelf->getUrl('/delete')) - ->see('Delete Book'); + ->get($shelf->getUrl('/delete')) + ->assertSee('Delete Book'); $this->setRestrictionsForTestRoles($shelf, ['view', 'update']); - $this->forceVisit($shelf->getUrl('/delete')) - ->see('You do not have permission')->seePageIs('/'); + $this->get($shelf->getUrl('/delete'))->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']); - $this->visit($shelf->getUrl('/delete')) - ->seePageIs($shelf->getUrl('/delete'))->see('Delete Book'); + $this->get($shelf->getUrl('/delete')) + ->assertOk() + ->assertSee('Delete Book'); } public function test_book_view_restriction() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookPage = $book->pages->first(); $bookChapter = $book->chapters->first(); $bookUrl = $book->getUrl(); $this->actingAs($this->user) - ->visit($bookUrl) - ->seePageIs($bookUrl); + ->get($bookUrl) + ->assertOk(); $this->setRestrictionsForTestRoles($book, []); - $this->forceVisit($bookUrl) - ->see('Book not found'); - $this->forceVisit($bookPage->getUrl()) - ->see('Page not found'); - $this->forceVisit($bookChapter->getUrl()) - ->see('Chapter not found'); + $this->followingRedirects()->get($bookUrl) + ->assertSee('Book not found'); + $this->followingRedirects()->get($bookPage->getUrl()) + ->assertSee('Page not found'); + $this->followingRedirects()->get($bookChapter->getUrl()) + ->assertSee('Chapter not found'); $this->setRestrictionsForTestRoles($book, ['view']); - $this->visit($bookUrl) - ->see($book->name); - $this->visit($bookPage->getUrl()) - ->see($bookPage->name); - $this->visit($bookChapter->getUrl()) - ->see($bookChapter->name); + $this->get($bookUrl) + ->assertSee($book->name); + $this->get($bookPage->getUrl()) + ->assertSee($bookPage->name); + $this->get($bookChapter->getUrl()) + ->assertSee($bookChapter->name); } public function test_book_create_restriction() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookUrl = $book->getUrl(); $this->actingAs($this->viewer) - ->visit($bookUrl) - ->dontSeeInElement('.actions', 'New Page') - ->dontSeeInElement('.actions', 'New Chapter'); + ->get($bookUrl) + ->assertElementNotContains('.actions', 'New Page') + ->assertElementNotContains('.actions', 'New Chapter'); $this->actingAs($this->user) - ->visit($bookUrl) - ->seeInElement('.actions', 'New Page') - ->seeInElement('.actions', 'New Chapter'); + ->get($bookUrl) + ->assertElementContains('.actions', 'New Page') + ->assertElementContains('.actions', 'New Chapter'); $this->setRestrictionsForTestRoles($book, ['view', 'delete', 'update']); - $this->forceVisit($bookUrl . '/create-chapter') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookUrl . '/create-page') - ->see('You do not have permission')->seePageIs('/'); - $this->visit($bookUrl)->dontSeeInElement('.actions', 'New Page') - ->dontSeeInElement('.actions', 'New Chapter'); + $this->get($bookUrl . '/create-chapter')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + + $this->get($bookUrl . '/create-page')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + + $this->get($bookUrl) + ->assertElementNotContains('.actions', 'New Page') + ->assertElementNotContains('.actions', 'New Chapter'); $this->setRestrictionsForTestRoles($book, ['view', 'create']); - $this->visit($bookUrl . '/create-chapter') - ->type('test chapter', 'name') - ->type('test description for chapter', 'description') - ->press('Save Chapter') - ->seePageIs($bookUrl . '/chapter/test-chapter'); - $this->visit($bookUrl . '/create-page') - ->type('test page', 'name') - ->type('test content', 'html') - ->press('Save Page') - ->seePageIs($bookUrl . '/page/test-page'); - $this->visit($bookUrl)->seeInElement('.actions', 'New Page') - ->seeInElement('.actions', 'New Chapter'); + $resp = $this->post($book->getUrl('/create-chapter'), [ + 'name' => 'test chapter', + 'description' => 'desc', + ]); + $resp->assertRedirect($book->getUrl('/chapter/test-chapter')); + + $this->get($book->getUrl('/create-page')); + /** @var Page $page */ + $page = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first(); + $resp = $this->post($page->getUrl(), [ + 'name' => 'test page', + 'html' => 'test content', + ]); + $resp->assertRedirect($book->getUrl('/page/test-page')); + + $this->get($bookUrl) + ->assertElementContains('.actions', 'New Page') + ->assertElementContains('.actions', 'New Chapter'); } public function test_book_update_restriction() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookPage = $book->pages->first(); $bookChapter = $book->chapters->first(); $bookUrl = $book->getUrl(); $this->actingAs($this->user) - ->visit($bookUrl . '/edit') - ->see('Edit Book'); + ->get($bookUrl . '/edit') + ->assertSee('Edit Book'); $this->setRestrictionsForTestRoles($book, ['view', 'delete']); - $this->forceVisit($bookUrl . '/edit') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookPage->getUrl() . '/edit') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookChapter->getUrl() . '/edit') - ->see('You do not have permission')->seePageIs('/'); + $this->get($bookUrl . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookPage->getUrl() . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookChapter->getUrl() . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($book, ['view', 'update']); - $this->visit($bookUrl . '/edit') - ->seePageIs($bookUrl . '/edit'); - $this->visit($bookPage->getUrl() . '/edit') - ->seePageIs($bookPage->getUrl() . '/edit'); - $this->visit($bookChapter->getUrl() . '/edit') - ->see('Edit Chapter'); + $this->get($bookUrl . '/edit')->assertOk(); + $this->get($bookPage->getUrl() . '/edit')->assertOk(); + $this->get($bookChapter->getUrl() . '/edit')->assertSee('Edit Chapter'); } public function test_book_delete_restriction() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookPage = $book->pages->first(); $bookChapter = $book->chapters->first(); $bookUrl = $book->getUrl(); - $this->actingAs($this->user) - ->visit($bookUrl . '/delete') - ->see('Delete Book'); + $this->actingAs($this->user)->get($bookUrl . '/delete') + ->assertSee('Delete Book'); $this->setRestrictionsForTestRoles($book, ['view', 'update']); - $this->forceVisit($bookUrl . '/delete') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookPage->getUrl() . '/delete') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookChapter->getUrl() . '/delete') - ->see('You do not have permission')->seePageIs('/'); + $this->get($bookUrl . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookPage->getUrl() . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookChapter->getUrl() . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($book, ['view', 'delete']); - $this->visit($bookUrl . '/delete') - ->seePageIs($bookUrl . '/delete')->see('Delete Book'); - $this->visit($bookPage->getUrl() . '/delete') - ->seePageIs($bookPage->getUrl() . '/delete')->see('Delete Page'); - $this->visit($bookChapter->getUrl() . '/delete') - ->see('Delete Chapter'); + $this->get($bookUrl . '/delete')->assertOk()->assertSee('Delete Book'); + $this->get($bookPage->getUrl('/delete'))->assertOk()->assertSee('Delete Page'); + $this->get($bookChapter->getUrl('/delete'))->assertSee('Delete Chapter'); } public function test_chapter_view_restriction() { - $chapter = Chapter::first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $chapterPage = $chapter->pages->first(); $chapterUrl = $chapter->getUrl(); - $this->actingAs($this->user) - ->visit($chapterUrl) - ->seePageIs($chapterUrl); + $this->actingAs($this->user)->get($chapterUrl)->assertOk(); $this->setRestrictionsForTestRoles($chapter, []); - $this->forceVisit($chapterUrl) - ->see('Chapter not found'); - $this->forceVisit($chapterPage->getUrl()) - ->see('Page not found'); + $this->followingRedirects()->get($chapterUrl)->assertSee('Chapter not found'); + $this->followingRedirects()->get($chapterPage->getUrl())->assertSee('Page not found'); $this->setRestrictionsForTestRoles($chapter, ['view']); - $this->visit($chapterUrl) - ->see($chapter->name); - $this->visit($chapterPage->getUrl()) - ->see($chapterPage->name); + $this->get($chapterUrl)->assertSee($chapter->name); + $this->get($chapterPage->getUrl())->assertSee($chapterPage->name); } public function test_chapter_create_restriction() { - $chapter = Chapter::first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $chapterUrl = $chapter->getUrl(); $this->actingAs($this->user) - ->visit($chapterUrl) - ->seeInElement('.actions', 'New Page'); + ->get($chapterUrl) + ->assertElementContains('.actions', 'New Page'); $this->setRestrictionsForTestRoles($chapter, ['view', 'delete', 'update']); - $this->forceVisit($chapterUrl . '/create-page') - ->see('You do not have permission')->seePageIs('/'); - $this->visit($chapterUrl)->dontSeeInElement('.actions', 'New Page'); + $this->get($chapterUrl . '/create-page')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($chapterUrl)->assertElementNotContains('.actions', 'New Page'); $this->setRestrictionsForTestRoles($chapter, ['view', 'create']); - $this->visit($chapterUrl . '/create-page') - ->type('test page', 'name') - ->type('test content', 'html') - ->press('Save Page') - ->seePageIs($chapter->book->getUrl() . '/page/test-page'); + $this->get($chapter->getUrl('/create-page')); + /** @var Page $page */ + $page = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first(); + $resp = $this->post($page->getUrl(), [ + 'name' => 'test page', + 'html' => 'test content', + ]); + $resp->assertRedirect($chapter->book->getUrl('/page/test-page')); - $this->visit($chapterUrl)->seeInElement('.actions', 'New Page'); + $this->get($chapterUrl)->assertElementContains('.actions', 'New Page'); } public function test_chapter_update_restriction() { - $chapter = Chapter::first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $chapterPage = $chapter->pages->first(); $chapterUrl = $chapter->getUrl(); - $this->actingAs($this->user) - ->visit($chapterUrl . '/edit') - ->see('Edit Chapter'); + $this->actingAs($this->user)->get($chapterUrl . '/edit') + ->assertSee('Edit Chapter'); $this->setRestrictionsForTestRoles($chapter, ['view', 'delete']); - $this->forceVisit($chapterUrl . '/edit') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($chapterPage->getUrl() . '/edit') - ->see('You do not have permission')->seePageIs('/'); + $this->get($chapterUrl . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($chapterPage->getUrl() . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($chapter, ['view', 'update']); - $this->visit($chapterUrl . '/edit') - ->seePageIs($chapterUrl . '/edit')->see('Edit Chapter'); - $this->visit($chapterPage->getUrl() . '/edit') - ->seePageIs($chapterPage->getUrl() . '/edit'); + $this->get($chapterUrl . '/edit')->assertOk()->assertSee('Edit Chapter'); + $this->get($chapterPage->getUrl() . '/edit')->assertOk(); } public function test_chapter_delete_restriction() { - $chapter = Chapter::first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $chapterPage = $chapter->pages->first(); $chapterUrl = $chapter->getUrl(); $this->actingAs($this->user) - ->visit($chapterUrl . '/delete') - ->see('Delete Chapter'); + ->get($chapterUrl . '/delete') + ->assertSee('Delete Chapter'); $this->setRestrictionsForTestRoles($chapter, ['view', 'update']); - $this->forceVisit($chapterUrl . '/delete') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($chapterPage->getUrl() . '/delete') - ->see('You do not have permission')->seePageIs('/'); + $this->get($chapterUrl . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($chapterPage->getUrl() . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($chapter, ['view', 'delete']); - $this->visit($chapterUrl . '/delete') - ->seePageIs($chapterUrl . '/delete')->see('Delete Chapter'); - $this->visit($chapterPage->getUrl() . '/delete') - ->seePageIs($chapterPage->getUrl() . '/delete')->see('Delete Page'); + $this->get($chapterUrl . '/delete')->assertOk()->assertSee('Delete Chapter'); + $this->get($chapterPage->getUrl() . '/delete')->assertOk()->assertSee('Delete Page'); } public function test_page_view_restriction() { - $page = Page::first(); + /** @var Page $page */ + $page = Page::query()->first(); $pageUrl = $page->getUrl(); - $this->actingAs($this->user) - ->visit($pageUrl) - ->seePageIs($pageUrl); + $this->actingAs($this->user)->get($pageUrl)->assertOk(); $this->setRestrictionsForTestRoles($page, ['update', 'delete']); - $this->forceVisit($pageUrl) - ->see('Page not found'); + $this->get($pageUrl)->assertSee('Page not found'); $this->setRestrictionsForTestRoles($page, ['view']); - $this->visit($pageUrl) - ->see($page->name); + $this->get($pageUrl)->assertSee($page->name); } public function test_page_update_restriction() { - $page = Chapter::first(); + /** @var Page $page */ + $page = Page::query()->first(); $pageUrl = $page->getUrl(); $this->actingAs($this->user) - ->visit($pageUrl . '/edit') - ->seeInField('name', $page->name); + ->get($pageUrl . '/edit') + ->assertElementExists('input[name="name"][value="' . $page->name . '"]'); $this->setRestrictionsForTestRoles($page, ['view', 'delete']); - $this->forceVisit($pageUrl . '/edit') - ->see('You do not have permission')->seePageIs('/'); + $this->get($pageUrl . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($page, ['view', 'update']); - $this->visit($pageUrl . '/edit') - ->seePageIs($pageUrl . '/edit')->seeInField('name', $page->name); + $this->get($pageUrl . '/edit') + ->assertOk() + ->assertElementExists('input[name="name"][value="' . $page->name . '"]'); } public function test_page_delete_restriction() { - $page = Page::first(); + /** @var Page $page */ + $page = Page::query()->first(); $pageUrl = $page->getUrl(); $this->actingAs($this->user) - ->visit($pageUrl . '/delete') - ->see('Delete Page'); + ->get($pageUrl . '/delete') + ->assertSee('Delete Page'); $this->setRestrictionsForTestRoles($page, ['view', 'update']); - $this->forceVisit($pageUrl . '/delete') - ->see('You do not have permission')->seePageIs('/'); + $this->get($pageUrl . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($page, ['view', 'delete']); - $this->visit($pageUrl . '/delete') - ->seePageIs($pageUrl . '/delete')->see('Delete Page'); + $this->get($pageUrl . '/delete')->assertOk()->assertSee('Delete Page'); + } + + protected function entityRestrictionFormTest(string $model, string $title, string $permission, string $roleId) + { + /** @var Entity $modelInstance */ + $modelInstance = $model::query()->first(); + $this->asAdmin()->get($modelInstance->getUrl('/permissions')) + ->assertSee($title); + + $this->put($modelInstance->getUrl('/permissions'), [ + 'restricted' => 'true', + 'restrictions' => [ + $roleId => [ + $permission => 'true', + ], + ], + ]); + + $this->assertDatabaseHas($modelInstance->getTable(), ['id' => $modelInstance->id, 'restricted' => true]); + $this->assertDatabaseHas('entity_permissions', [ + 'restrictable_id' => $modelInstance->id, + 'restrictable_type' => $modelInstance->getMorphClass(), + 'role_id' => $roleId, + 'action' => $permission, + ]); } public function test_bookshelf_restriction_form() { - $shelf = Bookshelf::first(); - $this->asAdmin()->visit($shelf->getUrl('/permissions')) - ->see('Bookshelf Permissions') - ->check('restricted') - ->check('restrictions[2][view]') - ->press('Save Permissions') - ->seeInDatabase('bookshelves', ['id' => $shelf->id, 'restricted' => true]) - ->seeInDatabase('entity_permissions', [ - 'restrictable_id' => $shelf->id, - 'restrictable_type' => Bookshelf::newModelInstance()->getMorphClass(), - 'role_id' => '2', - 'action' => 'view', - ]); + $this->entityRestrictionFormTest(Bookshelf::class, 'Bookshelf Permissions', 'view', '2'); } public function test_book_restriction_form() { - $book = Book::first(); - $this->asAdmin()->visit($book->getUrl() . '/permissions') - ->see('Book Permissions') - ->check('restricted') - ->check('restrictions[2][view]') - ->press('Save Permissions') - ->seeInDatabase('books', ['id' => $book->id, 'restricted' => true]) - ->seeInDatabase('entity_permissions', [ - 'restrictable_id' => $book->id, - 'restrictable_type' => Book::newModelInstance()->getMorphClass(), - 'role_id' => '2', - 'action' => 'view', - ]); + $this->entityRestrictionFormTest(Book::class, 'Book Permissions', 'view', '2'); } public function test_chapter_restriction_form() { - $chapter = Chapter::first(); - $this->asAdmin()->visit($chapter->getUrl() . '/permissions') - ->see('Chapter Permissions') - ->check('restricted') - ->check('restrictions[2][update]') - ->press('Save Permissions') - ->seeInDatabase('chapters', ['id' => $chapter->id, 'restricted' => true]) - ->seeInDatabase('entity_permissions', [ - 'restrictable_id' => $chapter->id, - 'restrictable_type' => Chapter::newModelInstance()->getMorphClass(), - 'role_id' => '2', - 'action' => 'update', - ]); + $this->entityRestrictionFormTest(Chapter::class, 'Chapter Permissions', 'update', '2'); } public function test_page_restriction_form() { - $page = Page::first(); - $this->asAdmin()->visit($page->getUrl() . '/permissions') - ->see('Page Permissions') - ->check('restricted') - ->check('restrictions[2][delete]') - ->press('Save Permissions') - ->seeInDatabase('pages', ['id' => $page->id, 'restricted' => true]) - ->seeInDatabase('entity_permissions', [ - 'restrictable_id' => $page->id, - 'restrictable_type' => Page::newModelInstance()->getMorphClass(), - 'role_id' => '2', - 'action' => 'delete', - ]); + $this->entityRestrictionFormTest(Page::class, 'Page Permissions', 'delete', '2'); } public function test_restricted_pages_not_visible_in_book_navigation_on_pages() { - $chapter = Chapter::first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $page = $chapter->pages->first(); $page2 = $chapter->pages[2]; $this->setRestrictionsForTestRoles($page, []); $this->actingAs($this->user) - ->visit($page2->getUrl()) - ->dontSeeInElement('.sidebar-page-list', $page->name); + ->get($page2->getUrl()) + ->assertElementNotContains('.sidebar-page-list', $page->name); } public function test_restricted_pages_not_visible_in_book_navigation_on_chapters() { - $chapter = Chapter::first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $page = $chapter->pages->first(); $this->setRestrictionsForTestRoles($page, []); $this->actingAs($this->user) - ->visit($chapter->getUrl()) - ->dontSeeInElement('.sidebar-page-list', $page->name); + ->get($chapter->getUrl()) + ->assertElementNotContains('.sidebar-page-list', $page->name); } public function test_restricted_pages_not_visible_on_chapter_pages() { - $chapter = Chapter::first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $page = $chapter->pages->first(); $this->setRestrictionsForTestRoles($page, []); $this->actingAs($this->user) - ->visit($chapter->getUrl()) - ->dontSee($page->name); + ->get($chapter->getUrl()) + ->assertDontSee($page->name); } public function test_restricted_chapter_pages_not_visible_on_book_page() { + /** @var Chapter $chapter */ $chapter = Chapter::query()->first(); $this->actingAs($this->user) - ->visit($chapter->book->getUrl()) - ->see($chapter->pages->first()->name); + ->get($chapter->book->getUrl()) + ->assertSee($chapter->pages->first()->name); foreach ($chapter->pages as $page) { $this->setRestrictionsForTestRoles($page, []); } $this->actingAs($this->user) - ->visit($chapter->book->getUrl()) - ->dontSee($chapter->pages->first()->name); + ->get($chapter->book->getUrl()) + ->assertDontSee($chapter->pages->first()->name); } public function test_bookshelf_update_restriction_override() { - $shelf = Bookshelf::first(); + /** @var Bookshelf $shelf */ + $shelf = Bookshelf::query()->first(); $this->actingAs($this->viewer) - ->visit($shelf->getUrl('/edit')) - ->dontSee('Edit Book'); + ->get($shelf->getUrl('/edit')) + ->assertDontSee('Edit Book'); $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']); - $this->forceVisit($shelf->getUrl('/edit')) - ->see('You do not have permission')->seePageIs('/'); + $this->get($shelf->getUrl('/edit'))->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($shelf, ['view', 'update']); - $this->visit($shelf->getUrl('/edit')) - ->seePageIs($shelf->getUrl('/edit')); + $this->get($shelf->getUrl('/edit'))->assertOk(); } public function test_bookshelf_delete_restriction_override() { - $shelf = Bookshelf::first(); + /** @var Bookshelf $shelf */ + $shelf = Bookshelf::query()->first(); $this->actingAs($this->viewer) - ->visit($shelf->getUrl('/delete')) - ->dontSee('Delete Book'); + ->get($shelf->getUrl('/delete')) + ->assertDontSee('Delete Book'); $this->setRestrictionsForTestRoles($shelf, ['view', 'update']); - $this->forceVisit($shelf->getUrl('/delete')) - ->see('You do not have permission')->seePageIs('/'); + $this->get($shelf->getUrl('/delete'))->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($shelf, ['view', 'delete']); - $this->visit($shelf->getUrl('/delete')) - ->seePageIs($shelf->getUrl('/delete'))->see('Delete Book'); + $this->get($shelf->getUrl('/delete'))->assertOk()->assertSee('Delete Book'); } public function test_book_create_restriction_override() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookUrl = $book->getUrl(); $this->actingAs($this->viewer) - ->visit($bookUrl) - ->dontSeeInElement('.actions', 'New Page') - ->dontSeeInElement('.actions', 'New Chapter'); + ->get($bookUrl) + ->assertElementNotContains('.actions', 'New Page') + ->assertElementNotContains('.actions', 'New Chapter'); $this->setRestrictionsForTestRoles($book, ['view', 'delete', 'update']); - $this->forceVisit($bookUrl . '/create-chapter') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookUrl . '/create-page') - ->see('You do not have permission')->seePageIs('/'); - $this->visit($bookUrl)->dontSeeInElement('.actions', 'New Page') - ->dontSeeInElement('.actions', 'New Chapter'); + $this->get($bookUrl . '/create-chapter')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookUrl . '/create-page')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookUrl)->assertElementNotContains('.actions', 'New Page') + ->assertElementNotContains('.actions', 'New Chapter'); $this->setRestrictionsForTestRoles($book, ['view', 'create']); - $this->visit($bookUrl . '/create-chapter') - ->type('test chapter', 'name') - ->type('test description for chapter', 'description') - ->press('Save Chapter') - ->seePageIs($bookUrl . '/chapter/test-chapter'); - $this->visit($bookUrl . '/create-page') - ->type('test page', 'name') - ->type('test content', 'html') - ->press('Save Page') - ->seePageIs($bookUrl . '/page/test-page'); - $this->visit($bookUrl)->seeInElement('.actions', 'New Page') - ->seeInElement('.actions', 'New Chapter'); + $resp = $this->post($book->getUrl('/create-chapter'), [ + 'name' => 'test chapter', + 'description' => 'test desc', + ]); + $resp->assertRedirect($book->getUrl('/chapter/test-chapter')); + + $this->get($book->getUrl('/create-page')); + /** @var Page $page */ + $page = Page::query()->where('draft', '=', true)->orderByDesc('id')->first(); + $resp = $this->post($page->getUrl(), [ + 'name' => 'test page', + 'html' => 'test desc', + ]); + $resp->assertRedirect($book->getUrl('/page/test-page')); + + $this->get($bookUrl) + ->assertElementContains('.actions', 'New Page') + ->assertElementContains('.actions', 'New Chapter'); } public function test_book_update_restriction_override() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookPage = $book->pages->first(); $bookChapter = $book->chapters->first(); $bookUrl = $book->getUrl(); - $this->actingAs($this->viewer) - ->visit($bookUrl . '/edit') - ->dontSee('Edit Book'); + $this->actingAs($this->viewer)->get($bookUrl . '/edit') + ->assertDontSee('Edit Book'); $this->setRestrictionsForTestRoles($book, ['view', 'delete']); - $this->forceVisit($bookUrl . '/edit') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookPage->getUrl() . '/edit') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookChapter->getUrl() . '/edit') - ->see('You do not have permission')->seePageIs('/'); + $this->get($bookUrl . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookPage->getUrl() . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookChapter->getUrl() . '/edit')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($book, ['view', 'update']); - $this->visit($bookUrl . '/edit') - ->seePageIs($bookUrl . '/edit'); - $this->visit($bookPage->getUrl() . '/edit') - ->seePageIs($bookPage->getUrl() . '/edit'); - $this->visit($bookChapter->getUrl() . '/edit') - ->see('Edit Chapter'); + $this->get($bookUrl . '/edit')->assertOk(); + $this->get($bookPage->getUrl() . '/edit')->assertOk(); + $this->get($bookChapter->getUrl() . '/edit')->assertSee('Edit Chapter'); } public function test_book_delete_restriction_override() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookPage = $book->pages->first(); $bookChapter = $book->chapters->first(); $bookUrl = $book->getUrl(); $this->actingAs($this->viewer) - ->visit($bookUrl . '/delete') - ->dontSee('Delete Book'); + ->get($bookUrl . '/delete') + ->assertDontSee('Delete Book'); $this->setRestrictionsForTestRoles($book, ['view', 'update']); - $this->forceVisit($bookUrl . '/delete') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookPage->getUrl() . '/delete') - ->see('You do not have permission')->seePageIs('/'); - $this->forceVisit($bookChapter->getUrl() . '/delete') - ->see('You do not have permission')->seePageIs('/'); + $this->get($bookUrl . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookPage->getUrl() . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); + $this->get($bookChapter->getUrl() . '/delete')->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $this->setRestrictionsForTestRoles($book, ['view', 'delete']); - $this->visit($bookUrl . '/delete') - ->seePageIs($bookUrl . '/delete')->see('Delete Book'); - $this->visit($bookPage->getUrl() . '/delete') - ->seePageIs($bookPage->getUrl() . '/delete')->see('Delete Page'); - $this->visit($bookChapter->getUrl() . '/delete') - ->see('Delete Chapter'); + $this->get($bookUrl . '/delete')->assertOk()->assertSee('Delete Book'); + $this->get($bookPage->getUrl() . '/delete')->assertOk()->assertSee('Delete Page'); + $this->get($bookChapter->getUrl() . '/delete')->assertSee('Delete Chapter'); } public function test_page_visible_if_has_permissions_when_book_not_visible() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $bookChapter = $book->chapters->first(); $bookPage = $bookChapter->pages->first(); @@ -655,34 +645,37 @@ class EntityPermissionsTest extends BrowserKitTest $this->setRestrictionsForTestRoles($bookPage, ['view']); $this->actingAs($this->viewer); - $this->get($bookPage->getUrl()); - $this->assertResponseOk(); - $this->see($bookPage->name); - $this->dontSee(substr($book->name, 0, 15)); - $this->dontSee(substr($bookChapter->name, 0, 15)); + $resp = $this->get($bookPage->getUrl()); + $resp->assertOk(); + $resp->assertSee($bookPage->name); + $resp->assertDontSee(substr($book->name, 0, 15)); + $resp->assertDontSee(substr($bookChapter->name, 0, 15)); } public function test_book_sort_view_permission() { - $firstBook = Book::first(); - $secondBook = Book::find(2); + /** @var Book $firstBook */ + $firstBook = Book::query()->first(); + /** @var Book $secondBook */ + $secondBook = Book::query()->find(2); $this->setRestrictionsForTestRoles($firstBook, ['view', 'update']); $this->setRestrictionsForTestRoles($secondBook, ['view']); // Test sort page visibility - $this->actingAs($this->user)->visit($secondBook->getUrl() . '/sort') - ->see('You do not have permission') - ->seePageIs('/'); + $this->actingAs($this->user)->get($secondBook->getUrl('/sort'))->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); // Check sort page on first book - $this->actingAs($this->user)->visit($firstBook->getUrl() . '/sort'); + $this->actingAs($this->user)->get($firstBook->getUrl('/sort')); } public function test_book_sort_permission() { - $firstBook = Book::first(); - $secondBook = Book::find(2); + /** @var Book $firstBook */ + $firstBook = Book::query()->first(); + /** @var Book $secondBook */ + $secondBook = Book::query()->find(2); $this->setRestrictionsForTestRoles($firstBook, ['view', 'update']); $this->setRestrictionsForTestRoles($secondBook, ['view']); @@ -703,9 +696,8 @@ class EntityPermissionsTest extends BrowserKitTest // Move chapter from first book to a second book $this->actingAs($this->user)->put($firstBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)]) - ->followRedirects() - ->see('You do not have permission') - ->seePageIs('/'); + ->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); $reqData = [ [ @@ -719,30 +711,30 @@ class EntityPermissionsTest extends BrowserKitTest // Move chapter from second book to first book $this->actingAs($this->user)->put($firstBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)]) - ->followRedirects() - ->see('You do not have permission') - ->seePageIs('/'); + ->assertRedirect('/'); + $this->get('/')->assertSee('You do not have permission'); } public function test_can_create_page_if_chapter_has_permissions_when_book_not_visible() { - $book = Book::first(); + /** @var Book $book */ + $book = Book::query()->first(); $this->setRestrictionsForTestRoles($book, []); $bookChapter = $book->chapters->first(); $this->setRestrictionsForTestRoles($bookChapter, ['view']); - $this->actingAs($this->user)->visit($bookChapter->getUrl()) - ->dontSee('New Page'); + $this->actingAs($this->user)->get($bookChapter->getUrl()) + ->assertDontSee('New Page'); $this->setRestrictionsForTestRoles($bookChapter, ['view', 'create']); - $this->actingAs($this->user)->visit($bookChapter->getUrl()) - ->click('New Page') - ->seeStatusCode(200) - ->type('test page', 'name') - ->type('test content', 'html') - ->press('Save Page') - ->seePageIs($book->getUrl('/page/test-page')) - ->seeStatusCode(200); + $this->get($bookChapter->getUrl('/create-page')); + /** @var Page $page */ + $page = Page::query()->where('draft', '=', true)->orderByDesc('id')->first(); + $resp = $this->post($page->getUrl(), [ + 'name' => 'test page', + 'html' => 'test content', + ]); + $resp->assertRedirect($book->getUrl('/page/test-page')); } } diff --git a/tests/Permissions/RolesTest.php b/tests/Permissions/RolesTest.php index b9b1805b6..5248ae152 100644 --- a/tests/Permissions/RolesTest.php +++ b/tests/Permissions/RolesTest.php @@ -2,18 +2,20 @@ namespace Tests\Permissions; +use BookStack\Actions\ActivityType; use BookStack\Actions\Comment; 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\Uploads\Image; -use Laravel\BrowserKitTesting\HttpException; -use Tests\BrowserKitTest; +use Tests\TestCase; +use Tests\TestResponse; -class RolesTest extends BrowserKitTest +class RolesTest extends TestCase { protected $user; @@ -25,17 +27,17 @@ class RolesTest extends BrowserKitTest public function test_admin_can_see_settings() { - $this->asAdmin()->visit('/settings')->see('Settings'); + $this->asAdmin()->get('/settings')->assertSee('Settings'); } public function test_cannot_delete_admin_role() { $adminRole = Role::getRole('admin'); $deletePageUrl = '/settings/roles/delete/' . $adminRole->id; - $this->asAdmin()->visit($deletePageUrl) - ->press('Confirm') - ->seePageIs($deletePageUrl) - ->see('cannot be deleted'); + + $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() @@ -44,10 +46,9 @@ class RolesTest extends BrowserKitTest $this->setSettings(['registration-role' => $newRole->id]); $deletePageUrl = '/settings/roles/delete/' . $newRole->id; - $this->asAdmin()->visit($deletePageUrl) - ->press('Confirm') - ->seePageIs($deletePageUrl) - ->see('cannot be deleted'); + $this->asAdmin()->get($deletePageUrl); + $this->delete($deletePageUrl)->assertRedirect($deletePageUrl); + $this->get($deletePageUrl)->assertSee('cannot be deleted'); } public function test_role_create_update_delete_flow() @@ -57,68 +58,104 @@ class RolesTest extends BrowserKitTest $testRoleUpdateName = 'An Super Updated role'; // Creation - $this->asAdmin()->visit('/settings') - ->click('Roles') - ->seePageIs('/settings/roles') - ->click('Create New Role') - ->type('Test Role', 'display_name') - ->type('A little test description', 'description') - ->press('Save Role') - ->seeInDatabase('roles', ['display_name' => $testRoleName, 'description' => $testRoleDesc, 'mfa_enforced' => false]) - ->seePageIs('/settings/roles'); + $resp = $this->asAdmin()->get('/settings'); + $resp->assertElementContains('a[href="' . url('/settings/roles') . '"]', 'Roles'); + + $resp = $this->get('/settings/roles'); + $resp->assertElementContains('a[href="' . url('/settings/roles/new') . '"]', 'Create New Role'); + + $resp = $this->get('/settings/roles/new'); + $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 - $this->asAdmin()->visit('/settings/roles') - ->see($testRoleDesc) - ->click($testRoleName) - ->type($testRoleUpdateName, '#display_name') - ->check('#mfa_enforced') - ->press('Save Role') - ->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'description' => $testRoleDesc, 'mfa_enforced' => true]) - ->seePageIs('/settings/roles'); + $resp = $this->get('/settings/roles/' . $role->id); + $resp->assertSee($testRoleName); + $resp->assertSee($testRoleDesc); + $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 - $this->asAdmin()->visit('/settings/roles') - ->click($testRoleUpdateName) - ->click('Delete Role') - ->see($testRoleUpdateName) - ->press('Confirm') - ->seePageIs('/settings/roles') - ->dontSee($testRoleUpdateName); + $resp = $this->get('/settings/roles/' . $role->id); + $resp->assertElementContains('a[href="' . url("/settings/roles/delete/$role->id") . '"]', 'Delete Role'); + + $resp = $this->get("/settings/roles/delete/$role->id"); + $resp->assertSee($testRoleUpdateName); + $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_last_admin() + public function test_admin_role_cannot_be_removed_if_user_last_admin() { - $adminRole = Role::where('system_name', '=', 'admin')->first(); + /** @var Role $adminRole */ + $adminRole = Role::query()->where('system_name', '=', 'admin')->first(); $adminUser = $this->getAdmin(); $adminRole->users()->where('id', '!=', $adminUser->id)->delete(); - $this->assertEquals($adminRole->users()->count(), 1); + $this->assertEquals(1, $adminRole->users()->count()); $viewerRole = $this->getViewer()->roles()->first(); $editUrl = '/settings/users/' . $adminUser->id; - $this->actingAs($adminUser)->put($editUrl, [ + $resp = $this->actingAs($adminUser)->put($editUrl, [ 'name' => $adminUser->name, 'email' => $adminUser->email, 'roles' => [ 'viewer' => strval($viewerRole->id), ], - ])->followRedirects(); + ]); - $this->seePageIs($editUrl); - $this->see('This user is the only user assigned to the administrator role'); + $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}"); - $deletePage->seeElement('select[name=migrate_role_id]'); - $this->asAdmin()->delete("/settings/roles/delete/{$roleB->id}", [ + $deletePage = $this->asAdmin()->get("/settings/roles/delete/$roleB->id"); + $deletePage->assertElementExists('select[name=migrate_role_id]'); + $this->asAdmin()->delete("/settings/roles/delete/$roleB->id", [ 'migrate_role_id' => $roleA->id, ]); @@ -128,21 +165,19 @@ class RolesTest extends BrowserKitTest public function test_manage_user_permission() { - $this->actingAs($this->user)->visit('/settings/users') - ->seePageIs('/'); + $this->actingAs($this->user)->get('/settings/users')->assertRedirect('/'); $this->giveUserPermissions($this->user, ['users-manage']); - $this->actingAs($this->user)->visit('/settings/users') - ->seePageIs('/settings/users'); + $this->actingAs($this->user)->get('/settings/users')->assertOk(); } public function test_manage_users_permission_shows_link_in_header_if_does_not_have_settings_manage_permision() { $usersLink = 'href="' . url('/settings/users') . '"'; - $this->actingAs($this->user)->visit('/')->dontSee($usersLink); + $this->actingAs($this->user)->get('/')->assertDontSee($usersLink); $this->giveUserPermissions($this->user, ['users-manage']); - $this->actingAs($this->user)->visit('/')->see($usersLink); + $this->actingAs($this->user)->get('/')->assertSee($usersLink); $this->giveUserPermissions($this->user, ['settings-manage', 'users-manage']); - $this->actingAs($this->user)->visit('/')->dontSee($usersLink); + $this->actingAs($this->user)->get('/')->assertDontSee($usersLink); } public function test_user_cannot_change_email_unless_they_have_manage_users_permission() @@ -151,14 +186,14 @@ class RolesTest extends BrowserKitTest $originalEmail = $this->user->email; $this->actingAs($this->user); - $this->visit($userProfileUrl) - ->assertResponseOk() - ->seeElement('input[name=email][disabled]'); + $this->get($userProfileUrl) + ->assertOk() + ->assertElementExists('input[name=email][disabled]'); $this->put($userProfileUrl, [ 'name' => 'my_new_name', 'email' => 'new_email@example.com', ]); - $this->seeInDatabase('users', [ + $this->assertDatabaseHas('users', [ 'id' => $this->user->id, 'email' => $originalEmail, 'name' => 'my_new_name', @@ -166,16 +201,16 @@ class RolesTest extends BrowserKitTest $this->giveUserPermissions($this->user, ['users-manage']); - $this->visit($userProfileUrl) - ->assertResponseOk() - ->dontSeeElement('input[name=email][disabled]') - ->seeElement('input[name=email]'); + $this->get($userProfileUrl) + ->assertOk() + ->assertElementNotExists('input[name=email][disabled]') + ->assertElementExists('input[name=email]'); $this->put($userProfileUrl, [ 'name' => 'my_new_name_2', 'email' => 'new_email@example.com', ]); - $this->seeInDatabase('users', [ + $this->assertDatabaseHas('users', [ 'id' => $this->user->id, 'email' => 'new_email@example.com', 'name' => 'my_new_name_2', @@ -184,40 +219,47 @@ class RolesTest extends BrowserKitTest public function test_user_roles_manage_permission() { - $this->actingAs($this->user)->visit('/settings/roles') - ->seePageIs('/')->visit('/settings/roles/1')->seePageIs('/'); + $this->actingAs($this->user)->get('/settings/roles')->assertRedirect('/'); + $this->get('/settings/roles/1')->assertRedirect('/'); $this->giveUserPermissions($this->user, ['user-roles-manage']); - $this->actingAs($this->user)->visit('/settings/roles') - ->seePageIs('/settings/roles')->click('Admin') - ->see('Edit Role'); + $this->actingAs($this->user)->get('/settings/roles')->assertOk(); + $this->get('/settings/roles/1') + ->assertOk() + ->assertSee('Admin'); } public function test_settings_manage_permission() { - $this->actingAs($this->user)->visit('/settings') - ->seePageIs('/'); + $this->actingAs($this->user)->get('/settings')->assertRedirect('/'); $this->giveUserPermissions($this->user, ['settings-manage']); - $this->actingAs($this->user)->visit('/settings') - ->seePageIs('/settings')->press('Save Settings')->see('Settings Saved'); + $this->get('/settings')->assertOk(); + + $resp = $this->post('/settings', []); + $resp->assertRedirect('/settings'); + $resp = $this->get('/settings'); + $resp->assertSee('Settings saved'); } public function test_restrictions_manage_all_permission() { - $page = Page::take(1)->get()->first(); - $this->actingAs($this->user)->visit($page->getUrl()) - ->dontSee('Permissions') - ->visit($page->getUrl() . '/permissions') - ->seePageIs('/'); + $page = Page::query()->get()->first(); + + $this->actingAs($this->user)->get($page->getUrl())->assertDontSee('Permissions'); + $this->get($page->getUrl('/permissions'))->assertRedirect('/'); + $this->giveUserPermissions($this->user, ['restrictions-manage-all']); - $this->actingAs($this->user)->visit($page->getUrl()) - ->see('Permissions') - ->click('Permissions') - ->see('Page Permissions')->seePageIs($page->getUrl() . '/permissions'); + + $this->actingAs($this->user)->get($page->getUrl())->assertSee('Permissions'); + + $this->get($page->getUrl('/permissions')) + ->assertOk() + ->assertSee('Page Permissions'); } public function test_restrictions_manage_own_permission() { - $otherUsersPage = Page::first(); + /** @var Page $otherUsersPage */ + $otherUsersPage = Page::query()->first(); $content = $this->createEntityChainBelongingToUser($this->user); // Set a different creator on the page we're checking to ensure @@ -228,57 +270,45 @@ class RolesTest extends BrowserKitTest $page->save(); // Check can't restrict other's content - $this->actingAs($this->user)->visit($otherUsersPage->getUrl()) - ->dontSee('Permissions') - ->visit($otherUsersPage->getUrl() . '/permissions') - ->seePageIs('/'); + $this->actingAs($this->user)->get($otherUsersPage->getUrl())->assertDontSee('Permissions'); + $this->get($otherUsersPage->getUrl('/permissions'))->assertRedirect('/'); + // Check can't restrict own content - $this->actingAs($this->user)->visit($page->getUrl()) - ->dontSee('Permissions') - ->visit($page->getUrl() . '/permissions') - ->seePageIs('/'); + $this->actingAs($this->user)->get($page->getUrl())->assertDontSee('Permissions'); + $this->get($page->getUrl('/permissions'))->assertRedirect('/'); $this->giveUserPermissions($this->user, ['restrictions-manage-own']); // Check can't restrict other's content - $this->actingAs($this->user)->visit($otherUsersPage->getUrl()) - ->dontSee('Permissions') - ->visit($otherUsersPage->getUrl() . '/permissions') - ->seePageIs('/'); + $this->actingAs($this->user)->get($otherUsersPage->getUrl())->assertDontSee('Permissions'); + $this->get($otherUsersPage->getUrl('/permissions'))->assertRedirect(); + // Check can restrict own content - $this->actingAs($this->user)->visit($page->getUrl()) - ->see('Permissions') - ->click('Permissions') - ->seePageIs($page->getUrl() . '/permissions'); + $this->actingAs($this->user)->get($page->getUrl())->assertSee('Permissions'); + $this->get($page->getUrl('/permissions'))->assertOk(); } /** * Check a standard entity access permission. - * - * @param string $permission - * @param array $accessUrls Urls that are only accessible after having the permission - * @param array $visibles Check this text, In the buttons toolbar, is only visible with the permission */ - private function checkAccessPermission($permission, $accessUrls = [], $visibles = []) + private function checkAccessPermission(string $permission, array $accessUrls = [], array $visibles = []) { foreach ($accessUrls as $url) { - $this->actingAs($this->user)->visit($url) - ->seePageIs('/'); + $this->actingAs($this->user)->get($url)->assertRedirect('/'); } + foreach ($visibles as $url => $text) { - $this->actingAs($this->user)->visit($url) - ->dontSeeInElement('.action-buttons', $text); + $this->actingAs($this->user)->get($url) + ->assertElementNotContains('.action-buttons', $text); } $this->giveUserPermissions($this->user, [$permission]); foreach ($accessUrls as $url) { - $this->actingAs($this->user)->visit($url) - ->seePageIs($url); + $this->actingAs($this->user)->get($url)->assertOk(); } foreach ($visibles as $url => $text) { - $this->actingAs($this->user)->visit($url) - ->see($text); + $this->actingAs($this->user)->get($url)->assertSee($text); } } @@ -290,16 +320,16 @@ class RolesTest extends BrowserKitTest '/shelves' => 'New Shelf', ]); - $this->visit('/create-shelf') - ->type('test shelf', 'name') - ->type('shelf desc', 'description') - ->press('Save Shelf') - ->seePageIs('/shelves/test-shelf'); + $this->post('/shelves', [ + 'name' => 'test shelf', + 'description' => 'shelf desc', + ])->assertRedirect('/shelves/test-shelf'); } public function test_bookshelves_edit_own_permission() { - $otherShelf = Bookshelf::first(); + /** @var Bookshelf $otherShelf */ + $otherShelf = Bookshelf::query()->first(); $ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']); $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save(); $this->regenEntityPermissions($ownShelf); @@ -310,15 +340,14 @@ class RolesTest extends BrowserKitTest $ownShelf->getUrl() => 'Edit', ]); - $this->visit($otherShelf->getUrl()) - ->dontSeeInElement('.action-buttons', 'Edit') - ->visit($otherShelf->getUrl('/edit')) - ->seePageIs('/'); + $this->get($otherShelf->getUrl())->assertElementNotContains('.action-buttons', 'Edit'); + $this->get($otherShelf->getUrl('/edit'))->assertRedirect('/'); } public function test_bookshelves_edit_all_permission() { - $otherShelf = Bookshelf::first(); + /** @var Bookshelf $otherShelf */ + $otherShelf = Bookshelf::query()->first(); $this->checkAccessPermission('bookshelf-update-all', [ $otherShelf->getUrl('/edit'), ], [ @@ -329,7 +358,8 @@ class RolesTest extends BrowserKitTest public function test_bookshelves_delete_own_permission() { $this->giveUserPermissions($this->user, ['bookshelf-update-all']); - $otherShelf = Bookshelf::first(); + /** @var Bookshelf $otherShelf */ + $otherShelf = Bookshelf::query()->first(); $ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']); $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save(); $this->regenEntityPermissions($ownShelf); @@ -340,30 +370,27 @@ class RolesTest extends BrowserKitTest $ownShelf->getUrl() => 'Delete', ]); - $this->visit($otherShelf->getUrl()) - ->dontSeeInElement('.action-buttons', 'Delete') - ->visit($otherShelf->getUrl('/delete')) - ->seePageIs('/'); - $this->visit($ownShelf->getUrl())->visit($ownShelf->getUrl('/delete')) - ->press('Confirm') - ->seePageIs('/shelves') - ->dontSee($ownShelf->name); + $this->get($otherShelf->getUrl())->assertElementNotContains('.action-buttons', 'Delete'); + $this->get($otherShelf->getUrl('/delete'))->assertRedirect('/'); + + $this->get($ownShelf->getUrl()); + $this->delete($ownShelf->getUrl())->assertRedirect('/shelves'); + $this->get('/shelves')->assertDontSee($ownShelf->name); } public function test_bookshelves_delete_all_permission() { $this->giveUserPermissions($this->user, ['bookshelf-update-all']); - $otherShelf = Bookshelf::first(); + /** @var Bookshelf $otherShelf */ + $otherShelf = Bookshelf::query()->first(); $this->checkAccessPermission('bookshelf-delete-all', [ $otherShelf->getUrl('/delete'), ], [ $otherShelf->getUrl() => 'Delete', ]); - $this->visit($otherShelf->getUrl())->visit($otherShelf->getUrl('/delete')) - ->press('Confirm') - ->seePageIs('/shelves') - ->dontSee($otherShelf->name); + $this->delete($otherShelf->getUrl())->assertRedirect('/shelves'); + $this->get('/shelves')->assertDontSee($otherShelf->name); } public function test_books_create_all_permissions() @@ -374,16 +401,16 @@ class RolesTest extends BrowserKitTest '/books' => 'Create New Book', ]); - $this->visit('/create-book') - ->type('test book', 'name') - ->type('book desc', 'description') - ->press('Save Book') - ->seePageIs('/books/test-book'); + $this->post('/books', [ + 'name' => 'test book', + 'description' => 'book desc', + ])->assertRedirect('/books/test-book'); } public function test_books_edit_own_permission() { - $otherBook = Book::take(1)->get()->first(); + /** @var Book $otherBook */ + $otherBook = Book::query()->take(1)->get()->first(); $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; $this->checkAccessPermission('book-update-own', [ $ownBook->getUrl() . '/edit', @@ -391,15 +418,14 @@ class RolesTest extends BrowserKitTest $ownBook->getUrl() => 'Edit', ]); - $this->visit($otherBook->getUrl()) - ->dontSeeInElement('.action-buttons', 'Edit') - ->visit($otherBook->getUrl() . '/edit') - ->seePageIs('/'); + $this->get($otherBook->getUrl())->assertElementNotContains('.action-buttons', 'Edit'); + $this->get($otherBook->getUrl('/edit'))->assertRedirect('/'); } public function test_books_edit_all_permission() { - $otherBook = Book::take(1)->get()->first(); + /** @var Book $otherBook */ + $otherBook = Book::query()->take(1)->get()->first(); $this->checkAccessPermission('book-update-all', [ $otherBook->getUrl() . '/edit', ], [ @@ -410,7 +436,8 @@ class RolesTest extends BrowserKitTest public function test_books_delete_own_permission() { $this->giveUserPermissions($this->user, ['book-update-all']); - $otherBook = Book::take(1)->get()->first(); + /** @var Book $otherBook */ + $otherBook = Book::query()->take(1)->get()->first(); $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; $this->checkAccessPermission('book-delete-own', [ $ownBook->getUrl() . '/delete', @@ -418,35 +445,33 @@ class RolesTest extends BrowserKitTest $ownBook->getUrl() => 'Delete', ]); - $this->visit($otherBook->getUrl()) - ->dontSeeInElement('.action-buttons', 'Delete') - ->visit($otherBook->getUrl() . '/delete') - ->seePageIs('/'); - $this->visit($ownBook->getUrl())->visit($ownBook->getUrl() . '/delete') - ->press('Confirm') - ->seePageIs('/books') - ->dontSee($ownBook->name); + $this->get($otherBook->getUrl())->assertElementNotContains('.action-buttons', 'Delete'); + $this->get($otherBook->getUrl('/delete'))->assertRedirect('/'); + $this->get($ownBook->getUrl()); + $this->delete($ownBook->getUrl())->assertRedirect('/books'); + $this->get('/books')->assertDontSee($ownBook->name); } public function test_books_delete_all_permission() { $this->giveUserPermissions($this->user, ['book-update-all']); - $otherBook = Book::take(1)->get()->first(); + /** @var Book $otherBook */ + $otherBook = Book::query()->take(1)->get()->first(); $this->checkAccessPermission('book-delete-all', [ $otherBook->getUrl() . '/delete', ], [ $otherBook->getUrl() => 'Delete', ]); - $this->visit($otherBook->getUrl())->visit($otherBook->getUrl() . '/delete') - ->press('Confirm') - ->seePageIs('/books') - ->dontSee($otherBook->name); + $this->get($otherBook->getUrl()); + $this->delete($otherBook->getUrl())->assertRedirect('/books'); + $this->get('/books')->assertDontSee($otherBook->name); } public function test_chapter_create_own_permissions() { - $book = Book::take(1)->get()->first(); + /** @var Book $book */ + $book = Book::query()->take(1)->get()->first(); $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; $this->checkAccessPermission('chapter-create-own', [ $ownBook->getUrl('/create-chapter'), @@ -454,37 +479,35 @@ class RolesTest extends BrowserKitTest $ownBook->getUrl() => 'New Chapter', ]); - $this->visit($ownBook->getUrl('/create-chapter')) - ->type('test chapter', 'name') - ->type('chapter desc', 'description') - ->press('Save Chapter') - ->seePageIs($ownBook->getUrl('/chapter/test-chapter')); + $this->post($ownBook->getUrl('/create-chapter'), [ + 'name' => 'test chapter', + 'description' => 'chapter desc', + ])->assertRedirect($ownBook->getUrl('/chapter/test-chapter')); - $this->visit($book->getUrl()) - ->dontSeeInElement('.action-buttons', 'New Chapter') - ->visit($book->getUrl('/create-chapter')) - ->seePageIs('/'); + $this->get($book->getUrl())->assertElementNotContains('.action-buttons', 'New Chapter'); + $this->get($book->getUrl('/create-chapter'))->assertRedirect('/'); } public function test_chapter_create_all_permissions() { - $book = Book::take(1)->get()->first(); + /** @var Book $book */ + $book = Book::query()->first(); $this->checkAccessPermission('chapter-create-all', [ $book->getUrl('/create-chapter'), ], [ $book->getUrl() => 'New Chapter', ]); - $this->visit($book->getUrl('/create-chapter')) - ->type('test chapter', 'name') - ->type('chapter desc', 'description') - ->press('Save Chapter') - ->seePageIs($book->getUrl('/chapter/test-chapter')); + $this->post($book->getUrl('/create-chapter'), [ + 'name' => 'test chapter', + 'description' => 'chapter desc', + ])->assertRedirect($book->getUrl('/chapter/test-chapter')); } public function test_chapter_edit_own_permission() { - $otherChapter = Chapter::take(1)->get()->first(); + /** @var Chapter $otherChapter */ + $otherChapter = Chapter::query()->first(); $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter']; $this->checkAccessPermission('chapter-update-own', [ $ownChapter->getUrl() . '/edit', @@ -492,15 +515,14 @@ class RolesTest extends BrowserKitTest $ownChapter->getUrl() => 'Edit', ]); - $this->visit($otherChapter->getUrl()) - ->dontSeeInElement('.action-buttons', 'Edit') - ->visit($otherChapter->getUrl() . '/edit') - ->seePageIs('/'); + $this->get($otherChapter->getUrl())->assertElementNotContains('.action-buttons', 'Edit'); + $this->get($otherChapter->getUrl('/edit'))->assertRedirect('/'); } public function test_chapter_edit_all_permission() { - $otherChapter = Chapter::take(1)->get()->first(); + /** @var Chapter $otherChapter */ + $otherChapter = Chapter::query()->take(1)->get()->first(); $this->checkAccessPermission('chapter-update-all', [ $otherChapter->getUrl() . '/edit', ], [ @@ -511,7 +533,8 @@ class RolesTest extends BrowserKitTest public function test_chapter_delete_own_permission() { $this->giveUserPermissions($this->user, ['chapter-update-all']); - $otherChapter = Chapter::take(1)->get()->first(); + /** @var Chapter $otherChapter */ + $otherChapter = Chapter::query()->first(); $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter']; $this->checkAccessPermission('chapter-delete-own', [ $ownChapter->getUrl() . '/delete', @@ -520,20 +543,18 @@ class RolesTest extends BrowserKitTest ]); $bookUrl = $ownChapter->book->getUrl(); - $this->visit($otherChapter->getUrl()) - ->dontSeeInElement('.action-buttons', 'Delete') - ->visit($otherChapter->getUrl() . '/delete') - ->seePageIs('/'); - $this->visit($ownChapter->getUrl())->visit($ownChapter->getUrl() . '/delete') - ->press('Confirm') - ->seePageIs($bookUrl) - ->dontSeeInElement('.book-content', $ownChapter->name); + $this->get($otherChapter->getUrl())->assertElementNotContains('.action-buttons', 'Delete'); + $this->get($otherChapter->getUrl('/delete'))->assertRedirect('/'); + $this->get($ownChapter->getUrl()); + $this->delete($ownChapter->getUrl())->assertRedirect($bookUrl); + $this->get($bookUrl)->assertElementNotContains('.book-content', $ownChapter->name); } public function test_chapter_delete_all_permission() { $this->giveUserPermissions($this->user, ['chapter-update-all']); - $otherChapter = Chapter::take(1)->get()->first(); + /** @var Chapter $otherChapter */ + $otherChapter = Chapter::query()->first(); $this->checkAccessPermission('chapter-delete-all', [ $otherChapter->getUrl() . '/delete', ], [ @@ -541,16 +562,17 @@ class RolesTest extends BrowserKitTest ]); $bookUrl = $otherChapter->book->getUrl(); - $this->visit($otherChapter->getUrl())->visit($otherChapter->getUrl() . '/delete') - ->press('Confirm') - ->seePageIs($bookUrl) - ->dontSeeInElement('.book-content', $otherChapter->name); + $this->get($otherChapter->getUrl()); + $this->delete($otherChapter->getUrl())->assertRedirect($bookUrl); + $this->get($bookUrl)->assertElementNotContains('.book-content', $otherChapter->name); } public function test_page_create_own_permissions() { - $book = Book::first(); - $chapter = Chapter::first(); + /** @var Book $book */ + $book = Book::query()->first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $entities = $this->createEntityChainBelongingToUser($this->user); $ownBook = $entities['book']; @@ -561,8 +583,7 @@ class RolesTest extends BrowserKitTest $accessUrls = [$createUrl, $createUrlChapter]; foreach ($accessUrls as $url) { - $this->actingAs($this->user)->visit($url) - ->seePageIs('/'); + $this->actingAs($this->user)->get($url)->assertRedirect('/'); } $this->checkAccessPermission('page-create-own', [], [ @@ -573,40 +594,39 @@ class RolesTest extends BrowserKitTest $this->giveUserPermissions($this->user, ['page-create-own']); foreach ($accessUrls as $index => $url) { - $this->actingAs($this->user)->visit($url); - $expectedUrl = Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl(); - $this->seePageIs($expectedUrl); + $resp = $this->actingAs($this->user)->get($url); + $expectedUrl = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl(); + $resp->assertRedirect($expectedUrl); } - $this->visit($createUrl) - ->type('test page', 'name') - ->type('page desc', 'html') - ->press('Save Page') - ->seePageIs($ownBook->getUrl('/page/test-page')); + $this->get($createUrl); + /** @var Page $draft */ + $draft = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first(); + $this->post($draft->getUrl(), [ + 'name' => 'test page', + 'html' => 'page desc', + ])->assertRedirect($ownBook->getUrl('/page/test-page')); - $this->visit($book->getUrl()) - ->dontSeeInElement('.action-buttons', 'New Page') - ->visit($book->getUrl() . '/create-page') - ->seePageIs('/'); - $this->visit($chapter->getUrl()) - ->dontSeeInElement('.action-buttons', 'New Page') - ->visit($chapter->getUrl() . '/create-page') - ->seePageIs('/'); + $this->get($book->getUrl())->assertElementNotContains('.action-buttons', 'New Page'); + $this->get($book->getUrl('/create-page'))->assertRedirect('/'); + + $this->get($chapter->getUrl())->assertElementNotContains('.action-buttons', 'New Page'); + $this->get($chapter->getUrl('/create-page'))->assertRedirect('/'); } public function test_page_create_all_permissions() { - $book = Book::take(1)->get()->first(); - $chapter = Chapter::take(1)->get()->first(); - $baseUrl = $book->getUrl() . '/page'; + /** @var Book $book */ + $book = Book::query()->first(); + /** @var Chapter $chapter */ + $chapter = Chapter::query()->first(); $createUrl = $book->getUrl('/create-page'); $createUrlChapter = $chapter->getUrl('/create-page'); $accessUrls = [$createUrl, $createUrlChapter]; foreach ($accessUrls as $url) { - $this->actingAs($this->user)->visit($url) - ->seePageIs('/'); + $this->actingAs($this->user)->get($url)->assertRedirect('/'); } $this->checkAccessPermission('page-create-all', [], [ @@ -617,27 +637,32 @@ class RolesTest extends BrowserKitTest $this->giveUserPermissions($this->user, ['page-create-all']); foreach ($accessUrls as $index => $url) { - $this->actingAs($this->user)->visit($url); - $expectedUrl = Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl(); - $this->seePageIs($expectedUrl); + $resp = $this->actingAs($this->user)->get($url); + $expectedUrl = Page::query()->where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl(); + $resp->assertRedirect($expectedUrl); } - $this->visit($createUrl) - ->type('test page', 'name') - ->type('page desc', 'html') - ->press('Save Page') - ->seePageIs($book->getUrl('/page/test-page')); + $this->get($createUrl); + /** @var Page $draft */ + $draft = Page::query()->where('draft', '=', true)->orderByDesc('id')->first(); + $this->post($draft->getUrl(), [ + 'name' => 'test page', + 'html' => 'page desc', + ])->assertRedirect($book->getUrl('/page/test-page')); - $this->visit($chapter->getUrl('/create-page')) - ->type('new test page', 'name') - ->type('page desc', 'html') - ->press('Save Page') - ->seePageIs($book->getUrl('/page/new-test-page')); + $this->get($chapter->getUrl('/create-page')); + /** @var Page $draft */ + $draft = Page::query()->where('draft', '=', true)->orderByDesc('id')->first(); + $this->post($draft->getUrl(), [ + 'name' => 'new test page', + 'html' => 'page desc', + ])->assertRedirect($book->getUrl('/page/new-test-page')); } public function test_page_edit_own_permission() { - $otherPage = Page::take(1)->get()->first(); + /** @var Page $otherPage */ + $otherPage = Page::query()->first(); $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $this->checkAccessPermission('page-update-own', [ $ownPage->getUrl() . '/edit', @@ -645,17 +670,16 @@ class RolesTest extends BrowserKitTest $ownPage->getUrl() => 'Edit', ]); - $this->visit($otherPage->getUrl()) - ->dontSeeInElement('.action-buttons', 'Edit') - ->visit($otherPage->getUrl() . '/edit') - ->seePageIs('/'); + $this->get($otherPage->getUrl())->assertElementNotContains('.action-buttons', 'Edit'); + $this->get($otherPage->getUrl() . '/edit')->assertRedirect('/'); } public function test_page_edit_all_permission() { - $otherPage = Page::take(1)->get()->first(); + /** @var Page $otherPage */ + $otherPage = Page::query()->first(); $this->checkAccessPermission('page-update-all', [ - $otherPage->getUrl() . '/edit', + $otherPage->getUrl('/edit'), ], [ $otherPage->getUrl() => 'Edit', ]); @@ -664,7 +688,8 @@ class RolesTest extends BrowserKitTest public function test_page_delete_own_permission() { $this->giveUserPermissions($this->user, ['page-update-all']); - $otherPage = Page::take(1)->get()->first(); + /** @var Page $otherPage */ + $otherPage = Page::query()->first(); $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $this->checkAccessPermission('page-delete-own', [ $ownPage->getUrl() . '/delete', @@ -673,122 +698,127 @@ class RolesTest extends BrowserKitTest ]); $parent = $ownPage->chapter ?? $ownPage->book; - $this->visit($otherPage->getUrl()) - ->dontSeeInElement('.action-buttons', 'Delete') - ->visit($otherPage->getUrl() . '/delete') - ->seePageIs('/'); - $this->visit($ownPage->getUrl())->visit($ownPage->getUrl() . '/delete') - ->press('Confirm') - ->seePageIs($parent->getUrl()) - ->dontSeeInElement('.book-content', $ownPage->name); + $this->get($otherPage->getUrl())->assertElementNotContains('.action-buttons', 'Delete'); + $this->get($otherPage->getUrl('/delete'))->assertRedirect('/'); + $this->get($ownPage->getUrl()); + $this->delete($ownPage->getUrl())->assertRedirect($parent->getUrl()); + $this->get($parent->getUrl())->assertElementNotContains('.book-content', $ownPage->name); } public function test_page_delete_all_permission() { $this->giveUserPermissions($this->user, ['page-update-all']); - $otherPage = Page::take(1)->get()->first(); + /** @var Page $otherPage */ + $otherPage = Page::query()->first(); + $this->checkAccessPermission('page-delete-all', [ $otherPage->getUrl() . '/delete', ], [ $otherPage->getUrl() => 'Delete', ]); + /** @var Entity $parent */ $parent = $otherPage->chapter ?? $otherPage->book; - $this->visit($otherPage->getUrl())->visit($otherPage->getUrl() . '/delete') - ->press('Confirm') - ->seePageIs($parent->getUrl()) - ->dontSeeInElement('.book-content', $otherPage->name); + $this->get($otherPage->getUrl()); + + $this->delete($otherPage->getUrl())->assertRedirect($parent->getUrl()); + $this->get($parent->getUrl())->assertDontSee($otherPage->name); } public function test_public_role_visible_in_user_edit_screen() { - $user = User::first(); + /** @var User $user */ + $user = User::query()->first(); $adminRole = Role::getSystemRole('admin'); $publicRole = Role::getSystemRole('public'); - $this->asAdmin()->visit('/settings/users/' . $user->id) - ->seeElement('[name="roles[' . $adminRole->id . ']"]') - ->seeElement('[name="roles[' . $publicRole->id . ']"]'); + $this->asAdmin()->get('/settings/users/' . $user->id) + ->assertElementExists('[name="roles[' . $adminRole->id . ']"]') + ->assertElementExists('[name="roles[' . $publicRole->id . ']"]'); } public function test_public_role_visible_in_role_listing() { - $this->asAdmin()->visit('/settings/roles') - ->see('Admin') - ->see('Public'); + $this->asAdmin()->get('/settings/roles') + ->assertSee('Admin') + ->assertSee('Public'); } public function test_public_role_visible_in_default_role_setting() { - $this->asAdmin()->visit('/settings') - ->seeElement('[data-system-role-name="admin"]') - ->seeElement('[data-system-role-name="public"]'); + $this->asAdmin()->get('/settings') + ->assertElementExists('[data-system-role-name="admin"]') + ->assertElementExists('[data-system-role-name="public"]'); } - public function test_public_role_not_deleteable() + public function test_public_role_not_deletable() { - $this->asAdmin()->visit('/settings/roles') - ->click('Public') - ->see('Edit Role') - ->click('Delete Role') - ->press('Confirm') - ->see('Delete Role') - ->see('Cannot be deleted'); + /** @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() { $this->giveUserPermissions($this->user, ['image-update-all']); - $page = Page::first(); - $image = factory(Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $this->user->id, 'updated_by' => $this->user->id]); + /** @var Page $page */ + $page = Page::query()->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); + $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403); $this->giveUserPermissions($this->user, ['image-delete-own']); - $this->actingAs($this->user)->json('delete', '/images/' . $image->id) - ->seeStatusCode(200) - ->dontSeeInDatabase('images', ['id' => $image->id]); + $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertOk(); + $this->assertDatabaseMissing('images', ['id' => $image->id]); } public function test_image_delete_all_permission() { $this->giveUserPermissions($this->user, ['image-update-all']); $admin = $this->getAdmin(); - $page = Page::first(); + /** @var Page $page */ + $page = Page::query()->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); + $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403); $this->giveUserPermissions($this->user, ['image-delete-own']); - $this->actingAs($this->user)->json('delete', '/images/' . $image->id) - ->seeStatusCode(403); + $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertStatus(403); $this->giveUserPermissions($this->user, ['image-delete-all']); - $this->actingAs($this->user)->json('delete', '/images/' . $image->id) - ->seeStatusCode(200) - ->dontSeeInDatabase('images', ['id' => $image->id]); + $this->actingAs($this->user)->json('delete', '/images/' . $image->id)->assertOk(); + $this->assertDatabaseMissing('images', ['id' => $image->id]); } public function test_role_permission_removal() { // To cover issue fixed in f99c8ff99aee9beb8c692f36d4b84dc6e651e50a. - $page = Page::first(); + /** @var Page $page */ + $page = Page::query()->first(); $viewerRole = Role::getRole('viewer'); $viewer = $this->getViewer(); - $this->actingAs($viewer)->visit($page->getUrl())->assertResponseStatus(200); + $this->actingAs($viewer)->get($page->getUrl())->assertOk(); $this->asAdmin()->put('/settings/roles/' . $viewerRole->id, [ 'display_name' => $viewerRole->display_name, 'description' => $viewerRole->description, 'permission' => [], - ])->assertResponseStatus(302); + ])->assertStatus(302); - $this->expectException(HttpException::class); - $this->actingAs($viewer)->visit($page->getUrl())->assertResponseStatus(404); + $this->actingAs($viewer)->get($page->getUrl())->assertStatus(404); } public function test_empty_state_actions_not_visible_without_permission() @@ -796,130 +826,120 @@ class RolesTest extends BrowserKitTest $admin = $this->getAdmin(); // Book links $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'); + $this->regenEntityPermissions($book); + $this->actingAs($this->getViewer())->get($book->getUrl()) + ->assertDontSee('Create a new page') + ->assertDontSee('Add a chapter'); // Chapter links $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') - ->dontSee('Sort the current book'); + $this->regenEntityPermissions($chapter); + $this->actingAs($this->getViewer())->get($chapter->getUrl()) + ->assertDontSee('Create a new page') + ->assertDontSee('Sort the current book'); } public function test_comment_create_permission() { $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; - $this->actingAs($this->user)->addComment($ownPage); - - $this->assertResponseStatus(403); + $this->actingAs($this->user) + ->addComment($ownPage) + ->assertStatus(403); $this->giveUserPermissions($this->user, ['comment-create-all']); - $this->actingAs($this->user)->addComment($ownPage); - $this->assertResponseStatus(200); + $this->actingAs($this->user) + ->addComment($ownPage) + ->assertOk(); } public function test_comment_update_own_permission() { $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $this->giveUserPermissions($this->user, ['comment-create-all']); - $commentId = $this->actingAs($this->user)->addComment($ownPage); + $this->actingAs($this->user)->addComment($ownPage); + /** @var Comment $comment */ + $comment = $ownPage->comments()->latest()->first(); // no comment-update-own - $this->actingAs($this->user)->updateComment($commentId); - $this->assertResponseStatus(403); + $this->actingAs($this->user)->updateComment($comment)->assertStatus(403); $this->giveUserPermissions($this->user, ['comment-update-own']); // now has comment-update-own - $this->actingAs($this->user)->updateComment($commentId); - $this->assertResponseStatus(200); + $this->actingAs($this->user)->updateComment($comment)->assertOk(); } public function test_comment_update_all_permission() { + /** @var Page $ownPage */ $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; - $commentId = $this->asAdmin()->addComment($ownPage); + $this->asAdmin()->addComment($ownPage); + /** @var Comment $comment */ + $comment = $ownPage->comments()->latest()->first(); // no comment-update-all - $this->actingAs($this->user)->updateComment($commentId); - $this->assertResponseStatus(403); + $this->actingAs($this->user)->updateComment($comment)->assertStatus(403); $this->giveUserPermissions($this->user, ['comment-update-all']); // now has comment-update-all - $this->actingAs($this->user)->updateComment($commentId); - $this->assertResponseStatus(200); + $this->actingAs($this->user)->updateComment($comment)->assertOk(); } public function test_comment_delete_own_permission() { + /** @var Page $ownPage */ $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $this->giveUserPermissions($this->user, ['comment-create-all']); - $commentId = $this->actingAs($this->user)->addComment($ownPage); + $this->actingAs($this->user)->addComment($ownPage); + + /** @var Comment $comment */ + $comment = $ownPage->comments()->latest()->first(); // no comment-delete-own - $this->actingAs($this->user)->deleteComment($commentId); - $this->assertResponseStatus(403); + $this->actingAs($this->user)->deleteComment($comment)->assertStatus(403); $this->giveUserPermissions($this->user, ['comment-delete-own']); // now has comment-update-own - $this->actingAs($this->user)->deleteComment($commentId); - $this->assertResponseStatus(200); + $this->actingAs($this->user)->deleteComment($comment)->assertOk(); } public function test_comment_delete_all_permission() { + /** @var Page $ownPage */ $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; - $commentId = $this->asAdmin()->addComment($ownPage); + $this->asAdmin()->addComment($ownPage); + /** @var Comment $comment */ + $comment = $ownPage->comments()->latest()->first(); // no comment-delete-all - $this->actingAs($this->user)->deleteComment($commentId); - $this->assertResponseStatus(403); + $this->actingAs($this->user)->deleteComment($comment)->assertStatus(403); $this->giveUserPermissions($this->user, ['comment-delete-all']); // now has comment-delete-all - $this->actingAs($this->user)->deleteComment($commentId); - $this->assertResponseStatus(200); + $this->actingAs($this->user)->deleteComment($comment)->assertOk(); } - private function addComment($page) + private function addComment(Page $page): TestResponse { $comment = factory(Comment::class)->make(); - $url = "/comment/$page->id"; - $request = [ - 'text' => $comment->text, - 'html' => $comment->html, - ]; - $this->postJson($url, $request); - $comment = $page->comments()->first(); - - return $comment === null ? null : $comment->id; + return $this->postJson("/comment/$page->id", $comment->only('text', 'html')); } - private function updateComment($commentId) + private function updateComment(Comment $comment): TestResponse { - $comment = factory(Comment::class)->make(); - $url = "/comment/$commentId"; - $request = [ - 'text' => $comment->text, - 'html' => $comment->html, - ]; + $commentData = factory(Comment::class)->make(); - return $this->putJson($url, $request); + return $this->putJson("/comment/{$comment->id}", $commentData->only('text', 'html')); } - private function deleteComment($commentId) + private function deleteComment(Comment $comment): TestResponse { - $url = '/comment/' . $commentId; - - return $this->json('DELETE', $url); + return $this->json('DELETE', '/comment/' . $comment->id); } } diff --git a/tests/SharedTestHelpers.php b/tests/SharedTestHelpers.php index df6c613df..e4d27c849 100644 --- a/tests/SharedTestHelpers.php +++ b/tests/SharedTestHelpers.php @@ -89,7 +89,7 @@ trait SharedTestHelpers /** * Get a user that's not a system user such as the guest user. */ - public function getNormalUser() + public function getNormalUser(): User { return User::query()->where('system_name', '=', null)->get()->last(); } @@ -211,6 +211,27 @@ trait SharedTestHelpers return $permissionRepo->saveNewRole($roleData); } + /** + * Create a group of entities that belong to a specific user. + * + * @return array{book: Book, chapter: Chapter, page: Page} + */ + protected function createEntityChainBelongingToUser(User $creatorUser, ?User $updaterUser = null): array + { + 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 compact('book', 'chapter', 'page'); + } + /** * Mock the HttpFetcher service and return the given data on fetch. */ diff --git a/tests/User/UserManagementTest.php b/tests/User/UserManagementTest.php index 4fd7bacc7..ed2fb5f04 100644 --- a/tests/User/UserManagementTest.php +++ b/tests/User/UserManagementTest.php @@ -3,12 +3,112 @@ namespace Tests\User; use BookStack\Actions\ActivityType; +use BookStack\Auth\Role; use BookStack\Auth\User; use BookStack\Entities\Models\Page; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Str; use Tests\TestCase; class UserManagementTest extends TestCase { + public function test_user_creation() + { + /** @var User $user */ + $user = factory(User::class)->make(); + $adminRole = Role::getRole('admin'); + + $resp = $this->asAdmin()->get('/settings/users'); + $resp->assertElementContains('a[href="' . url('/settings/users/create') . '"]', 'Add New User'); + + $this->get('/settings/users/create') + ->assertElementContains('form[action="' . url('/settings/users/create') . '"]', 'Save'); + + $resp = $this->post('/settings/users/create', [ + 'name' => $user->name, + 'email' => $user->email, + 'password' => $user->password, + 'password-confirm' => $user->password, + 'roles[' . $adminRole->id . ']' => 'true', + ]); + $resp->assertRedirect('/settings/users'); + + $resp = $this->get('/settings/users'); + $resp->assertSee($user->name); + + $this->assertDatabaseHas('users', $user->only('name', 'email')); + + $user->refresh(); + $this->assertStringStartsWith(Str::slug($user->name), $user->slug); + } + + public function test_user_updating() + { + $user = $this->getNormalUser(); + $password = $user->password; + + $resp = $this->asAdmin()->get('/settings/users/' . $user->id); + $resp->assertSee($user->email); + + $this->put($user->getEditUrl(), [ + 'name' => 'Barry Scott', + ])->assertRedirect('/settings/users'); + + $this->assertDatabaseHas('users', ['id' => $user->id, 'name' => 'Barry Scott', 'password' => $password]); + $this->assertDatabaseMissing('users', ['name' => $user->name]); + + $user->refresh(); + $this->assertStringStartsWith(Str::slug($user->name), $user->slug); + } + + public function test_user_password_update() + { + $user = $this->getNormalUser(); + $userProfilePage = '/settings/users/' . $user->id; + + $this->asAdmin()->get($userProfilePage); + $this->put($userProfilePage, [ + 'password' => 'newpassword', + ])->assertRedirect($userProfilePage); + + $this->get($userProfilePage)->assertSee('Password confirmation required'); + + $this->put($userProfilePage, [ + 'password' => 'newpassword', + 'password-confirm' => 'newpassword', + ])->assertRedirect('/settings/users'); + + $userPassword = User::query()->find($user->id)->password; + $this->assertTrue(Hash::check('newpassword', $userPassword)); + } + + public function test_user_cannot_be_deleted_if_last_admin() + { + $adminRole = Role::getRole('admin'); + + // Delete all but one admin user if there are more than one + $adminUsers = $adminRole->users; + if (count($adminUsers) > 1) { + /** @var User $user */ + foreach ($adminUsers->splice(1) as $user) { + $user->delete(); + } + } + + // Ensure we currently only have 1 admin user + $this->assertEquals(1, $adminRole->users()->count()); + /** @var User $user */ + $user = $adminRole->users->first(); + + $resp = $this->asAdmin()->delete('/settings/users/' . $user->id); + $resp->assertRedirect('/settings/users/' . $user->id); + + $resp = $this->get('/settings/users/' . $user->id); + $resp->assertSee('You cannot delete the only admin'); + + $this->assertDatabaseHas('users', ['id' => $user->id]); + } + public function test_delete() { $editor = $this->getEditor(); @@ -42,4 +142,26 @@ class UserManagementTest extends TestCase 'owned_by' => $newOwner->id, ]); } + + public function test_guest_profile_shows_limited_form() + { + $guest = User::getDefault(); + $resp = $this->asAdmin()->get('/settings/users/' . $guest->id); + $resp->assertSee('Guest'); + $resp->assertElementNotExists('#password'); + } + + public function test_guest_profile_cannot_be_deleted() + { + $guestUser = User::getDefault(); + $resp = $this->asAdmin()->get('/settings/users/' . $guestUser->id . '/delete'); + $resp->assertSee('Delete User'); + $resp->assertSee('Guest'); + $resp->assertElementContains('form[action$="/settings/users/' . $guestUser->id . '"] button', 'Confirm'); + + $resp = $this->delete('/settings/users/' . $guestUser->id); + $resp->assertRedirect('/settings/users/' . $guestUser->id); + $resp = $this->followRedirects($resp); + $resp->assertSee('cannot delete the guest user'); + } } diff --git a/tests/User/UserPreferencesTest.php b/tests/User/UserPreferencesTest.php index 1d5d3e729..b39c2c47c 100644 --- a/tests/User/UserPreferencesTest.php +++ b/tests/User/UserPreferencesTest.php @@ -2,6 +2,7 @@ namespace Tests\User; +use BookStack\Entities\Models\Bookshelf; use Tests\TestCase; class UserPreferencesTest extends TestCase @@ -106,4 +107,44 @@ class UserPreferencesTest extends TestCase $home = $this->get('/login'); $home->assertElementExists('.dark-mode'); } + + public function test_books_view_type_preferences_when_list() + { + $editor = $this->getEditor(); + setting()->putUser($editor, 'books_view_type', 'list'); + + $this->actingAs($editor)->get('/books') + ->assertElementNotExists('.featured-image-container') + ->assertElementExists('.content-wrap .entity-list-item'); + } + + public function test_books_view_type_preferences_when_grid() + { + $editor = $this->getEditor(); + setting()->putUser($editor, 'books_view_type', 'grid'); + + $this->actingAs($editor)->get('/books') + ->assertElementExists('.featured-image-container'); + } + + public function test_shelf_view_type_change() + { + $editor = $this->getEditor(); + /** @var Bookshelf $shelf */ + $shelf = Bookshelf::query()->first(); + setting()->putUser($editor, 'bookshelf_view_type', 'list'); + + $this->actingAs($editor)->get($shelf->getUrl()) + ->assertElementNotExists('.featured-image-container') + ->assertElementExists('.content-wrap .entity-list-item') + ->assertSee('Grid View'); + + $req = $this->patch("/settings/users/{$editor->id}/switch-shelf-view", ['view_type' => 'grid']); + $req->assertRedirect($shelf->getUrl()); + + $this->actingAs($editor)->get($shelf->getUrl()) + ->assertElementExists('.featured-image-container') + ->assertElementNotExists('.content-wrap .entity-list-item') + ->assertSee('List View'); + } } diff --git a/tests/User/UserProfileTest.php b/tests/User/UserProfileTest.php index 859a036e0..3942efa8e 100644 --- a/tests/User/UserProfileTest.php +++ b/tests/User/UserProfileTest.php @@ -5,11 +5,13 @@ namespace Tests\User; use Activity; use BookStack\Actions\ActivityType; use BookStack\Auth\User; -use BookStack\Entities\Models\Bookshelf; -use Tests\BrowserKitTest; +use Tests\TestCase; -class UserProfileTest extends BrowserKitTest +class UserProfileTest extends TestCase { + /** + * @var User + */ protected $user; public function setUp(): void @@ -21,74 +23,73 @@ class UserProfileTest extends BrowserKitTest public function test_profile_page_shows_name() { $this->asAdmin() - ->visit('/user/' . $this->user->slug) - ->see($this->user->name); + ->get('/user/' . $this->user->slug) + ->assertSee($this->user->name); } public function test_profile_page_shows_recent_entities() { $content = $this->createEntityChainBelongingToUser($this->user, $this->user); - $this->asAdmin() - ->visit('/user/' . $this->user->slug) - // Check the recently created page is shown - ->see($content['page']->name) - // Check the recently created chapter is shown - ->see($content['chapter']->name) - // Check the recently created book is shown - ->see($content['book']->name); + $resp = $this->asAdmin()->get('/user/' . $this->user->slug); + // Check the recently created page is shown + $resp->assertSee($content['page']->name); + // Check the recently created chapter is shown + $resp->assertSee($content['chapter']->name); + // Check the recently created book is shown + $resp->assertSee($content['book']->name); } public function test_profile_page_shows_created_content_counts() { - $newUser = $this->getNewBlankUser(); + $newUser = factory(User::class)->create(); - $this->asAdmin()->visit('/user/' . $newUser->slug) - ->see($newUser->name) - ->seeInElement('#content-counts', '0 Books') - ->seeInElement('#content-counts', '0 Chapters') - ->seeInElement('#content-counts', '0 Pages'); + $this->asAdmin()->get('/user/' . $newUser->slug) + ->assertSee($newUser->name) + ->assertElementContains('#content-counts', '0 Books') + ->assertElementContains('#content-counts', '0 Chapters') + ->assertElementContains('#content-counts', '0 Pages'); $this->createEntityChainBelongingToUser($newUser, $newUser); - $this->asAdmin()->visit('/user/' . $newUser->slug) - ->see($newUser->name) - ->seeInElement('#content-counts', '1 Book') - ->seeInElement('#content-counts', '1 Chapter') - ->seeInElement('#content-counts', '1 Page'); + $this->asAdmin()->get('/user/' . $newUser->slug) + ->assertSee($newUser->name) + ->assertElementContains('#content-counts', '1 Book') + ->assertElementContains('#content-counts', '1 Chapter') + ->assertElementContains('#content-counts', '1 Page'); } public function test_profile_page_shows_recent_activity() { - $newUser = $this->getNewBlankUser(); + $newUser = factory(User::class)->create(); $this->actingAs($newUser); $entities = $this->createEntityChainBelongingToUser($newUser, $newUser); Activity::addForEntity($entities['book'], ActivityType::BOOK_UPDATE); Activity::addForEntity($entities['page'], ActivityType::PAGE_CREATE); - $this->asAdmin()->visit('/user/' . $newUser->slug) - ->seeInElement('#recent-user-activity', 'updated book') - ->seeInElement('#recent-user-activity', 'created page') - ->seeInElement('#recent-user-activity', $entities['page']->name); + $this->asAdmin()->get('/user/' . $newUser->slug) + ->assertElementContains('#recent-user-activity', 'updated book') + ->assertElementContains('#recent-user-activity', 'created page') + ->assertElementContains('#recent-user-activity', $entities['page']->name); } - public function test_clicking_user_name_in_activity_leads_to_profile_page() + public function test_user_activity_has_link_leading_to_profile() { - $newUser = $this->getNewBlankUser(); + $newUser = factory(User::class)->create(); $this->actingAs($newUser); $entities = $this->createEntityChainBelongingToUser($newUser, $newUser); 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->slug) - ->see($newUser->name); + $linkSelector = '#recent-activity a[href$="/user/' . $newUser->slug . '"]'; + $this->asAdmin()->get('/') + ->assertElementContains($linkSelector, $newUser->name); } public function test_profile_has_search_links_in_created_entity_lists() { $user = $this->getEditor(); - $resp = $this->actingAs($this->getAdmin())->visit('/user/' . $user->slug); + $resp = $this->actingAs($this->getAdmin())->get('/user/' . $user->slug); $expectedLinks = [ '/search?term=%7Bcreated_by%3A' . $user->slug . '%7D+%7Btype%3Apage%7D', @@ -98,66 +99,7 @@ class UserProfileTest extends BrowserKitTest ]; foreach ($expectedLinks as $link) { - $resp->seeInElement('[href$="' . $link . '"]', 'View All'); + $resp->assertElementContains('[href$="' . $link . '"]', 'View All'); } } - - public function test_guest_profile_shows_limited_form() - { - $this->asAdmin() - ->visit('/settings/users') - ->click('Guest') - ->dontSeeElement('#password'); - } - - public function test_guest_profile_cannot_be_deleted() - { - $guestUser = User::getDefault(); - $this->asAdmin()->visit('/settings/users/' . $guestUser->id . '/delete') - ->see('Delete User')->see('Guest') - ->press('Confirm') - ->seePageIs('/settings/users/' . $guestUser->id) - ->see('cannot delete the guest user'); - } - - public function test_books_view_is_list() - { - $editor = $this->getEditor(); - setting()->putUser($editor, 'books_view_type', 'list'); - - $this->actingAs($editor) - ->visit('/books') - ->pageNotHasElement('.featured-image-container') - ->pageHasElement('.content-wrap .entity-list-item'); - } - - public function test_books_view_is_grid() - { - $editor = $this->getEditor(); - setting()->putUser($editor, 'books_view_type', 'grid'); - - $this->actingAs($editor) - ->visit('/books') - ->pageHasElement('.featured-image-container'); - } - - public function test_shelf_view_type_change() - { - $editor = $this->getEditor(); - $shelf = Bookshelf::query()->first(); - setting()->putUser($editor, 'bookshelf_view_type', 'list'); - - $this->actingAs($editor)->visit($shelf->getUrl()) - ->pageNotHasElement('.featured-image-container') - ->pageHasElement('.content-wrap .entity-list-item') - ->see('Grid View'); - - $req = $this->patch("/settings/users/{$editor->id}/switch-shelf-view", ['view_type' => 'grid']); - $req->assertRedirectedTo($shelf->getUrl()); - - $this->actingAs($editor)->visit($shelf->getUrl()) - ->pageHasElement('.featured-image-container') - ->pageNotHasElement('.content-wrap .entity-list-item') - ->see('List View'); - } }