359 lines
12 KiB
PHP
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);
|
|
}
|
|
}
|