repository = new InMemoryMangaRepository(); $this->filenameAnalyzer = new FilenameAnalyzer(); $this->handler = new FindMangaMatchByFilenameHandler( $this->filenameAnalyzer, $this->repository ); } public function testItFindsExactMatchByTitle(): void { // Given $manga = $this->createManga( id: '123', title: 'One Piece', slug: 'one-piece' ); $this->repository->save($manga); // When $query = new FindMangaMatchByFilename('one-piece_vol108_ch1094.cbz'); $response = $this->handler->handle($query); // Then $this->assertTrue($response->hasMatches()); $this->assertCount(1, $response->matches); $match = $response->matches[0]; $this->assertEquals('123', $match->id); $this->assertEquals('One Piece', $match->title); $this->assertEquals('one-piece', $match->slug); $this->assertEquals(1094.0, $match->chapterNumber); $this->assertEquals(108, $match->volumeNumber); // Vérifier aussi dans la réponse globale $this->assertEquals(1094.0, $response->chapterNumber); $this->assertEquals(108, $response->volumeNumber); $this->assertNotEmpty($response->possibleTitles); } public function testItReturnsEmptyMatchesWhenNoMangaFound(): void { // Given - no manga in repository // When $query = new FindMangaMatchByFilename('unknown-manga_vol1_ch1.cbz'); $response = $this->handler->handle($query); // Then $this->assertFalse($response->hasMatches()); $this->assertCount(0, $response->matches); $this->assertNull($response->getBestMatch()); } public function testItFindsMultipleMatchesAndSortsByScore(): void { // Given $manga1 = $this->createManga( id: '1', title: 'One Piece', slug: 'one-piece' ); $manga2 = $this->createManga( id: '2', title: 'One Piece Z', slug: 'one-piece-z' ); $manga3 = $this->createManga( id: '3', title: 'The One Piece', slug: 'the-one-piece' ); $this->repository->save($manga1); $this->repository->save($manga2); $this->repository->save($manga3); // When $query = new FindMangaMatchByFilename('one-piece_vol108_ch1094.cbz'); $response = $this->handler->handle($query); // Then $this->assertTrue($response->hasMatches()); $this->assertGreaterThanOrEqual(1, count($response->matches)); // Le meilleur match devrait être "One Piece" (correspondance exacte) $bestMatch = $response->getBestMatch(); $this->assertNotNull($bestMatch); $this->assertEquals('One Piece', $bestMatch->title); // Les scores doivent être triés par ordre décroissant $scores = array_map(fn ($match) => $match->matchScore, $response->matches); $sortedScores = $scores; rsort($sortedScores); $this->assertEquals($sortedScores, $scores); } public function testItExtractsChapterAndVolumeNumbers(): void { // Given $manga = $this->createManga( id: '123', title: 'Attack on Titan', slug: 'attack-on-titan' ); $this->repository->save($manga); // When $query = new FindMangaMatchByFilename('attack-on-titan_vol32_ch130.cbz'); $response = $this->handler->handle($query); // Then $this->assertEquals(130.0, $response->chapterNumber); $this->assertEquals(32, $response->volumeNumber); // Vérifier que chaque match contient aussi ces informations foreach ($response->matches as $match) { $this->assertEquals(130.0, $match->chapterNumber); $this->assertEquals(32, $match->volumeNumber); } } public function testItHandlesDecimalChapterNumbers(): void { // Given $manga = $this->createManga( id: '123', title: 'Naruto', slug: 'naruto' ); $this->repository->save($manga); // When $query = new FindMangaMatchByFilename('naruto_vol50_ch456.5.cbz'); $response = $this->handler->handle($query); // Then $this->assertEquals(456.5, $response->chapterNumber); $this->assertEquals(50, $response->volumeNumber); // Vérifier que chaque match contient aussi ces informations foreach ($response->matches as $match) { $this->assertEquals(456.5, $match->chapterNumber); $this->assertEquals(50, $match->volumeNumber); } } public function testItFindsMatchesWithAlternativeSlugs(): void { // Given $manga = $this->createManga( id: '123', title: 'Shingeki no Kyojin', slug: 'shingeki-no-kyojin', alternativeSlugs: ['attack-on-titan', 'aot'] ); $this->repository->save($manga); // When $query = new FindMangaMatchByFilename('attack-on-titan_vol1_ch1.cbz'); $response = $this->handler->handle($query); // Then $this->assertTrue($response->hasMatches()); $bestMatch = $response->getBestMatch(); $this->assertNotNull($bestMatch); $this->assertEquals('123', $bestMatch->id); $this->assertEquals('Shingeki no Kyojin', $bestMatch->title); } public function testItHandlesFilenameWithoutChapterOrVolume(): void { // Given $manga = $this->createManga( id: '123', title: 'One Piece', slug: 'one-piece' ); $this->repository->save($manga); // When $query = new FindMangaMatchByFilename('one-piece.cbz'); $response = $this->handler->handle($query); // Then $this->assertTrue($response->hasMatches()); $this->assertNull($response->chapterNumber); $this->assertNull($response->volumeNumber); $bestMatch = $response->getBestMatch(); $this->assertEquals('123', $bestMatch->id); $this->assertNull($bestMatch->chapterNumber); $this->assertNull($bestMatch->volumeNumber); } public function testItFindsSingleMatchWithUniqueId(): void { // Given $manga = $this->createManga( id: '123', title: 'One Piece', slug: 'one-piece' ); $this->repository->save($manga); // When $query = new FindMangaMatchByFilename('one-piece_vol1_ch1.cbz'); $response = $this->handler->handle($query); // Then - Vérifier qu'on a bien un seul résultat avec l'ID correct $this->assertCount(1, $response->matches); $this->assertEquals('123', $response->matches[0]->id); } public function testItProvidesPossibleTitlesInResponse(): void { // Given $manga = $this->createManga( id: '123', title: 'Dragon Ball', slug: 'dragon-ball' ); $this->repository->save($manga); // When $query = new FindMangaMatchByFilename('dragon-ball_vol1_ch5.cbz'); $response = $this->handler->handle($query); // Then $this->assertNotEmpty($response->possibleTitles); $this->assertEquals(['dragon-ball'], $response->possibleTitles); } public function testItHandlesFilenameWithOnlyVolume(): void { // Given $manga = $this->createManga( id: '123', title: 'One Piece', slug: 'one-piece' ); $this->repository->save($manga); // When - Fichier avec seulement un volume, sans chapitre $query = new FindMangaMatchByFilename('one-piece_vol108.cbz'); $response = $this->handler->handle($query); // Then $this->assertTrue($response->hasMatches()); $this->assertEquals(108, $response->volumeNumber); $this->assertNull($response->chapterNumber); $bestMatch = $response->getBestMatch(); $this->assertNotNull($bestMatch); $this->assertEquals('123', $bestMatch->id); $this->assertEquals(108, $bestMatch->volumeNumber); $this->assertNull($bestMatch->chapterNumber); } public function testItHandlesFilenameWithOnlyChapter(): void { // Given $manga = $this->createManga( id: '123', title: 'Naruto', slug: 'naruto' ); $this->repository->save($manga); // When - Fichier avec seulement un chapitre, sans volume $query = new FindMangaMatchByFilename('naruto_ch456.cbz'); $response = $this->handler->handle($query); // Then $this->assertTrue($response->hasMatches()); $this->assertEquals(456.0, $response->chapterNumber); $this->assertNull($response->volumeNumber); $bestMatch = $response->getBestMatch(); $this->assertNotNull($bestMatch); $this->assertEquals('123', $bestMatch->id); $this->assertEquals(456.0, $bestMatch->chapterNumber); $this->assertNull($bestMatch->volumeNumber); } public function testItHandlesVariousVolumeOnlyFormats(): void { // Given $manga = $this->createManga( id: '123', title: 'Attack on Titan', slug: 'attack-on-titan' ); $this->repository->save($manga); $testCases = [ ['filename' => 'attack-on-titan vol 32.cbz', 'expectedVolume' => 32], ['filename' => 'attack-on-titan-tome-15.cbz', 'expectedVolume' => 15], ['filename' => 'attack-on-titan_t10.cbz', 'expectedVolume' => 10], ['filename' => 'attack-on-titan Volume 5.cbr', 'expectedVolume' => 5], ]; foreach ($testCases as $case) { // When $query = new FindMangaMatchByFilename($case['filename']); $response = $this->handler->handle($query); // Then $this->assertTrue($response->hasMatches(), "Should find match for {$case['filename']}"); $this->assertEquals($case['expectedVolume'], $response->volumeNumber, "Failed volume extraction for: {$case['filename']}"); $this->assertNull($response->chapterNumber, "Should not have chapter for: {$case['filename']}"); $bestMatch = $response->getBestMatch(); $this->assertEquals($case['expectedVolume'], $bestMatch->volumeNumber, "Match should have correct volume for: {$case['filename']}"); $this->assertNull($bestMatch->chapterNumber, "Match should not have chapter for: {$case['filename']}"); } } public function testItHandlesVariousChapterOnlyFormats(): void { // Given $manga = $this->createManga( id: '123', title: 'My Hero Academia', slug: 'my-hero-academia' ); $this->repository->save($manga); $testCases = [ ['filename' => 'my-hero-academia ch 150.cbz', 'expectedChapter' => 150.0], ['filename' => 'my-hero-academia-chap-200.cbz', 'expectedChapter' => 200.0], ['filename' => 'my-hero-academia_chapter_75.cbz', 'expectedChapter' => 75.0], ['filename' => 'my-hero-academia chapitre 100.cbr', 'expectedChapter' => 100.0], ['filename' => 'my-hero-academia_ch99.5.cbz', 'expectedChapter' => 99.5], ]; foreach ($testCases as $case) { // When $query = new FindMangaMatchByFilename($case['filename']); $response = $this->handler->handle($query); // Then $this->assertTrue($response->hasMatches(), "Should find match for {$case['filename']}"); $this->assertEquals($case['expectedChapter'], $response->chapterNumber, "Failed chapter extraction for: {$case['filename']}"); $this->assertNull($response->volumeNumber, "Should not have volume for: {$case['filename']}"); $bestMatch = $response->getBestMatch(); $this->assertEquals($case['expectedChapter'], $bestMatch->chapterNumber, "Match should have correct chapter for: {$case['filename']}"); $this->assertNull($bestMatch->volumeNumber, "Match should not have volume for: {$case['filename']}"); } } private function createManga( string $id, string $title, string $slug, array $alternativeSlugs = [], ?string $thumbnailUrl = null, ): Manga { return new Manga( id: new MangaId($id), title: new MangaTitle($title), slug: new MangaSlug($slug), description: 'Test description', author: 'Test Author', publicationYear: 2000, genres: ['action'], status: 'ongoing', imageUrls: new ImageUrls( 'http://example.com/full.jpg', $thumbnailUrl ?? 'http://example.com/thumbnail.jpg' ), alternativeSlugs: $alternativeSlugs ); } protected function tearDown(): void { $this->repository->clear(); } }