feat: ajout de la gestion des chapitres de manga, incluant la récupération et la sauvegarde des chapitres en français et en anglais, ainsi que l'optimisation de la logique de sauvegarde pour éviter les doublons
This commit is contained in:
parent
34dfa57dc0
commit
0111f1b5f1
@@ -13,7 +13,7 @@ class InMemoryMangaRepository implements MangaRepositoryInterface
|
||||
{
|
||||
/** @var array<string, Manga> */
|
||||
private array $mangas = [];
|
||||
|
||||
|
||||
/** @var array<string, array<Chapter>> */
|
||||
private array $chapters = [];
|
||||
|
||||
@@ -28,13 +28,13 @@ class InMemoryMangaRepository implements MangaRepositoryInterface
|
||||
$valueA = $this->getPropertyValue($a, $sortBy);
|
||||
$valueB = $this->getPropertyValue($b, $sortBy);
|
||||
|
||||
return $sortOrder === 'asc'
|
||||
? $valueA <=> $valueB
|
||||
return $sortOrder === 'asc'
|
||||
? $valueA <=> $valueB
|
||||
: $valueB <=> $valueA;
|
||||
});
|
||||
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
|
||||
return array_slice($sortedMangas, $offset, $limit);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ class InMemoryMangaRepository implements MangaRepositoryInterface
|
||||
public function addChaptersToManga(string $mangaId, int $count): void
|
||||
{
|
||||
$this->chapters[$mangaId] = [];
|
||||
|
||||
|
||||
for ($i = 1; $i <= $count; $i++) {
|
||||
$this->chapters[$mangaId][] = new Chapter(
|
||||
id: new ChapterId((string)$i),
|
||||
@@ -179,4 +179,15 @@ class InMemoryMangaRepository implements MangaRepositoryInterface
|
||||
{
|
||||
return count($this->search($query, 1, PHP_INT_MAX));
|
||||
}
|
||||
public function findExistingChaptersByNumbers(string $mangaId, array $chapterNumbers): array
|
||||
{
|
||||
if (!isset($this->chapters[$mangaId])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter(
|
||||
$this->chapters[$mangaId],
|
||||
fn (Chapter $chapter) => in_array($chapter->getNumber(), $chapterNumbers)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,8 @@ class FetchMangaChaptersHandlerTest extends TestCase
|
||||
'attributes' => [
|
||||
'chapter' => '1',
|
||||
'title' => 'Chapter 1',
|
||||
'volume' => '1'
|
||||
'volume' => '1',
|
||||
'translatedLanguage' => 'fr'
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
@@ -8,27 +8,30 @@ use App\Domain\Scraping\Domain\Event\ChapterScraped;
|
||||
use App\Domain\Scraping\Domain\Event\ChapterScrapingFailed;
|
||||
use App\Domain\Scraping\Domain\Event\ChapterScrapingStarted;
|
||||
use App\Domain\Scraping\Domain\Model\Chapter;
|
||||
use App\Domain\Scraping\Domain\Model\ScrapingStatus;
|
||||
use App\Domain\Shared\Domain\Model\JobStatus;
|
||||
use App\Tests\Domain\Scraping\Adapter\InMemoryChapterRepository;
|
||||
use App\Tests\Domain\Scraping\Adapter\InMemoryCbzGenerator;
|
||||
use App\Tests\Domain\Scraping\Adapter\InMemoryEventBus;
|
||||
use App\Tests\Domain\Scraping\Adapter\InMemoryImageDownloader;
|
||||
use App\Tests\Domain\Scraping\Adapter\InMemoryMangaRepository;
|
||||
use App\Tests\Domain\Scraping\Adapter\InMemoryScraperAdapter;
|
||||
use App\Tests\Domain\Scraping\Adapter\InMemoryScrapingJobRepository;
|
||||
use App\Tests\Domain\Scraping\Adapter\InMemorySourceRepository;
|
||||
use App\Tests\Domain\Shared\Adapter\InMemoryJobRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
class ScrapeChapterHandlerTest extends TestCase
|
||||
{
|
||||
private InMemoryScraperAdapter $scraper;
|
||||
private InMemoryImageDownloader $imageDownloader;
|
||||
private InMemoryCbzGenerator $cbzGenerator;
|
||||
private InMemoryScrapingJobRepository $scrapingJobRepository;
|
||||
private InMemoryJobRepository $jobRepository;
|
||||
private InMemoryChapterRepository $chapterRepository;
|
||||
private InMemoryMangaRepository $mangaRepository;
|
||||
private InMemorySourceRepository $sourceRepository;
|
||||
private InMemoryEventBus $eventBus;
|
||||
private EntityManagerInterface|MockObject $entityManager;
|
||||
private ScrapeChapterHandler $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
@@ -36,11 +39,16 @@ class ScrapeChapterHandlerTest extends TestCase
|
||||
$this->scraper = new InMemoryScraperAdapter();
|
||||
$this->imageDownloader = new InMemoryImageDownloader();
|
||||
$this->cbzGenerator = new InMemoryCbzGenerator('/test/project/dir');
|
||||
$this->scrapingJobRepository = new InMemoryScrapingJobRepository();
|
||||
$this->jobRepository = new InMemoryJobRepository();
|
||||
$this->chapterRepository = new InMemoryChapterRepository();
|
||||
$this->mangaRepository = new InMemoryMangaRepository();
|
||||
$this->sourceRepository = new InMemorySourceRepository();
|
||||
$this->eventBus = new InMemoryEventBus();
|
||||
$this->entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
|
||||
$this->entityManager->method('beginTransaction')->willReturn(null);
|
||||
$this->entityManager->method('commit')->willReturn(null);
|
||||
$this->entityManager->method('rollback')->willReturn(null);
|
||||
|
||||
$this->chapterRepository->save(new Chapter(
|
||||
id: '1',
|
||||
@@ -54,11 +62,12 @@ class ScrapeChapterHandlerTest extends TestCase
|
||||
$this->scraper,
|
||||
$this->imageDownloader,
|
||||
$this->cbzGenerator,
|
||||
$this->scrapingJobRepository,
|
||||
$this->jobRepository,
|
||||
$this->chapterRepository,
|
||||
$this->mangaRepository,
|
||||
$this->sourceRepository,
|
||||
$this->eventBus
|
||||
$this->eventBus,
|
||||
$this->entityManager
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,15 +81,14 @@ class ScrapeChapterHandlerTest extends TestCase
|
||||
|
||||
$this->handler->handle($command);
|
||||
|
||||
$scrapingJobs = $this->scrapingJobRepository->getJobs();
|
||||
$this->assertCount(1, $scrapingJobs);
|
||||
$job = $scrapingJobs[0];
|
||||
$job = $this->jobRepository->findByType('scraping_job');
|
||||
$this->assertCount(1, $job);
|
||||
$job = array_values($job)[0];
|
||||
|
||||
$dispatchedMessages = $this->eventBus->getDispatchedMessages();
|
||||
$this->assertCount(2, $dispatchedMessages);
|
||||
$this->assertInstanceOf(ChapterScrapingStarted::class, $dispatchedMessages[0]);
|
||||
$this->assertInstanceOf(ChapterScraped::class, $dispatchedMessages[1]);
|
||||
$this->assertEquals($job->getId(), $dispatchedMessages[0]->getJobId());
|
||||
$this->assertCount(1, $dispatchedMessages);
|
||||
$this->assertInstanceOf(ChapterScraped::class, $dispatchedMessages[0]);
|
||||
$this->assertEquals($job->id, $dispatchedMessages[0]->getJobId());
|
||||
|
||||
$chapter = $this->chapterRepository->getByMangaIdAndChapterNumber('test-manga', 2);
|
||||
$this->assertNotNull($chapter->cbzPath);
|
||||
@@ -97,28 +105,25 @@ class ScrapeChapterHandlerTest extends TestCase
|
||||
$exception = new \Exception('Scraping failed');
|
||||
$this->scraper->simulateError($exception);
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
$this->expectExceptionMessage('Scraping failed');
|
||||
|
||||
$this->handler->handle($command);
|
||||
|
||||
$dispatchedMessages = $this->eventBus->getDispatchedMessages();
|
||||
$this->assertCount(2, $dispatchedMessages);
|
||||
$this->assertInstanceOf(ChapterScrapingStarted::class, $dispatchedMessages[0]);
|
||||
$this->assertInstanceOf(ChapterScrapingFailed::class, $dispatchedMessages[1]);
|
||||
$this->assertEquals('test-manga', $dispatchedMessages[1]->getMangaId());
|
||||
$this->assertEquals('2', $dispatchedMessages[1]->getChapterNumber());
|
||||
$this->assertEquals('Scraping failed', $dispatchedMessages[1]->getReason());
|
||||
$this->assertCount(1, $dispatchedMessages);
|
||||
$this->assertInstanceOf(ChapterScrapingFailed::class, $dispatchedMessages[0]);
|
||||
$this->assertEquals('test-manga', $dispatchedMessages[0]->getMangaId());
|
||||
$this->assertEquals('2', $dispatchedMessages[0]->getChapterNumber());
|
||||
$this->assertEquals('Scraping failed', $dispatchedMessages[0]->getReason());
|
||||
|
||||
$jobs = $this->scrapingJobRepository->getJobs();
|
||||
$jobs = $this->jobRepository->findByType('scraping_job');
|
||||
$job = array_values($jobs)[0];
|
||||
$this->assertCount(1, $jobs);
|
||||
$this->assertEquals(ScrapingStatus::FAILED, $jobs[0]->status);
|
||||
$this->assertEquals('Scraping failed', $jobs[0]->failureReason);
|
||||
$this->assertEquals(JobStatus::FAILED, $job->status);
|
||||
$this->assertEquals('Scraping failed', $job->failureReason);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$this->scrapingJobRepository->clear();
|
||||
$this->jobRepository->clear();
|
||||
$this->chapterRepository->clear();
|
||||
$this->mangaRepository->clear();
|
||||
$this->sourceRepository->clear();
|
||||
|
||||
104
tests/Domain/Shared/Adapter/InMemoryJobRepository.php
Normal file
104
tests/Domain/Shared/Adapter/InMemoryJobRepository.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Domain\Shared\Adapter;
|
||||
|
||||
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
|
||||
use App\Domain\Shared\Domain\Model\Job;
|
||||
use App\Domain\Shared\Domain\Model\JobStatus;
|
||||
|
||||
class InMemoryJobRepository implements JobRepositoryInterface
|
||||
{
|
||||
/** @var Job[] */
|
||||
private array $jobs = [];
|
||||
|
||||
public function save(Job $job): void
|
||||
{
|
||||
$this->jobs[$job->id] = $job;
|
||||
}
|
||||
|
||||
public function get(string $id): ?Job
|
||||
{
|
||||
return $this->jobs[$id] ?? null;
|
||||
}
|
||||
|
||||
public function findByStatus(JobStatus $status): array
|
||||
{
|
||||
return array_filter($this->jobs, fn(Job $job) => $job->status === $status);
|
||||
}
|
||||
|
||||
public function findByType(string $type): array
|
||||
{
|
||||
return array_filter($this->jobs, fn(Job $job) => $job->type === $type);
|
||||
}
|
||||
|
||||
public function findPendingJobs(): array
|
||||
{
|
||||
return $this->findByStatus(JobStatus::PENDING);
|
||||
}
|
||||
|
||||
public function findInProgressJobs(): array
|
||||
{
|
||||
return $this->findByStatus(JobStatus::IN_PROGRESS);
|
||||
}
|
||||
|
||||
public function findFailedJobs(): array
|
||||
{
|
||||
return $this->findByStatus(JobStatus::FAILED);
|
||||
}
|
||||
|
||||
public function findByCriteria(array $criteria): array
|
||||
{
|
||||
$jobs = $this->jobs;
|
||||
|
||||
if (isset($criteria['statuses']) && is_array($criteria['statuses']) && !empty($criteria['statuses'])) {
|
||||
$jobs = array_filter($jobs, fn(Job $job) => in_array($job->status, $criteria['statuses']));
|
||||
} elseif (isset($criteria['status'])) {
|
||||
$jobs = array_filter($jobs, fn(Job $job) => $job->status === $criteria['status']);
|
||||
}
|
||||
|
||||
if (isset($criteria['type'])) {
|
||||
$jobs = array_filter($jobs, fn(Job $job) => $job->type === $criteria['type']);
|
||||
}
|
||||
|
||||
if (isset($criteria['createdAfter'])) {
|
||||
$jobs = array_filter($jobs, fn(Job $job) => $job->createdAt >= $criteria['createdAfter']);
|
||||
}
|
||||
|
||||
if (isset($criteria['createdBefore'])) {
|
||||
$jobs = array_filter($jobs, fn(Job $job) => $job->createdAt <= $criteria['createdBefore']);
|
||||
}
|
||||
|
||||
if (isset($criteria['sortBy'])) {
|
||||
usort($jobs, function(Job $a, Job $b) use ($criteria) {
|
||||
$sortOrder = $criteria['sortOrder'] ?? 'ASC';
|
||||
$comparison = match($criteria['sortBy']) {
|
||||
'createdAt' => $a->createdAt <=> $b->createdAt,
|
||||
'startedAt' => ($a->startedAt ?? new \DateTimeImmutable()) <=> ($b->startedAt ?? new \DateTimeImmutable()),
|
||||
'completedAt' => ($a->completedAt ?? new \DateTimeImmutable()) <=> ($b->completedAt ?? new \DateTimeImmutable()),
|
||||
default => 0
|
||||
};
|
||||
return $sortOrder === 'ASC' ? $comparison : -$comparison;
|
||||
});
|
||||
}
|
||||
|
||||
if (isset($criteria['offset'])) {
|
||||
$jobs = array_slice($jobs, $criteria['offset']);
|
||||
}
|
||||
|
||||
if (isset($criteria['limit'])) {
|
||||
$jobs = array_slice($jobs, 0, $criteria['limit']);
|
||||
}
|
||||
|
||||
return array_values($jobs);
|
||||
}
|
||||
|
||||
public function countByCriteria(array $criteria): int
|
||||
{
|
||||
return count($this->findByCriteria($criteria));
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->jobs = [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user