first commit
This commit is contained in:
505
tests/Post/PostControllerTest.php
Normal file
505
tests/Post/PostControllerTest.php
Normal file
@@ -0,0 +1,505 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Post;
|
||||
|
||||
use App\Category\Category;
|
||||
use App\Category\CategoryServiceInterface;
|
||||
use App\Post\Post;
|
||||
use App\Post\PostController;
|
||||
use App\Post\PostServiceInterface;
|
||||
use App\Shared\Exception\NotFoundException;
|
||||
use App\Shared\Http\FlashServiceInterface;
|
||||
use App\Shared\Http\SessionManagerInterface;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Slim\Exception\HttpNotFoundException;
|
||||
use Tests\ControllerTestCase;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour PostController.
|
||||
*
|
||||
* Couvre les 7 actions publiques :
|
||||
* - index() : page d'accueil, recherche FTS, filtre catégorie
|
||||
* - show() : article trouvé, article introuvable (404)
|
||||
* - admin() : admin/éditeur voit tout, utilisateur ordinaire voit le sien
|
||||
* - form() : formulaire nouveau, formulaire édition, droits insuffisants, 404
|
||||
* - create() : succès, erreur de validation, erreur inattendue
|
||||
* - update() : 404, droits insuffisants, succès, erreur de validation
|
||||
* - delete() : 404, droits insuffisants, succès
|
||||
*/
|
||||
final class PostControllerTest extends ControllerTestCase
|
||||
{
|
||||
/** @var \Slim\Views\Twig&MockObject */
|
||||
private \Slim\Views\Twig $view;
|
||||
|
||||
/** @var PostServiceInterface&MockObject */
|
||||
private PostServiceInterface $postService;
|
||||
|
||||
/** @var CategoryServiceInterface&MockObject */
|
||||
private CategoryServiceInterface $categoryService;
|
||||
|
||||
/** @var FlashServiceInterface&MockObject */
|
||||
private FlashServiceInterface $flash;
|
||||
|
||||
/** @var SessionManagerInterface&MockObject */
|
||||
private SessionManagerInterface $sessionManager;
|
||||
|
||||
private PostController $controller;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->view = $this->makeTwigMock();
|
||||
$this->postService = $this->createMock(PostServiceInterface::class);
|
||||
$this->categoryService = $this->createMock(CategoryServiceInterface::class);
|
||||
$this->flash = $this->createMock(FlashServiceInterface::class);
|
||||
$this->sessionManager = $this->createMock(SessionManagerInterface::class);
|
||||
|
||||
$this->categoryService->method('findAll')->willReturn([]);
|
||||
|
||||
$this->controller = new PostController(
|
||||
$this->view,
|
||||
$this->postService,
|
||||
$this->categoryService,
|
||||
$this->flash,
|
||||
$this->sessionManager,
|
||||
);
|
||||
}
|
||||
|
||||
// ── index ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* index() doit appeler getAllPosts() sans filtre par défaut.
|
||||
*/
|
||||
public function testIndexCallsGetAllPostsWithNoFilter(): void
|
||||
{
|
||||
$this->postService->expects($this->once())->method('getAllPosts')->with(null)->willReturn([]);
|
||||
$this->postService->expects($this->never())->method('searchPosts');
|
||||
|
||||
$res = $this->controller->index($this->makeGet('/'), $this->makeResponse());
|
||||
|
||||
$this->assertStatus($res, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* index() doit appeler searchPosts() quand un paramètre q est fourni.
|
||||
*/
|
||||
public function testIndexCallsSearchPostsWhenQueryParamPresent(): void
|
||||
{
|
||||
$this->postService->expects($this->once())
|
||||
->method('searchPosts')
|
||||
->with('php', null)
|
||||
->willReturn([]);
|
||||
$this->postService->expects($this->never())->method('getAllPosts');
|
||||
|
||||
$this->controller->index($this->makeGet('/', ['q' => 'php']), $this->makeResponse());
|
||||
}
|
||||
|
||||
/**
|
||||
* index() doit résoudre le slug de catégorie et filtrer si le paramètre categorie est fourni.
|
||||
*/
|
||||
public function testIndexFiltersByCategoryWhenSlugProvided(): void
|
||||
{
|
||||
$category = new Category(3, 'PHP', 'php');
|
||||
$this->categoryService->method('findBySlug')->with('php')->willReturn($category);
|
||||
|
||||
$this->postService->expects($this->once())
|
||||
->method('getAllPosts')
|
||||
->with(3)
|
||||
->willReturn([]);
|
||||
|
||||
$this->controller->index(
|
||||
$this->makeGet('/', ['categorie' => 'php']),
|
||||
$this->makeResponse(),
|
||||
);
|
||||
}
|
||||
|
||||
// ── show ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* show() doit rendre la vue de détail si l'article est trouvé.
|
||||
*/
|
||||
public function testShowRendersDetailView(): void
|
||||
{
|
||||
$post = $this->buildPostEntity(1, 'Titre', 'Contenu', 'titre', 1);
|
||||
$this->postService->method('getPostBySlug')->willReturn($post);
|
||||
|
||||
$this->view->expects($this->once())
|
||||
->method('render')
|
||||
->with($this->anything(), 'pages/post/detail.twig', $this->anything())
|
||||
->willReturnArgument(0);
|
||||
|
||||
$res = $this->controller->show(
|
||||
$this->makeGet('/article/titre'),
|
||||
$this->makeResponse(),
|
||||
['slug' => 'titre'],
|
||||
);
|
||||
|
||||
$this->assertStatus($res, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* show() doit lancer HttpNotFoundException si l'article est introuvable.
|
||||
*/
|
||||
public function testShowThrowsHttpNotFoundWhenPostMissing(): void
|
||||
{
|
||||
$this->postService->method('getPostBySlug')
|
||||
->willThrowException(new NotFoundException('Article', 'missing'));
|
||||
|
||||
$this->expectException(HttpNotFoundException::class);
|
||||
|
||||
$this->controller->show(
|
||||
$this->makeGet('/article/missing'),
|
||||
$this->makeResponse(),
|
||||
['slug' => 'missing'],
|
||||
);
|
||||
}
|
||||
|
||||
// ── admin ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* admin() doit appeler getAllPosts() pour un administrateur.
|
||||
*/
|
||||
public function testAdminCallsGetAllPostsForAdmin(): void
|
||||
{
|
||||
$this->sessionManager->method('isAdmin')->willReturn(true);
|
||||
$this->sessionManager->method('isEditor')->willReturn(false);
|
||||
|
||||
$this->postService->expects($this->once())->method('getAllPosts')->willReturn([]);
|
||||
$this->postService->expects($this->never())->method('getPostsByUserId');
|
||||
|
||||
$res = $this->controller->admin($this->makeGet('/admin/posts'), $this->makeResponse());
|
||||
|
||||
$this->assertStatus($res, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* admin() doit appeler getPostsByUserId() pour un utilisateur ordinaire.
|
||||
*/
|
||||
public function testAdminCallsGetPostsByUserIdForRegularUser(): void
|
||||
{
|
||||
$this->sessionManager->method('isAdmin')->willReturn(false);
|
||||
$this->sessionManager->method('isEditor')->willReturn(false);
|
||||
$this->sessionManager->method('getUserId')->willReturn(5);
|
||||
|
||||
$this->postService->expects($this->once())->method('getPostsByUserId')->with(5, null)->willReturn([]);
|
||||
$this->postService->expects($this->never())->method('getAllPosts');
|
||||
|
||||
$this->controller->admin($this->makeGet('/admin/posts'), $this->makeResponse());
|
||||
}
|
||||
|
||||
/**
|
||||
* admin() doit passer authorId = null à searchPosts() pour un admin (recherche globale).
|
||||
*/
|
||||
public function testAdminSearchPassesNullAuthorIdForAdmin(): void
|
||||
{
|
||||
$this->sessionManager->method('isAdmin')->willReturn(true);
|
||||
$this->sessionManager->method('isEditor')->willReturn(false);
|
||||
|
||||
$this->postService->expects($this->once())
|
||||
->method('searchPosts')
|
||||
->with('php', null, null)
|
||||
->willReturn([]);
|
||||
|
||||
$this->controller->admin(
|
||||
$this->makeGet('/admin/posts', ['q' => 'php']),
|
||||
$this->makeResponse(),
|
||||
);
|
||||
}
|
||||
|
||||
// ── form ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* form() doit rendre le formulaire vide pour un nouvel article (id = 0).
|
||||
*/
|
||||
public function testFormRendersEmptyFormForNewPost(): void
|
||||
{
|
||||
$this->view->expects($this->once())
|
||||
->method('render')
|
||||
->with($this->anything(), 'admin/posts/form.twig', $this->callback(
|
||||
fn (array $d) => $d['post'] === null && $d['action'] === '/admin/posts/create'
|
||||
))
|
||||
->willReturnArgument(0);
|
||||
|
||||
$this->controller->form(
|
||||
$this->makeGet('/admin/posts/edit/0'),
|
||||
$this->makeResponse(),
|
||||
['id' => '0'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* form() doit rendre le formulaire pré-rempli pour un article existant dont l'utilisateur est auteur.
|
||||
*/
|
||||
public function testFormRendersFilledFormWhenUserIsAuthor(): void
|
||||
{
|
||||
$post = $this->buildPostEntity(7, 'Titre', 'Contenu', 'titre', 5);
|
||||
$this->postService->method('getPostById')->with(7)->willReturn($post);
|
||||
$this->sessionManager->method('isAdmin')->willReturn(false);
|
||||
$this->sessionManager->method('isEditor')->willReturn(false);
|
||||
$this->sessionManager->method('getUserId')->willReturn(5);
|
||||
|
||||
$this->view->expects($this->once())
|
||||
->method('render')
|
||||
->with($this->anything(), 'admin/posts/form.twig', $this->callback(
|
||||
fn (array $d) => $d['post'] === $post && $d['action'] === '/admin/posts/edit/7'
|
||||
))
|
||||
->willReturnArgument(0);
|
||||
|
||||
$this->controller->form(
|
||||
$this->makeGet('/admin/posts/edit/7'),
|
||||
$this->makeResponse(),
|
||||
['id' => '7'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* form() doit rediriger avec une erreur si l'utilisateur n'est pas l'auteur.
|
||||
*/
|
||||
public function testFormRedirectsWhenUserCannotEditPost(): void
|
||||
{
|
||||
$post = $this->buildPostEntity(7, 'Titre', 'Contenu', 'titre', 10); // auteur = 10
|
||||
$this->postService->method('getPostById')->willReturn($post);
|
||||
$this->sessionManager->method('isAdmin')->willReturn(false);
|
||||
$this->sessionManager->method('isEditor')->willReturn(false);
|
||||
$this->sessionManager->method('getUserId')->willReturn(5); // connecté = 5
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('post_error', $this->stringContains("n'êtes pas l'auteur"));
|
||||
|
||||
$res = $this->controller->form(
|
||||
$this->makeGet('/admin/posts/edit/7'),
|
||||
$this->makeResponse(),
|
||||
['id' => '7'],
|
||||
);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/posts');
|
||||
}
|
||||
|
||||
/**
|
||||
* form() doit lancer HttpNotFoundException si l'article est introuvable.
|
||||
*/
|
||||
public function testFormThrowsHttpNotFoundWhenPostMissing(): void
|
||||
{
|
||||
$this->postService->method('getPostById')
|
||||
->willThrowException(new NotFoundException('Article', 99));
|
||||
|
||||
$this->expectException(HttpNotFoundException::class);
|
||||
|
||||
$this->controller->form(
|
||||
$this->makeGet('/admin/posts/edit/99'),
|
||||
$this->makeResponse(),
|
||||
['id' => '99'],
|
||||
);
|
||||
}
|
||||
|
||||
// ── create ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* create() doit flasher un succès et rediriger vers /admin/posts en cas de succès.
|
||||
*/
|
||||
public function testCreateRedirectsToAdminPostsOnSuccess(): void
|
||||
{
|
||||
$this->sessionManager->method('getUserId')->willReturn(1);
|
||||
$this->postService->method('createPost')->willReturn(0);
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('post_success', $this->stringContains('créé'));
|
||||
|
||||
$req = $this->makePost('/admin/posts/create', ['title' => 'Mon titre', 'content' => 'Contenu']);
|
||||
$res = $this->controller->create($req, $this->makeResponse());
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/posts');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit flasher une erreur et rediriger vers le formulaire si la validation échoue.
|
||||
*/
|
||||
public function testCreateRedirectsToFormOnValidationError(): void
|
||||
{
|
||||
$this->postService->method('createPost')
|
||||
->willThrowException(new \InvalidArgumentException('Titre vide'));
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('post_error', 'Titre vide');
|
||||
|
||||
$req = $this->makePost('/admin/posts/create', ['title' => '', 'content' => 'x']);
|
||||
$res = $this->controller->create($req, $this->makeResponse());
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/posts/edit/0');
|
||||
}
|
||||
|
||||
/**
|
||||
* create() doit flasher une erreur générique et rediriger vers le formulaire en cas d'exception inattendue.
|
||||
*/
|
||||
public function testCreateRedirectsToFormOnUnexpectedError(): void
|
||||
{
|
||||
$this->postService->method('createPost')
|
||||
->willThrowException(new \RuntimeException('DB error'));
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('post_error', $this->stringContains('inattendue'));
|
||||
|
||||
$req = $this->makePost('/admin/posts/create', ['title' => 'T', 'content' => 'C']);
|
||||
$res = $this->controller->create($req, $this->makeResponse());
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/posts/edit/0');
|
||||
}
|
||||
|
||||
// ── update ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* update() doit lancer HttpNotFoundException si l'article est introuvable.
|
||||
*/
|
||||
public function testUpdateThrowsHttpNotFoundWhenPostMissing(): void
|
||||
{
|
||||
$this->postService->method('getPostById')
|
||||
->willThrowException(new NotFoundException('Article', 42));
|
||||
|
||||
$this->expectException(HttpNotFoundException::class);
|
||||
|
||||
$req = $this->makePost('/admin/posts/edit/42', ['title' => 'T', 'content' => 'C', 'slug' => 's']);
|
||||
$this->controller->update($req, $this->makeResponse(), ['id' => '42']);
|
||||
}
|
||||
|
||||
/**
|
||||
* update() doit rediriger avec une erreur si l'utilisateur n'est pas l'auteur.
|
||||
*/
|
||||
public function testUpdateRedirectsWhenUserCannotEditPost(): void
|
||||
{
|
||||
$post = $this->buildPostEntity(7, 'Titre', 'Contenu', 'titre', 10);
|
||||
$this->postService->method('getPostById')->willReturn($post);
|
||||
$this->sessionManager->method('isAdmin')->willReturn(false);
|
||||
$this->sessionManager->method('isEditor')->willReturn(false);
|
||||
$this->sessionManager->method('getUserId')->willReturn(5);
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('post_error', $this->stringContains("n'êtes pas l'auteur"));
|
||||
|
||||
$req = $this->makePost('/admin/posts/edit/7', ['title' => 'T', 'content' => 'C', 'slug' => 's']);
|
||||
$res = $this->controller->update($req, $this->makeResponse(), ['id' => '7']);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/posts');
|
||||
}
|
||||
|
||||
/**
|
||||
* update() doit rediriger vers /admin/posts avec un message de succès.
|
||||
*/
|
||||
public function testUpdateRedirectsToAdminPostsOnSuccess(): void
|
||||
{
|
||||
$post = $this->buildPostEntity(7, 'Titre', 'Contenu', 'titre', 5);
|
||||
$this->postService->method('getPostById')->willReturn($post);
|
||||
$this->sessionManager->method('isAdmin')->willReturn(true);
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('post_success', $this->stringContains('modifié'));
|
||||
|
||||
$req = $this->makePost('/admin/posts/edit/7', ['title' => 'Nouveau', 'content' => 'Contenu', 'slug' => 'nouveau']);
|
||||
$res = $this->controller->update($req, $this->makeResponse(), ['id' => '7']);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/posts');
|
||||
}
|
||||
|
||||
/**
|
||||
* update() doit rediriger vers le formulaire avec une erreur en cas d'InvalidArgumentException.
|
||||
*/
|
||||
public function testUpdateRedirectsToFormOnValidationError(): void
|
||||
{
|
||||
$post = $this->buildPostEntity(7, 'Titre', 'Contenu', 'titre', 5);
|
||||
$this->postService->method('getPostById')->willReturn($post);
|
||||
$this->sessionManager->method('isAdmin')->willReturn(true);
|
||||
$this->postService->method('updatePost')
|
||||
->willThrowException(new \InvalidArgumentException('Titre invalide'));
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('post_error', 'Titre invalide');
|
||||
|
||||
$req = $this->makePost('/admin/posts/edit/7', ['title' => '', 'content' => 'C', 'slug' => 's']);
|
||||
$res = $this->controller->update($req, $this->makeResponse(), ['id' => '7']);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/posts/edit/7');
|
||||
}
|
||||
|
||||
// ── delete ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* delete() doit lancer HttpNotFoundException si l'article est introuvable.
|
||||
*/
|
||||
public function testDeleteThrowsHttpNotFoundWhenPostMissing(): void
|
||||
{
|
||||
$this->postService->method('getPostById')
|
||||
->willThrowException(new NotFoundException('Article', 42));
|
||||
|
||||
$this->expectException(HttpNotFoundException::class);
|
||||
|
||||
$this->controller->delete(
|
||||
$this->makePost('/admin/posts/delete/42'),
|
||||
$this->makeResponse(),
|
||||
['id' => '42'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* delete() doit rediriger avec une erreur si l'utilisateur n'est pas l'auteur.
|
||||
*/
|
||||
public function testDeleteRedirectsWhenUserCannotDeletePost(): void
|
||||
{
|
||||
$post = $this->buildPostEntity(7, 'Titre', 'Contenu', 'titre', 10);
|
||||
$this->postService->method('getPostById')->willReturn($post);
|
||||
$this->sessionManager->method('isAdmin')->willReturn(false);
|
||||
$this->sessionManager->method('isEditor')->willReturn(false);
|
||||
$this->sessionManager->method('getUserId')->willReturn(5);
|
||||
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('post_error', $this->stringContains("n'êtes pas l'auteur"));
|
||||
|
||||
$res = $this->controller->delete(
|
||||
$this->makePost('/admin/posts/delete/7'),
|
||||
$this->makeResponse(),
|
||||
['id' => '7'],
|
||||
);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/posts');
|
||||
}
|
||||
|
||||
/**
|
||||
* delete() doit appeler deletePost() et rediriger avec succès pour un auteur.
|
||||
*/
|
||||
public function testDeleteRedirectsWithSuccessForAuthor(): void
|
||||
{
|
||||
$post = $this->buildPostEntity(7, 'Mon article', 'Contenu', 'mon-article', 5);
|
||||
$this->postService->method('getPostById')->willReturn($post);
|
||||
$this->sessionManager->method('isAdmin')->willReturn(false);
|
||||
$this->sessionManager->method('isEditor')->willReturn(false);
|
||||
$this->sessionManager->method('getUserId')->willReturn(5);
|
||||
|
||||
$this->postService->expects($this->once())->method('deletePost')->with(7);
|
||||
$this->flash->expects($this->once())->method('set')
|
||||
->with('post_success', $this->stringContains('Mon article'));
|
||||
|
||||
$res = $this->controller->delete(
|
||||
$this->makePost('/admin/posts/delete/7'),
|
||||
$this->makeResponse(),
|
||||
['id' => '7'],
|
||||
);
|
||||
|
||||
$this->assertRedirectTo($res, '/admin/posts');
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Crée une entité Post de test avec les paramètres minimaux.
|
||||
*
|
||||
* Nommé buildPostEntity (et non makePost) pour ne pas masquer
|
||||
* ControllerTestCase::makePost() qui forge une requête HTTP.
|
||||
*/
|
||||
private function buildPostEntity(
|
||||
int $id,
|
||||
string $title,
|
||||
string $content,
|
||||
string $slug,
|
||||
?int $authorId,
|
||||
): Post {
|
||||
return new Post($id, $title, $content, $slug, $authorId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user