158 lines
6.1 KiB
PHP
158 lines
6.1 KiB
PHP
<?php
|
||
|
||
use App\Livewire\Auth\ForgotPassword;
|
||
use App\Livewire\Auth\ResetPassword;
|
||
use App\Mail\ResetPasswordMail;
|
||
use App\Models\User;
|
||
use Illuminate\Support\Facades\Hash;
|
||
use Illuminate\Support\Facades\Mail;
|
||
use Illuminate\Support\Facades\Password;
|
||
use Livewire\Livewire;
|
||
|
||
/**
|
||
* Getting back in after forgetting the password.
|
||
*
|
||
* There was no way to. Fortify's resetPasswords feature was switched off, so
|
||
* there was no link on the sign-in form, no page and no route — a customer who
|
||
* forgot their password was locked out of their own cloud until somebody
|
||
* opened a shell. On a product whose selling point is that you can ring
|
||
* somebody, that is a support call a week and an embarrassing one.
|
||
*/
|
||
it('offers the way back on the sign-in form', function () {
|
||
$this->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 </x-mail.layout>. 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, '<x-mail.layout'))
|
||
->toBe(substr_count($body, '</x-mail.layout>'), basename($file));
|
||
}
|
||
});
|