first commit

This commit is contained in:
julien
2026-03-16 01:47:07 +01:00
commit 8f7e61bda0
185 changed files with 27731 additions and 0 deletions

View File

@@ -0,0 +1,316 @@
<?php
declare(strict_types=1);
namespace Tests\Auth;
use App\Auth\Exception\InvalidResetTokenException;
use App\Auth\PasswordResetRepositoryInterface;
use App\Auth\PasswordResetService;
use App\Shared\Mail\MailServiceInterface;
use App\User\Exception\WeakPasswordException;
use App\User\User;
use App\User\UserRepositoryInterface;
use PDO;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
/**
* Tests unitaires pour PasswordResetService.
*
* Vérifie la génération de token, la validation et la réinitialisation
* du mot de passe. Les dépendances sont mockées via leurs interfaces.
*/
final class PasswordResetServiceTest extends TestCase
{
/** @var PasswordResetRepositoryInterface&MockObject */
private PasswordResetRepositoryInterface $resetRepository;
/** @var UserRepositoryInterface&MockObject */
private UserRepositoryInterface $userRepository;
/** @var MailServiceInterface&MockObject */
private MailServiceInterface $mailService;
private PasswordResetService $service;
/** @var PDO&MockObject */
private PDO $db;
protected function setUp(): void
{
$this->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,
);
}
// ── requestReset ───────────────────────────────────────────────
/**
* requestReset() avec un email inconnu ne doit ni envoyer d'email
* ni lever d'exception (protection contre l'énumération d'emails).
*/
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');
}
/**
* requestReset() doit invalider les tokens précédents avant d'en créer un nouveau.
*/
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->method('create');
$this->mailService->method('send');
$this->service->requestReset('alice@example.com', 'https://blog.exemple.com');
}
/**
* requestReset() doit persister un nouveau token en base.
*/
public function testRequestResetCreatesTokenInDatabase(): void
{
$user = $this->makeUser();
$this->userRepository->method('findByEmail')->willReturn($user);
$this->resetRepository->method('invalidateByUserId');
$this->resetRepository->expects($this->once())
->method('create')
->with($user->getId(), $this->isType('string'), $this->isType('string'));
$this->mailService->method('send');
$this->service->requestReset('alice@example.com', 'https://blog.exemple.com');
}
/**
* requestReset() doit envoyer un email avec le bon destinataire et template.
*/
public function testRequestResetSendsEmailWithCorrectAddress(): void
{
$user = $this->makeUser();
$this->userRepository->method('findByEmail')->willReturn($user);
$this->resetRepository->method('invalidateByUserId');
$this->resetRepository->method('create');
$this->mailService->expects($this->once())
->method('send')
->with(
'alice@example.com',
$this->isType('string'),
'emails/password-reset.twig',
$this->isType('array'),
);
$this->service->requestReset('alice@example.com', 'https://blog.exemple.com');
}
/**
* L'URL de réinitialisation dans le contexte de l'email doit contenir le token brut.
*/
public function testRequestResetUrlContainsToken(): void
{
$user = $this->makeUser();
$this->userRepository->method('findByEmail')->willReturn($user);
$this->resetRepository->method('invalidateByUserId');
$this->resetRepository->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');
}
// ── validateToken ──────────────────────────────────────────────
/**
* validateToken() avec un token inexistant doit retourner null.
*/
public function testValidateTokenMissingToken(): void
{
$this->resetRepository->method('findActiveByHash')->willReturn(null);
$result = $this->service->validateToken('tokeninexistant');
$this->assertNull($result);
}
/**
* validateToken() avec un token expiré doit retourner null.
*/
public function testValidateTokenExpiredToken(): void
{
$tokenRaw = 'montokenbrut';
$tokenHash = hash('sha256', $tokenRaw);
$this->resetRepository->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);
}
/**
* validateToken() avec un token valide doit retourner l'utilisateur associé.
*/
public function testValidateTokenValidToken(): void
{
$user = $this->makeUser();
$tokenRaw = 'montokenbrut';
$tokenHash = hash('sha256', $tokenRaw);
$this->resetRepository->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->method('findById')->with($user->getId())->willReturn($user);
$result = $this->service->validateToken($tokenRaw);
$this->assertSame($user, $result);
}
// ── resetPassword ──────────────────────────────────────────────
/**
* resetPassword() avec un token invalide doit lever une InvalidArgumentException.
*/
/**
* validateToken() doit retourner null si le token est valide mais l'utilisateur a été supprimé.
*/
public function testValidateTokenDeletedUserReturnsNull(): void
{
$row = [
'user_id' => 999,
'expires_at' => date('Y-m-d H:i:s', time() + 3600),
'used_at' => null,
];
$this->resetRepository->method('findActiveByHash')->willReturn($row);
$this->userRepository->method('findById')->with(999)->willReturn(null);
$result = $this->service->validateToken('token-valide-mais-user-supprime');
$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->method('consumeActiveToken')->willReturn(null);
$this->expectException(InvalidResetTokenException::class);
$this->expectExceptionMessageMatches('/invalide ou a expiré/');
$this->service->resetPassword('tokeninvalide', 'nouveaumdp1');
}
/**
* resetPassword() avec un mot de passe trop court doit lever WeakPasswordException.
*/
public function testResetPasswordTooShortPasswordThrowsWeakPasswordException(): void
{
$user = $this->makeUser();
$tokenRaw = 'montokenbrut';
$tokenHash = hash('sha256', $tokenRaw);
$this->resetRepository->method('findActiveByHash')->willReturn([
'user_id' => $user->getId(),
'token_hash' => $tokenHash,
'expires_at' => date('Y-m-d H:i:s', time() + 3600),
'used_at' => null,
]);
$this->userRepository->method('findById')->willReturn($user);
$this->expectException(WeakPasswordException::class);
$this->service->resetPassword($tokenRaw, '1234567');
}
/**
* resetPassword() doit mettre à jour le mot de passe et marquer le token comme consommé.
*/
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->isType('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->method('findById')->willReturn($user);
$this->userRepository->expects($this->once())
->method('updatePassword')
->with($user->getId(), $this->callback('is_string'));
$this->service->resetPassword($tokenRaw, 'nouveaumdp1');
}
// ── Helpers ────────────────────────────────────────────────────
/**
* Crée un utilisateur de test standard.
*/
private function makeUser(): User
{
return new User(
1,
'alice',
'alice@example.com',
password_hash('motdepasse1', PASSWORD_BCRYPT),
);
}
}