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