382 lines
12 KiB
PHP
382 lines
12 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Category;
|
|
|
|
use App\Category\Category;
|
|
use App\Category\CategoryRepository;
|
|
use PDO;
|
|
use PDOStatement;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* Tests unitaires pour CategoryRepository.
|
|
*
|
|
* 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 CategoryRepositoryTest extends TestCase
|
|
{
|
|
/** @var PDO&MockObject */
|
|
private PDO $db;
|
|
|
|
private CategoryRepository $repository;
|
|
|
|
/**
|
|
* Données représentant une ligne catégorie en base de données.
|
|
*
|
|
* @var array<string, mixed>
|
|
*/
|
|
private array $rowPhp;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->db = $this->createMock(PDO::class);
|
|
$this->repository = new CategoryRepository($this->db);
|
|
|
|
$this->rowPhp = [
|
|
'id' => 1,
|
|
'name' => 'PHP',
|
|
'slug' => 'php',
|
|
];
|
|
}
|
|
|
|
// ── 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 stmtForScalar(mixed $value): PDOStatement&MockObject
|
|
{
|
|
$stmt = $this->createMock(PDOStatement::class);
|
|
$stmt->method('execute')->willReturn(true);
|
|
$stmt->method('fetchColumn')->willReturn($value);
|
|
|
|
return $stmt;
|
|
}
|
|
|
|
private function stmtForWrite(int $rowCount = 1): PDOStatement&MockObject
|
|
{
|
|
$stmt = $this->createMock(PDOStatement::class);
|
|
$stmt->method('execute')->willReturn(true);
|
|
$stmt->method('rowCount')->willReturn($rowCount);
|
|
|
|
return $stmt;
|
|
}
|
|
|
|
|
|
// ── findAll ────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* findAll() retourne un tableau vide si aucune catégorie n'existe.
|
|
*/
|
|
public function testFindAllReturnsEmptyArrayWhenNone(): void
|
|
{
|
|
$stmt = $this->stmtForRead([]);
|
|
$this->db->method('query')->willReturn($stmt);
|
|
|
|
$this->assertSame([], $this->repository->findAll());
|
|
}
|
|
|
|
/**
|
|
* findAll() retourne des instances Category hydratées.
|
|
*/
|
|
public function testFindAllReturnsCategoryInstances(): void
|
|
{
|
|
$stmt = $this->stmtForRead([$this->rowPhp]);
|
|
$this->db->method('query')->willReturn($stmt);
|
|
|
|
$result = $this->repository->findAll();
|
|
|
|
$this->assertCount(1, $result);
|
|
$this->assertInstanceOf(Category::class, $result[0]);
|
|
$this->assertSame('PHP', $result[0]->getName());
|
|
$this->assertSame('php', $result[0]->getSlug());
|
|
}
|
|
|
|
/**
|
|
* findAll() interroge la table 'categories' triée par name ASC.
|
|
*/
|
|
public function testFindAllQueriesWithAlphabeticOrder(): void
|
|
{
|
|
$stmt = $this->stmtForRead([]);
|
|
|
|
$this->db->expects($this->once())
|
|
->method('query')
|
|
->with($this->logicalAnd(
|
|
$this->stringContains('categories'),
|
|
$this->stringContains('name ASC'),
|
|
))
|
|
->willReturn($stmt);
|
|
|
|
$this->repository->findAll();
|
|
}
|
|
|
|
|
|
// ── findById ───────────────────────────────────────────────────
|
|
|
|
/**
|
|
* findById() retourne null si la catégorie est absente.
|
|
*/
|
|
public function testFindByIdReturnsNullWhenMissing(): void
|
|
{
|
|
$stmt = $this->stmtForRead(row: false);
|
|
$this->db->method('prepare')->willReturn($stmt);
|
|
|
|
$this->assertNull($this->repository->findById(99));
|
|
}
|
|
|
|
/**
|
|
* findById() retourne une instance Category si la catégorie existe.
|
|
*/
|
|
public function testFindByIdReturnsCategoryWhenFound(): void
|
|
{
|
|
$stmt = $this->stmtForRead(row: $this->rowPhp);
|
|
$this->db->method('prepare')->willReturn($stmt);
|
|
|
|
$result = $this->repository->findById(1);
|
|
|
|
$this->assertInstanceOf(Category::class, $result);
|
|
$this->assertSame(1, $result->getId());
|
|
$this->assertSame('PHP', $result->getName());
|
|
}
|
|
|
|
/**
|
|
* findById() exécute 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);
|
|
}
|
|
|
|
|
|
// ── findBySlug ─────────────────────────────────────────────────
|
|
|
|
/**
|
|
* findBySlug() retourne null si le slug est absent.
|
|
*/
|
|
public function testFindBySlugReturnsNullWhenMissing(): void
|
|
{
|
|
$stmt = $this->stmtForRead(row: false);
|
|
$this->db->method('prepare')->willReturn($stmt);
|
|
|
|
$this->assertNull($this->repository->findBySlug('inconnu'));
|
|
}
|
|
|
|
/**
|
|
* findBySlug() retourne une instance Category si le slug existe.
|
|
*/
|
|
public function testFindBySlugReturnsCategoryWhenFound(): void
|
|
{
|
|
$stmt = $this->stmtForRead(row: $this->rowPhp);
|
|
$this->db->method('prepare')->willReturn($stmt);
|
|
|
|
$result = $this->repository->findBySlug('php');
|
|
|
|
$this->assertInstanceOf(Category::class, $result);
|
|
$this->assertSame('php', $result->getSlug());
|
|
}
|
|
|
|
/**
|
|
* findBySlug() exécute avec le bon slug.
|
|
*/
|
|
public function testFindBySlugQueriesWithCorrectSlug(): void
|
|
{
|
|
$stmt = $this->stmtForRead(row: false);
|
|
$this->db->method('prepare')->willReturn($stmt);
|
|
|
|
$stmt->expects($this->once())
|
|
->method('execute')
|
|
->with([':slug' => 'php']);
|
|
|
|
$this->repository->findBySlug('php');
|
|
}
|
|
|
|
|
|
// ── create ─────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* create() prépare un INSERT avec le nom et le slug de la catégorie.
|
|
*/
|
|
public function testCreateCallsInsertWithNameAndSlug(): void
|
|
{
|
|
$category = Category::fromArray($this->rowPhp);
|
|
$stmt = $this->stmtForWrite();
|
|
|
|
$this->db->expects($this->once())->method('prepare')
|
|
->with($this->stringContains('INSERT INTO categories'))
|
|
->willReturn($stmt);
|
|
|
|
$stmt->expects($this->once())
|
|
->method('execute')
|
|
->with($this->callback(fn (array $data): bool =>
|
|
$data[':name'] === $category->getName()
|
|
&& $data[':slug'] === $category->getSlug()
|
|
));
|
|
|
|
$this->db->method('lastInsertId')->willReturn('1');
|
|
|
|
$this->repository->create($category);
|
|
}
|
|
|
|
/**
|
|
* create() retourne l'identifiant généré par la base de données.
|
|
*/
|
|
public function testCreateReturnsGeneratedId(): void
|
|
{
|
|
$category = Category::fromArray($this->rowPhp);
|
|
$stmt = $this->stmtForWrite();
|
|
$this->db->method('prepare')->willReturn($stmt);
|
|
$this->db->method('lastInsertId')->willReturn('7');
|
|
|
|
$this->assertSame(7, $this->repository->create($category));
|
|
}
|
|
|
|
|
|
// ── delete ─────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* delete() prépare un DELETE avec le bon identifiant.
|
|
*/
|
|
public function testDeleteCallsDeleteWithCorrectId(): void
|
|
{
|
|
$stmt = $this->stmtForWrite(1);
|
|
|
|
$this->db->expects($this->once())
|
|
->method('prepare')
|
|
->with($this->stringContains('DELETE FROM categories'))
|
|
->willReturn($stmt);
|
|
|
|
$stmt->expects($this->once())
|
|
->method('execute')
|
|
->with([':id' => 3]);
|
|
|
|
$this->repository->delete(3);
|
|
}
|
|
|
|
/**
|
|
* delete() retourne le nombre de lignes supprimées.
|
|
*/
|
|
public function testDeleteReturnsDeletedRowCount(): void
|
|
{
|
|
$stmt = $this->stmtForWrite(1);
|
|
$this->db->method('prepare')->willReturn($stmt);
|
|
|
|
$this->assertSame(1, $this->repository->delete(3));
|
|
}
|
|
|
|
/**
|
|
* delete() retourne 0 si la catégorie n'existait plus.
|
|
*/
|
|
public function testDeleteReturnsZeroWhenNotFound(): void
|
|
{
|
|
$stmt = $this->stmtForWrite(0);
|
|
$this->db->method('prepare')->willReturn($stmt);
|
|
|
|
$this->assertSame(0, $this->repository->delete(99));
|
|
}
|
|
|
|
|
|
// ── nameExists ─────────────────────────────────────────────────
|
|
|
|
/**
|
|
* nameExists() retourne true si le nom existe déjà.
|
|
*/
|
|
public function testNameExistsReturnsTrueWhenTaken(): void
|
|
{
|
|
$stmt = $this->stmtForScalar(1);
|
|
$this->db->method('prepare')->willReturn($stmt);
|
|
|
|
$this->assertTrue($this->repository->nameExists('PHP'));
|
|
}
|
|
|
|
/**
|
|
* nameExists() retourne false si le nom est disponible.
|
|
*/
|
|
public function testNameExistsReturnsFalseWhenFree(): void
|
|
{
|
|
$stmt = $this->stmtForScalar(false);
|
|
$this->db->method('prepare')->willReturn($stmt);
|
|
|
|
$this->assertFalse($this->repository->nameExists('Nouveau'));
|
|
}
|
|
|
|
/**
|
|
* nameExists() exécute avec le bon nom.
|
|
*/
|
|
public function testNameExistsQueriesWithCorrectName(): void
|
|
{
|
|
$stmt = $this->stmtForScalar(false);
|
|
$this->db->method('prepare')->willReturn($stmt);
|
|
|
|
$stmt->expects($this->once())
|
|
->method('execute')
|
|
->with([':name' => 'PHP']);
|
|
|
|
$this->repository->nameExists('PHP');
|
|
}
|
|
|
|
|
|
// ── hasPost ────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* hasPost() retourne true si au moins un article référence la catégorie.
|
|
*/
|
|
public function testHasPostReturnsTrueWhenPostAttached(): void
|
|
{
|
|
$stmt = $this->stmtForScalar(1);
|
|
$this->db->method('prepare')->willReturn($stmt);
|
|
|
|
$this->assertTrue($this->repository->hasPost(1));
|
|
}
|
|
|
|
/**
|
|
* hasPost() retourne false si aucun article ne référence la catégorie.
|
|
*/
|
|
public function testHasPostReturnsFalseWhenNoPost(): void
|
|
{
|
|
$stmt = $this->stmtForScalar(false);
|
|
$this->db->method('prepare')->willReturn($stmt);
|
|
|
|
$this->assertFalse($this->repository->hasPost(1));
|
|
}
|
|
|
|
/**
|
|
* hasPost() interroge la table 'posts' avec le bon category_id.
|
|
*/
|
|
public function testHasPostQueriesPostsTableWithCorrectId(): void
|
|
{
|
|
$stmt = $this->stmtForScalar(false);
|
|
|
|
$this->db->expects($this->once())
|
|
->method('prepare')
|
|
->with($this->stringContains('posts'))
|
|
->willReturn($stmt);
|
|
|
|
$stmt->expects($this->once())
|
|
->method('execute')
|
|
->with([':id' => 5]);
|
|
|
|
$this->repository->hasPost(5);
|
|
}
|
|
}
|