From 94969e7f9737dc5813072a521c2659c5d159a44c Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Sat, 18 Feb 2017 12:27:21 +0000 Subject: [PATCH] Get Laravel Dusk installed using PhantomJS so it can work with Travis CI easily --- .travis.yml | 2 + app/Providers/AppServiceProvider.php | 5 +- composer.json | 17 +- composer.lock | 249 ++++++++-------- phpunit.xml | 10 +- tests/ArticlesTest.php | 78 ----- tests/Browser/ExampleTest.php | 23 ++ tests/Browser/Pages/HomePage.php | 40 +++ tests/Browser/Pages/Page.php | 20 ++ tests/Browser/screenshots/.gitignore | 2 + tests/ContactsTest.php | 52 ---- ...wserKitTest.php => CreatesApplication.php} | 14 +- tests/DuskTestCase.php | 35 +++ tests/Feature/ExampleTest.php | 22 ++ tests/IndieAuthTest.php | 79 ----- tests/MicropubClientTest.php | 69 ----- tests/MicropubTest.php | 271 ------------------ tests/NotesAdminTest.php | 33 --- tests/NotesTest.php | 182 ------------ tests/PlacesTest.php | 52 ---- tests/TestCase.php | 27 +- tests/TokenServiceTest.php | 43 --- tests/Unit/ExampleTest.php | 20 ++ tests/WebMentionsTest.php | 92 ------ tests/aaron.png | Bin 6527 -> 0 bytes 25 files changed, 314 insertions(+), 1123 deletions(-) delete mode 100644 tests/ArticlesTest.php create mode 100644 tests/Browser/ExampleTest.php create mode 100644 tests/Browser/Pages/HomePage.php create mode 100644 tests/Browser/Pages/Page.php create mode 100644 tests/Browser/screenshots/.gitignore delete mode 100644 tests/ContactsTest.php rename tests/{BrowserKitTest.php => CreatesApplication.php} (56%) create mode 100644 tests/DuskTestCase.php create mode 100644 tests/Feature/ExampleTest.php delete mode 100644 tests/IndieAuthTest.php delete mode 100644 tests/MicropubClientTest.php delete mode 100644 tests/MicropubTest.php delete mode 100644 tests/NotesAdminTest.php delete mode 100644 tests/NotesTest.php delete mode 100644 tests/PlacesTest.php delete mode 100644 tests/TokenServiceTest.php create mode 100644 tests/Unit/ExampleTest.php delete mode 100644 tests/WebMentionsTest.php delete mode 100644 tests/aaron.png diff --git a/.travis.yml b/.travis.yml index 6033e8fa..7c82d2a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,9 +38,11 @@ before_script: - php artisan migrate - php artisan db:seed - php artisan token:generate + - phantomjs --webserver=127.0.0.1:9515 & - php artisan serve & - sleep 5 # Give artisan some time to start serving script: - phpdbg -qrr vendor/bin/phpunit --coverage-text + - php artisan dusk - php artisan security:check diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 3c5c8377..d3aa0488 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,6 +5,7 @@ namespace App\Providers; use App\Tag; use App\Note; use Validator; +use Laravel\Dusk\DuskServiceProvider; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -54,6 +55,8 @@ class AppServiceProvider extends ServiceProvider */ public function register() { - // + if ($this->app->environment('local', 'testing')) { + $this->app->register(DuskServiceProvider::class); + } } } diff --git a/composer.json b/composer.json index 8b6e90f1..d37abaea 100644 --- a/composer.json +++ b/composer.json @@ -29,14 +29,12 @@ "laravel/tinker": "^1.0" }, "require-dev": { - "fzaninotto/faker": "~1.4", - "mockery/mockery": "0.9.*", - "phpunit/phpunit": "~5.7", - "symfony/css-selector": "3.1.*", - "symfony/dom-crawler": "3.1.*", "barryvdh/laravel-debugbar": "~2.0", + "fzaninotto/faker": "~1.4", "jakub-onderka/php-parallel-lint": "^0.9.2", - "laravel/browser-kit-testing": "^1.0" + "laravel/dusk": "^1.0", + "mockery/mockery": "0.9.*", + "phpunit/phpunit": "~5.7" }, "autoload": { "classmap": [ @@ -47,10 +45,9 @@ } }, "autoload-dev": { - "classmap": [ - "tests/TestCase.php", - "tests/BrowserKitTest.php" - ] + "psr-4": { + "Tests\\": "tests" + } }, "scripts": { "post-root-package-install": [ diff --git a/composer.lock b/composer.lock index eae3833f..56ab84cb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "16af842252275cac279c7f118ad6e6d4", + "content-hash": "e4236aef74a9e56de4a85dc50bf32dcf", "packages": [ { "name": "anahkiasen/underscore-php", @@ -58,16 +58,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.22.4", + "version": "3.22.7", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "916f708c1a643f86f74eacd3c5be787b40d814f8" + "reference": "eea83aaac2b6c86f72a5c85c54d1839b70d4fd21" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/916f708c1a643f86f74eacd3c5be787b40d814f8", - "reference": "916f708c1a643f86f74eacd3c5be787b40d814f8", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/eea83aaac2b6c86f72a5c85c54d1839b70d4fd21", + "reference": "eea83aaac2b6c86f72a5c85c54d1839b70d4fd21", "shasum": "" }, "require": { @@ -134,7 +134,7 @@ "s3", "sdk" ], - "time": "2017-02-14T21:23:54+00:00" + "time": "2017-02-17T20:09:40+00:00" }, { "name": "barnabywalters/mf-cleaner", @@ -3280,16 +3280,16 @@ }, { "name": "symfony/console", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "7a8405a9fc175f87fed8a3c40856b0d866d61936" + "reference": "0e5e6899f82230fcb1153bcaf0e106ffaa44b870" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/7a8405a9fc175f87fed8a3c40856b0d866d61936", - "reference": "7a8405a9fc175f87fed8a3c40856b0d866d61936", + "url": "https://api.github.com/repos/symfony/console/zipball/0e5e6899f82230fcb1153bcaf0e106ffaa44b870", + "reference": "0e5e6899f82230fcb1153bcaf0e106ffaa44b870", "shasum": "" }, "require": { @@ -3339,20 +3339,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-02-06T12:04:21+00:00" + "time": "2017-02-16T14:07:22+00:00" }, { "name": "symfony/css-selector", - "version": "v3.1.10", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d" + "reference": "f0e628f04fc055c934b3211cfabdb1c59eefbfaa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d", - "reference": "722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/f0e628f04fc055c934b3211cfabdb1c59eefbfaa", + "reference": "f0e628f04fc055c934b3211cfabdb1c59eefbfaa", "shasum": "" }, "require": { @@ -3361,7 +3361,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -3392,20 +3392,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2017-01-02T20:31:54+00:00" + "time": "2017-01-02T20:32:22+00:00" }, { "name": "symfony/debug", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "b4d9818f127c60ce21ed62c395da7df868dc8477" + "reference": "9b98854cb45bc59d100b7d4cc4cf9e05f21026b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/b4d9818f127c60ce21ed62c395da7df868dc8477", - "reference": "b4d9818f127c60ce21ed62c395da7df868dc8477", + "url": "https://api.github.com/repos/symfony/debug/zipball/9b98854cb45bc59d100b7d4cc4cf9e05f21026b9", + "reference": "9b98854cb45bc59d100b7d4cc4cf9e05f21026b9", "shasum": "" }, "require": { @@ -3449,11 +3449,11 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2017-01-28T02:37:08+00:00" + "time": "2017-02-16T16:34:18+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -3513,7 +3513,7 @@ }, { "name": "symfony/finder", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", @@ -3562,16 +3562,16 @@ }, { "name": "symfony/http-foundation", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e192b04de44aa1ed0e39d6793f7e06f5e0b672a0" + "reference": "a90da6dd679605d88c9803a57a6fc1fb7a19a6e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e192b04de44aa1ed0e39d6793f7e06f5e0b672a0", - "reference": "e192b04de44aa1ed0e39d6793f7e06f5e0b672a0", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a90da6dd679605d88c9803a57a6fc1fb7a19a6e0", + "reference": "a90da6dd679605d88c9803a57a6fc1fb7a19a6e0", "shasum": "" }, "require": { @@ -3611,20 +3611,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2017-02-02T13:47:35+00:00" + "time": "2017-02-16T22:46:52+00:00" }, { "name": "symfony/http-kernel", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "96443239baf674b143604fb87cb27cb01672ab77" + "reference": "4cd0d4bc31819095c6ef13573069f621eb321081" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/96443239baf674b143604fb87cb27cb01672ab77", - "reference": "96443239baf674b143604fb87cb27cb01672ab77", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4cd0d4bc31819095c6ef13573069f621eb321081", + "reference": "4cd0d4bc31819095c6ef13573069f621eb321081", "shasum": "" }, "require": { @@ -3693,7 +3693,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2017-02-06T13:15:19+00:00" + "time": "2017-02-16T23:59:56+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -3756,16 +3756,16 @@ }, { "name": "symfony/process", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "32646a7cf53f3956c76dcb5c82555224ae321858" + "reference": "0ab87c1e7570b3534a6e51eb4ca8e9f6d7327856" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/32646a7cf53f3956c76dcb5c82555224ae321858", - "reference": "32646a7cf53f3956c76dcb5c82555224ae321858", + "url": "https://api.github.com/repos/symfony/process/zipball/0ab87c1e7570b3534a6e51eb4ca8e9f6d7327856", + "reference": "0ab87c1e7570b3534a6e51eb4ca8e9f6d7327856", "shasum": "" }, "require": { @@ -3801,11 +3801,11 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-02-03T12:11:38+00:00" + "time": "2017-02-16T14:07:22+00:00" }, { "name": "symfony/routing", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", @@ -3880,16 +3880,16 @@ }, { "name": "symfony/translation", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "ca032cc56976d88b85e7386b17020bc6dc95dbc5" + "reference": "d6825c6bb2f1da13f564678f9f236fe8242c0029" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/ca032cc56976d88b85e7386b17020bc6dc95dbc5", - "reference": "ca032cc56976d88b85e7386b17020bc6dc95dbc5", + "url": "https://api.github.com/repos/symfony/translation/zipball/d6825c6bb2f1da13f564678f9f236fe8242c0029", + "reference": "d6825c6bb2f1da13f564678f9f236fe8242c0029", "shasum": "" }, "require": { @@ -3940,20 +3940,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2017-01-21T17:06:35+00:00" + "time": "2017-02-16T22:46:52+00:00" }, { "name": "symfony/var-dumper", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "5bb4435a03a4f05c211f4a9a8ee2756965924511" + "reference": "cb50260b674ee1c2d4ab49f2395a42e0b4681e20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/5bb4435a03a4f05c211f4a9a8ee2756965924511", - "reference": "5bb4435a03a4f05c211f4a9a8ee2756965924511", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cb50260b674ee1c2d4ab49f2395a42e0b4681e20", + "reference": "cb50260b674ee1c2d4ab49f2395a42e0b4681e20", "shasum": "" }, "require": { @@ -4003,7 +4003,7 @@ "debug", "dump" ], - "time": "2017-01-24T12:58:58+00:00" + "time": "2017-02-16T22:46:52+00:00" }, { "name": "themattharris/tmhoauth", @@ -4298,6 +4298,55 @@ ], "time": "2015-06-14T21:17:01+00:00" }, + { + "name": "facebook/webdriver", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/facebook/php-webdriver.git", + "reference": "77300c4ab2025d4316635f592ec849ca7323bd8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/77300c4ab2025d4316635f592ec849ca7323bd8c", + "reference": "77300c4ab2025d4316635f592ec849ca7323bd8c", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": "^5.5 || ~7.0", + "symfony/process": "^2.8 || ^3.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^1.11", + "php-mock/php-mock-phpunit": "^1.1", + "phpunit/phpunit": "4.6.* || ~5.0", + "satooshi/php-coveralls": "^1.0", + "squizlabs/php_codesniffer": "^2.6" + }, + "suggest": { + "phpdocumentor/phpdocumentor": "2.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Facebook\\WebDriver\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "A PHP client for WebDriver", + "homepage": "https://github.com/facebook/php-webdriver", + "keywords": [ + "facebook", + "php", + "selenium", + "webdriver" + ], + "time": "2017-01-13T15:48:08+00:00" + }, { "name": "fzaninotto/faker", "version": "v1.6.0", @@ -4439,23 +4488,30 @@ "time": "2015-12-15T10:42:16+00:00" }, { - "name": "laravel/browser-kit-testing", - "version": "v1.0.3", + "name": "laravel/dusk", + "version": "v1.0.6", "source": { "type": "git", - "url": "https://github.com/laravel/browser-kit-testing.git", - "reference": "0adfb725147815bff5516d157577f375a6e66ebd" + "url": "https://github.com/laravel/dusk.git", + "reference": "804bf2ef20de7d86caac1aff433c761399b55e56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/0adfb725147815bff5516d157577f375a6e66ebd", - "reference": "0adfb725147815bff5516d157577f375a6e66ebd", + "url": "https://api.github.com/repos/laravel/dusk/zipball/804bf2ef20de7d86caac1aff433c761399b55e56", + "reference": "804bf2ef20de7d86caac1aff433c761399b55e56", "shasum": "" }, "require": { - "php": ">=5.5.9", - "symfony/css-selector": "~3.1", - "symfony/dom-crawler": "~3.1" + "facebook/webdriver": "~1.0", + "illuminate/support": "~5.3", + "nesbot/carbon": "~1.20", + "php": ">=5.6.4", + "symfony/console": "~3.2", + "symfony/process": "~3.2" + }, + "require-dev": { + "mockery/mockery": "^0.9.6", + "phpunit/phpunit": "^5.7" }, "type": "library", "extra": { @@ -4465,7 +4521,7 @@ }, "autoload": { "psr-4": { - "Laravel\\BrowserKitTesting\\": "src/" + "Laravel\\Dusk\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4478,12 +4534,13 @@ "email": "taylor@laravel.com" } ], - "description": "Provides backwards compatibility for BrowserKit testing in Laravel 5.4.", + "description": "Laravel Dusk provides simple end-to-end testing and browser automation.", "keywords": [ "laravel", - "testing" + "testing", + "webdriver" ], - "time": "2017-02-08T22:32:37+00:00" + "time": "2017-02-15T20:21:39+00:00" }, { "name": "maximebf/debugbar", @@ -5760,74 +5817,18 @@ "homepage": "https://github.com/sebastianbergmann/version", "time": "2016-10-03T07:35:21+00:00" }, - { - "name": "symfony/dom-crawler", - "version": "v3.1.10", - "source": { - "type": "git", - "url": "https://github.com/symfony/dom-crawler.git", - "reference": "7eede2a901a19928494194f7d1815a77b9a473a0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/7eede2a901a19928494194f7d1815a77b9a473a0", - "reference": "7eede2a901a19928494194f7d1815a77b9a473a0", - "shasum": "" - }, - "require": { - "php": ">=5.5.9", - "symfony/polyfill-mbstring": "~1.0" - }, - "require-dev": { - "symfony/css-selector": "~2.8|~3.0" - }, - "suggest": { - "symfony/css-selector": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\DomCrawler\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony DomCrawler Component", - "homepage": "https://symfony.com", - "time": "2017-01-21T17:13:55+00:00" - }, { "name": "symfony/yaml", - "version": "v3.2.3", + "version": "v3.2.4", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "e1718c6bf57e1efbb8793ada951584b2ab27775b" + "reference": "9724c684646fcb5387d579b4bfaa63ee0b0c64c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e1718c6bf57e1efbb8793ada951584b2ab27775b", - "reference": "e1718c6bf57e1efbb8793ada951584b2ab27775b", + "url": "https://api.github.com/repos/symfony/yaml/zipball/9724c684646fcb5387d579b4bfaa63ee0b0c64c8", + "reference": "9724c684646fcb5387d579b4bfaa63ee0b0c64c8", "shasum": "" }, "require": { @@ -5869,7 +5870,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-01-21T17:06:35+00:00" + "time": "2017-02-16T22:46:52+00:00" }, { "name": "webmozart/assert", diff --git a/phpunit.xml b/phpunit.xml index 3e884d17..88860829 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -9,16 +9,16 @@ processIsolation="false" stopOnFailure="false"> - - ./tests + + ./tests/Feature + + + ./tests/Unit ./app - - ./app/Http/routes.php - diff --git a/tests/ArticlesTest.php b/tests/ArticlesTest.php deleted file mode 100644 index 850adeee..00000000 --- a/tests/ArticlesTest.php +++ /dev/null @@ -1,78 +0,0 @@ -appurl = config('app.url'); - } - - /** - * Test the `/blog` page returns the article, this - * means the database is being hit. - * - * @return void - */ - public function testArticlesPage() - { - $this->visit($this->appurl . '/blog') - ->see('My New Blog'); - } - - /** - * Test the `/blog/{year}` page returns the article, this - * means the database is being hit. - * - * @return void - */ - public function testArticlesYearPage() - { - $this->visit($this->appurl . '/blog/2016') - ->see('My New Blog'); - } - - /** - * Test the `/blog/{year}/{month}` page returns the article, - * this means the database is being hit. - * - * @return void - */ - public function testArticlesMonthPage() - { - $this->visit($this->appurl . '/blog/2016/01') - ->see('My New Blog'); - } - - /** - * Test a single article page. - * - * @return void - */ - public function testSingleArticlePage() - { - $this->visit($this->appurl . '/blog/2016/01/my-new-blog') - ->see('My New Blog'); - } - - /** - * Test the RSS feed. - * - * @return void - */ - public function testRSSFeed() - { - $response = $this->call('GET', $this->appurl . '/feed'); - - $this->assertEquals('application/rss+xml', $response->headers->get('Content-Type')); - } -} diff --git a/tests/Browser/ExampleTest.php b/tests/Browser/ExampleTest.php new file mode 100644 index 00000000..b388384d --- /dev/null +++ b/tests/Browser/ExampleTest.php @@ -0,0 +1,23 @@ +browse(function (Browser $browser) { + $browser->visit('/') + ->assertSee('Jonny Barnes'); + }); + } +} diff --git a/tests/Browser/Pages/HomePage.php b/tests/Browser/Pages/HomePage.php new file mode 100644 index 00000000..c04a7f0a --- /dev/null +++ b/tests/Browser/Pages/HomePage.php @@ -0,0 +1,40 @@ + '#selector', + ]; + } +} diff --git a/tests/Browser/Pages/Page.php b/tests/Browser/Pages/Page.php new file mode 100644 index 00000000..f8d76222 --- /dev/null +++ b/tests/Browser/Pages/Page.php @@ -0,0 +1,20 @@ + '#selector', + ]; + } +} diff --git a/tests/Browser/screenshots/.gitignore b/tests/Browser/screenshots/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/tests/Browser/screenshots/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/ContactsTest.php b/tests/ContactsTest.php deleted file mode 100644 index 6dddbfae..00000000 --- a/tests/ContactsTest.php +++ /dev/null @@ -1,52 +0,0 @@ -appurl = config('app.url'); - } - - /** - * Test the `/contacts` page and see if response is OK. - * - * @return void - */ - public function testContactsPage() - { - $this->visit($this->appurl . '/contacts') - ->assertResponseOK(); - } - - /** - * Test an individual contact page with default profile image. - * - * @return void - */ - public function testContactPageWithDefaultPic() - { - $this->visit($this->appurl . '/contacts/tantek') - ->see(''); - } - - /** - * Test an individual contact page with a specific profile image. - * - * @return void - */ - public function testContactPageWithSpecificPic() - { - $this->visit($this->appurl . '/contacts/aaron') - ->see(''); - } -} diff --git a/tests/BrowserKitTest.php b/tests/CreatesApplication.php similarity index 56% rename from tests/BrowserKitTest.php rename to tests/CreatesApplication.php index a2429b4f..a2e89522 100644 --- a/tests/BrowserKitTest.php +++ b/tests/CreatesApplication.php @@ -1,17 +1,11 @@ make(Kernel::class)->bootstrap(); - return $app; } } diff --git a/tests/DuskTestCase.php b/tests/DuskTestCase.php new file mode 100644 index 00000000..6b71b600 --- /dev/null +++ b/tests/DuskTestCase.php @@ -0,0 +1,35 @@ +get('/'); + $response->assertStatus(200); + } +} diff --git a/tests/IndieAuthTest.php b/tests/IndieAuthTest.php deleted file mode 100644 index 97fdadf7..00000000 --- a/tests/IndieAuthTest.php +++ /dev/null @@ -1,79 +0,0 @@ -appurl = config('app.url'); - } - - /** - * Test the getAuthorizationEndpoint calls the correct service methods, - * though these methods are actually mocked. - * - * @return void - */ - public function testIndieAuthServiceDiscoversEndpoint() - { - $service = new \App\Services\IndieAuthService(); - $client = new \IndieAuth\Client(); - $result = $service->getAuthorizationEndpoint($this->appurl, $client); - $this->assertSame('https://indieauth.com/auth', $result); - } - - /** - * Test that the Service build the correct redirect URL. - * - * @return void - */ - public function testIndieAuthServiceBuildRedirectURL() - { - $client = new \IndieAuth\Client(); - $service = new \App\Services\IndieAuthService(); - $result = $service->buildAuthorizationURL( - 'https://indieauth.com/auth', - $this->appurl, - $client - ); - $this->assertSame( - 'https://indieauth.com/auth?me=', - substr($result, 0, 30) - ); - } - - /** - * Test the `start` method redirects to the client on error. - * - * @return void - */ - public function testIndieAuthControllerBeginAuthRedirectsToClientOnFail() - { - $response = $this->call('GET', $this->appurl . '/indieauth/start', ['me' => 'http://example.org']); - $this->assertSame($this->appurl . '/micropub/create', $response->headers->get('Location')); - } - - /** - * Now we test the `start` method as a whole. - * - * @return void - */ - public function testIndieAuthControllerBeginAuthRedirectsToEndpoint() - { - $response = $this->call('GET', $this->appurl . '/indieauth/start', ['me' => $this->appurl]); - $this->assertSame( - 'https://indieauth.com/auth?me=', - substr($response->headers->get('Location'), 0, 30) - ); - $response = null; - } -} diff --git a/tests/MicropubClientTest.php b/tests/MicropubClientTest.php deleted file mode 100644 index 67299155..00000000 --- a/tests/MicropubClientTest.php +++ /dev/null @@ -1,69 +0,0 @@ -appurl = config('app.url'); - } - - /** - * Test the client gets shown for an unauthorised request. - * - * @return void - */ - public function testClientPageUnauthorised() - { - $this->visit($this->appurl . '/micropub/create') - ->see('IndieAuth'); - } - - public function testClientPageRecentAuth() - { - $this->visit($this->appurl . '/micropub/create') - ->see($this->appurl); - } - - public function testClientCreatesNewNoteWithTag() - { - //in this test, the syndication targets are blank - $faker = \Faker\Factory::create(); - $note = 'Fake note from #PHPUnit: ' . $faker->text; - $this->visit($this->appurl . '/micropub/create') - ->type($note, 'content') - ->press('Submit'); - $this->seeInDatabase('notes', ['note' => $note]); - $this->visit($this->appurl . '/notes/tagged/PHPUnit') - ->see('PHPUnit'); - //my client has made a request to my endpoint, which then adds - //to the db, so database transaction don’t work - //so lets manually delete the new entry - //first, if we are using algolia, we need to delete it - if (env('SCOUT_DRIVER') == 'algolia') { - //we need to allow the index to update in order to query it - sleep(2); - $client = new \AlgoliaSearch\Client(env('ALGOLIA_APP_ID'), env('ALGOLIA_SECRET')); - $index = $client->initIndex('notes'); - //here we query for the new note and tell algolia too delete it - $res = $index->deleteByQuery('Fake note from'); - if ($res == 0) { - //somehow the new not didn’t get deleted - $this->fail('Didn’t delete the note from the index'); - } - } - $newNote = \App\Note::where('note', $note)->first(); - $newNote->forceDelete(); - } -} diff --git a/tests/MicropubTest.php b/tests/MicropubTest.php deleted file mode 100644 index de9e3f7f..00000000 --- a/tests/MicropubTest.php +++ /dev/null @@ -1,271 +0,0 @@ -appurl = config('app.url'); - } - - public function testMicropubRequestWithoutToken() - { - $this->call('GET', $this->appurl . '/api/post'); - $this->assertResponseStatus(400); - $this->seeJson(['error_description' => 'No token provided with request']); - } - - public function testMicropubRequestWithoutValidToken() - { - $this->call('GET', $this->appurl . '/api/post', [], [], [], ['HTTP_Authorization' => 'Bearer abc123']); - $this->assertResponseStatus(400); - $this->seeJson(['error_description' => 'The provided token did not pass validation']); - } - - public function testMicropubRequestWithValidToken() - { - $this->call('GET', $this->appurl . '/api/post', [], [], [], ['HTTP_Authorization' => 'Bearer ' . $this->getToken()]); - $this->seeJson(['response' => 'token']); - } - - public function testMicropubRequestForSyndication() - { - $this->call('GET', $this->appurl . '/api/post', ['q' => 'syndicate-to'], [], [], ['HTTP_Authorization' => 'Bearer ' . $this->getToken()]); - $this->seeJson(['uid' => 'https://twitter.com/jonnybarnes']); - } - - public function testMicropubRequestForNearbyPlacesThatExist() - { - $this->call('GET', $this->appurl . '/api/post', ['q' => 'geo:53.5,-2.38'], [], [], ['HTTP_Authorization' => 'Bearer ' . $this->getToken()]); - $this->see('the-bridgewater-pub'); - } - - public function testMicropubRequestForNearbyPlacesThatExistWithUncertaintyParameter() - { - $this->call('GET', $this->appurl . '/api/post', ['q' => 'geo:53.5,-2.38;u=35'], [], [], ['HTTP_Authorization' => 'Bearer ' . $this->getToken()]); - $this->see('the-bridgewater-pub'); - } - - public function testMicropubRequestForNearbyPlacesThatDoNotExist() - { - $this->call('GET', $this->appurl . '/api/post', ['q' => 'geo:1.23,4.56'], [], [], ['HTTP_Authorization' => 'Bearer ' . $this->getToken()]); - $this->see('[]'); - } - - public function testMicropubRequestForConfig() - { - $this->call('GET', $this->appurl . '/api/post', ['q' => 'config'], [], [], ['HTTP_Authorization' => 'Bearer ' . $this->getToken()]); - $this->seeJson(['uid' => 'https://twitter.com/jonnybarnes']); - } - - public function testMicropubRequestCreateNewNote() - { - $faker = \Faker\Factory::create(); - $note = $faker->text; - $this->call( - 'POST', - $this->appurl . '/api/post', - [ - 'h' => 'entry', - 'content' => $note - ], - [], - [], - ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] - ); - $this->seeInDatabase('notes', ['note' => $note]); - } - - public function testMicropubRequestCreateNewPlace() - { - $this->call( - 'POST', - $this->appurl . '/api/post', - [ - 'h' => 'card', - 'name' => 'The Barton Arms', - 'geo' => 'geo:53.4974,-2.3768' - ], - [], - [], - ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] - ); - $this->seeInDatabase('places', ['slug' => 'the-barton-arms']); - } - - public function testMicropubJSONRequestCreateNewNote() - { - $faker = \Faker\Factory::create(); - $note = $faker->text; - $this->json( - 'POST', - $this->appurl . '/api/post', - [ - 'type' => ['h-entry'], - 'properties' => [ - 'content' => [$note], - ], - ], - ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] - )->seeJson([ - 'response' => 'created' - ])->assertResponseStatus(201); - } - - public function testMicropubJSONRequestCreateNewNoteWithoutToken() - { - $faker = \Faker\Factory::create(); - $note = $faker->text; - $this->json( - 'POST', - $this->appurl . '/api/post', - [ - 'type' => ['h-entry'], - 'properties' => [ - 'content' => [$note], - ], - ] - )->seeJson([ - 'response' => 'error', - 'error' => 'no_token' - ])->assertResponseStatus(400); - } - - public function testMicropubJSONRequestCreateNewNoteWithInvalidToken() - { - $faker = \Faker\Factory::create(); - $note = $faker->text; - $this->json( - 'POST', - $this->appurl . '/api/post', - [ - 'type' => ['h-entry'], - 'properties' => [ - 'content' => [$note], - ], - ], - ['HTTP_Authorization' => 'Bearer ' . $this->getInvalidToken()] - )->seeJson([ - 'response' => 'error', - 'error' => 'invalid_token' - ]); - } - - public function testMicropubJSONRequestCreateNewPlace() - { - $faker = \Faker\Factory::create(); - $this->json( - 'POST', - $this->appurl . '/api/post', - [ - 'type' => ['h-card'], - 'properties' => [ - 'name' => $faker->name, - 'geo' => 'geo:' . $faker->latitude . ',' . $faker->longitude - ], - ], - ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] - )->seeJson([ - 'response' => 'created' - ])->assertResponseStatus(201); - } - - public function testMicropubJSONRequestCreateNewPlaceWithoutToken() - { - $faker = \Faker\Factory::create(); - $this->json( - 'POST', - $this->appurl . '/api/post', - [ - 'type' => ['h-entry'], - 'properties' => [ - 'name' => $faker->name, - 'geo' => 'geo:' . $faker->latitude . ',' . $faker->longitude - ], - ] - )->seeJson([ - 'response' => 'error', - 'error' => 'no_token' - ])->assertResponseStatus(400); - } - - public function testMicropubJSONRequestCreateNewPlaceWithInvalidToken() - { - $faker = \Faker\Factory::create(); - $this->json( - 'POST', - $this->appurl . '/api/post', - [ - 'type' => ['h-entry'], - 'properties' => [ - 'name' => $faker->name, - 'geo' => 'geo:' . $faker->latitude . ',' . $faker->longitude - ], - ], - ['HTTP_Authorization' => 'Bearer ' . $this->getInvalidToken()] - )->seeJson([ - 'response' => 'error', - 'error' => 'invalid_token' - ]); - } - - public function testMicropubJSONRequestCreateNewPlaceWithUncertaintyParam() - { - $faker = \Faker\Factory::create(); - $this->json( - 'POST', - $this->appurl . '/api/post', - [ - 'type' => ['h-card'], - 'properties' => [ - 'name' => $faker->name, - 'geo' => 'geo:' . $faker->latitude . ',' . $faker->longitude . ';u=35' - ], - ], - ['HTTP_Authorization' => 'Bearer ' . $this->getToken()] - )->seeJson([ - 'response' => 'created' - ])->assertResponseStatus(201); - } - - private function getToken() - { - $signer = new Sha256(); - $token = (new Builder()) - ->set('client_id', 'https://quill.p3k.io') - ->set('me', 'https://jonnybarnes.localhost') - ->set('scope', 'post') - ->set('issued_at', time()) - ->sign($signer, env('APP_KEY')) - ->getToken(); - - return $token; - } - - private function getInvalidToken() - { - $signer = new Sha256(); - $token = (new Builder()) - ->set('client_id', 'https://quill.p3k.io') - ->set('me', 'https://jonnybarnes.localhost') - ->set('scope', 'view') - ->set('issued_at', time()) - ->sign($signer, env('APP_KEY')) - ->getToken(); - - return $token; - } -} diff --git a/tests/NotesAdminTest.php b/tests/NotesAdminTest.php deleted file mode 100644 index be476736..00000000 --- a/tests/NotesAdminTest.php +++ /dev/null @@ -1,33 +0,0 @@ -appurl = config('app.url'); - $this->notesAdminController = new \App\Http\Controllers\NotesAdminController(); - } - - public function testCreatedNoteDispatchesSendWebmentionsJob() - { - $this->expectsJobs(\App\Jobs\SendWebMentions::class); - - $this->withSession(['loggedin' => true]) - ->visit($this->appurl . '/admin/note/new') - ->type('Mentioning', 'content') - ->press('Submit'); - } -} diff --git a/tests/NotesTest.php b/tests/NotesTest.php deleted file mode 100644 index 8112b32f..00000000 --- a/tests/NotesTest.php +++ /dev/null @@ -1,182 +0,0 @@ -appurl = config('app.url'); - $this->notesController = new \App\Http\Controllers\NotesController(); - } - - /** - * Test the `/notes` page returns 200, this should - * mean the database is being hit. - * - * @return void - */ - public function testNotesPage() - { - $this->visit($this->appurl . '/notes') - ->assertResponseOk(); - } - - /** - * Test a specific note so that `singleNote()` get called. - * - * @return void - */ - public function testSpecificNote() - { - $this->visit($this->appurl . '/notes/B') - ->see('#beer'); - } - - /** - * Test that `/note/{decID}` redirects to `/notes/{nb60id}`. - * - * @return void - */ - public function testDecIDRedirect() - { - $this->get($this->appurl . '/note/11') - ->assertRedirectedTo(config('app.url') . '/notes/B'); - } - - /** - * Visit the tagged page and see text from the note. - * - * @return void - */ - public function testTaggedNotesPage() - { - $this->visit($this->appurl . '/notes/tagged/beer') - ->see('at the local.'); - } - - /** - * Look for a default image in the contact’s h-card. - * - * @return void - */ - public function testDefaultImageUsed() - { - $this->visit($this->appurl . '/notes/C') - ->see(''); - } - - /** - * Look for a specific profile image in the contact’s h-card. - * - * @return void - */ - public function testProfileImageUsed() - { - $this->visit($this->appurl . '/notes/D') - ->see(''); - } - - /** - * Look for twitter URL when there’s no associated contact. - * - * @return void - */ - public function testTwitterLinkCreatedWhenNoContactFound() - { - $this->visit($this->appurl . '/notes/E') - ->see('@bob'); - } - - /** - * Test hashtag linking. - * - * @return void - */ - public function testHashtags() - { - $this->visit($this->appurl . '/notes/B') - ->see(''); - } - - /** - * Look for the client name after the note. - * - * @return void - */ - public function testClientNameDisplayed() - { - $this->visit($this->appurl . '/notes/D') - ->see('JBL5'); - } - - /** - * Look for the client URL after the note. - * - * @return void - */ - public function testClientURLDisplayed() - { - $this->visit($this->appurl . '/notes/E') - ->see('quill.p3k.io'); - } - - /** - * Test a correct profile link is formed from a generic URL. - * - * @return void - */ - public function testCreatePhotoLinkWithNonCachedImage() - { - $homepage = 'https://example.org/profile.png'; - $expected = 'https://example.org/profile.png'; - $this->assertEquals($expected, $this->notesController->createPhotoLink($homepage)); - } - - /** - * Test a correct profile link is formed from a generic URL. - * - * @return void - */ - public function testCreatePhotoLinkWithCachedImage() - { - $homepage = 'https://aaronparecki.com/profile.png'; - $expected = '/assets/profile-images/aaronparecki.com/image'; - $this->assertEquals($expected, $this->notesController->createPhotoLink($homepage)); - } - - /** - * Test a correct profile link is formed from a twitter URL. - * - * @return void - */ - public function testCreatePhotoLinkWithTwimgProfileImageURL() - { - $twitterProfileImage = 'http://pbs.twimg.com/1234'; - $expected = 'https://pbs.twimg.com/1234'; - $this->assertEquals($expected, $this->notesController->createPhotoLink($twitterProfileImage)); - } - - /** - * Test `null` is returned for a twitter profile. - * - * @return void - */ - public function testCreatePhotoLinkWithCachedTwitterURL() - { - $twitterURL = 'https://twitter.com/example'; - $expected = 'https://pbs.twimg.com/static_profile_link.jpg'; - Cache::put($twitterURL, $expected, 1); - $this->assertEquals($expected, $this->notesController->createPhotoLink($twitterURL)); - } -} diff --git a/tests/PlacesTest.php b/tests/PlacesTest.php deleted file mode 100644 index 7af4c6ac..00000000 --- a/tests/PlacesTest.php +++ /dev/null @@ -1,52 +0,0 @@ -appurl = config('app.url'); - } - - /** - * Test the `/places` page for OK response. - * - * @return void - */ - public function testPlacesPage() - { - $this->visit($this->appurl . '/places') - ->assertResponseOK(); - } - - /** - * Test a specific place. - * - * @return void - */ - public function testSinglePlace() - { - $this->visit($this->appurl . '/places/the-bridgewater-pub') - ->see('The Bridgewater Pub'); - } - - /** - * Test the nearby method returns a collection. - * - * @return void - */ - public function testNearbyMethod() - { - $nearby = \App\Place::near(53.5, -2.38, 1000); - $this->assertEquals('the-bridgewater-pub', $nearby[0]->slug); - } -} diff --git a/tests/TestCase.php b/tests/TestCase.php index 8208edca..2932d4a6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,25 +1,10 @@ make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); - - return $app; - } + use CreatesApplication; } diff --git a/tests/TokenServiceTest.php b/tests/TokenServiceTest.php deleted file mode 100644 index 8ca43fd7..00000000 --- a/tests/TokenServiceTest.php +++ /dev/null @@ -1,43 +0,0 @@ -appurl = config('app.url'); - $this->tokenService = new \App\Services\TokenService(); - } - - /** - * Given the token is dependent on a random nonce, the time of creation and - * the APP_KEY, to test, we shall create a token, and then verify it. - * - * @return void - */ - public function testTokenCreationAndValidation() - { - $data = [ - 'me' => 'https://example.org', - 'client_id' => 'https://quill.p3k.io', - 'scope' => 'post' - ]; - $token = $this->tokenService->getNewToken($data); - $valid = $this->tokenService->validateToken($token); - $validData = [ - 'me' => $valid->getClaim('me'), - 'client_id' => $valid->getClaim('client_id'), - 'scope' => $valid->getClaim('scope') - ]; - $this->assertSame($data, $validData); - } -} diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php new file mode 100644 index 00000000..5663bb49 --- /dev/null +++ b/tests/Unit/ExampleTest.php @@ -0,0 +1,20 @@ +assertTrue(true); + } +} diff --git a/tests/WebMentionsTest.php b/tests/WebMentionsTest.php deleted file mode 100644 index e296f96e..00000000 --- a/tests/WebMentionsTest.php +++ /dev/null @@ -1,92 +0,0 @@ -appurl = config('app.url'); - } - - /** - * Test webmentions without source and target are rejected. - * - * @return void - */ - public function testWebmentionsWithoutSourceAndTargetAreRejected() - { - $this->call('POST', $this->appurl . '/webmention', ['source' => 'https://example.org/post/123']); - $this->assertResponseStatus(400) - ->see('You need both the target and source parameters'); - } - - /** - * Test invalid target gets a 400 response. - * - * @return void - */ - public function testInvalidTargetReturns400Response() - { - $this->call('POST', $this->appurl . '/webmention', [ - 'source' => 'https://example.org/post/123', - 'target' => $this->appurl . '/invalid/target' - ]); - $this->assertResponseStatus(400) - ->see('Invalid request'); - } - - /** - * Test blog target gets a 501 response. - * - * @return void - */ - public function testBlogpostTargetReturns501Response() - { - $this->call('POST', $this->appurl . '/webmention', [ - 'source' => 'https://example.org/post/123', - 'target' => $this->appurl . '/blog/target' - ]); - $this->assertResponseStatus(501) - ->see('I don’t accept webmentions for blog posts yet.'); - } - - /** - * Test that a non-existant note gives a 400 response. - * - * @return void - */ - public function testNonexistantNoteReturns400Response() - { - $this->call('POST', $this->appurl . '/webmention', [ - 'source' => 'https://example.org/post/123', - 'target' => $this->appurl . '/notes/ZZZZZ' - ]); - $this->assertResponseStatus(400) - ->see('This note doesn’t exist.'); - } - - /** - * Test a legit webmention triggers the ProcessWebMention job. - * - * @return void - */ - public function testLegitimateWebmnetionTriggersProcessWebMentionJob() - { - $this->expectsJobs(\App\Jobs\ProcessWebMention::class); - $this->call('POST', $this->appurl . '/webmention', [ - 'source' => 'https://example.org/post/123', - 'target' => $this->appurl . '/notes/B' - ]); - $this->assertResponseStatus(202) - ->see('Webmention received, it will be processed shortly'); - } -} diff --git a/tests/aaron.png b/tests/aaron.png deleted file mode 100644 index 289234388005ba7bc557b74edc5d48ce2d1115f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6527 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