Replace hand-rolled auth with Laravel Breeze (Livewire stack)
Adds the auth Breeze scaffolds that were missing before: password
reset and email verification, plus a profile page (update name/email,
change password, delete account). User now implements MustVerifyEmail
so verification is actually enforced, not just scaffolded.
This does mean accepting Tailwind v3 (postcss/tailwind.config.js)
over the v4 CSS-first setup the app started with, and Livewire drops
from ^4.3 to ^3.6.4 — both forced by Breeze/Volt's current
compatibility requirements. Restored the Instrument Sans font Breeze's
installer dropped so styling stays close to what we had.
Reconciliation, since Breeze's installer assumes a blank-slate app:
- Deleted the hand-rolled Login/Register Livewire components/views/
test — superseded by Breeze's Volt-based auth pages
(resources/views/livewire/pages/auth/*), which also cover
forgot-password, reset-password, verify-email, and confirm-password.
- Deleted Breeze's own default welcome/dashboard views and its
separate app layout + nav component — kept our existing
components/layouts/app.blade.php as the one real app layout, used
Breeze's minimal layouts/guest.blade.php only for the auth pages.
- Rewrote routes/web.php merging our routes (home, sites, discover)
with routes/auth.php; replaced every hardcoded route('dashboard')
reference (5 Blade files plus, easy to miss, a plain PHP controller
VerifyEmailController) with sites.index.
- Logout: Breeze's Actions\Logout::__invoke() returns void, so wired
directly as a route it silently no-ops instead of redirecting;
fixed with a thin closure that calls it then redirects.
- Fixed Breeze's own generated tests (Auth/AuthenticationTest,
RegistrationTest, EmailVerificationTest, PasswordConfirmationTest)
where they assumed the deleted dashboard route/nav component.
Verified: full suite (28 tests, including Breeze's Registration/
Authentication/PasswordReset/PasswordConfirmation/PasswordUpdate/
EmailVerification/Profile tests plus our own GuestPreviewTest) passes
against the huginn_testing MySQL database. Also smoke-tested for real:
all key pages return 200, /profile redirects guests to /login, guest
preview capping still works, and a real password-reset email was
generated and logged with correct branding and a working link.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Volt\Volt;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuthenticationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_login_screen_can_be_rendered(): void
|
||||
{
|
||||
$response = $this->get('/login');
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSeeVolt('pages.auth.login');
|
||||
}
|
||||
|
||||
public function test_users_can_authenticate_using_the_login_screen(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$component = Volt::test('pages.auth.login')
|
||||
->set('form.email', $user->email)
|
||||
->set('form.password', 'password');
|
||||
|
||||
$component->call('login');
|
||||
|
||||
$component
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(route('sites.index', absolute: false));
|
||||
|
||||
$this->assertAuthenticated();
|
||||
}
|
||||
|
||||
public function test_users_can_not_authenticate_with_invalid_password(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$component = Volt::test('pages.auth.login')
|
||||
->set('form.email', $user->email)
|
||||
->set('form.password', 'wrong-password');
|
||||
|
||||
$component->call('login');
|
||||
|
||||
$component
|
||||
->assertHasErrors()
|
||||
->assertNoRedirect();
|
||||
|
||||
$this->assertGuest();
|
||||
}
|
||||
|
||||
public function test_navigation_shows_the_authenticated_user(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get(route('sites.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSeeText($user->name);
|
||||
}
|
||||
|
||||
public function test_users_can_logout(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('logout'))
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertGuest();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EmailVerificationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_email_verification_screen_can_be_rendered(): void
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get('/verify-email');
|
||||
|
||||
$response
|
||||
->assertSeeVolt('pages.auth.verify-email')
|
||||
->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_email_can_be_verified(): void
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
Event::fake();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1($user->email)]
|
||||
);
|
||||
|
||||
$response = $this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
Event::assertDispatched(Verified::class);
|
||||
$this->assertTrue($user->fresh()->hasVerifiedEmail());
|
||||
$response->assertRedirect(route('sites.index', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
public function test_email_is_not_verified_with_invalid_hash(): void
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1('wrong-email')]
|
||||
);
|
||||
|
||||
$this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
$this->assertFalse($user->fresh()->hasVerifiedEmail());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Volt\Volt;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordConfirmationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_confirm_password_screen_can_be_rendered(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get('/confirm-password');
|
||||
|
||||
$response
|
||||
->assertSeeVolt('pages.auth.confirm-password')
|
||||
->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_password_can_be_confirmed(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('pages.auth.confirm-password')
|
||||
->set('password', 'password');
|
||||
|
||||
$component->call('confirmPassword');
|
||||
|
||||
$component
|
||||
->assertRedirect('/sites')
|
||||
->assertHasNoErrors();
|
||||
}
|
||||
|
||||
public function test_password_is_not_confirmed_with_invalid_password(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('pages.auth.confirm-password')
|
||||
->set('password', 'wrong-password');
|
||||
|
||||
$component->call('confirmPassword');
|
||||
|
||||
$component
|
||||
->assertNoRedirect()
|
||||
->assertHasErrors('password');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Livewire\Volt\Volt;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordResetTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_reset_password_link_screen_can_be_rendered(): void
|
||||
{
|
||||
$response = $this->get('/forgot-password');
|
||||
|
||||
$response
|
||||
->assertSeeVolt('pages.auth.forgot-password')
|
||||
->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_reset_password_link_can_be_requested(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
Volt::test('pages.auth.forgot-password')
|
||||
->set('email', $user->email)
|
||||
->call('sendPasswordResetLink');
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class);
|
||||
}
|
||||
|
||||
public function test_reset_password_screen_can_be_rendered(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
Volt::test('pages.auth.forgot-password')
|
||||
->set('email', $user->email)
|
||||
->call('sendPasswordResetLink');
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
|
||||
$response = $this->get('/reset-password/'.$notification->token);
|
||||
|
||||
$response
|
||||
->assertSeeVolt('pages.auth.reset-password')
|
||||
->assertStatus(200);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_password_can_be_reset_with_valid_token(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
Volt::test('pages.auth.forgot-password')
|
||||
->set('email', $user->email)
|
||||
->call('sendPasswordResetLink');
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
|
||||
$component = Volt::test('pages.auth.reset-password', ['token' => $notification->token])
|
||||
->set('email', $user->email)
|
||||
->set('password', 'password')
|
||||
->set('password_confirmation', 'password');
|
||||
|
||||
$component->call('resetPassword');
|
||||
|
||||
$component
|
||||
->assertRedirect('/login')
|
||||
->assertHasNoErrors();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Volt\Volt;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordUpdateTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_password_can_be_updated(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('profile.update-password-form')
|
||||
->set('current_password', 'password')
|
||||
->set('password', 'new-password')
|
||||
->set('password_confirmation', 'new-password')
|
||||
->call('updatePassword');
|
||||
|
||||
$component
|
||||
->assertHasNoErrors()
|
||||
->assertNoRedirect();
|
||||
|
||||
$this->assertTrue(Hash::check('new-password', $user->refresh()->password));
|
||||
}
|
||||
|
||||
public function test_correct_password_must_be_provided_to_update_password(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('profile.update-password-form')
|
||||
->set('current_password', 'wrong-password')
|
||||
->set('password', 'new-password')
|
||||
->set('password_confirmation', 'new-password')
|
||||
->call('updatePassword');
|
||||
|
||||
$component
|
||||
->assertHasErrors(['current_password'])
|
||||
->assertNoRedirect();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Volt\Volt;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RegistrationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_registration_screen_can_be_rendered(): void
|
||||
{
|
||||
$response = $this->get('/register');
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSeeVolt('pages.auth.register');
|
||||
}
|
||||
|
||||
public function test_new_users_can_register(): void
|
||||
{
|
||||
$component = Volt::test('pages.auth.register')
|
||||
->set('name', 'Test User')
|
||||
->set('email', 'test@example.com')
|
||||
->set('password', 'password')
|
||||
->set('password_confirmation', 'password');
|
||||
|
||||
$component->call('register');
|
||||
|
||||
$component->assertRedirect(route('sites.index', absolute: false));
|
||||
|
||||
$this->assertAuthenticated();
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Livewire\Auth\Login;
|
||||
use App\Livewire\Auth\Register;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuthFlowTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_a_user_can_register(): void
|
||||
{
|
||||
Livewire::test(Register::class)
|
||||
->set('name', 'Test User')
|
||||
->set('email', 'testuser@example.com')
|
||||
->set('password', 'correct-horse-battery-staple')
|
||||
->set('password_confirmation', 'correct-horse-battery-staple')
|
||||
->call('register')
|
||||
->assertRedirect(route('sites.index'));
|
||||
|
||||
$this->assertAuthenticated();
|
||||
$this->assertDatabaseHas('users', ['email' => 'testuser@example.com']);
|
||||
}
|
||||
|
||||
public function test_a_user_can_log_in(): void
|
||||
{
|
||||
$user = User::factory()->create(['password' => 'correct-horse-battery-staple']);
|
||||
|
||||
Livewire::test(Login::class)
|
||||
->set('email', $user->email)
|
||||
->set('password', 'correct-horse-battery-staple')
|
||||
->call('login')
|
||||
->assertRedirect(route('sites.index'));
|
||||
|
||||
$this->assertAuthenticatedAs($user);
|
||||
}
|
||||
|
||||
public function test_login_fails_with_wrong_password(): void
|
||||
{
|
||||
$user = User::factory()->create(['password' => 'correct-horse-battery-staple']);
|
||||
|
||||
Livewire::test(Login::class)
|
||||
->set('email', $user->email)
|
||||
->set('password', 'wrong-password')
|
||||
->call('login')
|
||||
->assertHasErrors('email');
|
||||
|
||||
$this->assertGuest();
|
||||
}
|
||||
|
||||
public function test_a_user_can_log_out(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('logout'))
|
||||
->assertRedirect(route('home'));
|
||||
|
||||
$this->assertGuest();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Volt\Volt;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProfileTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_profile_page_is_displayed(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get('/profile');
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSeeVolt('profile.update-profile-information-form')
|
||||
->assertSeeVolt('profile.update-password-form')
|
||||
->assertSeeVolt('profile.delete-user-form');
|
||||
}
|
||||
|
||||
public function test_profile_information_can_be_updated(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('profile.update-profile-information-form')
|
||||
->set('name', 'Test User')
|
||||
->set('email', 'test@example.com')
|
||||
->call('updateProfileInformation');
|
||||
|
||||
$component
|
||||
->assertHasNoErrors()
|
||||
->assertNoRedirect();
|
||||
|
||||
$user->refresh();
|
||||
|
||||
$this->assertSame('Test User', $user->name);
|
||||
$this->assertSame('test@example.com', $user->email);
|
||||
$this->assertNull($user->email_verified_at);
|
||||
}
|
||||
|
||||
public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('profile.update-profile-information-form')
|
||||
->set('name', 'Test User')
|
||||
->set('email', $user->email)
|
||||
->call('updateProfileInformation');
|
||||
|
||||
$component
|
||||
->assertHasNoErrors()
|
||||
->assertNoRedirect();
|
||||
|
||||
$this->assertNotNull($user->refresh()->email_verified_at);
|
||||
}
|
||||
|
||||
public function test_user_can_delete_their_account(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('profile.delete-user-form')
|
||||
->set('password', 'password')
|
||||
->call('deleteUser');
|
||||
|
||||
$component
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertGuest();
|
||||
$this->assertNull($user->fresh());
|
||||
}
|
||||
|
||||
public function test_correct_password_must_be_provided_to_delete_account(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Volt::test('profile.delete-user-form')
|
||||
->set('password', 'wrong-password')
|
||||
->call('deleteUser');
|
||||
|
||||
$component
|
||||
->assertHasErrors('password')
|
||||
->assertNoRedirect();
|
||||
|
||||
$this->assertNotNull($user->fresh());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user