repository = new InMemoryMangaRepository(); $this->handler = new GetMangaChaptersHandler($this->repository); } public function testHandleThrowsExceptionWhenMangaNotFound(): void { $this->expectException(MangaNotFoundException::class); $query = new GetMangaChapters('non-existent-id'); $this->handler->handle($query); } public function testHandleReturnsEmptyListWhenNoChapters(): void { // Arrange $this->givenMangaExists('123'); // Act $query = new GetMangaChapters('123'); $response = $this->handler->handle($query); // Assert $this->assertEmpty($response->chapters); $this->assertEquals(0, $response->total); $this->assertEquals(1, $response->page); $this->assertEquals(20, $response->limit); $this->assertFalse($response->hasNextPage()); $this->assertFalse($response->hasPreviousPage()); } public function testHandleReturnsPaginatedChapters(): void { // Arrange $this->givenMangaExistsWithChapters('123', 25); // Act $query = new GetMangaChapters('123', page: 2, limit: 10); $response = $this->handler->handle($query); // Assert $this->assertCount(10, $response->chapters); $this->assertEquals(25, $response->total); $this->assertEquals(2, $response->page); $this->assertEquals(10, $response->limit); $this->assertTrue($response->hasNextPage()); $this->assertTrue($response->hasPreviousPage()); } public function testGroupsVolumeChaptersWithSharedPagesDirectory(): void { // Arrange $this->givenMangaExists('1'); $sharedDir = '/manga/vol1/'; foreach ([1, 2, 3] as $num) { $this->repository->addChapter('1', new Chapter( id: new ChapterId((string) $num), mangaId: new MangaId('1'), number: (float) $num, title: null, volume: 1, isVisible: true, pagesDirectory: $sharedDir, )); } // Act $response = $this->handler->handle(new GetMangaChapters('1')); // Assert $this->assertCount(1, $response->chapters); $this->assertEquals(1, $response->total); $item = $response->chapters[0]; $this->assertTrue($item->isVolumeGroup); $this->assertEquals('1-3', $item->volumeChaptersRange); $this->assertEquals(3, $item->volumeChapterCount); $this->assertEquals(1, $item->volume); $this->assertEquals(1.0, $item->number); } public function testGroupsSingleVolumeChapter(): void { // Arrange $this->givenMangaExists('1'); $this->repository->addChapter('1', new Chapter( id: new ChapterId('10'), mangaId: new MangaId('1'), number: 5.0, title: null, volume: 2, isVisible: true, pagesDirectory: '/manga/vol2/', )); // Act $response = $this->handler->handle(new GetMangaChapters('1')); // Assert $this->assertCount(1, $response->chapters); $item = $response->chapters[0]; $this->assertTrue($item->isVolumeGroup); $this->assertEquals('5', $item->volumeChaptersRange); $this->assertEquals(1, $item->volumeChapterCount); } public function testDoesNotGroupChaptersWithDistinctPagesDirectory(): void { // Arrange — 3 chapitres scrapés avec pagesDirectory distinctes, pas de volume $this->givenMangaExists('1'); foreach ([1, 2, 3] as $num) { $this->repository->addChapter('1', new Chapter( id: new ChapterId((string) $num), mangaId: new MangaId('1'), number: (float) $num, title: null, volume: null, isVisible: true, pagesDirectory: '/manga/ch'.$num.'/', )); } // Act $response = $this->handler->handle(new GetMangaChapters('1')); // Assert — 3 items distincts, aucun groupe $this->assertCount(3, $response->chapters); $this->assertEquals(3, $response->total); foreach ($response->chapters as $item) { $this->assertFalse($item->isVolumeGroup); } } public function testMixedNormalAndVolumeChapters(): void { // Arrange — 2 chapitres scrapés + 3 chapitres de volume importé $this->givenMangaExists('1'); // Chapitres scrapés (pagesDirectory individuel, pas de volume) foreach ([1, 2] as $num) { $this->repository->addChapter('1', new Chapter( id: new ChapterId((string) $num), mangaId: new MangaId('1'), number: (float) $num, title: null, volume: null, isVisible: true, pagesDirectory: '/manga/ch'.$num.'/', )); } // Volume importé — 3 chapitres avec même pagesDirectory $sharedDir = '/manga/vol1/'; foreach ([3, 4, 5] as $num) { $this->repository->addChapter('1', new Chapter( id: new ChapterId((string) ($num + 10)), mangaId: new MangaId('1'), number: (float) $num, title: null, volume: 1, isVisible: true, pagesDirectory: $sharedDir, )); } // Act $response = $this->handler->handle(new GetMangaChapters('1', sortOrder: 'asc')); // Assert — 2 chapitres normaux + 1 groupe = 3 items $this->assertCount(3, $response->chapters); $this->assertEquals(3, $response->total); // Les 2 premiers sont des chapitres normaux $this->assertFalse($response->chapters[0]->isVolumeGroup); $this->assertFalse($response->chapters[1]->isVolumeGroup); // Le 3e est un groupe de volume $volumeItem = $response->chapters[2]; $this->assertTrue($volumeItem->isVolumeGroup); $this->assertEquals('3-5', $volumeItem->volumeChaptersRange); $this->assertEquals(3, $volumeItem->volumeChapterCount); } protected function tearDown(): void { $this->repository->clear(); } private function givenMangaExists(string $id): void { $manga = $this->createManga($id); $this->repository->save($manga); } private function givenMangaExistsWithChapters(string $id, int $chapterCount): void { $this->givenMangaExists($id); // Note: We'll need to implement this in InMemoryMangaRepository $this->repository->addChaptersToManga($id, $chapterCount); } private function createManga(string $id): Manga { return new Manga( id: new MangaId($id), title: new MangaTitle('Test Manga'), slug: new MangaSlug('test-manga'), description: 'This is a test manga', author: 'Test Author', publicationYear: 2024, genres: ['Action', 'Adventure'], status: 'Ongoing', externalId: null, imageUrl: null, rating: null ); } }