repository = $this->createMock(PostRepositoryInterface::class); $this->sanitizer = $this->createMock(HtmlSanitizerInterface::class); $slugGenerator = new PostSlugGenerator(); $transactionManager = new class () implements TransactionManagerInterface { public function run(callable $operation): mixed { return $operation(); } }; $referenceExtractor = new class () implements PostMediaReferenceExtractorInterface { public function extractMediaIds(string $html): array { return []; } }; $usageRepository = new class () implements PostMediaUsageRepositoryInterface { public function countUsages(int $mediaId): int { return 0; } public function countUsagesByMediaIds(array $mediaIds): array { return []; } public function findUsages(int $mediaId, int $limit = 5): array { return []; } public function findUsagesByMediaIds(array $mediaIds, int $limit = 5): array { return []; } public function syncPostMedia(int $postId, array $mediaIds): void {} }; $this->service = new PostApplicationService( $this->repository, new CreatePost($this->repository, $this->sanitizer, $slugGenerator, $transactionManager, $referenceExtractor, $usageRepository), new UpdatePost($this->repository, $this->sanitizer, $slugGenerator, $transactionManager, $referenceExtractor, $usageRepository), new DeletePost($this->repository), ); } // ── Lectures déléguées ───────────────────────────────────────── /** * findAll() 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->findAll()); } /** * findRecent() 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->findRecent(5)); } /** * findByUserId() 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->findByUserId(3)); } /** * findBySlug() lève NotFoundException si l'article est introuvable. */ public function testGetPostBySlugThrowsNotFoundExceptionWhenMissing(): void { $this->repository->method('findBySlug')->willReturn(null); $this->expectException(NotFoundException::class); $this->service->findBySlug('slug-inexistant'); } /** * findBySlug() retourne l'article tel que stocké — la sanitisation * se fait à l'écriture (create / update), 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->findBySlug('mon-slug'); $this->assertSame('Contenu stocké
', $result->getContent()); } /** * findById() lève NotFoundException si l'article est introuvable. */ public function testGetPostByIdThrowsNotFoundExceptionWhenMissing(): void { $this->repository->method('findById')->willReturn(null); $this->expectException(NotFoundException::class); $this->service->findById(999); } /** * findById() 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->findById(7)); } // ── create ───────────────────────────────────────────────── /** * create() lève InvalidArgumentException si le titre est vide. */ public function testCreatePostThrowsWhenTitleEmpty(): void { $this->expectException(\InvalidArgumentException::class); $this->service->create('', 'Contenu
', 1); } /** * create() lève InvalidArgumentException si le contenu est vide. */ public function testCreatePostThrowsWhenContentEmpty(): void { $this->sanitizer->method('sanitize')->willReturn(''); $this->expectException(\InvalidArgumentException::class); $this->service->create('Titre', '', 1); } /** * create() 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->create('Mon Titre', 'Contenu brut
', 1); $this->assertSame(42, $id); } // ── delete ───────────────────────────────────────────────── /** * delete() 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->delete(99); } /** * delete() 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->delete(5); } // ── search ──────────────────────────────────────────────── /** * search() 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->search('mot')); } // ── update ───────────────────────────────────────────────── /** * update() 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->update(999, 'Titre', 'Contenu
'); } private function makePost(int $id, string $title, string $slug, string $content = 'Contenu
'): Post { return new Post($id, $title, $content, $slug); } }