179 lines
9.3 KiB
PHP
179 lines
9.3 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\User;
|
|
use App\Notifications\ResetAccountPassword;
|
|
use App\Notifications\VerifyAccountEmail;
|
|
use Illuminate\Auth\Events\Verified;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Event;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Notification;
|
|
use Illuminate\Support\Facades\Password;
|
|
use Illuminate\Support\Facades\URL;
|
|
use Inertia\Testing\AssertableInertia as Assert;
|
|
use Tests\TestCase;
|
|
|
|
class AccountAccessFeatureTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Notification::fake();
|
|
}
|
|
|
|
public function test_recovery_sends_a_single_email_and_hides_whether_account_exists(): void
|
|
{
|
|
$user = User::factory()->create(['email' => 'player@example.com']);
|
|
|
|
$this->from('/forgot-password')->post('/forgot-password', ['email' => ' PLAYER@example.com '])
|
|
->assertRedirect('/forgot-password')->assertSessionHas('status');
|
|
$status = session('status');
|
|
$this->from('/forgot-password')->post('/forgot-password', ['email' => $user->email])
|
|
->assertSessionHas('status', $status);
|
|
$this->from('/forgot-password')->post('/forgot-password', ['email' => 'unknown@example.com'])
|
|
->assertSessionHas('status', $status);
|
|
|
|
Notification::assertSentToTimes($user, ResetAccountPassword::class, 1);
|
|
$notification = Notification::sent($user, ResetAccountPassword::class)->first();
|
|
$this->assertDatabaseMissing('password_reset_tokens', ['token' => $notification->token]);
|
|
$this->assertDatabaseHas('password_reset_tokens', ['email' => $user->email]);
|
|
}
|
|
|
|
public function test_reset_token_can_be_used_only_once_and_never_confirms_email(): void
|
|
{
|
|
$user = User::factory()->unverified()->create(['email_verification_required' => true]);
|
|
$token = Password::broker('web_users')->createToken($user);
|
|
$payload = ['email' => $user->email, 'token' => $token, 'password' => 'NewPass123', 'password_confirmation' => 'NewPass123'];
|
|
|
|
$this->post('/reset-password', $payload)->assertRedirect(route('login'))->assertSessionHas('status');
|
|
$this->assertTrue(Hash::check('NewPass123', $user->fresh()->password));
|
|
$this->assertTrue($user->fresh()->needsEmailVerification());
|
|
$this->assertDatabaseMissing('password_reset_tokens', ['email' => $user->email]);
|
|
$this->post('/reset-password', [...$payload, 'password' => 'OtherPass123', 'password_confirmation' => 'OtherPass123'])
|
|
->assertSessionHasErrors('token');
|
|
$this->assertTrue(Hash::check('NewPass123', $user->fresh()->password));
|
|
}
|
|
|
|
public function test_expired_reset_token_and_wrong_email_do_not_change_password(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$other = User::factory()->create();
|
|
$original = $user->password;
|
|
$token = Password::broker('web_users')->createToken($user);
|
|
$payload = ['email' => $other->email, 'token' => $token, 'password' => 'NewPass123', 'password_confirmation' => 'NewPass123'];
|
|
$this->post('/reset-password', $payload)->assertSessionHasErrors('token');
|
|
$this->travel(61)->minutes();
|
|
$this->post('/reset-password', [...$payload, 'email' => $user->email])->assertSessionHasErrors('token');
|
|
$this->assertSame($original, $user->fresh()->password);
|
|
}
|
|
|
|
public function test_reset_enforces_password_rules_and_confirmation(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$token = Password::broker('web_users')->createToken($user);
|
|
foreach (['short', 'onlyletters', '123456789', 'LongPassword123456789', 'Valid123'] as $password) {
|
|
$this->post('/reset-password', [
|
|
'email' => $user->email, 'token' => $token, 'password' => $password, 'password_confirmation' => 'Mismatch123',
|
|
])->assertSessionHasErrors('password');
|
|
}
|
|
$this->assertTrue(Password::broker('web_users')->tokenExists($user, $token));
|
|
}
|
|
|
|
public function test_signed_verification_works_without_login_and_is_idempotent(): void
|
|
{
|
|
Event::fake([Verified::class]);
|
|
$user = User::factory()->unverified()->create(['email_verification_required' => true]);
|
|
$url = $this->verificationUrl($user);
|
|
$this->get($url)->assertOk()->assertInertia(fn (Assert $page) => $page->component('Auth/AccountAccess', false)->where('mode', 'verified'));
|
|
$this->assertFalse($user->fresh()->needsEmailVerification());
|
|
$this->get($url)->assertOk()->assertInertia(fn (Assert $page) => $page->where('mode', 'verified'));
|
|
Event::assertDispatchedTimes(Verified::class, 1);
|
|
$this->assertGuest();
|
|
}
|
|
|
|
public function test_expired_modified_or_wrong_email_verification_links_are_rejected(): void
|
|
{
|
|
$user = User::factory()->unverified()->create(['email_verification_required' => true]);
|
|
$url = $this->verificationUrl($user);
|
|
$this->get($url.'&tampered=1')->assertInertia(fn (Assert $page) => $page->where('mode', 'invalid'));
|
|
$this->get(URL::temporarySignedRoute('verification.verify', now()->addHour(), ['id' => $user->id, 'hash' => sha1('other@example.com')]))
|
|
->assertInertia(fn (Assert $page) => $page->where('mode', 'invalid'));
|
|
$this->travel(61)->minutes();
|
|
$this->get($url)->assertInertia(fn (Assert $page) => $page->where('mode', 'invalid'));
|
|
$this->assertTrue($user->fresh()->needsEmailVerification());
|
|
}
|
|
|
|
public function test_resend_is_throttled_per_email_and_does_not_disclose_accounts(): void
|
|
{
|
|
$user = User::factory()->unverified()->create(['email' => 'player@example.com', 'email_verification_required' => true]);
|
|
$verified = User::factory()->create();
|
|
foreach ([' PLAYER@example.com ', $user->email, $verified->email, 'unknown@example.com'] as $email) {
|
|
$this->from('/email/verify')->post('/email/verification-notification', ['email' => $email])
|
|
->assertRedirect('/email/verify')->assertSessionHas('status');
|
|
}
|
|
Notification::assertSentToTimes($user, VerifyAccountEmail::class, 1);
|
|
Notification::assertNotSentTo($verified, VerifyAccountEmail::class);
|
|
$this->travel(61)->seconds();
|
|
$this->post('/email/verification-notification', ['email' => $user->email]);
|
|
Notification::assertSentToTimes($user, VerifyAccountEmail::class, 2);
|
|
}
|
|
|
|
public function test_emails_have_required_sender_and_render_the_branded_template(): void
|
|
{
|
|
$user = User::factory()->create(['name' => 'PLAYER']);
|
|
foreach ([new VerifyAccountEmail, new ResetAccountPassword('test-token')] as $notification) {
|
|
$mail = $notification->toMail($user);
|
|
$this->assertSame(['no-reply@moon-well.online', 'MoonWell'], $mail->from);
|
|
$html = view($mail->view, $mail->viewData)->render();
|
|
$this->assertStringContainsString('MOONWELL', $html);
|
|
$this->assertStringContainsString($mail->viewData['url'], html_entity_decode($html));
|
|
$this->assertStringContainsString('PLAYER', $html);
|
|
}
|
|
}
|
|
|
|
public function test_auth_pages_render_and_email_forms_validate_input(): void
|
|
{
|
|
$this->get('/forgot-password')->assertOk()->assertInertia(fn (Assert $page) => $page->where('mode', 'forgot'));
|
|
$this->get('/email/verify')->assertOk()->assertInertia(fn (Assert $page) => $page->where('mode', 'verification'));
|
|
$this->get('/reset-password/example?email=player@example.com')->assertOk()
|
|
->assertInertia(fn (Assert $page) => $page->where('mode', 'reset')->where('email', 'player@example.com')->where('token', 'example'));
|
|
$this->post('/forgot-password', ['email' => 'invalid'])->assertSessionHasErrors('email');
|
|
$this->post('/email/verification-notification', [])->assertSessionHasErrors('email');
|
|
}
|
|
|
|
public function test_token_pages_disable_caching_referrers_and_analytics(): void
|
|
{
|
|
$this->get('/reset-password/example?email=player@example.com')
|
|
->assertOk()->assertHeader('Referrer-Policy', 'no-referrer')
|
|
->assertHeader('X-Robots-Tag', 'noindex')
|
|
->assertDontSee('mc.yandex.ru')
|
|
->assertHeader('Cache-Control', 'no-store, private');
|
|
}
|
|
|
|
public function test_reset_uses_email_from_its_link_despite_previous_form_input(): void
|
|
{
|
|
$this->withSession(['verification_email' => 'previous@example.com', '_old_input' => ['email' => 'other@example.com']])
|
|
->get('/reset-password/example?email=player@example.com')
|
|
->assertInertia(fn (Assert $page) => $page->where('email', 'player@example.com'));
|
|
}
|
|
|
|
public function test_notification_links_always_use_configured_public_origin(): void
|
|
{
|
|
config(['app.url' => 'https://moon-well.online']);
|
|
app('request')->headers->set('host', 'untrusted.example');
|
|
$user = User::factory()->create();
|
|
foreach ([new VerifyAccountEmail, new ResetAccountPassword('test-token')] as $notification) {
|
|
$this->assertStringStartsWith('https://moon-well.online/', $notification->toMail($user)->viewData['url']);
|
|
}
|
|
}
|
|
|
|
private function verificationUrl(User $user): string
|
|
{
|
|
return URL::temporarySignedRoute('verification.verify', now()->addHour(), ['id' => $user->id, 'hash' => sha1($user->email)]);
|
|
}
|
|
}
|