first commit
This commit is contained in:
270
tests/User/UserServiceTest.php
Normal file
270
tests/User/UserServiceTest.php
Normal file
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\User;
|
||||
|
||||
use App\User\Exception\DuplicateEmailException;
|
||||
use App\User\Exception\DuplicateUsernameException;
|
||||
use App\User\Exception\InvalidRoleException;
|
||||
use App\User\Exception\WeakPasswordException;
|
||||
use App\User\User;
|
||||
use App\User\UserRepositoryInterface;
|
||||
use App\User\UserService;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour UserService.
|
||||
*
|
||||
* Vérifie la création de compte : normalisation, unicité du nom d'utilisateur
|
||||
* et de l'email, validation de la complexité du mot de passe.
|
||||
* Les dépendances sont remplacées par des mocks via leurs interfaces pour
|
||||
* isoler le service.
|
||||
*/
|
||||
final class UserServiceTest extends TestCase
|
||||
{
|
||||
/** @var UserRepositoryInterface&MockObject */
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
private UserService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userRepository = $this->createMock(UserRepositoryInterface::class);
|
||||
$this->service = new UserService($this->userRepository);
|
||||
}
|
||||
|
||||
|
||||
// ── createUser ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* createUser() doit créer et retourner un utilisateur avec les bonnes données.
|
||||
*/
|
||||
public function testCreateUserWithValidData(): void
|
||||
{
|
||||
$this->userRepository->method('findByUsername')->willReturn(null);
|
||||
$this->userRepository->method('findByEmail')->willReturn(null);
|
||||
$this->userRepository->expects($this->once())->method('create');
|
||||
|
||||
$user = $this->service->createUser('Alice', 'alice@example.com', 'motdepasse1');
|
||||
|
||||
$this->assertSame('alice', $user->getUsername());
|
||||
$this->assertSame('alice@example.com', $user->getEmail());
|
||||
$this->assertSame(User::ROLE_USER, $user->getRole());
|
||||
}
|
||||
|
||||
/**
|
||||
* createUser() doit normaliser le nom d'utilisateur et l'email en minuscules.
|
||||
*/
|
||||
public function testCreateUserNormalizesToLowercase(): void
|
||||
{
|
||||
$this->userRepository->method('findByUsername')->willReturn(null);
|
||||
$this->userRepository->method('findByEmail')->willReturn(null);
|
||||
$this->userRepository->method('create');
|
||||
|
||||
$user = $this->service->createUser(' ALICE ', ' ALICE@EXAMPLE.COM ', 'motdepasse1');
|
||||
|
||||
$this->assertSame('alice', $user->getUsername());
|
||||
$this->assertSame('alice@example.com', $user->getEmail());
|
||||
}
|
||||
|
||||
/**
|
||||
* createUser() doit lever DuplicateUsernameException si le nom est déjà pris.
|
||||
*/
|
||||
public function testCreateUserDuplicateUsernameThrowsDuplicateUsernameException(): void
|
||||
{
|
||||
$existingUser = $this->makeUser('alice', 'alice@example.com');
|
||||
$this->userRepository->method('findByUsername')->willReturn($existingUser);
|
||||
|
||||
$this->expectException(DuplicateUsernameException::class);
|
||||
|
||||
$this->service->createUser('alice', 'autre@example.com', 'motdepasse1');
|
||||
}
|
||||
|
||||
/**
|
||||
* createUser() doit lever DuplicateEmailException si l'email est déjà utilisé.
|
||||
*/
|
||||
public function testCreateUserDuplicateEmailThrowsDuplicateEmailException(): void
|
||||
{
|
||||
$existingUser = $this->makeUser('bob', 'alice@example.com');
|
||||
$this->userRepository->method('findByUsername')->willReturn(null);
|
||||
$this->userRepository->method('findByEmail')->willReturn($existingUser);
|
||||
|
||||
$this->expectException(DuplicateEmailException::class);
|
||||
|
||||
$this->service->createUser('newuser', 'alice@example.com', 'motdepasse1');
|
||||
}
|
||||
|
||||
/**
|
||||
* createUser() doit lever WeakPasswordException si le mot de passe est trop court.
|
||||
*/
|
||||
public function testCreateUserTooShortPasswordThrowsWeakPasswordException(): void
|
||||
{
|
||||
$this->userRepository->method('findByUsername')->willReturn(null);
|
||||
$this->userRepository->method('findByEmail')->willReturn(null);
|
||||
|
||||
$this->expectException(WeakPasswordException::class);
|
||||
|
||||
$this->service->createUser('alice', 'alice@example.com', '1234567');
|
||||
}
|
||||
|
||||
/**
|
||||
* createUser() avec exactement 8 caractères de mot de passe doit réussir.
|
||||
*/
|
||||
public function testCreateUserMinimumPasswordLength(): void
|
||||
{
|
||||
$this->userRepository->method('findByUsername')->willReturn(null);
|
||||
$this->userRepository->method('findByEmail')->willReturn(null);
|
||||
$this->userRepository->method('create');
|
||||
|
||||
$user = $this->service->createUser('alice', 'alice@example.com', '12345678');
|
||||
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* createUser() doit stocker un hash bcrypt, jamais le mot de passe en clair.
|
||||
*/
|
||||
public function testCreateUserPasswordIsHashed(): void
|
||||
{
|
||||
$plainPassword = 'motdepasse1';
|
||||
|
||||
$this->userRepository->method('findByUsername')->willReturn(null);
|
||||
$this->userRepository->method('findByEmail')->willReturn(null);
|
||||
$this->userRepository->method('create');
|
||||
|
||||
$user = $this->service->createUser('alice', 'alice@example.com', $plainPassword);
|
||||
|
||||
$this->assertNotSame($plainPassword, $user->getPasswordHash());
|
||||
$this->assertTrue(password_verify($plainPassword, $user->getPasswordHash()));
|
||||
}
|
||||
|
||||
/**
|
||||
* createUser() doit attribuer le rôle passé en paramètre.
|
||||
*/
|
||||
public function testCreateUserWithEditorRole(): void
|
||||
{
|
||||
$this->userRepository->method('findByUsername')->willReturn(null);
|
||||
$this->userRepository->method('findByEmail')->willReturn(null);
|
||||
$this->userRepository->method('create');
|
||||
|
||||
$user = $this->service->createUser('alice', 'alice@example.com', 'motdepasse1', User::ROLE_EDITOR);
|
||||
|
||||
$this->assertSame(User::ROLE_EDITOR, $user->getRole());
|
||||
}
|
||||
|
||||
|
||||
// ── findAll ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* findAll() délègue au repository et retourne la liste.
|
||||
*/
|
||||
public function testFindAllDelegatesToRepository(): void
|
||||
{
|
||||
$users = [$this->makeUser('alice', 'alice@example.com')];
|
||||
$this->userRepository->method('findAll')->willReturn($users);
|
||||
|
||||
$this->assertSame($users, $this->service->findAll());
|
||||
}
|
||||
|
||||
|
||||
// ── findById ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* findById() retourne null si l'utilisateur est introuvable.
|
||||
*/
|
||||
public function testFindByIdReturnsNullWhenMissing(): void
|
||||
{
|
||||
$this->userRepository->method('findById')->willReturn(null);
|
||||
|
||||
$this->assertNull($this->service->findById(99));
|
||||
}
|
||||
|
||||
/**
|
||||
* findById() retourne l'utilisateur trouvé.
|
||||
*/
|
||||
public function testFindByIdReturnsUser(): void
|
||||
{
|
||||
$user = $this->makeUser('alice', 'alice@example.com');
|
||||
$this->userRepository->method('findById')->with(1)->willReturn($user);
|
||||
|
||||
$this->assertSame($user, $this->service->findById(1));
|
||||
}
|
||||
|
||||
|
||||
// ── delete ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* delete() délègue la suppression au repository.
|
||||
*/
|
||||
public function testDeleteDelegatesToRepository(): void
|
||||
{
|
||||
$this->userRepository->method('findById')->with(5)->willReturn($this->makeUser('alice', 'alice@example.com'));
|
||||
$this->userRepository->expects($this->once())->method('delete')->with(5);
|
||||
|
||||
$this->service->delete(5);
|
||||
}
|
||||
|
||||
|
||||
// ── updateRole ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* updateRole() doit déléguer au repository avec le rôle validé.
|
||||
*/
|
||||
public function testUpdateRoleDelegatesToRepository(): void
|
||||
{
|
||||
$this->userRepository->method('findById')->with(3)->willReturn($this->makeUser('alice', 'alice@example.com'));
|
||||
$this->userRepository->expects($this->once())
|
||||
->method('updateRole')
|
||||
->with(3, User::ROLE_EDITOR);
|
||||
|
||||
$this->service->updateRole(3, User::ROLE_EDITOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* updateRole() doit lever InvalidArgumentException pour un rôle inconnu.
|
||||
*/
|
||||
public function testUpdateRoleThrowsOnInvalidRole(): void
|
||||
{
|
||||
$this->userRepository->expects($this->never())->method('updateRole');
|
||||
|
||||
$this->expectException(InvalidRoleException::class);
|
||||
|
||||
$this->service->updateRole(1, 'superadmin');
|
||||
}
|
||||
|
||||
/**
|
||||
* updateRole() accepte les trois rôles valides sans lever d'exception.
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('validRolesProvider')]
|
||||
public function testUpdateRoleAcceptsAllValidRoles(string $role): void
|
||||
{
|
||||
$this->userRepository->method('findById')->with(1)->willReturn($this->makeUser('alice', 'alice@example.com'));
|
||||
$this->userRepository->expects($this->once())->method('updateRole')->with(1, $role);
|
||||
|
||||
$this->service->updateRole(1, $role);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{string}>
|
||||
*/
|
||||
public static function validRolesProvider(): array
|
||||
{
|
||||
return [
|
||||
'user' => [User::ROLE_USER],
|
||||
'editor' => [User::ROLE_EDITOR],
|
||||
'admin' => [User::ROLE_ADMIN],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Crée un utilisateur de test avec un hash bcrypt du mot de passe fourni.
|
||||
*/
|
||||
private function makeUser(string $username, string $email): User
|
||||
{
|
||||
return new User(1, $username, $email, password_hash('motdepasse1', PASSWORD_BCRYPT));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user