From c86690a69595c207f2b368eab2d6798fa8994b6e Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 31 May 2018 21:45:49 -0700 Subject: [PATCH 1/8] Get base code for dusk tests --- .env.dusk | 25 ++++++ composer.json | 2 + composer.lock | 123 ++++++++++++++++++++++++++- phpunit.xml | 3 + tests/Browser/BrowserTestCase.php | 44 ++++++++++ tests/Browser/PterodactylBrowser.php | 9 ++ tests/Browser/console/.gitignore | 2 + tests/Browser/screenshots/.gitignore | 2 + 8 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 .env.dusk create mode 100644 tests/Browser/BrowserTestCase.php create mode 100644 tests/Browser/PterodactylBrowser.php create mode 100644 tests/Browser/console/.gitignore create mode 100644 tests/Browser/screenshots/.gitignore diff --git a/.env.dusk b/.env.dusk new file mode 100644 index 000000000..1934ad3d5 --- /dev/null +++ b/.env.dusk @@ -0,0 +1,25 @@ +APP_ENV=local +APP_DEBUG=false +APP_KEY=NDWgIKKi9ovNK1PXZpzfNVSBdfCXGb5i +APP_JWT_KEY=test1234 +APP_TIMEZONE=America/Los_Angeles +APP_URL=http://192.168.1.249 + +CACHE_DRIVER=file +SESSION_DRIVER=file + +HASHIDS_SALT=IqRr0g82tCTeuyxGs8RV +HASHIDS_LENGTH=8 + +MAIL_DRIVER=log +MAIL_FROM=support@pterodactyl.io +QUEUE_DRIVER=array + +APP_SERVICE_AUTHOR=testing@pterodactyl.io +MAIL_FROM_NAME="Pterodactyl Panel" +RECAPTCHA_ENABLED=false + +DB_HOST=services.pterodactyl.local +DB_DATABASE=panel_test +DB_USERNAME=panel_test +DB_PASSWORD=Test1234 diff --git a/composer.json b/composer.json index d2ac885f0..e0b6dd39d 100644 --- a/composer.json +++ b/composer.json @@ -47,6 +47,7 @@ "filp/whoops": "^2.1", "friendsofphp/php-cs-fixer": "^2.11.1", "fzaninotto/faker": "^1.6", + "laravel/dusk": "^3.0", "martinlindhe/laravel-vue-i18n-generator": "^0.1.28", "mockery/mockery": "^1.0", "nunomaduro/collision": "^2.0", @@ -67,6 +68,7 @@ }, "autoload-dev": { "psr-4": { + "Pterodactyl\\Tests\\Browser\\": "tests/Browser", "Pterodactyl\\Tests\\Integration\\": "tests/Integration", "Tests\\": "tests/" } diff --git a/composer.lock b/composer.lock index bbb170e99..b55adab6f 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": "9232ff40da15c9430731254edc662eb7", + "content-hash": "9055a451d415d482a2f7287e0787bbc3", "packages": [ { "name": "appstract/laravel-blade-directives", @@ -4634,6 +4634,66 @@ ], "time": "2017-07-22T11:58:36+00:00" }, + { + "name": "facebook/webdriver", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/facebook/php-webdriver.git", + "reference": "bd8c740097eb9f2fc3735250fc1912bc811a954e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/bd8c740097eb9f2fc3735250fc1912bc811a954e", + "reference": "bd8c740097eb9f2fc3735250fc1912bc811a954e", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-zip": "*", + "php": "^5.6 || ~7.0", + "symfony/process": "^2.8 || ^3.1 || ^4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.0", + "jakub-onderka/php-parallel-lint": "^0.9.2", + "php-coveralls/php-coveralls": "^2.0", + "php-mock/php-mock-phpunit": "^1.1", + "phpunit/phpunit": "^5.7", + "sebastian/environment": "^1.3.4 || ^2.0 || ^3.0", + "squizlabs/php_codesniffer": "^2.6", + "symfony/var-dumper": "^3.3 || ^4.0" + }, + "suggest": { + "ext-SimpleXML": "For Firefox profile creation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-community": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "Facebook\\WebDriver\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "A PHP client for Selenium WebDriver", + "homepage": "https://github.com/facebook/php-webdriver", + "keywords": [ + "facebook", + "php", + "selenium", + "webdriver" + ], + "time": "2018-05-16T17:37:13+00:00" + }, { "name": "filp/whoops", "version": "2.1.14", @@ -4944,6 +5004,67 @@ ], "time": "2016-02-11T16:21:17+00:00" }, + { + "name": "laravel/dusk", + "version": "v3.0.8", + "source": { + "type": "git", + "url": "https://github.com/laravel/dusk.git", + "reference": "c6201427e63b869b0c1ee83d91c1d1958b71968e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/dusk/zipball/c6201427e63b869b0c1ee83d91c1d1958b71968e", + "reference": "c6201427e63b869b0c1ee83d91c1d1958b71968e", + "shasum": "" + }, + "require": { + "facebook/webdriver": "~1.0", + "illuminate/console": "~5.6", + "illuminate/support": "~5.6", + "nesbot/carbon": "~1.20", + "php": ">=7.1.0", + "symfony/console": "~4.0", + "symfony/process": "~4.0" + }, + "require-dev": { + "mockery/mockery": "~1.0", + "phpunit/phpunit": "~7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Dusk\\DuskServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Dusk\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Dusk provides simple end-to-end testing and browser automation.", + "keywords": [ + "laravel", + "testing", + "webdriver" + ], + "time": "2018-04-29T19:15:23+00:00" + }, { "name": "martinlindhe/laravel-vue-i18n-generator", "version": "0.1.28", diff --git a/phpunit.xml b/phpunit.xml index 0b67ad6ea..1bf73c4c6 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,6 +10,9 @@ processIsolation="false" stopOnFailure="false"> + + ./tests/Browser/Processes + ./tests/Integration diff --git a/tests/Browser/BrowserTestCase.php b/tests/Browser/BrowserTestCase.php new file mode 100644 index 000000000..3e8ae90ae --- /dev/null +++ b/tests/Browser/BrowserTestCase.php @@ -0,0 +1,44 @@ +addArguments([ + '--disable-gpu', + ]); + + return RemoteWebDriver::create( + 'http://services.pterodactyl.local:4444/wd/hub', DesiredCapabilities::chrome()->setCapability( + ChromeOptions::CAPABILITY, $options + ) + ); + } + + /** + * Return an instance of the browser to be used for tests. + * + * @param \Facebook\WebDriver\Remote\RemoteWebDriver $driver + * @return \Pterodactyl\Tests\Browser\PterodactylBrowser + */ + protected function newBrowser($driver): PterodactylBrowser + { + return new PterodactylBrowser($driver); + } +} diff --git a/tests/Browser/PterodactylBrowser.php b/tests/Browser/PterodactylBrowser.php new file mode 100644 index 000000000..ba08a0707 --- /dev/null +++ b/tests/Browser/PterodactylBrowser.php @@ -0,0 +1,9 @@ + Date: Thu, 31 May 2018 22:30:05 -0700 Subject: [PATCH 2/8] :100: Lets not accidentally drop the entire database again. --- .env.dusk | 11 ++++++----- tests/Browser/BrowserTestCase.php | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.env.dusk b/.env.dusk index 1934ad3d5..4c8e50527 100644 --- a/.env.dusk +++ b/.env.dusk @@ -3,7 +3,7 @@ APP_DEBUG=false APP_KEY=NDWgIKKi9ovNK1PXZpzfNVSBdfCXGb5i APP_JWT_KEY=test1234 APP_TIMEZONE=America/Los_Angeles -APP_URL=http://192.168.1.249 +APP_URL=http://pterodactyl.local CACHE_DRIVER=file SESSION_DRIVER=file @@ -19,7 +19,8 @@ APP_SERVICE_AUTHOR=testing@pterodactyl.io MAIL_FROM_NAME="Pterodactyl Panel" RECAPTCHA_ENABLED=false -DB_HOST=services.pterodactyl.local -DB_DATABASE=panel_test -DB_USERNAME=panel_test -DB_PASSWORD=Test1234 +DB_CONNECTION=testing +TESTING_DB_HOST=services.pterodactyl.local +TESTING_DB_DATABASE=panel_test +TESTING_DB_USERNAME=panel_test +TESTING_DB_PASSWORD=Test1234 diff --git a/tests/Browser/BrowserTestCase.php b/tests/Browser/BrowserTestCase.php index 3e8ae90ae..3e7d08c01 100644 --- a/tests/Browser/BrowserTestCase.php +++ b/tests/Browser/BrowserTestCase.php @@ -3,7 +3,9 @@ namespace Pterodactyl\Tests\Browser; use Laravel\Dusk\TestCase; +use BadMethodCallException; use Tests\CreatesApplication; +use Illuminate\Database\Eloquent\Model; use Facebook\WebDriver\Chrome\ChromeOptions; use Facebook\WebDriver\Remote\RemoteWebDriver; use Facebook\WebDriver\Remote\DesiredCapabilities; @@ -13,6 +15,23 @@ abstract class BrowserTestCase extends TestCase { use CreatesApplication, DatabaseMigrations; + /** + * Setup tests. + */ + protected function setUp() + { + // Don't accidentally run the migrations aganist the non-testing database. Ask me + // how many times I've accidentally dropped my database... + if (env('DB_CONNECTION') !== 'testing') { + throw new BadMethodCallException('Cannot call browser tests using the non-testing database connection.'); + } + + parent::setUp(); + + // Gotta unset this to continue avoiding issues with the validation. + Model::unsetEventDispatcher(); + } + /** * Create the RemoteWebDriver instance. * From f8fa62e3d6c48b4735556a86f63ce1a86a348b1d Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 31 May 2018 22:42:52 -0700 Subject: [PATCH 3/8] First round of authentication tests --- .../scripts/components/auth/LoginForm.vue | 13 +-- tests/Browser/BrowserTestCase.php | 1 + tests/Browser/Pages/BasePage.php | 16 ++++ tests/Browser/Pages/LoginPage.php | 24 +++++ .../Authentication/LoginProcessTest.php | 88 +++++++++++++++++++ 5 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 tests/Browser/Pages/BasePage.php create mode 100644 tests/Browser/Pages/LoginPage.php create mode 100644 tests/Browser/Processes/Authentication/LoginProcessTest.php diff --git a/resources/assets/scripts/components/auth/LoginForm.vue b/resources/assets/scripts/components/auth/LoginForm.vue index 07236d550..067fb9290 100644 --- a/resources/assets/scripts/components/auth/LoginForm.vue +++ b/resources/assets/scripts/components/auth/LoginForm.vue @@ -5,29 +5,30 @@ >
- - +
- - +
-
- {{ $t('auth.forgot_password.label') }} diff --git a/tests/Browser/BrowserTestCase.php b/tests/Browser/BrowserTestCase.php index 3e7d08c01..dd96930a8 100644 --- a/tests/Browser/BrowserTestCase.php +++ b/tests/Browser/BrowserTestCase.php @@ -41,6 +41,7 @@ abstract class BrowserTestCase extends TestCase { $options = (new ChromeOptions)->addArguments([ '--disable-gpu', + '--disable-infobars', ]); return RemoteWebDriver::create( diff --git a/tests/Browser/Pages/BasePage.php b/tests/Browser/Pages/BasePage.php new file mode 100644 index 000000000..7d8efb513 --- /dev/null +++ b/tests/Browser/Pages/BasePage.php @@ -0,0 +1,16 @@ + '#grid-username', + '@password' => '#grid-password', + '@loginButton' => '#grid-login-button', + '@forgotPassword' => 'a[aria-label="Forgot password"]', + ]; + } +} diff --git a/tests/Browser/Processes/Authentication/LoginProcessTest.php b/tests/Browser/Processes/Authentication/LoginProcessTest.php new file mode 100644 index 000000000..1e0f8a0be --- /dev/null +++ b/tests/Browser/Processes/Authentication/LoginProcessTest.php @@ -0,0 +1,88 @@ +user = factory(User::class)->create([ + 'email' => 'test@example.com', + 'password' => Hash::make('Password123'), + ]); + } + + /** + * Test that a user can login successfully using their email address. + */ + public function testLoginUsingEmail() + { + $this->browse(function (PterodactylBrowser $browser) { + $browser->visit(new LoginPage) + ->waitFor('@username') + ->type('@username', 'test@example.com') + ->type('@password', 'Password123') + ->click('@loginButton') + ->waitForReload() + ->assertPathIs('/') + ->assertAuthenticatedAs($this->user); + }); + } + + /** + * Test that a user can login successfully using their username. + */ + public function testLoginUsingUsername() + { + $this->browse(function (PterodactylBrowser $browser) { + $browser->visit(new LoginPage) + ->waitFor('@username') + ->type('@username', $this->user->username) + ->type('@password', 'Password123') + ->click('@loginButton') + ->waitForReload() + ->assertPathIs('/') + ->assertAuthenticatedAs($this->user); + }); + } + + /** + * Test that entering the wrong password shows the expected error and then allows + * us to login without clearing the username field. + */ + public function testLoginWithErrors() + { + $this->browse(function (PterodactylBrowser $browser) { + $browser->logout() + ->visit(new LoginPage()) + ->waitFor('@username') + ->type('@username', 'test@example.com') + ->type('@password', 'invalid') + ->click('@loginButton') + ->waitFor('.alert.error') + ->assertSeeIn('.alert.error', trans('auth.failed')) + ->assertValue('@username', 'test@example.com') + ->assertValue('@password', '') + ->assertFocused('@password') + ->type('@password', 'Password123') + ->keys('@password', [WebDriverKeys::ENTER]) + ->waitForReload() + ->assertPathIs('/') + ->assertAuthenticatedAs($this->user); + }); + } +} From ebb7b6de9bcd2b8df73e1d5f2d4c27782f2cb673 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 2 Jun 2018 15:54:52 -0700 Subject: [PATCH 4/8] Let gulp build the necessary core files using artisan --- gulpfile.js | 32 +++++++++++++++++++++++++++++++- package.json | 1 + 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index a7bcf73ce..b843f890a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -2,6 +2,7 @@ const babel = require('gulp-babel'); const concat = require('gulp-concat'); const cssmin = require('gulp-cssmin'); const del = require('del'); +const exec = require('child_process').exec; const gulp = require('gulp'); const gulpif = require('gulp-if'); const postcss = require('gulp-postcss'); @@ -74,6 +75,32 @@ function watch() { }, scripts)); } +/** + * Generate the language files to be consumed by front end. + * + * @returns {Promise} + */ +function i18n() { + return new Promise((resolve, reject) => { + exec('php artisan vue-i18n:generate', {}, (err, stdout, stderr) => { + return err ? reject(err) : resolve({ stdout, stderr }); + }) + }) +} + +/** + * Generate the routes file to be used in Vue files. + * + * @returns {Promise} + */ +function routes() { + return new Promise((resolve, reject) => { + exec('php artisan ziggy:generate resources/assets/scripts/helpers/ziggy.js', {}, (err, stdout, stderr) => { + return err ? reject(err) : resolve({ stdout, stderr }); + }); + }) +} + /** * Cleanup unused versions of hashed assets. */ @@ -82,9 +109,12 @@ function clean() { } exports.clean = clean; +exports.i18n = i18n; +exports.routes = routes; exports.styles = styles; exports.scripts = scripts; exports.watch = watch; +gulp.task('components', gulp.parallel(i18n, routes)); gulp.task('scripts', gulp.series(clean, scripts)); -gulp.task('default', gulp.series(clean, styles, scripts)); +gulp.task('default', gulp.series(clean, i18n, routes, styles, scripts)); diff --git a/package.json b/package.json index a790e437f..1eea87689 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "build:filemanager": "./node_modules/babel-cli/bin/babel.js public/themes/pterodactyl/js/frontend/files/src --source-maps --out-file public/themes/pterodactyl/js/frontend/files/filemanager.min.js", "watch": "./node_modules/gulp-cli/bin/gulp.js watch", "build": "./node_modules/gulp-cli/bin/gulp.js default", + "build:components": "./node_modules/gulp-cli/bin/gulp.js components", "build:styles": "./node_modules/gulp-cli/bin/gulp.js styles", "build:scripts": "./node_modules/gulp-cli/bin/gulp.js scripts" }, From 7a1d73ba9ee4882bd78f19635735520448694676 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 2 Jun 2018 15:54:52 -0700 Subject: [PATCH 5/8] Let gulp build the necessary core files using artisan --- gulpfile.js | 32 +++++++++++++++++++++++++++++++- package.json | 1 + 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index a7bcf73ce..b843f890a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -2,6 +2,7 @@ const babel = require('gulp-babel'); const concat = require('gulp-concat'); const cssmin = require('gulp-cssmin'); const del = require('del'); +const exec = require('child_process').exec; const gulp = require('gulp'); const gulpif = require('gulp-if'); const postcss = require('gulp-postcss'); @@ -74,6 +75,32 @@ function watch() { }, scripts)); } +/** + * Generate the language files to be consumed by front end. + * + * @returns {Promise} + */ +function i18n() { + return new Promise((resolve, reject) => { + exec('php artisan vue-i18n:generate', {}, (err, stdout, stderr) => { + return err ? reject(err) : resolve({ stdout, stderr }); + }) + }) +} + +/** + * Generate the routes file to be used in Vue files. + * + * @returns {Promise} + */ +function routes() { + return new Promise((resolve, reject) => { + exec('php artisan ziggy:generate resources/assets/scripts/helpers/ziggy.js', {}, (err, stdout, stderr) => { + return err ? reject(err) : resolve({ stdout, stderr }); + }); + }) +} + /** * Cleanup unused versions of hashed assets. */ @@ -82,9 +109,12 @@ function clean() { } exports.clean = clean; +exports.i18n = i18n; +exports.routes = routes; exports.styles = styles; exports.scripts = scripts; exports.watch = watch; +gulp.task('components', gulp.parallel(i18n, routes)); gulp.task('scripts', gulp.series(clean, scripts)); -gulp.task('default', gulp.series(clean, styles, scripts)); +gulp.task('default', gulp.series(clean, i18n, routes, styles, scripts)); diff --git a/package.json b/package.json index a790e437f..1eea87689 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "build:filemanager": "./node_modules/babel-cli/bin/babel.js public/themes/pterodactyl/js/frontend/files/src --source-maps --out-file public/themes/pterodactyl/js/frontend/files/filemanager.min.js", "watch": "./node_modules/gulp-cli/bin/gulp.js watch", "build": "./node_modules/gulp-cli/bin/gulp.js default", + "build:components": "./node_modules/gulp-cli/bin/gulp.js components", "build:styles": "./node_modules/gulp-cli/bin/gulp.js styles", "build:scripts": "./node_modules/gulp-cli/bin/gulp.js scripts" }, From 92c03d49534141c3ae46ae35f75e32f4b0d7b48c Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 2 Jun 2018 16:39:49 -0700 Subject: [PATCH 6/8] Add tests for password reset page functionality --- .../components/auth/ForgotPassword.vue | 6 ++- tests/Browser/Pages/LoginPage.php | 7 ++- .../ForgotPasswordProcessTest.php | 50 +++++++++++++++++++ tests/Browser/PterodactylBrowser.php | 32 ++++++++++++ 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 tests/Browser/Processes/Authentication/ForgotPasswordProcessTest.php diff --git a/resources/assets/scripts/components/auth/ForgotPassword.vue b/resources/assets/scripts/components/auth/ForgotPassword.vue index e06fa40b4..c224fd778 100644 --- a/resources/assets/scripts/components/auth/ForgotPassword.vue +++ b/resources/assets/scripts/components/auth/ForgotPassword.vue @@ -5,13 +5,14 @@ >
- - +

{{ $t('auth.forgot_password.label_help') }}

@@ -25,6 +26,7 @@
{{ $t('auth.go_to_login') }} diff --git a/tests/Browser/Pages/LoginPage.php b/tests/Browser/Pages/LoginPage.php index 06ec27b57..5fd42cbf6 100644 --- a/tests/Browser/Pages/LoginPage.php +++ b/tests/Browser/Pages/LoginPage.php @@ -15,10 +15,15 @@ class LoginPage extends BasePage public function elements() { return [ + '@email' => '#grid-email', '@username' => '#grid-username', '@password' => '#grid-password', '@loginButton' => '#grid-login-button', - '@forgotPassword' => 'a[aria-label="Forgot password"]', + '@submitButton' => 'button.btn.btn-jumbo[type="submit"]', + '@forgotPassword' => 'a[href="/auth/password"][aria-label="Forgot password"]', + '@goToLogin' => 'a[href="/auth/login"][aria-label="Go to login"]', + '@alertSuccess' => 'div[role="alert"].success > span.message', + '@alertDanger' => 'div[role="alert"].danger > span.message', ]; } } diff --git a/tests/Browser/Processes/Authentication/ForgotPasswordProcessTest.php b/tests/Browser/Processes/Authentication/ForgotPasswordProcessTest.php new file mode 100644 index 000000000..ab8c9bc8a --- /dev/null +++ b/tests/Browser/Processes/Authentication/ForgotPasswordProcessTest.php @@ -0,0 +1,50 @@ +browse(function (PterodactylBrowser $browser) { + $browser->visit(new LoginPage) + ->assertSee(trans('auth.forgot_password.label')) + ->click('@forgotPassword') + ->waitForLocation('/auth/password') + ->assertFocused('@email') + ->assertSeeIn('.input-open > p.text-xs', trans('auth.forgot_password.label_help')) + ->assertSeeIn('@submitButton', trans('auth.forgot_password.button')) + ->type('@email', 'unassociated@example.com') + ->assertSeeIn('@goToLogin', trans('auth.go_to_login')) + ->press('@submitButton') + ->waitForLocation('/auth/login') + ->assertSeeIn('div[role="alert"].success > span.message', 'We have e-mailed your password reset link!') + ->assertFocused('@username') + ->assertValue('@username', 'unassociated@example.com'); + }); + } + + /** + * Test that you can type in your email address and then click forgot password and have + * the email maintained on the new page. + */ + public function testEmailCarryover() + { + $this->browse(function (PterodactylBrowser $browser) { + $browser->visit(new LoginPage) + ->type('@username', 'dane@example.com') + ->click('@forgotPassword') + ->waitForLocation('/auth/password') + ->assertFocused('@email') + ->assertValue('@email', 'dane@example.com'); + }); + } +} diff --git a/tests/Browser/PterodactylBrowser.php b/tests/Browser/PterodactylBrowser.php index ba08a0707..57a790844 100644 --- a/tests/Browser/PterodactylBrowser.php +++ b/tests/Browser/PterodactylBrowser.php @@ -3,7 +3,39 @@ namespace Pterodactyl\Tests\Browser; use Laravel\Dusk\Browser; +use Illuminate\Support\Str; +use PHPUnit\Framework\Assert as PHPUnit; class PterodactylBrowser extends Browser { + /** + * Perform a case insensitive search for a string in the body. + * + * @param string $text + * @return \Pterodactyl\Tests\Browser\PterodactylBrowser + */ + public function assertSee($text) + { + return $this->assertSeeIn('', $text); + } + + /** + * Perform a case insensitive search for a string in a given selector. + * + * @param string $selector + * @param string $text + * @return \Pterodactyl\Tests\Browser\PterodactylBrowser + */ + public function assertSeeIn($selector, $text) + { + $fullSelector = $this->resolver->format($selector); + $element = $this->resolver->findOrFail($selector); + + PHPUnit::assertTrue( + Str::contains(mb_strtolower($element->getText()), mb_strtolower($text)), + "Did not see expected text [{$text}] within element [{$fullSelector}] using case-insensitive search." + ); + + return $this; + } } From 4209be021e0c74623a2f723308bc806e9f14a635 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 2 Jun 2018 16:59:16 -0700 Subject: [PATCH 7/8] Add handlers for non-successful responses from the panel --- resources/assets/scripts/bootstrap.js | 2 +- .../assets/scripts/components/auth/ForgotPassword.vue | 4 ++++ resources/assets/scripts/components/auth/LoginForm.vue | 9 ++++++++- .../assets/scripts/components/auth/ResetPassword.vue | 4 ++++ .../assets/scripts/components/auth/TwoFactorForm.vue | 4 ++++ 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/resources/assets/scripts/bootstrap.js b/resources/assets/scripts/bootstrap.js index 8d2009067..f29ff0528 100644 --- a/resources/assets/scripts/bootstrap.js +++ b/resources/assets/scripts/bootstrap.js @@ -17,8 +17,8 @@ try { */ window.axios = require('axios'); - window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; +window.axios.defaults.headers.common['Accept'] = 'application/json'; /** * Next we will register the CSRF Token as a common header with Axios so that diff --git a/resources/assets/scripts/components/auth/ForgotPassword.vue b/resources/assets/scripts/components/auth/ForgotPassword.vue index e06fa40b4..0c72b884c 100644 --- a/resources/assets/scripts/components/auth/ForgotPassword.vue +++ b/resources/assets/scripts/components/auth/ForgotPassword.vue @@ -68,6 +68,10 @@ email: this.$props.email, }) .then(function (response) { + if (!(response.data instanceof Object)) { + throw new Error('An error was encountered while processing this request.'); + } + self.$data.submitDisabled = false; self.$data.showSpinner = false; self.success(response.data.status); diff --git a/resources/assets/scripts/components/auth/LoginForm.vue b/resources/assets/scripts/components/auth/LoginForm.vue index 07236d550..0028db4e2 100644 --- a/resources/assets/scripts/components/auth/LoginForm.vue +++ b/resources/assets/scripts/components/auth/LoginForm.vue @@ -81,6 +81,12 @@ password: this.$props.user.password, }) .then(function (response) { + // If there is a 302 redirect or some other odd behavior (basically, response that isnt + // in JSON format) throw an error and don't try to continue with the login. + if (!(response.data instanceof Object)) { + throw new Error('An error was encountered while processing this request.'); + } + if (response.data.complete) { return window.location = '/'; } @@ -92,6 +98,8 @@ .catch(function (err) { self.$props.user.password = ''; self.$data.showSpinner = false; + self.$refs.password.focus(); + if (!err.response) { return console.error(err); } @@ -101,7 +109,6 @@ response.data.errors.forEach(function (error) { self.error(error.detail); }); - self.$refs.password.focus(); } }); }, diff --git a/resources/assets/scripts/components/auth/ResetPassword.vue b/resources/assets/scripts/components/auth/ResetPassword.vue index 2a7cf17a6..cda6716ac 100644 --- a/resources/assets/scripts/components/auth/ResetPassword.vue +++ b/resources/assets/scripts/components/auth/ResetPassword.vue @@ -93,6 +93,10 @@ token: this.$props.token, }) .then(function (response) { + if (!(response.data instanceof Object)) { + throw new Error('An error was encountered while processing this login.'); + } + return window.location = response.data.redirect_to; }) .catch(function (err) { diff --git a/resources/assets/scripts/components/auth/TwoFactorForm.vue b/resources/assets/scripts/components/auth/TwoFactorForm.vue index 27d2b2282..fb51090f3 100644 --- a/resources/assets/scripts/components/auth/TwoFactorForm.vue +++ b/resources/assets/scripts/components/auth/TwoFactorForm.vue @@ -49,6 +49,10 @@ authentication_code: this.$data.code, }) .then(function (response) { + if (!(response.data instanceof Object)) { + throw new Error('An error was encountered while processing this login.'); + } + window.location = response.data.intended; }) .catch(function (err) { From dec969bf9f6e1d4b28904923be44e0f965bdb8c4 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 2 Jun 2018 17:01:54 -0700 Subject: [PATCH 8/8] Fix checkpoint behavior to only work when a token is provided --- resources/assets/scripts/app.js | 2 +- resources/assets/scripts/components/auth/TwoFactorForm.vue | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/assets/scripts/app.js b/resources/assets/scripts/app.js index 180df2a40..1a6b9476c 100644 --- a/resources/assets/scripts/app.js +++ b/resources/assets/scripts/app.js @@ -35,7 +35,7 @@ const router = new VueRouter({ routes: [ { name: 'login', path: '/auth/login', component: Login }, { name: 'forgot-password', path: '/auth/password', component: Login }, - { name: 'checkpoint', path: '/checkpoint', component: Login }, + { name: 'checkpoint', path: '/auth/checkpoint', component: Login }, { name: 'reset-password', path: '/auth/password/reset/:token', diff --git a/resources/assets/scripts/components/auth/TwoFactorForm.vue b/resources/assets/scripts/components/auth/TwoFactorForm.vue index fb51090f3..84a0461d5 100644 --- a/resources/assets/scripts/components/auth/TwoFactorForm.vue +++ b/resources/assets/scripts/components/auth/TwoFactorForm.vue @@ -37,6 +37,10 @@ }; }, mounted: function () { + if ((this.$route.query.token || '').length < 1) { + return this.$router.push({ name: 'login' }); + } + this.$refs.code.focus(); }, methods: {