first commit
This commit is contained in:
441
tests/User/UserControllerTest.php
Normal file
441
tests/User/UserControllerTest.php
Normal file
@@ -0,0 +1,441 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\User;
|
||||
|
||||
use App\Shared\Http\FlashServiceInterface;
|
||||
use App\Shared\Http\SessionManagerInterface;
|
||||
use App\User\Exception\DuplicateEmailException;
|
||||
use App\User\Exception\DuplicateUsernameException;
|
||||
use App\User\Exception\WeakPasswordException;
|
||||
use App\User\User;
|
||||
use App\User\UserController;
|
||||
use App\User\UserServiceInterface;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Tests\ControllerTestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour UserController.
|
||||
*
|
||||
* Couvre les 5 actions publiques :
|
||||
* - index() : rendu de la liste
|
||||
* - showCreate() : rendu du formulaire
|
||||
* - create() : mismatch, username dupliqué, email dupliqué, mot de passe faible, succès
|
||||
* - updateRole() : introuvable, propre rôle, cible admin, rôle invalide, succès
|
||||
* - delete() : introuvable, cible admin, soi-même, succès
|
||||
*/
|
||||
final class UserControllerTest extends ControllerTestCase
|
||||
{
|
||||
/** @var \Slim\Views\Twig&MockObject */
|
||||
private \Slim\Views\Twig $view;
|
||||
|
||||
/** @var UserServiceInterface&MockObject */
|
||||
private UserServiceInterface $userService;
|
||||
|
||||
/** @var FlashServiceInterface&MockObject */
|
||||
private FlashServiceInterface $flash;
|
||||
|
||||
/** @var SessionManagerInterface&MockObject */
|
||||
private SessionManagerInterface $sessionManager;
|
||||
|
||||
private UserController $controller;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->view = $this->makeTwigMock();
|
||||
$this->userService = $this->createMock(UserServiceInterface::class);
|
||||
$this->flash = $this->createMock(FlashServiceInterface::class);
|
||||
$this->sessionManager = $this->createMock(SessionManagerInterface::class);
|
||||
|
||||
$this->controller = new UserController(
|
||||
$this->view,
|
||||
$this->userService,
|
||||
$this->flash,
|
||||
$this->sessionManager,
|
||||
);
|
||||
}
|
||||
|
||||
// ── index ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* index() doit rendre la vue avec la liste des utilisateurs.
|
||||
*/
|
||||
public function testIndexRendersWithUserList(): void
|
||||
{
|
||||
$this->userService->method('findAll')->willReturn([]);
|
||||
$this->sessionManager->method('getUserId')->willReturn(1);
|
||||
|
||||
$this->view->expects($this->once())
|
||||
->method('render')
|
||||
->with($this->anything(), 'admin/users/index.twig', $this->anything())
|
||||
->willReturnArgument(0);
|
||||
|
||||
$res = $this->controller->index($this->makeGet('/admin/users'), $this->makeResponse());
|
||||
|
||||
$this->assertStatus($res, 200);
|
||||
}
|
||||
|
||||
// ── showCreate ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* showCreate() doit rendre le formulaire de création.
|
||||
*/
|
||||
public function testShowCreateRendersForm(): void
|
||||
{
|
||||
$this->view->expects($this->once())
|
||||
->method('render')
|
||||
->with($this->anything(), 'admin/users/form.twig', $this->anything())
|
||||
->willReturnArgument(0);
|
||||
|
||||
$res = $this->controller->showCreate($this->makeGet('/admin/users/create'), $this->makeResponse());
|
||||
|
||||
$this->assertStatus($res, 200);
|
||||
}
|
||||
|
||||
// ── create ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* create() doit rediriger avec une erreur si les mots de passe ne correspondent pas.
|
||||
*/
|
||||
public function testCreateRedirectsWhenPasswordMismatch(): void
|
||||
{
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('user_error', 'Les mots de passe ne correspondent pas');
|
||||
|
||||
$req = $this->makePost('/admin/users/create', [
|
||||
'username' => 'alice',
|
||||
'email' => 'alice@example.com',
|
||||
'password' => 'pass1',
|
||||
'password_confirm' => 'pass2',
|
||||
]);
|
||||
$res = $this->controller->create($req, $this->makeResponse());
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/users/create');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() ne doit pas appeler userService si les mots de passe ne correspondent pas.
|
||||
*/
|
||||
public function testCreateDoesNotCallServiceOnMismatch(): void
|
||||
{
|
||||
$this->userService->expects($this->never())->method('createUser');
|
||||
$this->flash->method('set');
|
||||
|
||||
$req = $this->makePost('/admin/users/create', [
|
||||
'username' => 'alice',
|
||||
'email' => 'alice@example.com',
|
||||
'password' => 'aaa',
|
||||
'password_confirm' => 'bbb',
|
||||
]);
|
||||
$this->controller->create($req, $this->makeResponse());
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit rediriger avec une erreur si le nom d'utilisateur est déjà pris.
|
||||
*/
|
||||
public function testCreateRedirectsOnDuplicateUsername(): void
|
||||
{
|
||||
$this->userService->method('createUser')
|
||||
->willThrowException(new DuplicateUsernameException('alice'));
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('user_error', $this->stringContains("nom d'utilisateur est déjà pris"));
|
||||
|
||||
$req = $this->makePost('/admin/users/create', [
|
||||
'username' => 'alice',
|
||||
'email' => 'alice@example.com',
|
||||
'password' => 'password123',
|
||||
'password_confirm' => 'password123',
|
||||
]);
|
||||
$res = $this->controller->create($req, $this->makeResponse());
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/users/create');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit rediriger avec une erreur si l'email est déjà utilisé.
|
||||
*/
|
||||
public function testCreateRedirectsOnDuplicateEmail(): void
|
||||
{
|
||||
$this->userService->method('createUser')
|
||||
->willThrowException(new DuplicateEmailException('alice@example.com'));
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('user_error', $this->stringContains('e-mail est déjà utilisée'));
|
||||
|
||||
$req = $this->makePost('/admin/users/create', [
|
||||
'username' => 'alice',
|
||||
'email' => 'alice@example.com',
|
||||
'password' => 'password123',
|
||||
'password_confirm' => 'password123',
|
||||
]);
|
||||
$res = $this->controller->create($req, $this->makeResponse());
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/users/create');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit rediriger avec une erreur si le mot de passe est trop court.
|
||||
*/
|
||||
public function testCreateRedirectsOnWeakPassword(): void
|
||||
{
|
||||
$this->userService->method('createUser')
|
||||
->willThrowException(new WeakPasswordException());
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('user_error', $this->stringContains('8 caractères'));
|
||||
|
||||
$req = $this->makePost('/admin/users/create', [
|
||||
'username' => 'alice',
|
||||
'email' => 'alice@example.com',
|
||||
'password' => 'short',
|
||||
'password_confirm' => 'short',
|
||||
]);
|
||||
$res = $this->controller->create($req, $this->makeResponse());
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/users/create');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit flasher un succès et rediriger vers /admin/users en cas de succès.
|
||||
*/
|
||||
public function testCreateRedirectsToUsersListOnSuccess(): void
|
||||
{
|
||||
$this->userService->method('createUser')->willReturn($this->makeUser(99, 'alice', User::ROLE_USER));
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('user_success', $this->stringContains('alice'));
|
||||
|
||||
$req = $this->makePost('/admin/users/create', [
|
||||
'username' => 'alice',
|
||||
'email' => 'alice@example.com',
|
||||
'password' => 'password123',
|
||||
'password_confirm' => 'password123',
|
||||
]);
|
||||
$res = $this->controller->create($req, $this->makeResponse());
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/users');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit forcer le rôle 'user' si un rôle admin est soumis dans le formulaire.
|
||||
*/
|
||||
public function testCreateForcesRoleUserWhenAdminRoleSubmitted(): void
|
||||
{
|
||||
$this->userService->expects($this->once())
|
||||
->method('createUser')
|
||||
->with('alice', 'alice@example.com', 'password123', User::ROLE_USER);
|
||||
|
||||
$this->flash->method('set');
|
||||
|
||||
$req = $this->makePost('/admin/users/create', [
|
||||
'username' => 'alice',
|
||||
'email' => 'alice@example.com',
|
||||
'password' => 'password123',
|
||||
'password_confirm' => 'password123',
|
||||
'role' => User::ROLE_ADMIN, // rôle injecté par l'attaquant
|
||||
]);
|
||||
$this->controller->create($req, $this->makeResponse());
|
||||
}
|
||||
|
||||
// ── updateRole ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* updateRole() doit rediriger avec une erreur si l'utilisateur est introuvable.
|
||||
*/
|
||||
public function testUpdateRoleRedirectsWhenUserNotFound(): void
|
||||
{
|
||||
$this->userService->method('findById')->willReturn(null);
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('user_error', 'Utilisateur introuvable');
|
||||
|
||||
$res = $this->controller->updateRole(
|
||||
$this->makePost('/admin/users/role/99', ['role' => User::ROLE_EDITOR]),
|
||||
$this->makeResponse(),
|
||||
['id' => '99'],
|
||||
);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/users');
|
||||
}
|
||||
|
||||
/**
|
||||
* updateRole() doit rediriger avec une erreur si l'admin tente de changer son propre rôle.
|
||||
*/
|
||||
public function testUpdateRoleRedirectsWhenAdminTriesToChangeOwnRole(): void
|
||||
{
|
||||
$user = $this->makeUser(1, 'admin', User::ROLE_USER);
|
||||
$this->userService->method('findById')->willReturn($user);
|
||||
$this->sessionManager->method('getUserId')->willReturn(1); // même ID
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('user_error', $this->stringContains('propre rôle'));
|
||||
|
||||
$res = $this->controller->updateRole(
|
||||
$this->makePost('/admin/users/role/1', ['role' => User::ROLE_EDITOR]),
|
||||
$this->makeResponse(),
|
||||
['id' => '1'],
|
||||
);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/users');
|
||||
}
|
||||
|
||||
/**
|
||||
* updateRole() doit rediriger avec une erreur si l'utilisateur cible est déjà admin.
|
||||
*/
|
||||
public function testUpdateRoleRedirectsWhenTargetIsAdmin(): void
|
||||
{
|
||||
$user = $this->makeUser(2, 'superadmin', User::ROLE_ADMIN);
|
||||
$this->userService->method('findById')->willReturn($user);
|
||||
$this->sessionManager->method('getUserId')->willReturn(1);
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('user_error', $this->stringContains("administrateur ne peut pas être modifié"));
|
||||
|
||||
$res = $this->controller->updateRole(
|
||||
$this->makePost('/admin/users/role/2', ['role' => User::ROLE_EDITOR]),
|
||||
$this->makeResponse(),
|
||||
['id' => '2'],
|
||||
);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/users');
|
||||
}
|
||||
|
||||
/**
|
||||
* updateRole() doit rediriger avec une erreur si le rôle soumis est invalide.
|
||||
*/
|
||||
public function testUpdateRoleRedirectsOnInvalidRole(): void
|
||||
{
|
||||
$user = $this->makeUser(2, 'bob', User::ROLE_USER);
|
||||
$this->userService->method('findById')->willReturn($user);
|
||||
$this->sessionManager->method('getUserId')->willReturn(1);
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('user_error', 'Rôle invalide');
|
||||
|
||||
$res = $this->controller->updateRole(
|
||||
$this->makePost('/admin/users/role/2', ['role' => 'superuser']),
|
||||
$this->makeResponse(),
|
||||
['id' => '2'],
|
||||
);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/users');
|
||||
}
|
||||
|
||||
/**
|
||||
* updateRole() doit appeler userService et rediriger avec succès.
|
||||
*/
|
||||
public function testUpdateRoleRedirectsWithSuccessFlash(): void
|
||||
{
|
||||
$user = $this->makeUser(2, 'bob', User::ROLE_USER);
|
||||
$this->userService->method('findById')->willReturn($user);
|
||||
$this->sessionManager->method('getUserId')->willReturn(1);
|
||||
|
||||
$this->userService->expects($this->once())->method('updateRole')->with(2, User::ROLE_EDITOR);
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('user_success', $this->stringContains('bob'));
|
||||
|
||||
$res = $this->controller->updateRole(
|
||||
$this->makePost('/admin/users/role/2', ['role' => User::ROLE_EDITOR]),
|
||||
$this->makeResponse(),
|
||||
['id' => '2'],
|
||||
);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/users');
|
||||
}
|
||||
|
||||
// ── delete ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* delete() doit rediriger avec une erreur si l'utilisateur est introuvable.
|
||||
*/
|
||||
public function testDeleteRedirectsWhenUserNotFound(): void
|
||||
{
|
||||
$this->userService->method('findById')->willReturn(null);
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('user_error', 'Utilisateur introuvable');
|
||||
|
||||
$res = $this->controller->delete(
|
||||
$this->makePost('/admin/users/delete/99'),
|
||||
$this->makeResponse(),
|
||||
['id' => '99'],
|
||||
);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/users');
|
||||
}
|
||||
|
||||
/**
|
||||
* delete() doit rediriger avec une erreur si la cible est administrateur.
|
||||
*/
|
||||
public function testDeleteRedirectsWhenTargetIsAdmin(): void
|
||||
{
|
||||
$user = $this->makeUser(2, 'superadmin', User::ROLE_ADMIN);
|
||||
$this->userService->method('findById')->willReturn($user);
|
||||
$this->sessionManager->method('getUserId')->willReturn(1);
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('user_error', $this->stringContains('administrateur ne peut pas être supprimé'));
|
||||
|
||||
$res = $this->controller->delete(
|
||||
$this->makePost('/admin/users/delete/2'),
|
||||
$this->makeResponse(),
|
||||
['id' => '2'],
|
||||
);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/users');
|
||||
}
|
||||
|
||||
/**
|
||||
* delete() doit rediriger avec une erreur si l'admin tente de supprimer son propre compte.
|
||||
*/
|
||||
public function testDeleteRedirectsWhenAdminTriesToDeleteOwnAccount(): void
|
||||
{
|
||||
$user = $this->makeUser(1, 'alice', User::ROLE_USER);
|
||||
$this->userService->method('findById')->willReturn($user);
|
||||
$this->sessionManager->method('getUserId')->willReturn(1); // même ID
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('user_error', $this->stringContains('propre compte'));
|
||||
|
||||
$res = $this->controller->delete(
|
||||
$this->makePost('/admin/users/delete/1'),
|
||||
$this->makeResponse(),
|
||||
['id' => '1'],
|
||||
);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/users');
|
||||
}
|
||||
|
||||
/**
|
||||
* delete() doit appeler userService et rediriger avec succès.
|
||||
*/
|
||||
public function testDeleteRedirectsWithSuccessFlash(): void
|
||||
{
|
||||
$user = $this->makeUser(2, 'bob', User::ROLE_USER);
|
||||
$this->userService->method('findById')->willReturn($user);
|
||||
$this->sessionManager->method('getUserId')->willReturn(1);
|
||||
|
||||
$this->userService->expects($this->once())->method('delete')->with(2);
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('user_success', $this->stringContains('bob'));
|
||||
|
||||
$res = $this->controller->delete(
|
||||
$this->makePost('/admin/users/delete/2'),
|
||||
$this->makeResponse(),
|
||||
['id' => '2'],
|
||||
);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/users');
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Crée un utilisateur de test avec les paramètres minimaux.
|
||||
*/
|
||||
private function makeUser(int $id, string $username, string $role): User
|
||||
{
|
||||
return new User($id, $username, "{$username}@example.com", password_hash('secret', PASSWORD_BCRYPT), $role);
|
||||
}
|
||||
}
|
||||
357
tests/User/UserRepositoryTest.php
Normal file
357
tests/User/UserRepositoryTest.php
Normal file
@@ -0,0 +1,357 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\User;
|
||||
|
||||
use App\User\User;
|
||||
use App\User\UserRepository;
|
||||
use PDO;
|
||||
use PDOStatement;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour UserRepository.
|
||||
*
|
||||
* Vérifie que chaque méthode du dépôt construit le bon SQL,
|
||||
* lie les bons paramètres et retourne les bonnes valeurs.
|
||||
*
|
||||
* PDO et PDOStatement sont mockés pour isoler complètement
|
||||
* le dépôt de la base de données.
|
||||
*/
|
||||
final class UserRepositoryTest extends TestCase
|
||||
{
|
||||
/** @var PDO&MockObject */
|
||||
private PDO $db;
|
||||
|
||||
private UserRepository $repository;
|
||||
|
||||
/**
|
||||
* Données représentant une ligne utilisateur en base de données.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private array $rowAlice;
|
||||
|
||||
/**
|
||||
* Initialise le mock PDO, le dépôt et les données de test avant chaque test.
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->db = $this->createMock(PDO::class);
|
||||
$this->repository = new UserRepository($this->db);
|
||||
|
||||
$this->rowAlice = [
|
||||
'id' => 1,
|
||||
'username' => 'alice',
|
||||
'email' => 'alice@example.com',
|
||||
'password_hash' => password_hash('secret', PASSWORD_BCRYPT),
|
||||
'role' => User::ROLE_USER,
|
||||
'created_at' => '2024-01-01 00:00:00',
|
||||
];
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────
|
||||
|
||||
private function stmtForRead(array $rows = [], array|false $row = false): PDOStatement&MockObject
|
||||
{
|
||||
$stmt = $this->createMock(PDOStatement::class);
|
||||
$stmt->method('execute')->willReturn(true);
|
||||
$stmt->method('fetchAll')->willReturn($rows);
|
||||
$stmt->method('fetch')->willReturn($row);
|
||||
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
private function stmtForWrite(): PDOStatement&MockObject
|
||||
{
|
||||
$stmt = $this->createMock(PDOStatement::class);
|
||||
$stmt->method('execute')->willReturn(true);
|
||||
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
|
||||
// ── findAll ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* findAll() doit retourner un tableau vide si aucun utilisateur n'existe.
|
||||
*/
|
||||
public function testFindAllReturnsEmptyArrayWhenNone(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead([]);
|
||||
$this->db->method('query')->willReturn($stmt);
|
||||
|
||||
$this->assertSame([], $this->repository->findAll());
|
||||
}
|
||||
|
||||
/**
|
||||
* findAll() doit retourner un tableau d'instances User hydratées.
|
||||
*/
|
||||
public function testFindAllReturnsUserInstances(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead([$this->rowAlice]);
|
||||
$this->db->method('query')->willReturn($stmt);
|
||||
|
||||
$result = $this->repository->findAll();
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertInstanceOf(User::class, $result[0]);
|
||||
$this->assertSame('alice', $result[0]->getUsername());
|
||||
}
|
||||
|
||||
/**
|
||||
* findAll() doit interroger la table 'users' avec un tri par created_at ASC.
|
||||
*/
|
||||
public function testFindAllQueriesWithAscendingOrder(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead([]);
|
||||
|
||||
$this->db->expects($this->once())
|
||||
->method('query')
|
||||
->with($this->logicalAnd(
|
||||
$this->stringContains('users'),
|
||||
$this->stringContains('created_at ASC'),
|
||||
))
|
||||
->willReturn($stmt);
|
||||
|
||||
$this->repository->findAll();
|
||||
}
|
||||
|
||||
|
||||
// ── findById ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* findById() doit retourner null si aucun utilisateur ne correspond à cet identifiant.
|
||||
*/
|
||||
public function testFindByIdReturnsNullWhenMissing(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead(row: false);
|
||||
$this->db->method('prepare')->willReturn($stmt);
|
||||
|
||||
$this->assertNull($this->repository->findById(99));
|
||||
}
|
||||
|
||||
/**
|
||||
* findById() doit retourner une instance User hydratée si l'utilisateur existe.
|
||||
*/
|
||||
public function testFindByIdReturnsUserWhenFound(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead(row: $this->rowAlice);
|
||||
$this->db->method('prepare')->willReturn($stmt);
|
||||
|
||||
$result = $this->repository->findById(1);
|
||||
|
||||
$this->assertInstanceOf(User::class, $result);
|
||||
$this->assertSame(1, $result->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* findById() doit exécuter avec le bon identifiant.
|
||||
*/
|
||||
public function testFindByIdQueriesWithCorrectId(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead(row: false);
|
||||
$this->db->method('prepare')->willReturn($stmt);
|
||||
|
||||
$stmt->expects($this->once())
|
||||
->method('execute')
|
||||
->with([':id' => 42]);
|
||||
|
||||
$this->repository->findById(42);
|
||||
}
|
||||
|
||||
|
||||
// ── findByUsername ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* findByUsername() doit retourner null si le nom d'utilisateur est introuvable.
|
||||
*/
|
||||
public function testFindByUsernameReturnsNullWhenMissing(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead(row: false);
|
||||
$this->db->method('prepare')->willReturn($stmt);
|
||||
|
||||
$this->assertNull($this->repository->findByUsername('inconnu'));
|
||||
}
|
||||
|
||||
/**
|
||||
* findByUsername() doit retourner une instance User si le nom est trouvé.
|
||||
*/
|
||||
public function testFindByUsernameReturnsUserWhenFound(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead(row: $this->rowAlice);
|
||||
$this->db->method('prepare')->willReturn($stmt);
|
||||
|
||||
$result = $this->repository->findByUsername('alice');
|
||||
|
||||
$this->assertInstanceOf(User::class, $result);
|
||||
$this->assertSame('alice', $result->getUsername());
|
||||
}
|
||||
|
||||
/**
|
||||
* findByUsername() doit exécuter avec le bon nom d'utilisateur.
|
||||
*/
|
||||
public function testFindByUsernameQueriesWithCorrectName(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead(row: false);
|
||||
$this->db->method('prepare')->willReturn($stmt);
|
||||
|
||||
$stmt->expects($this->once())
|
||||
->method('execute')
|
||||
->with([':username' => 'alice']);
|
||||
|
||||
$this->repository->findByUsername('alice');
|
||||
}
|
||||
|
||||
|
||||
// ── findByEmail ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* findByEmail() doit retourner null si l'adresse e-mail est introuvable.
|
||||
*/
|
||||
public function testFindByEmailReturnsNullWhenMissing(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead(row: false);
|
||||
$this->db->method('prepare')->willReturn($stmt);
|
||||
|
||||
$this->assertNull($this->repository->findByEmail('inconnu@example.com'));
|
||||
}
|
||||
|
||||
/**
|
||||
* findByEmail() doit retourner une instance User si l'e-mail est trouvé.
|
||||
*/
|
||||
public function testFindByEmailReturnsUserWhenFound(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead(row: $this->rowAlice);
|
||||
$this->db->method('prepare')->willReturn($stmt);
|
||||
|
||||
$result = $this->repository->findByEmail('alice@example.com');
|
||||
|
||||
$this->assertInstanceOf(User::class, $result);
|
||||
$this->assertSame('alice@example.com', $result->getEmail());
|
||||
}
|
||||
|
||||
/**
|
||||
* findByEmail() doit exécuter avec la bonne adresse e-mail.
|
||||
*/
|
||||
public function testFindByEmailQueriesWithCorrectEmail(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead(row: false);
|
||||
$this->db->method('prepare')->willReturn($stmt);
|
||||
|
||||
$stmt->expects($this->once())
|
||||
->method('execute')
|
||||
->with([':email' => 'alice@example.com']);
|
||||
|
||||
$this->repository->findByEmail('alice@example.com');
|
||||
}
|
||||
|
||||
|
||||
// ── create ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* create() doit préparer un INSERT sur la table 'users' avec les bonnes données.
|
||||
*/
|
||||
public function testCreateCallsInsertWithCorrectData(): void
|
||||
{
|
||||
$user = User::fromArray($this->rowAlice);
|
||||
$stmt = $this->stmtForWrite();
|
||||
|
||||
$this->db->method('prepare')
|
||||
->with($this->stringContains('INSERT INTO users'))
|
||||
->willReturn($stmt);
|
||||
|
||||
$stmt->expects($this->once())
|
||||
->method('execute')
|
||||
->with($this->callback(function (array $data) use ($user): bool {
|
||||
return $data[':username'] === $user->getUsername()
|
||||
&& $data[':email'] === $user->getEmail()
|
||||
&& $data[':password_hash'] === $user->getPasswordHash()
|
||||
&& $data[':role'] === $user->getRole()
|
||||
&& isset($data[':created_at']);
|
||||
}));
|
||||
|
||||
$this->db->method('lastInsertId')->willReturn('1');
|
||||
|
||||
$this->repository->create($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit retourner l'identifiant généré par la base de données.
|
||||
*/
|
||||
public function testCreateReturnsGeneratedId(): void
|
||||
{
|
||||
$user = User::fromArray($this->rowAlice);
|
||||
$stmt = $this->stmtForWrite();
|
||||
$this->db->method('prepare')->willReturn($stmt);
|
||||
$this->db->method('lastInsertId')->willReturn('42');
|
||||
|
||||
$this->assertSame(42, $this->repository->create($user));
|
||||
}
|
||||
|
||||
|
||||
// ── updatePassword ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* updatePassword() doit préparer un UPDATE avec le nouveau hash et le bon identifiant.
|
||||
*/
|
||||
public function testUpdatePasswordCallsUpdateWithCorrectHash(): void
|
||||
{
|
||||
$newHash = password_hash('nouveaumdp', PASSWORD_BCRYPT);
|
||||
$stmt = $this->stmtForWrite();
|
||||
|
||||
$this->db->method('prepare')
|
||||
->with($this->stringContains('UPDATE users'))
|
||||
->willReturn($stmt);
|
||||
|
||||
$stmt->expects($this->once())
|
||||
->method('execute')
|
||||
->with([':password_hash' => $newHash, ':id' => 1]);
|
||||
|
||||
$this->repository->updatePassword(1, $newHash);
|
||||
}
|
||||
|
||||
|
||||
// ── updateRole ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* updateRole() doit préparer un UPDATE avec le bon rôle et le bon identifiant.
|
||||
*/
|
||||
public function testUpdateRoleCallsUpdateWithCorrectRole(): void
|
||||
{
|
||||
$stmt = $this->stmtForWrite();
|
||||
|
||||
$this->db->method('prepare')
|
||||
->with($this->stringContains('UPDATE users'))
|
||||
->willReturn($stmt);
|
||||
|
||||
$stmt->expects($this->once())
|
||||
->method('execute')
|
||||
->with([':role' => User::ROLE_EDITOR, ':id' => 1]);
|
||||
|
||||
$this->repository->updateRole(1, User::ROLE_EDITOR);
|
||||
}
|
||||
|
||||
|
||||
// ── delete ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* delete() doit préparer un DELETE avec le bon identifiant.
|
||||
*/
|
||||
public function testDeleteCallsDeleteWithCorrectId(): void
|
||||
{
|
||||
$stmt = $this->stmtForWrite();
|
||||
|
||||
$this->db->expects($this->once())
|
||||
->method('prepare')
|
||||
->with($this->stringContains('DELETE FROM users'))
|
||||
->willReturn($stmt);
|
||||
|
||||
$stmt->expects($this->once())
|
||||
->method('execute')
|
||||
->with([':id' => 7]);
|
||||
|
||||
$this->repository->delete(7);
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
232
tests/User/UserTest.php
Normal file
232
tests/User/UserTest.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\User;
|
||||
|
||||
use App\User\User;
|
||||
use DateTime;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour le modèle User.
|
||||
*
|
||||
* Vérifie la construction, la validation, les accesseurs
|
||||
* et l'hydratation depuis un tableau de base de données.
|
||||
*/
|
||||
final class UserTest extends TestCase
|
||||
{
|
||||
|
||||
// ── Construction valide ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Un utilisateur construit avec des données valides ne doit pas lever d'exception.
|
||||
*/
|
||||
public function testValidConstruction(): void
|
||||
{
|
||||
$user = new User(1, 'alice', 'alice@example.com', password_hash('secret123', PASSWORD_BCRYPT));
|
||||
|
||||
$this->assertSame(1, $user->getId());
|
||||
$this->assertSame('alice', $user->getUsername());
|
||||
$this->assertSame('alice@example.com', $user->getEmail());
|
||||
$this->assertSame(User::ROLE_USER, $user->getRole());
|
||||
}
|
||||
|
||||
/**
|
||||
* Le rôle par défaut doit être 'user'.
|
||||
*/
|
||||
public function testDefaultRole(): void
|
||||
{
|
||||
$user = new User(1, 'alice', 'alice@example.com', password_hash('secret', PASSWORD_BCRYPT));
|
||||
|
||||
$this->assertSame(User::ROLE_USER, $user->getRole());
|
||||
$this->assertFalse($user->isAdmin());
|
||||
$this->assertFalse($user->isEditor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Un utilisateur avec le rôle 'admin' doit être reconnu comme administrateur.
|
||||
*/
|
||||
public function testAdminRole(): void
|
||||
{
|
||||
$user = new User(1, 'alice', 'alice@example.com', password_hash('secret', PASSWORD_BCRYPT), User::ROLE_ADMIN);
|
||||
|
||||
$this->assertTrue($user->isAdmin());
|
||||
$this->assertFalse($user->isEditor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Un utilisateur avec le rôle 'editor' doit être reconnu comme éditeur.
|
||||
*/
|
||||
public function testEditorRole(): void
|
||||
{
|
||||
$user = new User(1, 'alice', 'alice@example.com', password_hash('secret', PASSWORD_BCRYPT), User::ROLE_EDITOR);
|
||||
|
||||
$this->assertFalse($user->isAdmin());
|
||||
$this->assertTrue($user->isEditor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Une date de création explicite doit être conservée.
|
||||
*/
|
||||
public function testExplicitCreationDate(): void
|
||||
{
|
||||
$date = new DateTime('2024-01-15 10:00:00');
|
||||
$user = new User(1, 'alice', 'alice@example.com', password_hash('secret', PASSWORD_BCRYPT), User::ROLE_USER, $date);
|
||||
|
||||
$this->assertEquals($date, $user->getCreatedAt());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sans date explicite, la date de création doit être définie à maintenant.
|
||||
*/
|
||||
public function testDefaultCreationDate(): void
|
||||
{
|
||||
$before = new DateTime();
|
||||
$user = new User(1, 'alice', 'alice@example.com', password_hash('secret', PASSWORD_BCRYPT));
|
||||
$after = new DateTime();
|
||||
|
||||
$this->assertGreaterThanOrEqual($before, $user->getCreatedAt());
|
||||
$this->assertLessThanOrEqual($after, $user->getCreatedAt());
|
||||
}
|
||||
|
||||
|
||||
// ── Validation — nom d'utilisateur ─────────────────────────────
|
||||
|
||||
/**
|
||||
* Un nom d'utilisateur de moins de 3 caractères doit lever une exception.
|
||||
*/
|
||||
public function testUsernameTooShort(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessageMatches('/3 caractères/');
|
||||
|
||||
new User(1, 'ab', 'alice@example.com', password_hash('secret', PASSWORD_BCRYPT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Un nom d'utilisateur de plus de 50 caractères doit lever une exception.
|
||||
*/
|
||||
public function testUsernameTooLong(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessageMatches('/50 caractères/');
|
||||
|
||||
new User(1, str_repeat('a', 51), 'alice@example.com', password_hash('secret', PASSWORD_BCRYPT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Un nom d'utilisateur de exactement 3 caractères doit être accepté.
|
||||
*/
|
||||
public function testUsernameMinimumLength(): void
|
||||
{
|
||||
$user = new User(1, 'ali', 'alice@example.com', password_hash('secret', PASSWORD_BCRYPT));
|
||||
|
||||
$this->assertSame('ali', $user->getUsername());
|
||||
}
|
||||
|
||||
/**
|
||||
* Un nom d'utilisateur de exactement 50 caractères doit être accepté.
|
||||
*/
|
||||
public function testUsernameMaximumLength(): void
|
||||
{
|
||||
$username = str_repeat('a', 50);
|
||||
$user = new User(1, $username, 'alice@example.com', password_hash('secret', PASSWORD_BCRYPT));
|
||||
|
||||
$this->assertSame($username, $user->getUsername());
|
||||
}
|
||||
|
||||
|
||||
// ── Validation — email ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Un email invalide doit lever une exception.
|
||||
*/
|
||||
public function testInvalidEmail(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessageMatches('/email/i');
|
||||
|
||||
new User(1, 'alice', 'pas-un-email', password_hash('secret', PASSWORD_BCRYPT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Un email vide doit lever une exception.
|
||||
*/
|
||||
public function testEmptyEmail(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
|
||||
new User(1, 'alice', '', password_hash('secret', PASSWORD_BCRYPT));
|
||||
}
|
||||
|
||||
|
||||
// ── Validation — hash du mot de passe ──────────────────────────
|
||||
|
||||
/**
|
||||
* Un hash de mot de passe vide doit lever une exception.
|
||||
*/
|
||||
public function testEmptyPasswordHash(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessageMatches('/hash/i');
|
||||
|
||||
new User(1, 'alice', 'alice@example.com', '');
|
||||
}
|
||||
|
||||
|
||||
// ── Validation — rôle ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Un rôle invalide doit lever une exception.
|
||||
*/
|
||||
public function testInvalidRole(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessageMatches('/rôle/i');
|
||||
|
||||
new User(1, 'alice', 'alice@example.com', password_hash('secret', PASSWORD_BCRYPT), 'superadmin');
|
||||
}
|
||||
|
||||
|
||||
// ── Hydratation depuis un tableau ──────────────────────────────
|
||||
|
||||
/**
|
||||
* fromArray() doit hydrater correctement l'utilisateur depuis une ligne de base de données.
|
||||
*/
|
||||
public function testFromArray(): void
|
||||
{
|
||||
$hash = password_hash('secret', PASSWORD_BCRYPT);
|
||||
|
||||
$user = User::fromArray([
|
||||
'id' => 42,
|
||||
'username' => 'bob',
|
||||
'email' => 'bob@example.com',
|
||||
'password_hash' => $hash,
|
||||
'role' => 'editor',
|
||||
'created_at' => '2024-06-01 12:00:00',
|
||||
]);
|
||||
|
||||
$this->assertSame(42, $user->getId());
|
||||
$this->assertSame('bob', $user->getUsername());
|
||||
$this->assertSame('bob@example.com', $user->getEmail());
|
||||
$this->assertSame($hash, $user->getPasswordHash());
|
||||
$this->assertSame('editor', $user->getRole());
|
||||
$this->assertTrue($user->isEditor());
|
||||
}
|
||||
|
||||
/**
|
||||
* fromArray() avec une date absente ne doit pas lever d'exception.
|
||||
*/
|
||||
public function testFromArrayWithoutDate(): void
|
||||
{
|
||||
$user = User::fromArray([
|
||||
'id' => 1,
|
||||
'username' => 'alice',
|
||||
'email' => 'alice@example.com',
|
||||
'password_hash' => password_hash('secret', PASSWORD_BCRYPT),
|
||||
]);
|
||||
|
||||
$this->assertInstanceOf(DateTime::class, $user->getCreatedAt());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user