diff --git a/.env.dusk.ci b/.env.dusk.ci
new file mode 100644
index 0000000..6e20d81
--- /dev/null
+++ b/.env.dusk.ci
@@ -0,0 +1,60 @@
+APP_NAME='DevDojo Auth'
+APP_ENV=testing
+APP_KEY=base64:La7oMA4FgO9U3dEgYzu7UwlpYjFvGKSgP2uwnkbaPK8=
+APP_DEBUG=true
+APP_TIMEZONE=UTC
+APP_URL=http://127.0.0.1:8000
+
+APP_LOCALE=en
+APP_FALLBACK_LOCALE=en
+APP_FAKER_LOCALE=en_US
+
+APP_MAINTENANCE_DRIVER=file
+APP_MAINTENANCE_STORE=database
+
+BCRYPT_ROUNDS=12
+
+LOG_CHANNEL=stack
+LOG_STACK=single
+LOG_DEPRECATIONS_CHANNEL=null
+LOG_LEVEL=debug
+
+DB_CONNECTION=sqlite
+DB_DATABASE=/home/runner/work/auth/auth/laravel_app/database/dusk.sqlite
+
+SESSION_DRIVER=file
+SESSION_LIFETIME=120
+SESSION_ENCRYPT=false
+SESSION_PATH=/
+SESSION_DOMAIN=null
+
+BROADCAST_CONNECTION=log
+FILESYSTEM_DISK=local
+QUEUE_CONNECTION=database
+
+CACHE_STORE=file
+CACHE_PREFIX=
+
+MEMCACHED_HOST=127.0.0.1
+
+REDIS_CLIENT=phpredis
+REDIS_HOST=127.0.0.1
+REDIS_PASSWORD=null
+REDIS_PORT=6379
+
+MAIL_MAILER=log
+MAIL_HOST=127.0.0.1
+MAIL_PORT=2525
+MAIL_USERNAME=null
+MAIL_PASSWORD=null
+MAIL_ENCRYPTION=null
+MAIL_FROM_ADDRESS="hello@example.com"
+MAIL_FROM_NAME="${APP_NAME}"
+
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+AWS_DEFAULT_REGION=us-east-1
+AWS_BUCKET=
+AWS_USE_PATH_STYLE_ENDPOINT=false
+
+VITE_APP_NAME="${APP_NAME}"
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index a2897a1..d75b02f 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -108,20 +108,60 @@ jobs:
composer install
working-directory: ./laravel_app
- - name: Install PestPHP and PHPStan
+ - name: Install PestPHP, PHPStan, Dusk, and Dusk API Conf
run: |
composer require pestphp/pest --dev --with-all-dependencies
composer require larastan/larastan:^2.0 --dev --with-all-dependencies
+ composer require laravel/dusk --dev --with-all-dependencies
+ composer require alebatistella/duskapiconf --dev --with-all-dependencies
+ composer require protonemedia/laravel-dusk-fakes:^1.6 --dev --with-all-dependencies
working-directory: ./laravel_app
+ - name: Upgrade Chrome Driver
+ run: php artisan dusk:chrome-driver --detect
+ working-directory: ./laravel_app
+
+ - name: Start Chrome Driver
+ run: ./vendor/laravel/dusk/bin/chromedriver-linux &
+ working-directory: ./laravel_app
+
+ - name: Check Chrome & ChromeDriver Versions
+ run: |
+ google-chrome --version
+ chromedriver --version
+
- name: Clear all view caches
run: php artisan view:clear
working-directory: ./laravel_app
+ - name: Run Artisan Serve
+ run: php artisan serve --no-reload &
+ working-directory: ./laravel_app
+
- name: Run Tests
run: ./vendor/bin/pest
working-directory: ./laravel_app
+ - name: Run Dusk Tests
+ env:
+ APP_URL: http://127.0.0.1:8000
+ APP_ENV: testing
+ run: php artisan dusk -vvv
+ working-directory: ./laravel_app
+
+ - name: Upload Screenshots
+ if: failure()
+ uses: actions/upload-artifact@v2
+ with:
+ name: screenshots
+ path: tests/Browser/screenshots
+ - name: Upload Console Logs
+ if: failure()
+ uses: actions/upload-artifact@v2
+ with:
+ name: console
+ path: tests/Browser/console
+
- name: Move the PHP config file to the root directory
run: cp vendor/devdojo/auth/phpstan.neon phpstan.neon
working-directory: ./laravel_app
diff --git a/README.md b/README.md
index 7e84660..90d4a98 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ Be sure to visit the official documentation at Laravel Starter Kits .
```
composer require devdojo/auth
diff --git a/composer.json b/composer.json
index 5297061..fba0a85 100644
--- a/composer.json
+++ b/composer.json
@@ -34,7 +34,10 @@
"pestphp/pest": "^2.34",
"pestphp/pest-plugin-laravel": "^2.4",
"larastan/larastan": "^2.0",
- "phpstan/phpstan": "^1.11"
+ "phpstan/phpstan": "^1.11",
+ "laravel/dusk": "^8.2",
+ "protonemedia/laravel-dusk-fakes": "^1.6",
+ "alebatistella/duskapiconf": "^1.2"
},
"autoload": {
"psr-4": {
diff --git a/database/migrations/2024_04_24_000002_update_passwords_field_to_be_nullable.php b/database/migrations/2024_04_24_000002_update_passwords_field_to_be_nullable.php
index ece8fc8..1c97524 100644
--- a/database/migrations/2024_04_24_000002_update_passwords_field_to_be_nullable.php
+++ b/database/migrations/2024_04_24_000002_update_passwords_field_to_be_nullable.php
@@ -2,6 +2,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
@@ -22,9 +23,14 @@ public function up()
*/
public function down()
{
+ // Update records with NULL values to avoid constraint violations
+ DB::table('users')->whereNull('name')->update(['name' => '']);
+ DB::table('users')->whereNull('password')->update(['password' => '']);
+
+ // Change the table structure
Schema::table('users', function (Blueprint $table) {
- $table->string('password')->nullable(false)->change();
$table->string('name')->nullable(false)->change();
+ $table->string('password')->nullable(false)->change();
});
}
};
diff --git a/resources/views/components/elements/input-placeholder.blade.php b/resources/views/components/elements/input-placeholder.blade.php
index a9fcb9f..39c5f59 100644
--- a/resources/views/components/elements/input-placeholder.blade.php
+++ b/resources/views/components/elements/input-placeholder.blade.php
@@ -2,7 +2,7 @@
'value' => ''
])
-
merge(['class' => 'px-3.5 bg-gray-50 py-2.5 text-sm flex items-center justify-between border rounded-md border-gray-300']) }}>
+
merge(['class' => 'px-3.5 bg-gray-50 py-2.5 text-sm flex items-center justify-between border rounded-md border-gray-300']) }}>
{{ $value }}
{{ $slot }}
\ No newline at end of file
diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/components/layouts/app.blade.php
index 9a04e75..a2dbce2 100644
--- a/resources/views/components/layouts/app.blade.php
+++ b/resources/views/components/layouts/app.blade.php
@@ -4,7 +4,10 @@
@include('auth::includes.head')
-
+ @php
+ $dyanicPageId = str_replace('/', '-', str_replace('.', '', Request::path()));
+ @endphp
+
@if(config('devdojo.auth.appearance.background.image'))
diff --git a/resources/views/components/setup/sidebar-links.blade.php b/resources/views/components/setup/sidebar-links.blade.php
index a6dff49..6537535 100644
--- a/resources/views/components/setup/sidebar-links.blade.php
+++ b/resources/views/components/setup/sidebar-links.blade.php
@@ -43,7 +43,7 @@
>
diff --git a/resources/views/pages/auth/login.blade.php b/resources/views/pages/auth/login.blade.php
index e474984..120e041 100644
--- a/resources/views/pages/auth/login.blade.php
+++ b/resources/views/pages/auth/login.blade.php
@@ -135,11 +135,11 @@ public function authenticate()
@if($showPasswordField)
- Edit
+ Edit
@else
@if($showIdentifierInput)
-
+
@endif
@endif
@@ -158,19 +158,19 @@ public function authenticate()
@endif
@if($showPasswordField)
-
+
- Forgot your password?
+ Forgot your password?
@endif
-
Continue
+
Continue
Don't have an account?
- Sign up
+ Sign up
@if(config('devdojo.auth.settings.login_show_social_providers') && config('devdojo.auth.settings.social_providers_location') != 'top')
diff --git a/resources/views/pages/auth/password/reset.blade.php b/resources/views/pages/auth/password/reset.blade.php
index 5878d62..37647f3 100644
--- a/resources/views/pages/auth/password/reset.blade.php
+++ b/resources/views/pages/auth/password/reset.blade.php
@@ -51,7 +51,7 @@ public function sendResetPasswordLink()
/>
@if ($emailSentMessage)
-
+
diff --git a/resources/views/pages/auth/register.blade.php b/resources/views/pages/auth/register.blade.php
index 32db621..7c7c0af 100644
--- a/resources/views/pages/auth/register.blade.php
+++ b/resources/views/pages/auth/register.blade.php
@@ -128,19 +128,19 @@ public function register()
@php
$autofocusEmail = ($showNameField) ? false : true;
@endphp
-
+
@endif
@if($showPasswordField)
-
+
@endif
- Continue
+ Continue
Already have an account?
- Sign in
+ Sign in
@if(config('devdojo.auth.settings.social_providers_location') != 'top')
diff --git a/resources/views/pages/auth/verify.blade.php b/resources/views/pages/auth/verify.blade.php
index 0dba519..507f57a 100644
--- a/resources/views/pages/auth/verify.blade.php
+++ b/resources/views/pages/auth/verify.blade.php
@@ -57,7 +57,7 @@ public function resend()
@endif
-
Before proceeding, please check your email for a verification link. If you did not receive the email, click here to request another .
+
Before proceeding, please check your email for a verification link. If you did not receive the email, click here to request another .
diff --git a/src/AuthServiceProvider.php b/src/AuthServiceProvider.php
index 59826ae..fe8d988 100644
--- a/src/AuthServiceProvider.php
+++ b/src/AuthServiceProvider.php
@@ -144,5 +144,10 @@ public function register()
$this->app->singleton(Google2FA::class, function ($app) {
return new Google2FA();
});
+
+ // Register the DuskServiceProvider
+ if (($this->app->environment('local') || $this->app->environment('testing')) && class_exists(\Laravel\Dusk\DuskServiceProvider::class)) {
+ $this->app->register(\Devdojo\Auth\Providers\DuskServiceProvider::class);
+ }
}
}
diff --git a/src/Providers/DuskServiceProvider.php b/src/Providers/DuskServiceProvider.php
new file mode 100644
index 0000000..367ab45
--- /dev/null
+++ b/src/Providers/DuskServiceProvider.php
@@ -0,0 +1,71 @@
+script("document.querySelector('$selector').setAttribute('$attribute', '$value');");
+
+ return $this;
+ });
+
+ Browser::macro('authAttributeRemove', function (?string $selector, string $attribute) {
+ $this->script("document.querySelector('$selector').removeAttribute('$attribute');");
+
+ return $this;
+ });
+
+ Browser::macro('testValidationErrorOnSubmit', function (string $message = '') {
+ $this
+ ->click('@submit-button')
+ ->waitForText($message)
+ ->assertSee($message);
+
+ return $this;
+ });
+
+ Browser::macro('createJohnDoe', function () {
+ $user = \App\Models\User::factory()->create([
+ 'email' => 'johndoe@gmail.com',
+ 'password' => \Hash::make('password'),
+ ]);
+
+ return $this;
+ });
+
+ Browser::macro('assertRedirectAfterAuthUrlIsCorrect', function () {
+ $redirectExpectedToBe = '/';
+ if (class_exists(\Devdojo\Genesis\Genesis::class)) {
+ $redirectExpectedToBe = '/dashboard';
+ }
+ $this->assertPathIs($redirectExpectedToBe);
+
+ return $this;
+ });
+
+ Browser::macro('clearLogFile', function () {
+ file_put_contents(storage_path('logs/laravel.log'), '');
+
+ return $this;
+ });
+
+ Browser::macro('getLogFile', function ($callback) {
+ $content = file_get_contents(storage_path('logs/laravel.log'));
+ $callback($content);
+
+ return $this;
+ });
+ }
+}
diff --git a/tests/Browser/LoginTest.php b/tests/Browser/LoginTest.php
new file mode 100644
index 0000000..0d2ac63
--- /dev/null
+++ b/tests/Browser/LoginTest.php
@@ -0,0 +1,110 @@
+browse(function (Browser $browser) {
+ $browser->visit(new Login)
+ ->createJohnDoe()
+ ->loginAsJohnDoe();
+ });
+});
+
+test('Validation Error for Empty Fields', function () {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Login)
+ ->authAttributeRemove('#email', 'required')
+ ->testValidationErrorOnSubmit('The email field is required')
+ ->typeAndSubmit('@email-input', 'johndoe@gmail.com')
+ ->waitFor('@password-input')
+ ->authAttributeRemove('#password', 'required')
+ ->testValidationErrorOnSubmit('The password field is required');
+
+ });
+});
+
+test('Invalid Email', function () {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Login)
+ ->authAttributeChange('#email', 'type', 'text')
+ ->type('@email-input', 'johndoe')
+ ->testValidationErrorOnSubmit('The email field must be a valid email address');
+ });
+});
+
+test('Incorrect Password', function () {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Login)
+ ->createJohnDoe()
+ ->typeAndSubmit('@email-input', 'johndoe@gmail.com')
+ ->waitFor('@password-input')
+ ->type('@password-input', 'password123')
+ ->testValidationErrorOnSubmit('These credentials do not match our records');
+ });
+});
+
+test('Can Edit Email Address', function () {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Login)
+ ->typeAndSubmit('@email-input', 'canichange@myemail.com')
+ ->waitFor('@edit-email-button')
+ ->click('@edit-email-button')
+ ->waitFor('@email-input')
+ ->typeAndSubmit('@email-input', 'yesicanchange@myemail.com')
+ ->waitFor('@email-read-only-placeholder')
+ ->assertSeeIn('@email-read-only-placeholder', 'yesicanchange@myemail.com');
+ });
+});
+
+test('Link to Register Page', function () {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Login)
+ ->click('@register-link')
+ ->waitFor('@auth-register')
+ ->assertPathIs('/auth/register');
+
+ });
+});
+
+test('Link to Forgot Password Page', function () {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Login)
+ ->typeAndSubmit('@email-input', 'testingForgotPassword@gmail.com')
+ ->waitFor('@forgot-password-link')
+ ->click('@forgot-password-link')
+ ->waitFor('@auth-password-reset')
+ ->assertPathIs('/auth/password/reset');
+
+ });
+});
+
+/* ---------------------------------------------------------
+* Here are a few more that we'll need to add once
+* we have reveal password functionality and remember me
+------------------------------------------------------------ */
+
+test('Verify Password Visibility Toggle', function () {
+ $this->browse(function (Browser $browser) {
+ // Test here
+ });
+})->todo();
+
+test('Verify Remember Me Functionality', function () {
+ $this->browse(function (Browser $browser) {
+ // Test here
+ });
+})->todo();
+
+/* ---------------------------------------------------------
+* Investigate further how we can do an accessiblity check
+------------------------------------------------------------ */
+
+test('Verify Page Accessibility', function () {
+ $this->browse(function (Browser $browser) {
+ // Test here
+ });
+})->todo();
diff --git a/tests/Browser/Pages/Login.php b/tests/Browser/Pages/Login.php
new file mode 100644
index 0000000..d3ad4a9
--- /dev/null
+++ b/tests/Browser/Pages/Login.php
@@ -0,0 +1,39 @@
+visit('/auth/login')
+ ->type('@email-input', 'johndoe@gmail.com')
+ ->click('@submit-button')
+ ->waitFor('@password-input')
+ ->type('@password-input', 'password')
+ ->clickAndWaitForReload('@submit-button')
+ ->assertRedirectAfterAuthUrlIsCorrect();
+
+ return $this;
+ }
+
+ public function typeAndSubmit(Browser $browser, $selector, $value)
+ {
+ $browser->type($selector, $value)
+ ->click('@submit-button');
+
+ return $this;
+ }
+}
diff --git a/tests/Browser/Pages/Register.php b/tests/Browser/Pages/Register.php
new file mode 100644
index 0000000..3c4a306
--- /dev/null
+++ b/tests/Browser/Pages/Register.php
@@ -0,0 +1,46 @@
+type('@email-input', 'johndoe@gmail.com')
+ ->type('@password-input', 'password')
+ ->clickAndWaitForReload('@submit-button');
+
+ return $browser;
+
+ }
+
+ public function assertUserReceivedEmail()
+ {
+ // Mail::fake();
+ $user = \App\Models\User::where('email', 'johndoe@gmail.com')->first();
+ Mail::assertSent(MailMessage::class, function ($mail) use ($user) {
+ return $mail->hasTo($user->email);
+ });
+ }
+}
diff --git a/tests/Browser/Pages/VerifyEmail.php b/tests/Browser/Pages/VerifyEmail.php
new file mode 100644
index 0000000..8da865d
--- /dev/null
+++ b/tests/Browser/Pages/VerifyEmail.php
@@ -0,0 +1,16 @@
+browse(function (Browser $browser) {
+ $browser->visit(new Register)
+ ->registerAsJohnDoe()
+ ->assertRedirectAfterAuthUrlIsCorrect();
+ });
+});
+
+test('Validation Error for Empty Fields', function () {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Register)
+ ->authAttributeRemove('#email', 'required')
+ ->authAttributeRemove('#password', 'required')
+ ->testValidationErrorOnSubmit('The email field is required')
+ ->assertSee('The password field is required');
+ });
+});
+
+test('Invalid Email Address', function () {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Register)
+ ->authAttributeChange('#email', 'type', 'text')
+ ->authAttributeRemove('#password', 'required')
+ ->type('@email-input', 'johndoe')
+ ->testValidationErrorOnSubmit('The email field must be a valid email address');
+ });
+});
+
+test('Invalid Password', function () {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Register)
+ ->type('@email-input', 'johndoe@gmail.com')
+ ->type('@password-input', 'pass')
+ ->testValidationErrorOnSubmit('The password field must be at least 8 characters');
+ });
+});
+
+test('Email already taken', function () {
+ $this->browse(function (Browser $browser) {
+ $browser
+ ->visit(new Register)
+ ->createJohnDoe()
+ ->type('@email-input', 'johndoe@gmail.com')
+ ->type('@password-input', 'password')
+ ->testValidationErrorOnSubmit('The email has already been taken');
+ });
+});
+
+test('Return to Login', function () {
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Register)
+ ->click('@login-link')
+ ->waitFor('@auth-login')
+ ->assertPathIs('/auth/login');
+ });
+});
+
+// Add more tests to test when the Name field is shown on the register page, or if the user keeps the password on a separate screen
diff --git a/tests/Browser/Traits.php b/tests/Browser/Traits.php
new file mode 100644
index 0000000..f1dc961
--- /dev/null
+++ b/tests/Browser/Traits.php
@@ -0,0 +1,13 @@
+setConfig('devdojo.auth.settings.registration_require_email_verification', true);
+ $browser = $this->browse(function (Browser $browser) {
+ $browser
+ ->visit(new Register)
+ ->disableFitOnFailure()
+ ->clearLogFile()
+ ->registerAsJohnDoe()
+ ->assertPathIs('/auth/verify')
+ ->getLogFile(function ($content) use ($browser) {
+
+ $foundLine = $this->findLineContainingSubstring($content, 'Verify Email Address:');
+ $url = str_replace('Verify Email Address: ', '', $foundLine);
+ $browser
+ ->visit($url)
+ ->assertRedirectAfterAuthUrlIsCorrect();
+ });
+
+ });
+ $this->resetConfig();
+});
+
+test('Resend Email Verification Link', function () {
+ // Turn on Require Email Validation before running tests
+ $this->setConfig('devdojo.auth.settings.registration_require_email_verification', true);
+ $browser = $this->browse(function (Browser $browser) {
+ $browser
+ ->visit(new Register)
+ ->disableFitOnFailure()
+ ->registerAsJohnDoe()
+ ->assertPathIs('/auth/verify')
+ ->clearLogFile()
+ ->click('@verify-email-resend-link')
+ ->waitForText('A new link has been sent to your email address')
+ ->getLogFile(function ($content) use ($browser) {
+ $foundLine = $this->findLineContainingSubstring($content, 'Verify Email Address:');
+ $url = str_replace('Verify Email Address: ', '', $foundLine);
+ $browser
+ ->visit($url)
+ ->assertRedirectAfterAuthUrlIsCorrect();
+ });
+
+ });
+ $this->resetConfig();
+
+});
diff --git a/tests/Browser/console/.gitignore b/tests/Browser/console/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/tests/Browser/console/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/tests/Browser/screenshots/.gitignore b/tests/Browser/screenshots/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/tests/Browser/screenshots/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/tests/Browser/source/.gitignore b/tests/Browser/source/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/tests/Browser/source/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/tests/DuskTestCase.php b/tests/DuskTestCase.php
new file mode 100644
index 0000000..e2e8590
--- /dev/null
+++ b/tests/DuskTestCase.php
@@ -0,0 +1,68 @@
+resetConfig();
+ // parent::setUp();
+ // }
+
+ /**
+ * Prepare for Dusk test execution.
+ */
+ #[BeforeClass]
+ public static function prepare(): void
+ {
+ if (! static::runningInSail()) {
+ static::startChromeDriver();
+ }
+ }
+
+ public function findLineContainingSubstring($content, $substring)
+ {
+ $lines = explode("\n", $content);
+ $foundLine = current(array_filter($lines, fn ($line) => strpos($line, $substring) === 0));
+
+ return $foundLine ?: null;
+ }
+
+ /**
+ * Create the RemoteWebDriver instance.
+ */
+ protected function driver(): RemoteWebDriver
+ {
+ $chromeOptions = [
+ '--disable-gpu',
+ '--window-size=1920,1080',
+ '--no-sandbox',
+ '--disable-dev-shm-usage',
+ '--disable-software-rasterizer',
+ ];
+
+ if (env('APP_ENV') != 'local') {
+ $chromeOptions[] = '--headless';
+ }
+
+ $options = (new ChromeOptions)->addArguments($chromeOptions);
+
+ $options->setExperimentalOption('mobileEmulation', ['userAgent' => 'laravel/dusk']);
+
+ return RemoteWebDriver::create(
+ 'http://localhost:9515', DesiredCapabilities::chrome()->setCapability(
+ ChromeOptions::CAPABILITY, $options
+ )
+ );
+ }
+}
diff --git a/tests/Pest.php b/tests/Pest.php
index dba58d7..50e4fe7 100644
--- a/tests/Pest.php
+++ b/tests/Pest.php
@@ -1,5 +1,10 @@
in('Browser');
+
use Illuminate\Foundation\Testing\RefreshDatabase;
/*