126 lines
4.2 KiB
PHP
126 lines
4.2 KiB
PHP
<?php
|
||
|
||
use App\Mail\VerifyEmailMail;
|
||
use App\Models\User;
|
||
use Illuminate\Support\Facades\Mail;
|
||
use Illuminate\Support\Facades\URL;
|
||
|
||
/**
|
||
* Nobody gets into the portal on an address they have not proved is theirs.
|
||
*
|
||
* Anybody can type a stranger's address into a registration form. Until it is
|
||
* confirmed, an account is not known to belong to the person holding it — and
|
||
* everything behind the portal, the servers and the backups and the invoices,
|
||
* is worth as much to the person who typed it as to the person who owns it.
|
||
*/
|
||
it('does not let an unconfirmed account into the portal', function () {
|
||
$user = User::factory()->unverified()->create();
|
||
|
||
$this->actingAs($user)
|
||
->get(route('dashboard'))
|
||
->assertRedirect(route('verification.notice'));
|
||
});
|
||
|
||
it('lets a confirmed account through', function () {
|
||
$this->actingAs(User::factory()->create())
|
||
->get(route('dashboard'))
|
||
->assertOk();
|
||
});
|
||
|
||
it('sends the confirmation in this product’s design, not the framework’s', function () {
|
||
// Laravel's own notification would arrive with its logo, its button and
|
||
// its footer — as the first thing anybody ever receives from CluPilot.
|
||
Mail::fake();
|
||
|
||
User::factory()->unverified()->create()->sendEmailVerificationNotification();
|
||
|
||
Mail::assertQueued(VerifyEmailMail::class);
|
||
});
|
||
|
||
it('confirms the address when the signed link is opened', function () {
|
||
$user = User::factory()->unverified()->create();
|
||
|
||
$this->actingAs($user)->get(verificationUrlFor($user))->assertRedirect();
|
||
|
||
expect($user->refresh()->hasVerifiedEmail())->toBeTrue();
|
||
});
|
||
|
||
it('refuses a link whose signature has been tampered with', function () {
|
||
$user = User::factory()->unverified()->create();
|
||
|
||
$tampered = verificationUrlFor($user).'x';
|
||
|
||
$this->actingAs($user)->get($tampered)->assertForbidden();
|
||
|
||
expect($user->refresh()->hasVerifiedEmail())->toBeFalse();
|
||
});
|
||
|
||
it('refuses a link that has expired', function () {
|
||
$user = User::factory()->unverified()->create();
|
||
$url = verificationUrlFor($user, 60);
|
||
|
||
$this->travel(61)->minutes();
|
||
|
||
$this->actingAs($user)->get($url)->assertForbidden();
|
||
|
||
expect($user->refresh()->hasVerifiedEmail())->toBeFalse();
|
||
});
|
||
|
||
it('stops working once the address it was issued for has changed', function () {
|
||
// The address is part of what is signed. Without that, a link mailed to an
|
||
// address somebody has since corrected would still confirm the account —
|
||
// confirming an address nobody can read.
|
||
$user = User::factory()->unverified()->create(['email' => 'typo@example.com']);
|
||
$url = verificationUrlFor($user);
|
||
|
||
$user->update(['email' => 'correct@example.com']);
|
||
|
||
$this->actingAs($user)->get($url)->assertForbidden();
|
||
|
||
expect($user->refresh()->hasVerifiedEmail())->toBeFalse();
|
||
});
|
||
|
||
it('will not let one account confirm another', function () {
|
||
$mine = User::factory()->unverified()->create();
|
||
$theirs = User::factory()->unverified()->create();
|
||
|
||
$this->actingAs($mine)->get(verificationUrlFor($theirs))->assertForbidden();
|
||
|
||
expect($theirs->refresh()->hasVerifiedEmail())->toBeFalse();
|
||
});
|
||
|
||
it('throttles the resend button', function () {
|
||
// Without it this is a button that makes the server send mail to an
|
||
// arbitrary address as fast as it can be clicked.
|
||
Mail::fake();
|
||
|
||
$user = User::factory()->unverified()->create();
|
||
|
||
$component = Livewire\Livewire::actingAs($user)->test(App\Livewire\Auth\VerifyEmail::class);
|
||
|
||
foreach (range(1, 6) as $ignored) {
|
||
$component->call('resend');
|
||
}
|
||
|
||
Mail::assertQueuedCount(6);
|
||
|
||
$component->call('resend');
|
||
|
||
Mail::assertQueuedCount(6);
|
||
});
|
||
|
||
it('sends nobody back to the waiting room once they are through', function () {
|
||
$this->actingAs(User::factory()->create())
|
||
->get(route('verification.notice'))
|
||
->assertRedirect(route('dashboard'));
|
||
});
|
||
|
||
/** The signed link the mail carries, built the same way VerifyEmailMail builds it. */
|
||
function verificationUrlFor(User $user, int $minutes = 60): string
|
||
{
|
||
return URL::temporarySignedRoute('verification.verify', now()->addMinutes($minutes), [
|
||
'id' => $user->getKey(),
|
||
'hash' => sha1($user->getEmailForVerification()),
|
||
]);
|
||
}
|