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)]); } }