first commit
This commit is contained in:
51
tests/Taxonomy/TaxonModelTest.php
Normal file
51
tests/Taxonomy/TaxonModelTest.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Taxonomy;
|
||||
|
||||
use Netig\Netslim\Taxonomy\Domain\Entity\Taxon;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
|
||||
final class TaxonModelTest extends TestCase
|
||||
{
|
||||
public function testConstructAndGettersExposeTaxonData(): void
|
||||
{
|
||||
$taxon = new Taxon(4, 'PHP', 'php');
|
||||
|
||||
self::assertSame(4, $taxon->getId());
|
||||
self::assertSame('PHP', $taxon->getName());
|
||||
self::assertSame('php', $taxon->getSlug());
|
||||
}
|
||||
|
||||
public function testFromArrayHydratesTaxon(): void
|
||||
{
|
||||
$taxon = Taxon::fromArray([
|
||||
'id' => '6',
|
||||
'name' => 'Tests',
|
||||
'slug' => 'tests',
|
||||
]);
|
||||
|
||||
self::assertSame(6, $taxon->getId());
|
||||
self::assertSame('Tests', $taxon->getName());
|
||||
self::assertSame('tests', $taxon->getSlug());
|
||||
}
|
||||
|
||||
public function testValidationRejectsEmptyName(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Le nom du terme ne peut pas être vide');
|
||||
|
||||
new Taxon(1, '', 'slug');
|
||||
}
|
||||
|
||||
public function testValidationRejectsTooLongName(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Le nom du terme ne peut pas dépasser 100 caractères');
|
||||
|
||||
new Taxon(1, str_repeat('a', 101), 'slug');
|
||||
}
|
||||
}
|
||||
336
tests/Taxonomy/TaxonRepositoryTest.php
Normal file
336
tests/Taxonomy/TaxonRepositoryTest.php
Normal file
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Taxonomy;
|
||||
|
||||
use Netig\Netslim\Taxonomy\Domain\Entity\Taxon;
|
||||
use Netig\Netslim\Taxonomy\Infrastructure\PdoTaxonRepository;
|
||||
use PDO;
|
||||
use PDOStatement;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour PdoTaxonRepository.
|
||||
*
|
||||
* 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 TaxonRepositoryTest extends TestCase
|
||||
{
|
||||
/** @var PDO&MockObject */
|
||||
private PDO $db;
|
||||
|
||||
private PdoTaxonRepository $repository;
|
||||
|
||||
/**
|
||||
* Données représentant une ligne taxon 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 PdoTaxonRepository($this->db);
|
||||
|
||||
$this->rowPhp = [
|
||||
'id' => 1,
|
||||
'name' => 'PHP',
|
||||
'slug' => 'php',
|
||||
];
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────
|
||||
|
||||
private function stmtForRead(array $rows = [], array|false $row = false): MockObject&PDOStatement
|
||||
{
|
||||
$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): MockObject&PDOStatement
|
||||
{
|
||||
$stmt = $this->createMock(PDOStatement::class);
|
||||
$stmt->method('execute')->willReturn(true);
|
||||
$stmt->method('fetchColumn')->willReturn($value);
|
||||
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
private function stmtForWrite(int $rowCount = 1): MockObject&PDOStatement
|
||||
{
|
||||
$stmt = $this->createMock(PDOStatement::class);
|
||||
$stmt->method('execute')->willReturn(true);
|
||||
$stmt->method('rowCount')->willReturn($rowCount);
|
||||
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
|
||||
// ── findAll ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* findAll() retourne un tableau vide si aucun taxon n'existe.
|
||||
*/
|
||||
public function testFindAllReturnsEmptyArrayWhenNone(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead([]);
|
||||
$this->db->method('query')->willReturn($stmt);
|
||||
|
||||
$this->assertSame([], $this->repository->findAll());
|
||||
}
|
||||
|
||||
/**
|
||||
* findAll() retourne des instances Taxon hydratées.
|
||||
*/
|
||||
public function testFindAllReturnsTaxonInstances(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead([$this->rowPhp]);
|
||||
$this->db->method('query')->willReturn($stmt);
|
||||
|
||||
$result = $this->repository->findAll();
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertInstanceOf(Taxon::class, $result[0]);
|
||||
$this->assertSame('PHP', $result[0]->getName());
|
||||
$this->assertSame('php', $result[0]->getSlug());
|
||||
}
|
||||
|
||||
/**
|
||||
* findAll() interroge bien la table historique `categories`.
|
||||
*/
|
||||
public function testFindAllRequestsCategoriesQuery(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead([]);
|
||||
|
||||
$this->db->expects($this->once())
|
||||
->method('query')
|
||||
->with($this->stringContains('FROM categories'))
|
||||
->willReturn($stmt);
|
||||
|
||||
$this->repository->findAll();
|
||||
}
|
||||
|
||||
|
||||
// ── findById ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* findById() retourne null si le taxon est absent.
|
||||
*/
|
||||
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() retourne une instance Taxon si le taxon existe.
|
||||
*/
|
||||
public function testFindByIdReturnsTaxonWhenFound(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead(row: $this->rowPhp);
|
||||
$this->db->method('prepare')->willReturn($stmt);
|
||||
|
||||
$result = $this->repository->findById(1);
|
||||
|
||||
$this->assertInstanceOf(Taxon::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($this->callback(fn (array $params): bool => in_array(42, $params, true)));
|
||||
|
||||
$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 Taxon si le slug existe.
|
||||
*/
|
||||
public function testFindBySlugReturnsTaxonWhenFound(): void
|
||||
{
|
||||
$stmt = $this->stmtForRead(row: $this->rowPhp);
|
||||
$this->db->method('prepare')->willReturn($stmt);
|
||||
|
||||
$result = $this->repository->findBySlug('php');
|
||||
|
||||
$this->assertInstanceOf(Taxon::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($this->callback(fn (array $params): bool => in_array('php', $params, true)));
|
||||
|
||||
$this->repository->findBySlug('php');
|
||||
}
|
||||
|
||||
|
||||
// ── create ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* create() prépare un INSERT avec le nom et le slug du taxon.
|
||||
*/
|
||||
public function testCreateCallsInsertWithNameAndSlug(): void
|
||||
{
|
||||
$taxon = Taxon::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'] === $taxon->getName()
|
||||
&& $data[':slug'] === $taxon->getSlug(),
|
||||
));
|
||||
|
||||
$this->db->method('lastInsertId')->willReturn('1');
|
||||
|
||||
$this->repository->create($taxon);
|
||||
}
|
||||
|
||||
/**
|
||||
* create() retourne l'identifiant généré par la base de données.
|
||||
*/
|
||||
public function testCreateReturnsGeneratedId(): void
|
||||
{
|
||||
$taxon = Taxon::fromArray($this->rowPhp);
|
||||
$stmt = $this->stmtForWrite();
|
||||
$this->db->method('prepare')->willReturn($stmt);
|
||||
$this->db->method('lastInsertId')->willReturn('7');
|
||||
|
||||
$this->assertSame(7, $this->repository->create($taxon));
|
||||
}
|
||||
|
||||
|
||||
// ── 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($this->callback(fn (array $params): bool => in_array(3, $params, true)));
|
||||
|
||||
$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 le taxon 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($this->callback(fn (array $params): bool => in_array('PHP', $params, true)));
|
||||
|
||||
$this->repository->nameExists('PHP');
|
||||
}
|
||||
}
|
||||
201
tests/Taxonomy/TaxonomyControllerTest.php
Normal file
201
tests/Taxonomy/TaxonomyControllerTest.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Taxonomy;
|
||||
|
||||
use Netig\Netslim\Kernel\Http\Application\Flash\FlashServiceInterface;
|
||||
use Netig\Netslim\Kernel\Pagination\Application\PaginatedResult;
|
||||
use Netig\Netslim\Taxonomy\Application\TaxonomyServiceInterface;
|
||||
use Netig\Netslim\Taxonomy\Domain\Entity\Taxon;
|
||||
use Netig\Netslim\Taxonomy\UI\Http\TaxonomyController;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Tests\ControllerTestBase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour TaxonomyController.
|
||||
*
|
||||
* Couvre index(), create() et delete() :
|
||||
* rendu de la liste, création réussie, erreur de création,
|
||||
* suppression avec taxon introuvable, succès et erreur métier.
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class TaxonomyControllerTest extends ControllerTestBase
|
||||
{
|
||||
/** @var \Slim\Views\Twig&MockObject */
|
||||
private \Slim\Views\Twig $view;
|
||||
|
||||
/** @var TaxonomyServiceInterface&MockObject */
|
||||
private TaxonomyServiceInterface $taxonomyService;
|
||||
|
||||
/** @var FlashServiceInterface&MockObject */
|
||||
private FlashServiceInterface $flash;
|
||||
|
||||
private TaxonomyController $controller;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->view = $this->makeTwigMock();
|
||||
$this->taxonomyService = $this->createMock(TaxonomyServiceInterface::class);
|
||||
$this->flash = $this->createMock(FlashServiceInterface::class);
|
||||
|
||||
$this->controller = new TaxonomyController(
|
||||
$this->view,
|
||||
$this->taxonomyService,
|
||||
$this->flash,
|
||||
);
|
||||
}
|
||||
|
||||
// ── index ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* index() doit rendre la vue avec la liste des taxons.
|
||||
*/
|
||||
public function testIndexRendersWithCategories(): void
|
||||
{
|
||||
$this->taxonomyService->method('findPaginated')->willReturn(new PaginatedResult([], 0, 1, 20));
|
||||
|
||||
$this->view->expects($this->once())
|
||||
->method('render')
|
||||
->with($this->anything(), '@Taxonomy/admin/index.twig', $this->anything())
|
||||
->willReturnArgument(0);
|
||||
|
||||
$res = $this->controller->index($this->makeGet('/admin/categories'), $this->makeResponse());
|
||||
|
||||
$this->assertStatus($res, 200);
|
||||
}
|
||||
|
||||
// ── create ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* create() doit flasher un succès et rediriger en cas de création réussie.
|
||||
*/
|
||||
public function testCreateRedirectsWithSuccessFlash(): void
|
||||
{
|
||||
$this->taxonomyService->method('create')->willReturn(1);
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('taxonomy_success', $this->stringContains('PHP'));
|
||||
|
||||
$req = $this->makePost('/admin/categories/create', ['name' => 'PHP']);
|
||||
$res = $this->controller->create($req, $this->makeResponse());
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/categories');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit flasher une erreur si le service lève une InvalidArgumentException.
|
||||
*/
|
||||
public function testCreateRedirectsWithErrorOnInvalidArgument(): void
|
||||
{
|
||||
$this->taxonomyService->method('create')
|
||||
->willThrowException(new \InvalidArgumentException('Ce terme existe déjà'));
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('taxonomy_error', 'Ce terme existe déjà');
|
||||
|
||||
$req = $this->makePost('/admin/categories/create', ['name' => 'Duplicate']);
|
||||
$res = $this->controller->create($req, $this->makeResponse());
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/categories');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit flasher une erreur générique pour toute autre exception.
|
||||
*/
|
||||
public function testCreateRedirectsWithGenericErrorOnUnexpectedException(): void
|
||||
{
|
||||
$this->taxonomyService->method('create')
|
||||
->willThrowException(new \RuntimeException('DB error'));
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('taxonomy_error', $this->stringContains('inattendue'));
|
||||
|
||||
$req = $this->makePost('/admin/categories/create', ['name' => 'PHP']);
|
||||
$res = $this->controller->create($req, $this->makeResponse());
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/categories');
|
||||
}
|
||||
|
||||
// ── delete ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* delete() doit flasher une erreur et rediriger si le taxon est introuvable.
|
||||
*/
|
||||
public function testDeleteRedirectsWithErrorWhenNotFound(): void
|
||||
{
|
||||
$this->taxonomyService->method('findById')->willReturn(null);
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('taxonomy_error', 'Terme de taxonomie introuvable');
|
||||
|
||||
$res = $this->controller->delete(
|
||||
$this->makePost('/admin/categories/delete/99'),
|
||||
$this->makeResponse(),
|
||||
['id' => '99'],
|
||||
);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/categories');
|
||||
}
|
||||
|
||||
/**
|
||||
* delete() doit flasher un succès et rediriger en cas de suppression réussie.
|
||||
*/
|
||||
public function testDeleteRedirectsWithSuccessFlash(): void
|
||||
{
|
||||
$category = new Taxon(3, 'PHP', 'php');
|
||||
$this->taxonomyService->method('findById')->willReturn($category);
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('taxonomy_success', $this->stringContains('PHP'));
|
||||
|
||||
$res = $this->controller->delete(
|
||||
$this->makePost('/admin/categories/delete/3'),
|
||||
$this->makeResponse(),
|
||||
['id' => '3'],
|
||||
);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/categories');
|
||||
}
|
||||
|
||||
/**
|
||||
* delete() doit flasher une erreur si le service refuse la suppression
|
||||
* (ex: des contenus sont rattachés au taxon).
|
||||
*/
|
||||
public function testDeleteRedirectsWithErrorWhenServiceRefuses(): void
|
||||
{
|
||||
$category = new Taxon(3, 'PHP', 'php');
|
||||
$this->taxonomyService->method('findById')->willReturn($category);
|
||||
$this->taxonomyService->method('delete')
|
||||
->willThrowException(new \InvalidArgumentException('Le terme « PHP » est encore utilisé et ne peut pas être supprimé'));
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('taxonomy_error', 'Le terme « PHP » est encore utilisé et ne peut pas être supprimé');
|
||||
|
||||
$res = $this->controller->delete(
|
||||
$this->makePost('/admin/categories/delete/3'),
|
||||
$this->makeResponse(),
|
||||
['id' => '3'],
|
||||
);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/categories');
|
||||
}
|
||||
|
||||
/**
|
||||
* delete() doit passer l'identifiant de route au service findById().
|
||||
*/
|
||||
public function testDeletePassesCorrectIdToService(): void
|
||||
{
|
||||
$this->taxonomyService->expects($this->once())
|
||||
->method('findById')
|
||||
->with(7)
|
||||
->willReturn(null);
|
||||
|
||||
$this->flash->method('set');
|
||||
|
||||
$this->controller->delete(
|
||||
$this->makePost('/admin/categories/delete/7'),
|
||||
$this->makeResponse(),
|
||||
['id' => '7'],
|
||||
);
|
||||
}
|
||||
}
|
||||
52
tests/Taxonomy/TaxonomyServiceReaderTest.php
Normal file
52
tests/Taxonomy/TaxonomyServiceReaderTest.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Taxonomy;
|
||||
|
||||
use Netig\Netslim\Taxonomy\Application\TaxonomyServiceInterface;
|
||||
use Netig\Netslim\Taxonomy\Domain\Entity\Taxon;
|
||||
use Netig\Netslim\Taxonomy\Infrastructure\TaxonomyServiceReader;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class TaxonomyServiceReaderTest extends TestCase
|
||||
{
|
||||
/** @var TaxonomyServiceInterface&MockObject */
|
||||
private TaxonomyServiceInterface $taxonomyService;
|
||||
|
||||
private TaxonomyServiceReader $reader;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->taxonomyService = $this->createMock(TaxonomyServiceInterface::class);
|
||||
$this->reader = new TaxonomyServiceReader($this->taxonomyService);
|
||||
}
|
||||
|
||||
public function testFindAllMapsCategoriesToTaxonViews(): void
|
||||
{
|
||||
$this->taxonomyService->expects($this->once())
|
||||
->method('findAll')
|
||||
->willReturn([
|
||||
new Taxon(1, 'PHP', 'php'),
|
||||
new Taxon(2, 'Slim', 'slim'),
|
||||
]);
|
||||
|
||||
$taxons = $this->reader->findAll();
|
||||
|
||||
self::assertCount(2, $taxons);
|
||||
self::assertSame('PHP', $taxons[0]->name);
|
||||
self::assertSame('slim', $taxons[1]->slug);
|
||||
}
|
||||
|
||||
public function testFindBySlugReturnsNullWhenMissing(): void
|
||||
{
|
||||
$this->taxonomyService->expects($this->once())
|
||||
->method('findBySlug')
|
||||
->with('missing')
|
||||
->willReturn(null);
|
||||
|
||||
self::assertNull($this->reader->findBySlug('missing'));
|
||||
}
|
||||
}
|
||||
203
tests/Taxonomy/TaxonomyServiceTest.php
Normal file
203
tests/Taxonomy/TaxonomyServiceTest.php
Normal file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Taxonomy;
|
||||
|
||||
use Netig\Netslim\Taxonomy\Application\TaxonomyApplicationService;
|
||||
use Netig\Netslim\Taxonomy\Application\UseCase\CreateTaxon;
|
||||
use Netig\Netslim\Taxonomy\Application\UseCase\DeleteTaxon;
|
||||
use Netig\Netslim\Taxonomy\Contracts\TaxonUsageCheckerInterface;
|
||||
use Netig\Netslim\Taxonomy\Domain\Entity\Taxon;
|
||||
use Netig\Netslim\Taxonomy\Domain\Repository\TaxonRepositoryInterface;
|
||||
use Netig\Netslim\Taxonomy\Domain\Service\TaxonSlugGenerator;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour TaxonomyApplicationService.
|
||||
*
|
||||
* Vérifie la création (génération de slug, unicité du nom, validation du modèle)
|
||||
* et la suppression (blocage si le terme est encore utilisé).
|
||||
* Le repository est remplacé par un mock pour isoler le service.
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
||||
final class TaxonomyServiceTest extends TestCase
|
||||
{
|
||||
/** @var TaxonRepositoryInterface&MockObject */
|
||||
private TaxonRepositoryInterface $repository;
|
||||
|
||||
/** @var TaxonUsageCheckerInterface&MockObject */
|
||||
private TaxonUsageCheckerInterface $taxonUsageChecker;
|
||||
|
||||
private TaxonomyApplicationService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = $this->createMock(TaxonRepositoryInterface::class);
|
||||
$this->taxonUsageChecker = $this->createMock(TaxonUsageCheckerInterface::class);
|
||||
$this->service = new TaxonomyApplicationService(
|
||||
$this->repository,
|
||||
new CreateTaxon($this->repository, new TaxonSlugGenerator()),
|
||||
new DeleteTaxon($this->repository, $this->taxonUsageChecker),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// ── create ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* create() doit générer le slug depuis le nom et persister le terme.
|
||||
*/
|
||||
public function testCreateGeneratesSlugAndPersists(): void
|
||||
{
|
||||
$this->repository->method('nameExists')->willReturn(false);
|
||||
$this->repository->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(
|
||||
fn (Taxon $c) =>
|
||||
$c->getName() === 'Développement web'
|
||||
&& $c->getSlug() === 'developpement-web',
|
||||
))
|
||||
->willReturn(1);
|
||||
|
||||
$id = $this->service->create('Développement web');
|
||||
|
||||
$this->assertSame(1, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit trimmer le nom avant de générer le slug.
|
||||
*/
|
||||
public function testCreateTrimsName(): void
|
||||
{
|
||||
$this->repository->method('nameExists')->willReturn(false);
|
||||
$this->repository->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(fn (Taxon $c) => $c->getName() === 'PHP'))
|
||||
->willReturn(2);
|
||||
|
||||
$this->service->create(' PHP ');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit lever InvalidArgumentException si le slug généré est vide.
|
||||
*/
|
||||
public function testCreateNonAsciiNameThrowsException(): void
|
||||
{
|
||||
$this->repository->expects($this->never())->method('create');
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('slug URL valide');
|
||||
|
||||
$this->service->create('日本語');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit lever InvalidArgumentException si le nom est existe déjà.
|
||||
*/
|
||||
public function testCreateDuplicateNameThrowsException(): void
|
||||
{
|
||||
$this->repository->method('nameExists')->willReturn(true);
|
||||
$this->repository->expects($this->never())->method('create');
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('existe déjà');
|
||||
|
||||
$this->service->create('PHP');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit lever InvalidArgumentException si le nom est vide.
|
||||
*/
|
||||
public function testCreateEmptyNameThrowsException(): void
|
||||
{
|
||||
$this->repository->method('nameExists')->willReturn(false);
|
||||
$this->repository->expects($this->never())->method('create');
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
$this->service->create('');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit lever InvalidArgumentException si le nom dépasse 100 caractères.
|
||||
*/
|
||||
public function testCreateNameTooLongThrowsException(): void
|
||||
{
|
||||
$longName = str_repeat('a', 101);
|
||||
$this->repository->method('nameExists')->willReturn(false);
|
||||
$this->repository->expects($this->never())->method('create');
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
$this->service->create($longName);
|
||||
}
|
||||
|
||||
|
||||
// ── delete ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* delete() doit supprimer le terme s'il n'est pas utilisé.
|
||||
*/
|
||||
public function testDeleteSucceedsWhenTaxonIsUnused(): void
|
||||
{
|
||||
$taxon = new Taxon(5, 'PHP', 'php');
|
||||
|
||||
$this->taxonUsageChecker->expects($this->once())->method('isTaxonInUse')->with(5)->willReturn(false);
|
||||
$this->repository->expects($this->once())->method('delete')->with(5);
|
||||
|
||||
$this->service->delete($taxon);
|
||||
}
|
||||
|
||||
/**
|
||||
* delete() doit lever InvalidArgumentException si le terme est encore utilisé.
|
||||
*/
|
||||
public function testDeleteBlockedWhenTaxonIsStillUsed(): void
|
||||
{
|
||||
$taxon = new Taxon(5, 'PHP', 'php');
|
||||
|
||||
$this->taxonUsageChecker->expects($this->once())->method('isTaxonInUse')->with(5)->willReturn(true);
|
||||
$this->repository->expects($this->never())->method('delete');
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('est encore utilisé');
|
||||
|
||||
$this->service->delete($taxon);
|
||||
}
|
||||
|
||||
|
||||
// ── Lectures déléguées ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* findAll() doit déléguer au repository et retourner son résultat.
|
||||
*/
|
||||
public function testFindAllDelegatesToRepository(): void
|
||||
{
|
||||
$cats = [new Taxon(1, 'PHP', 'php'), new Taxon(2, 'CSS', 'css')];
|
||||
$this->repository->method('findAll')->willReturn($cats);
|
||||
|
||||
$this->assertSame($cats, $this->service->findAll());
|
||||
}
|
||||
|
||||
/**
|
||||
* findById() doit retourner null si le terme n'existe pas.
|
||||
*/
|
||||
public function testFindByIdReturnsNullWhenMissing(): void
|
||||
{
|
||||
$this->repository->method('findById')->willReturn(null);
|
||||
|
||||
$this->assertNull($this->service->findById(99));
|
||||
}
|
||||
|
||||
/**
|
||||
* findBySlug() doit retourner le terme correspondant.
|
||||
*/
|
||||
public function testFindBySlugReturnsTaxonWhenFound(): void
|
||||
{
|
||||
$taxon = new Taxon(3, 'PHP', 'php');
|
||||
$this->repository->expects($this->once())->method('findBySlug')->with('php')->willReturn($taxon);
|
||||
|
||||
$this->assertSame($taxon, $this->service->findBySlug('php'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user