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