*/ 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); } }