repository = $this->createMock(PostRepositoryInterface::class); $this->sanitizer = $this->createMock(HtmlSanitizerInterface::class); $this->service = new PostApplicationService($this->repository, $this->sanitizer); } // ── Lectures déléguées ───────────────────────────────────────── /** * getAllPosts() délègue au repository. */ public function testGetAllPostsDelegatesToRepository(): void { $posts = [$this->makePost(1, 'Titre', 'slug-titre')]; $this->repository->method('findAll')->willReturn($posts); $this->assertSame($posts, $this->service->getAllPosts()); } /** * getRecentPosts() délègue au repository. */ public function testGetRecentPostsDelegatesToRepository(): void { $posts = [$this->makePost(1, 'Titre', 'slug-titre')]; $this->repository->method('findRecent')->willReturn($posts); $this->assertSame($posts, $this->service->getRecentPosts(5)); } /** * getPostsByUserId() délègue au repository. */ public function testGetPostsByUserIdDelegatesToRepository(): void { $posts = [$this->makePost(1, 'Titre', 'slug-titre')]; $this->repository->expects($this->once())->method('findByUserId')->with(3, null)->willReturn($posts); $this->assertSame($posts, $this->service->getPostsByUserId(3)); } /** * getPostBySlug() lève NotFoundException si l'article est introuvable. */ public function testGetPostBySlugThrowsNotFoundExceptionWhenMissing(): void { $this->repository->method('findBySlug')->willReturn(null); $this->expectException(NotFoundException::class); $this->service->getPostBySlug('slug-inexistant'); } /** * getPostBySlug() retourne l'article tel que stocké — la sanitisation * se fait à l'écriture (createPost / updatePost), pas à la lecture. */ public function testGetPostBySlugReturnsPost(): void { $post = $this->makePost(1, 'Titre', 'mon-slug', '
Contenu stocké
'); $this->repository->method('findBySlug')->willReturn($post); $result = $this->service->getPostBySlug('mon-slug'); $this->assertSame('Contenu stocké
', $result->getContent()); } /** * getPostById() lève NotFoundException si l'article est introuvable. */ public function testGetPostByIdThrowsNotFoundExceptionWhenMissing(): void { $this->repository->method('findById')->willReturn(null); $this->expectException(NotFoundException::class); $this->service->getPostById(999); } /** * getPostById() retourne l'article trouvé. */ public function testGetPostByIdReturnsPost(): void { $post = $this->makePost(7, 'Titre', 'slug-7'); $this->repository->expects($this->once())->method('findById')->with(7)->willReturn($post); self::assertSame($post, $this->service->getPostById(7)); } // ── createPost ───────────────────────────────────────────────── /** * createPost() lève InvalidArgumentException si le titre est vide. */ public function testCreatePostThrowsWhenTitleEmpty(): void { $this->expectException(\InvalidArgumentException::class); $this->service->createPost('', 'Contenu
', 1); } /** * createPost() lève InvalidArgumentException si le contenu est vide. */ public function testCreatePostThrowsWhenContentEmpty(): void { $this->sanitizer->method('sanitize')->willReturn(''); $this->expectException(\InvalidArgumentException::class); $this->service->createPost('Titre', '', 1); } /** * createPost() sanitise le contenu et délègue la persistance. */ public function testCreatePostSanitizesAndPersists(): void { $this->sanitizer->method('sanitize')->willReturn('Contenu sûr
'); $this->repository->method('findBySlug')->willReturn(null); $this->repository->expects($this->once())->method('create')->willReturn(42); $id = $this->service->createPost('Mon Titre', 'Contenu brut
', 1); $this->assertSame(42, $id); } // ── deletePost ───────────────────────────────────────────────── /** * deletePost() throws NotFoundException when the repository returns 0 affected rows. */ public function testDeletePostThrowsNotFoundExceptionWhenMissing(): void { $this->repository->method('delete')->willReturn(0); $this->expectException(NotFoundException::class); $this->service->deletePost(99); } /** * deletePost() délègue la suppression au repository si l'article existe. */ public function testDeletePostDelegatesToRepository(): void { $this->repository->expects($this->once())->method('delete')->with(5)->willReturn(1); $this->service->deletePost(5); } // ── searchPosts ──────────────────────────────────────────────── /** * searchPosts() délègue la recherche au repository. */ public function testSearchPostsDelegatesToRepository(): void { $posts = [$this->makePost(1, 'Résultat', 'resultat')]; $this->repository->expects($this->once())->method('search')->with('mot', null, null)->willReturn($posts); $this->assertSame($posts, $this->service->searchPosts('mot')); } // ── updatePost ───────────────────────────────────────────────── /** * updatePost() lève NotFoundException si l'article n'existe plus. */ public function testUpdatePostThrowsNotFoundExceptionWhenMissing(): void { $this->repository->method('findById')->willReturn(null); $this->expectException(NotFoundException::class); $this->service->updatePost(99, 'Titre', 'Contenu
'); } /** * updatePost() sanitise le contenu et met à jour l'article. */ public function testUpdatePostSanitizesAndUpdates(): void { $post = $this->makePost(1, 'Ancien titre', 'ancien-titre', 'Ancien contenu
'); $this->repository->method('findById')->willReturn($post); $this->sanitizer->method('sanitize')->willReturn('Nouveau contenu sûr
'); $this->repository->method('findBySlug')->willReturn(null); $this->repository->expects($this->once())->method('update')->willReturn(1); $this->service->updatePost(1, 'Nouveau titre', 'Nouveau contenu
'); } /** * updatePost() lève NotFoundException si la ligne disparaît entre lecture et écriture. */ public function testUpdatePostThrowsWhenRepositoryUpdateAffectsZeroRows(): void { $post = $this->makePost(3, 'Titre courant', 'titre-courant', 'Ancien contenu
'); $this->repository->expects($this->once())->method('findById')->with(3)->willReturn($post); $this->sanitizer->method('sanitize')->willReturn('Contenu sûr
'); $this->repository->expects($this->once())->method('update')->with(3, $this->isInstanceOf(Post::class), 'titre-courant', null)->willReturn(0); $this->expectException(NotFoundException::class); $this->service->updatePost(3, 'Titre courant', 'Contenu
'); } /** * updatePost() normalise et rend unique un slug personnalisé. */ public function testUpdatePostUsesNormalizedUniqueCustomSlug(): void { $current = $this->makePost(4, 'Titre courant', 'ancien-slug', 'Ancien contenu
'); $this->repository->expects($this->once())->method('findById')->with(4)->willReturn($current); $this->sanitizer->method('sanitize')->willReturn('Contenu sûr
'); $this->repository->expects($this->exactly(2)) ->method('slugExists') ->withAnyParameters() ->willReturnOnConsecutiveCalls(true, false); $this->repository->expects($this->once()) ->method('update') ->with(4, $this->isInstanceOf(Post::class), 'nouveau-slug-1', 2) ->willReturn(1); $this->service->updatePost(4, 'Titre courant', 'Contenu
', ' Nouveau slug !! ', 2); } // ── Helpers ──────────────────────────────────────────────────── /** * Crée un Post de test. */ private function makePost(int $id, string $title, string $slug, string $content = 'Contenu
'): Post { return new Post($id, $title, $content, $slug); } }