508 lines
20 KiB
PHP
508 lines
20 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Post;
|
|
|
|
use App\Category\Category;
|
|
use App\Category\CategoryServiceInterface;
|
|
use App\Post\Post;
|
|
use App\Post\Http\PostController;
|
|
use App\Post\PostServiceInterface;
|
|
use App\Shared\Exception\NotFoundException;
|
|
use App\Shared\Http\FlashServiceInterface;
|
|
use App\Shared\Http\SessionManagerInterface;
|
|
use App\Shared\Pagination\PaginatedResult;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use Slim\Exception\HttpNotFoundException;
|
|
use Tests\ControllerTestBase;
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
|
|
final class PostControllerTest extends ControllerTestBase
|
|
{
|
|
/** @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('getAllPostsPaginated')->with(1, 6, null)->willReturn(new PaginatedResult([], 0, 1, 6));
|
|
$this->postService->expects($this->never())->method('searchPostsPaginated');
|
|
|
|
$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('searchPostsPaginated')
|
|
->with('php', 1, 6, null)
|
|
->willReturn(new PaginatedResult([], 0, 1, 6));
|
|
$this->postService->expects($this->never())->method('getAllPostsPaginated');
|
|
|
|
$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->expects($this->once())->method('findBySlug')->with('php')->willReturn($category);
|
|
|
|
$this->postService->expects($this->once())
|
|
->method('getAllPostsPaginated')
|
|
->with(1, 6, 3)
|
|
->willReturn(new PaginatedResult([], 0, 1, 6));
|
|
|
|
$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('getAllPostsPaginated')->with(1, 12, null)->willReturn(new PaginatedResult([], 0, 1, 12));
|
|
$this->postService->expects($this->never())->method('getPostsByUserIdPaginated');
|
|
|
|
$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('getPostsByUserIdPaginated')->with(5, 1, 12, null)->willReturn(new PaginatedResult([], 0, 1, 12));
|
|
$this->postService->expects($this->never())->method('getAllPostsPaginated');
|
|
|
|
$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('searchPostsPaginated')
|
|
->with('php', 1, 12, null, null)
|
|
->willReturn(new PaginatedResult([], 0, 1, 12));
|
|
|
|
$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->expects($this->once())->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
|
|
* ControllerTestBase::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);
|
|
}
|
|
}
|