Files
slim-blog/tests/User/UserRepositoryTest.php
2026-03-16 09:25:44 +01:00

359 lines
12 KiB
PHP

<?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.
*/
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
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->expects($this->once())->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->expects($this->once())->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->expects($this->once())->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->expects($this->once())->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->expects($this->once())->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->expects($this->once())->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);
}
}