- PHP 8.3 → 8.4 (Dockerfile + composer.json) - Symfony 7.0 → 8.0 (tous les composants symfony/*) - API Platform 3.x → 4.x : migration openapiContext → openapi: new Operation(...) - Doctrine DBAL 3 → 4 : suppression use_savepoints, replace prepare/executeQuery - Doctrine ORM 2.x → 3.x : ClassMetadataInfo → ClassMetadata, setParameters → setParameter - Doctrine Bundle 2.x → 3.x, Fixtures Bundle 3.x → 4.x - zenstruck/foundry 1.x → 2.x : ModelFactory → PersistentObjectFactory, getDefaults → defaults - phpmd/phpmd 2.x → 3.x-dev (seule version supportant Symfony 8) - phparkitect 0.3 → 0.8 : NotDependsOnTheseNamespaces prend un array - symfony/mercure-bundle 0.3 → 0.4, symfony/monolog-bundle 3 → 4 - Suppression de runtime/frankenphp-symfony (intégré nativement dans symfony/runtime 8) - worker.Caddyfile : suppression de APP_RUNTIME (détection automatique Symfony 8) - Routes errors.xml/wdt.xml/profiler.xml → .php (Symfony 8 supprime le XML) - Types::ARRAY → Types::JSON dans Entity/Manga.php (DBAL 4 retire array type) - Suppression de src/Schedule.php (doublon vide avec MonitoringSchedule) - Tests : hydra:Collection → Collection, hydra:member → member (API Platform 4)
414 lines
14 KiB
PHP
414 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Domain\Manga\Application\QueryHandler;
|
|
|
|
use App\Domain\Manga\Application\Query\FindMangaMatchByFilename;
|
|
use App\Domain\Manga\Application\QueryHandler\FindMangaMatchByFilenameHandler;
|
|
use App\Domain\Manga\Domain\Model\Manga;
|
|
use App\Domain\Manga\Domain\Model\ValueObject\ImageUrls;
|
|
use App\Domain\Manga\Domain\Model\ValueObject\MangaId;
|
|
use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug;
|
|
use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle;
|
|
use App\Domain\Manga\Infrastructure\Service\FilenameAnalyzer;
|
|
use App\Tests\Domain\Manga\Adapter\InMemoryMangaRepository;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class FindMangaMatchByFilenameHandlerTest extends TestCase
|
|
{
|
|
private InMemoryMangaRepository $repository;
|
|
private FilenameAnalyzer $filenameAnalyzer;
|
|
private FindMangaMatchByFilenameHandler $handler;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->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();
|
|
}
|
|
}
|