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:
m105
2026-07-25 15:36:43 +03:00
parent f5c9129774
commit 6435bccb20
54 changed files with 2878 additions and 321 deletions
+77
View File
@@ -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');
}
}
+84
View File
@@ -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;
});
}
}
+50
View File
@@ -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();
}
}
+36
View File
@@ -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();
}
}