resetRepository = $this->createMock(PasswordResetRepositoryInterface::class); $this->userRepository = $this->createMock(UserRepositoryInterface::class); $this->mailService = $this->createMock(MailServiceInterface::class); $this->db = $this->createMock(PDO::class); $this->service = new PasswordResetService( $this->resetRepository, $this->userRepository, $this->mailService, $this->db, ); } public function testRequestResetUnknownEmailReturnsSilently(): void { $this->userRepository->method('findByEmail')->willReturn(null); $this->mailService->expects($this->never())->method('send'); $this->resetRepository->expects($this->never())->method('create'); $this->service->requestReset('inconnu@example.com', 'https://blog.exemple.com'); } public function testRequestResetInvalidatesPreviousTokens(): void { $user = $this->makeUser(); $this->userRepository->method('findByEmail')->willReturn($user); $this->resetRepository->expects($this->once()) ->method('invalidateByUserId') ->with($user->getId()); $this->resetRepository->expects($this->once()) ->method('create'); $this->mailService->expects($this->once()) ->method('send'); $this->service->requestReset('alice@example.com', 'https://blog.exemple.com'); } public function testRequestResetCreatesTokenInDatabase(): void { $user = $this->makeUser(); $this->userRepository->method('findByEmail')->willReturn($user); $this->resetRepository->expects($this->once()) ->method('invalidateByUserId'); $this->resetRepository->expects($this->once()) ->method('create') ->with($user->getId(), $this->callback('is_string'), $this->callback('is_string')); $this->mailService->expects($this->once()) ->method('send'); $this->service->requestReset('alice@example.com', 'https://blog.exemple.com'); } public function testRequestResetSendsEmailWithCorrectAddress(): void { $user = $this->makeUser(); $this->userRepository->method('findByEmail')->willReturn($user); $this->resetRepository->expects($this->once()) ->method('invalidateByUserId'); $this->resetRepository->expects($this->once()) ->method('create'); $this->mailService->expects($this->once()) ->method('send') ->with( 'alice@example.com', $this->callback('is_string'), 'emails/password-reset.twig', $this->callback('is_array'), ); $this->service->requestReset('alice@example.com', 'https://blog.exemple.com'); } public function testRequestResetUrlContainsToken(): void { $user = $this->makeUser(); $this->userRepository->method('findByEmail')->willReturn($user); $this->resetRepository->expects($this->once()) ->method('invalidateByUserId'); $this->resetRepository->expects($this->once()) ->method('create'); $this->mailService->expects($this->once()) ->method('send') ->with( $this->anything(), $this->anything(), $this->anything(), $this->callback(function (array $context): bool { return isset($context['resetUrl']) && str_contains($context['resetUrl'], '/password/reset?token='); }), ); $this->service->requestReset('alice@example.com', 'https://blog.exemple.com'); } public function testValidateTokenMissingToken(): void { $this->resetRepository->expects($this->once()) ->method('findActiveByHash') ->willReturn(null); $result = $this->service->validateToken('tokeninexistant'); $this->assertNull($result); } public function testValidateTokenExpiredToken(): void { $tokenRaw = 'montokenbrut'; $tokenHash = hash('sha256', $tokenRaw); $this->resetRepository->expects($this->once()) ->method('findActiveByHash') ->with($tokenHash) ->willReturn([ 'user_id' => 1, 'token_hash' => $tokenHash, 'expires_at' => date('Y-m-d H:i:s', time() - 3600), 'used_at' => null, ]); $result = $this->service->validateToken($tokenRaw); $this->assertNull($result); } public function testValidateTokenValidToken(): void { $user = $this->makeUser(); $tokenRaw = 'montokenbrut'; $tokenHash = hash('sha256', $tokenRaw); $this->resetRepository->expects($this->once()) ->method('findActiveByHash') ->with($tokenHash) ->willReturn([ 'user_id' => $user->getId(), 'token_hash' => $tokenHash, 'expires_at' => date('Y-m-d H:i:s', time() + 3600), 'used_at' => null, ]); $this->userRepository->expects($this->once()) ->method('findById') ->with($user->getId()) ->willReturn($user); $result = $this->service->validateToken($tokenRaw); $this->assertSame($user, $result); } public function testValidateTokenDeletedUserReturnsNull(): void { $tokenRaw = 'token-valide-mais-user-supprime'; $tokenHash = hash('sha256', $tokenRaw); $this->resetRepository->expects($this->once()) ->method('findActiveByHash') ->with($tokenHash) ->willReturn([ 'user_id' => 999, 'expires_at' => date('Y-m-d H:i:s', time() + 3600), 'used_at' => null, ]); $this->userRepository->expects($this->once()) ->method('findById') ->with(999) ->willReturn(null); $result = $this->service->validateToken($tokenRaw); $this->assertNull($result); } public function testResetPasswordInvalidToken(): void { $this->db->method('beginTransaction')->willReturn(true); $this->db->method('inTransaction')->willReturn(true); $this->db->expects($this->once())->method('rollBack'); $this->resetRepository->expects($this->once())->method('consumeActiveToken')->willReturn(null); $this->expectException(InvalidResetTokenException::class); $this->expectExceptionMessageMatches('/invalide ou a expiré/'); $this->service->resetPassword('tokeninvalide', 'nouveaumdp1'); } public function testResetPasswordTooShortPasswordThrowsWeakPasswordException(): void { $this->expectException(WeakPasswordException::class); $this->service->resetPassword('montokenbrut', '1234567'); } public function testResetPasswordUpdatesPasswordAndConsumesToken(): void { $user = $this->makeUser(); $tokenRaw = 'montokenbrut'; $tokenHash = hash('sha256', $tokenRaw); $this->db->method('beginTransaction')->willReturn(true); $this->db->method('inTransaction')->willReturn(true); $this->db->expects($this->once())->method('commit'); $this->resetRepository->expects($this->once()) ->method('consumeActiveToken') ->with($tokenHash, $this->callback('is_string')) ->willReturn([ 'user_id' => $user->getId(), 'token_hash' => $tokenHash, 'expires_at' => date('Y-m-d H:i:s', time() + 3600), 'used_at' => null, ]); $this->userRepository->expects($this->once()) ->method('findById') ->with($user->getId()) ->willReturn($user); $this->userRepository->expects($this->once()) ->method('updatePassword') ->with($user->getId(), $this->callback('is_string')); $this->service->resetPassword($tokenRaw, 'nouveaumdp1'); } private function makeUser(): User { return new User( 1, 'alice', 'alice@example.com', password_hash('motdepasse1', PASSWORD_BCRYPT), ); } }