191 lines
6.7 KiB
PHP
191 lines
6.7 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Auth;
|
|
|
|
use App\Models\User;
|
|
use App\Notifications\ResetPasswordNotification;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Notification;
|
|
use Laravel\Passport\Client;
|
|
use Laravel\Passport\Token;
|
|
use Tests\TestCase;
|
|
|
|
// NOTA: Notification::fake() nunca ejecuta toMail() (solo registra que se
|
|
// "envió"), así que un error real ahí adentro (como pasó acá: llamar a un
|
|
// método que no existe en esta versión de MailMessage) no lo detecta ningún
|
|
// test de arriba — solo se vio al procesar la cola de verdad en un browser
|
|
// check manual. test_the_mail_actually_renders_without_throwing cubre
|
|
// justamente ese hueco.
|
|
|
|
class PasswordResetTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_requesting_a_reset_link_for_an_existing_email_sends_the_notification(): void
|
|
{
|
|
Notification::fake();
|
|
|
|
$user = User::factory()->approved()->create();
|
|
|
|
$response = $this->postJson('/web-api/forgot-password', ['email' => $user->email]);
|
|
|
|
$response->assertOk();
|
|
Notification::assertSentTo($user, ResetPasswordNotification::class);
|
|
}
|
|
|
|
public function test_requesting_a_reset_link_for_an_unknown_email_returns_the_same_generic_response(): void
|
|
{
|
|
Notification::fake();
|
|
|
|
$known = $this->postJson('/web-api/forgot-password', ['email' => 'known@example.com']);
|
|
$unknown = $this->postJson('/web-api/forgot-password', ['email' => 'nobody@example.com']);
|
|
|
|
$known->assertOk();
|
|
$unknown->assertOk();
|
|
$this->assertSame($known->json('message'), $unknown->json('message'));
|
|
Notification::assertNothingSent();
|
|
}
|
|
|
|
public function test_forgot_password_validates_email_format(): void
|
|
{
|
|
$this->postJson('/web-api/forgot-password', ['email' => 'not-an-email'])
|
|
->assertUnprocessable()
|
|
->assertJsonValidationErrors('email');
|
|
}
|
|
|
|
public function test_can_reset_password_with_a_valid_token(): void
|
|
{
|
|
Notification::fake();
|
|
|
|
$user = User::factory()->approved()->create(['password' => Hash::make('old-password')]);
|
|
|
|
$this->postJson('/web-api/forgot-password', ['email' => $user->email])->assertOk();
|
|
|
|
$token = null;
|
|
Notification::assertSentTo($user, ResetPasswordNotification::class, function ($notification) use (&$token) {
|
|
$token = $notification->token;
|
|
|
|
return true;
|
|
});
|
|
$this->assertNotNull($token);
|
|
|
|
$response = $this->postJson('/web-api/reset-password', [
|
|
'token' => $token,
|
|
'email' => $user->email,
|
|
'password' => 'brand-new-password',
|
|
'password_confirmation' => 'brand-new-password',
|
|
]);
|
|
|
|
$response->assertNoContent();
|
|
|
|
$this->assertTrue(Hash::check('brand-new-password', $user->fresh()->password));
|
|
$this->assertDatabaseHas('audit_logs', [
|
|
'action' => 'user.password_reset',
|
|
'subject_id' => (string) $user->id,
|
|
]);
|
|
}
|
|
|
|
public function test_resetting_the_password_revokes_all_active_sessions(): void
|
|
{
|
|
Notification::fake();
|
|
|
|
$user = User::factory()->approved()->create(['password' => Hash::make('old-password')]);
|
|
$client = Client::create([
|
|
'owner_id' => $user->id,
|
|
'owner_type' => User::class,
|
|
'name' => 'Some App',
|
|
'secret' => null,
|
|
'provider' => null,
|
|
'redirect_uris' => [],
|
|
'grant_types' => ['authorization_code', 'refresh_token'],
|
|
'revoked' => false,
|
|
]);
|
|
$token = Token::create([
|
|
'id' => \Illuminate\Support\Str::random(80),
|
|
'user_id' => $user->id,
|
|
'client_id' => $client->id,
|
|
'name' => null,
|
|
'scopes' => ['users:read'],
|
|
'revoked' => false,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'expires_at' => now()->addDays(15),
|
|
]);
|
|
|
|
$this->postJson('/web-api/forgot-password', ['email' => $user->email]);
|
|
$resetToken = null;
|
|
Notification::assertSentTo($user, ResetPasswordNotification::class, function ($notification) use (&$resetToken) {
|
|
$resetToken = $notification->token;
|
|
|
|
return true;
|
|
});
|
|
|
|
$this->postJson('/web-api/reset-password', [
|
|
'token' => $resetToken,
|
|
'email' => $user->email,
|
|
'password' => 'brand-new-password',
|
|
'password_confirmation' => 'brand-new-password',
|
|
])->assertNoContent();
|
|
|
|
$this->assertTrue($token->fresh()->revoked);
|
|
}
|
|
|
|
public function test_reset_fails_with_an_invalid_token(): void
|
|
{
|
|
$user = User::factory()->approved()->create(['password' => Hash::make('old-password')]);
|
|
|
|
$response = $this->postJson('/web-api/reset-password', [
|
|
'token' => 'not-a-real-token',
|
|
'email' => $user->email,
|
|
'password' => 'brand-new-password',
|
|
'password_confirmation' => 'brand-new-password',
|
|
]);
|
|
|
|
$response->assertUnprocessable();
|
|
$this->assertTrue(Hash::check('old-password', $user->fresh()->password));
|
|
}
|
|
|
|
public function test_reset_validates_password_confirmation_and_minimum_length(): void
|
|
{
|
|
$user = User::factory()->approved()->create();
|
|
|
|
$this->postJson('/web-api/reset-password', [
|
|
'token' => 'irrelevant',
|
|
'email' => $user->email,
|
|
'password' => 'short',
|
|
'password_confirmation' => 'short',
|
|
])->assertUnprocessable()->assertJsonValidationErrors('password');
|
|
|
|
$this->postJson('/web-api/reset-password', [
|
|
'token' => 'irrelevant',
|
|
'email' => $user->email,
|
|
'password' => 'a-long-enough-password',
|
|
'password_confirmation' => 'does-not-match',
|
|
])->assertUnprocessable()->assertJsonValidationErrors('password');
|
|
}
|
|
|
|
public function test_the_mail_actually_renders_without_throwing(): void
|
|
{
|
|
$user = User::factory()->approved()->create();
|
|
|
|
$mail = (new ResetPasswordNotification('a-token', 'es'))->toMail($user);
|
|
$rendered = $mail->render();
|
|
|
|
$this->assertStringContainsString('reset-password?token=a-token', $rendered);
|
|
$this->assertStringContainsString(urlencode($user->email), $rendered);
|
|
$this->assertStringContainsString('Restablecer contraseña', $rendered);
|
|
}
|
|
|
|
public function test_forgot_password_is_throttled(): void
|
|
{
|
|
Notification::fake();
|
|
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$this->postJson('/web-api/forgot-password', ['email' => 'someone@example.com'])->assertOk();
|
|
}
|
|
|
|
$this->postJson('/web-api/forgot-password', ['email' => 'someone@example.com'])->assertStatus(429);
|
|
}
|
|
}
|