first commit
This commit is contained in:
304
tests/Identity/UserServiceTest.php
Normal file
304
tests/Identity/UserServiceTest.php
Normal file
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Identity;
|
||||
|
||||
use Netig\Netslim\Identity\Application\UseCase\AdminDeleteUser;
|
||||
use Netig\Netslim\Identity\Application\UseCase\AdminUpdateUserRole;
|
||||
use Netig\Netslim\Identity\Application\UseCase\CreateUser;
|
||||
use Netig\Netslim\Identity\Application\UseCase\DeleteUser;
|
||||
use Netig\Netslim\Identity\Application\UseCase\UpdateUserRole;
|
||||
use Netig\Netslim\Identity\Application\UserApplicationService;
|
||||
use Netig\Netslim\Identity\Domain\Entity\User;
|
||||
use Netig\Netslim\Identity\Domain\Exception\DuplicateEmailException;
|
||||
use Netig\Netslim\Identity\Domain\Exception\DuplicateUsernameException;
|
||||
use Netig\Netslim\Identity\Domain\Exception\InvalidRoleException;
|
||||
use Netig\Netslim\Identity\Domain\Exception\WeakPasswordException;
|
||||
use Netig\Netslim\Identity\Domain\Policy\PasswordPolicy;
|
||||
use Netig\Netslim\Identity\Domain\Policy\RolePolicy;
|
||||
use Netig\Netslim\Identity\Domain\Repository\UserRepositoryInterface;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour UserApplicationService.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class UserServiceTest extends TestCase
|
||||
{
|
||||
/** @var UserRepositoryInterface&MockObject */
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
private UserApplicationService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userRepository = $this->createMock(UserRepositoryInterface::class);
|
||||
$rolePolicy = new RolePolicy();
|
||||
$passwordPolicy = new PasswordPolicy();
|
||||
$this->service = new UserApplicationService(
|
||||
$this->userRepository,
|
||||
new CreateUser($this->userRepository, $rolePolicy, $passwordPolicy),
|
||||
new UpdateUserRole($this->userRepository, $rolePolicy),
|
||||
new DeleteUser($this->userRepository),
|
||||
new AdminUpdateUserRole($this->userRepository, $rolePolicy),
|
||||
new AdminDeleteUser($this->userRepository),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// ── create ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* create() 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->create('Alice', 'alice@example.com', 'motdepasse12');
|
||||
|
||||
$this->assertSame('alice', $user->getUsername());
|
||||
$this->assertSame('alice@example.com', $user->getEmail());
|
||||
$this->assertSame(User::ROLE_USER, $user->getRole());
|
||||
}
|
||||
|
||||
/**
|
||||
* create() 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->create(' ALICE ', ' ALICE@EXAMPLE.COM ', 'motdepasse12');
|
||||
|
||||
$this->assertSame('alice', $user->getUsername());
|
||||
$this->assertSame('alice@example.com', $user->getEmail());
|
||||
}
|
||||
|
||||
/**
|
||||
* create() 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->create('alice', 'autre@example.com', 'motdepasse12');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() 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->create('newuser', 'alice@example.com', 'motdepasse12');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() 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->create('alice', 'alice@example.com', '12345678901');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() avec exactement 12 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->create('alice', 'alice@example.com', '123456789012');
|
||||
|
||||
$this->assertInstanceOf(User::class, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit stocker un hash bcrypt, jamais le mot de passe en clair.
|
||||
*/
|
||||
public function testCreateUserPasswordIsHashed(): void
|
||||
{
|
||||
$plainPassword = 'motdepasse12';
|
||||
|
||||
$this->userRepository->method('findByUsername')->willReturn(null);
|
||||
$this->userRepository->method('findByEmail')->willReturn(null);
|
||||
$this->userRepository->method('create');
|
||||
|
||||
$user = $this->service->create('alice', 'alice@example.com', $plainPassword);
|
||||
|
||||
$this->assertNotSame($plainPassword, $user->getPasswordHash());
|
||||
$this->assertTrue(password_verify($plainPassword, $user->getPasswordHash()));
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit conserver exactement les espaces du mot de passe saisi.
|
||||
*/
|
||||
public function testCreateUserPreservesPasswordWhitespace(): void
|
||||
{
|
||||
$plainPassword = ' motdepasse12 ';
|
||||
|
||||
$this->userRepository->method('findByUsername')->willReturn(null);
|
||||
$this->userRepository->method('findByEmail')->willReturn(null);
|
||||
$this->userRepository->method('create');
|
||||
|
||||
$user = $this->service->create('alice', 'alice@example.com', $plainPassword);
|
||||
|
||||
$this->assertTrue(password_verify($plainPassword, $user->getPasswordHash()));
|
||||
$this->assertFalse(password_verify(trim($plainPassword), $user->getPasswordHash()));
|
||||
}
|
||||
|
||||
/**
|
||||
* create() 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->create('alice', 'alice@example.com', 'motdepasse12', 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->expects($this->once())->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->expects($this->once())->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->expects($this->once())->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 uniquement les rôles attribuables depuis l'interface.
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('validRolesProvider')]
|
||||
public function testUpdateRoleAcceptsAllValidRoles(string $role): void
|
||||
{
|
||||
$this->userRepository->expects($this->once())->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],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// ── 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('motdepasse12', PASSWORD_BCRYPT));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user