get(route('login')) ->assertOk() ->assertSee(__('auth.forgot_link')) ->assertSee(route('password.request'), false); }); it('sends a link in this product’s own design', function () { Mail::fake(); $user = User::factory()->create(['email' => 'kunde@example.test']); Livewire::test(ForgotPassword::class) ->set('email', $user->email) ->call('send') ->assertHasNoErrors(); // Not the framework's default MailMessage: somebody who has just been // locked out is exactly the person a phishing mail is aimed at, and a // message that looks nothing like the rest of our post is one they cannot // check against the others. Mail::assertQueued(ResetPasswordMail::class, fn ($mail) => $mail->hasTo($user->email)); }); it('answers the same whether the address is known or not', function () { // Otherwise the form is a way of finding out who is a customer, which is // the first thing anybody targeting a business does. Mail::fake(); $known = Livewire::test(ForgotPassword::class)->set('email', User::factory()->create()->email)->call('send'); $unknown = Livewire::test(ForgotPassword::class)->set('email', 'niemand@example.test')->call('send'); $known->assertSet('sent', true)->assertHasNoErrors(); $unknown->assertSet('sent', true)->assertHasNoErrors(); Mail::assertQueuedCount(1); }); it('sets the new password and kills every other session', function () { $user = User::factory()->create(['password' => Hash::make('das-alte-passwort')]); $before = $user->remember_token; $token = Password::broker()->createToken($user); Livewire::test(ResetPassword::class, ['token' => $token]) ->set('email', $user->email) ->set('password', 'ein-langes-neues-passwort') ->set('password_confirmation', 'ein-langes-neues-passwort') ->call('save') ->assertHasNoErrors() ->assertSet('done', true); $user->refresh(); expect(Hash::check('ein-langes-neues-passwort', $user->password))->toBeTrue() // Whoever knew the old password may still be signed in somewhere. A // reset that leaves them there has fixed nothing. ->and($user->remember_token)->not->toBe($before); }); it('does not sign the visitor in', function () { // The link arrived by email. A mailbox somebody else can read would // otherwise be a session somebody else gets. $user = User::factory()->create(); $token = Password::broker()->createToken($user); Livewire::test(ResetPassword::class, ['token' => $token]) ->set('email', $user->email) ->set('password', 'ein-langes-neues-passwort') ->set('password_confirmation', 'ein-langes-neues-passwort') ->call('save'); expect(auth()->check())->toBeFalse(); }); it('refuses a token that is not this user’s', function () { $mine = User::factory()->create(); $theirs = User::factory()->create(); Livewire::test(ResetPassword::class, ['token' => Password::broker()->createToken($theirs)]) ->set('email', $mine->email) ->set('password', 'ein-langes-neues-passwort') ->set('password_confirmation', 'ein-langes-neues-passwort') ->call('save') ->assertHasErrors('email') ->assertSet('done', false); }); it('spends the token, so a forwarded link cannot be used twice', function () { $user = User::factory()->create(); $token = Password::broker()->createToken($user); $reset = fn () => Livewire::test(ResetPassword::class, ['token' => $token]) ->set('email', $user->email) ->set('password', 'ein-langes-neues-passwort') ->set('password_confirmation', 'ein-langes-neues-passwort') ->call('save'); $reset()->assertSet('done', true); $reset()->assertSet('done', false)->assertHasErrors('email'); }); it('insists on a password long enough to be worth resetting to', function () { $user = User::factory()->create(); Livewire::test(ResetPassword::class, ['token' => Password::broker()->createToken($user)]) ->set('email', $user->email) ->set('password', 'kurz') ->set('password_confirmation', 'kurz') ->call('save') ->assertHasErrors('password'); }); it('renders the reset mail, which it could not do at all', function () { // The view was missing its closing . The component's // contents were never terminated, the layout's own @if ran off the end of // the file, and the mail raised a Blade syntax error instead of rendering — // so the link never reached anybody who asked for one. Every test until now // asserted that the mail was QUEUED, which it faithfully was. $user = App\Models\User::factory()->create(['name' => 'Bea Berger']); $html = (new App\Mail\ResetPasswordMail($user, 'https://example.test/reset/abc', 60)) ->render(); expect($html)->toContain('https://example.test/reset/abc') ->toContain(__('reset_password.action')); }); it('closes every mail layout it opens', function () { // One unterminated component is a mail that cannot be sent, and the failure // only shows at render time — long after the test that queued it went green. foreach (glob(resource_path('views/mail/*.blade.php')) as $file) { $body = file_get_contents($file); expect(substr_count($body, 'toBe(substr_count($body, ''), basename($file)); } });