From 84c4557abfd4ca45db89557141b5556f185f17a3 Mon Sep 17 00:00:00 2001 From: "ext.jeremy.guillot@maxicoffee.domains" Date: Tue, 17 Mar 2026 15:33:20 +0100 Subject: [PATCH] =?UTF-8?q?refactor(scraping):=20job=20PENDING=20d=C3=A8s?= =?UTF-8?q?=20le=20POST=20HTTP,=20handler=20sans=20Doctrine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ScrapingJob: mangaId/chapterNumber/sourceId optionnels (nullable) pour permettre la création en PENDING sans lookup DB dans le StateProcessor - ScrapeChapter: ajoute jobId (pré-généré par le StateProcessor) - ScrapeChapterStateProcessor: crée et persiste le job PENDING avant dispatch; injecte JobRepositoryInterface uniquement - ScrapeChapterHandler: supprime EntityManagerInterface, beginTransaction/ commit/rollback; charge le job existant via jobId, complete() sur succès seulement, fail() si toutes les sources échouent - ScrapeChapterHandlerTest: pré-crée le job, passe jobId dans la commande, supprime le mock EntityManagerInterface - ScrapeChapterTest: accès aux messages via static InMemoryMessageBus, vérifie la présence du jobId dans la commande dispatchée --- .../application/store/activityStore.js | 122 +++------ .../presentation/pages/ActivityPage.vue | 246 ++++++++---------- config/packages/messenger.yaml | 1 - .../Application/Command/ScrapeChapter.php | 3 +- .../CommandHandler/ScrapeChapterHandler.php | 168 ++++-------- .../Scraping/Domain/Model/ScrapingJob.php | 6 +- .../Processor/ScrapeChapterStateProcessor.php | 17 +- .../ScrapingEventSubscriber.php | 17 ++ .../ScrapeChapterHandlerTest.php | 23 +- tests/Feature/Scraping/ScrapeChapterTest.php | 5 +- 10 files changed, 252 insertions(+), 356 deletions(-) diff --git a/assets/vue/app/domain/activity/application/store/activityStore.js b/assets/vue/app/domain/activity/application/store/activityStore.js index 2a422d5..8fc2e7d 100644 --- a/assets/vue/app/domain/activity/application/store/activityStore.js +++ b/assets/vue/app/domain/activity/application/store/activityStore.js @@ -3,11 +3,14 @@ import { ApiJobRepository } from '../../infrastructure/api/ApiJobRepository'; const jobRepository = new ApiJobRepository(); +const ACTIVE_STATUSES = ['pending', 'in_progress']; + export const useActivityStore = defineStore('activity', { state: () => ({ jobs: [], loading: false, error: null, + mercureEventSource: null, // Pagination currentPage: 1, totalPages: 0, @@ -15,21 +18,15 @@ export const useActivityStore = defineStore('activity', { limit: 20, hasNextPage: false, hasPreviousPage: false, - // Filtres - filter: { - status: ['pending', 'in_progress'], // Par défaut, ne montrer que les actifs - sortBy: 'createdAt', - sortOrder: 'DESC' - } + // Tri + sortBy: 'createdAt', + sortOrder: 'DESC', }), getters: { activeJobs: state => state.jobs.filter(job => job.isActive()), - completedJobs: state => state.jobs.filter(job => job.isCompleted()), - failedJobs: state => state.jobs.filter(job => job.hasError()), isLoading: state => state.loading, hasError: state => !!state.error, - // Getters pour la pagination paginationInfo: state => ({ currentPage: state.currentPage, totalPages: state.totalPages, @@ -41,44 +38,25 @@ export const useActivityStore = defineStore('activity', { }, actions: { - /** - * Charge la liste des jobs selon les filtres actuels - * @param {number} page - Numéro de page optionnel - */ async loadJobs(page = null) { this.loading = true; this.error = null; try { - const options = { + const jobCollection = await jobRepository.getJobs({ page: page || this.currentPage, limit: this.limit, - sortBy: this.filter.sortBy, - sortOrder: this.filter.sortOrder, - status: this.filter.status - }; + sortBy: this.sortBy, + sortOrder: this.sortOrder, + status: ACTIVE_STATUSES, + }); - const jobCollection = await jobRepository.getJobs(options); - - // Mettre à jour les données this.jobs = jobCollection.items; this.currentPage = jobCollection.page; this.total = jobCollection.total; this.hasNextPage = jobCollection.hasNextPage; this.hasPreviousPage = jobCollection.hasPreviousPage; - - // Calculer le nombre total de pages this.totalPages = Math.ceil(this.total / this.limit); - - console.log('Store updated with:', { - jobs: this.jobs.length, - currentPage: this.currentPage, - total: this.total, - limit: this.limit, - totalPages: this.totalPages, - hasNextPage: this.hasNextPage, - hasPreviousPage: this.hasPreviousPage - }); } catch (error) { this.error = error.message; console.error('Error loading jobs:', error); @@ -87,10 +65,6 @@ export const useActivityStore = defineStore('activity', { } }, - /** - * Va à une page spécifique - * @param {number} page - */ async goToPage(page) { if (page >= 1 && page <= this.totalPages && page !== this.currentPage) { this.currentPage = page; @@ -98,39 +72,26 @@ export const useActivityStore = defineStore('activity', { } }, - /** - * Met à jour les filtres et recharge la liste - * @param {Object} filter - */ - async updateFilter(filter) { - this.filter = { ...this.filter, ...filter }; - this.currentPage = 1; // Retourner à la première page lors du changement de filtre + async updateSort(sortBy, sortOrder) { + this.sortBy = sortBy; + this.sortOrder = sortOrder; + this.currentPage = 1; await this.loadJobs(1); }, - /** - * Met à jour la limite par page - * @param {number} limit - */ async updateLimit(limit) { this.limit = limit; - this.currentPage = 1; // Retourner à la première page + this.currentPage = 1; await this.loadJobs(1); }, - /** - * Supprime un job par son ID - * @param {string} id - */ async deleteJob(id) { this.loading = true; this.error = null; try { await jobRepository.deleteJob(id); - // Supprimer le job de la liste locale this.jobs = this.jobs.filter(job => job.id !== id); - // Recharger la page courante pour avoir les bons totaux await this.loadJobs(this.currentPage); } catch (error) { this.error = error.message; @@ -140,17 +101,37 @@ export const useActivityStore = defineStore('activity', { } }, - /** - * Supprime tous les jobs correspondant aux critères - * @param {Object} criteria - */ + updateJobProgress(jobId, progress) { + const job = this.jobs.find(j => j.id === jobId); + if (job) job.progress = progress; + }, + + subscribeMercure() { + if (this.mercureEventSource) return; + const url = new URL('/.well-known/mercure', window.location.origin); + url.searchParams.append('topic', 'jobs/activity'); + this.mercureEventSource = new EventSource(url.toString()); + this.mercureEventSource.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data.type === 'job.progress_updated') { + this.updateJobProgress(data.jobId, data.progress); + } + }; + }, + + unsubscribeMercure() { + if (this.mercureEventSource) { + this.mercureEventSource.close(); + this.mercureEventSource = null; + } + }, + async deleteJobs(criteria = {}) { this.loading = true; this.error = null; try { const deleted = await jobRepository.deleteJobs(criteria); - // Recharger la liste après suppression await this.loadJobs(this.currentPage); return deleted; } catch (error) { @@ -160,26 +141,5 @@ export const useActivityStore = defineStore('activity', { this.loading = false; } }, - - /** - * Supprime tous les jobs terminés - */ - async deleteCompletedJobs() { - return this.deleteJobs({ status: ['COMPLETED'] }); - }, - - /** - * Supprime tous les jobs en erreur - */ - async deleteFailedJobs() { - return this.deleteJobs({ status: ['ERROR'] }); - }, - - /** - * Supprime tous les jobs - */ - async deleteAllJobs() { - return this.deleteJobs({}); - } } }); diff --git a/assets/vue/app/domain/activity/presentation/pages/ActivityPage.vue b/assets/vue/app/domain/activity/presentation/pages/ActivityPage.vue index 460b85b..f6a9f05 100644 --- a/assets/vue/app/domain/activity/presentation/pages/ActivityPage.vue +++ b/assets/vue/app/domain/activity/presentation/pages/ActivityPage.vue @@ -1,169 +1,153 @@ diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml index 3fffec8..4e3572a 100644 --- a/config/packages/messenger.yaml +++ b/config/packages/messenger.yaml @@ -17,7 +17,6 @@ framework: command.bus: middleware: - validation - - doctrine_transaction event.bus: default_middleware: allow_no_handlers diff --git a/src/Domain/Scraping/Application/Command/ScrapeChapter.php b/src/Domain/Scraping/Application/Command/ScrapeChapter.php index fe483f4..ccff324 100644 --- a/src/Domain/Scraping/Application/Command/ScrapeChapter.php +++ b/src/Domain/Scraping/Application/Command/ScrapeChapter.php @@ -5,7 +5,8 @@ namespace App\Domain\Scraping\Application\Command; readonly class ScrapeChapter { public function __construct( - public string $chapterId + public string $chapterId, + public string $jobId ) { } } diff --git a/src/Domain/Scraping/Application/CommandHandler/ScrapeChapterHandler.php b/src/Domain/Scraping/Application/CommandHandler/ScrapeChapterHandler.php index 0f09d66..1f80e72 100644 --- a/src/Domain/Scraping/Application/CommandHandler/ScrapeChapterHandler.php +++ b/src/Domain/Scraping/Application/CommandHandler/ScrapeChapterHandler.php @@ -13,14 +13,11 @@ use App\Domain\Shared\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\ScrapingJob; use App\Domain\Scraping\Domain\Model\Source; use App\Domain\Scraping\Domain\Model\ValueObject\ScrapingRequest; use App\Domain\Scraping\Domain\Model\ValueObject\TempDirectory; use App\Domain\Shared\Domain\Contract\JobRepositoryInterface; -use Ramsey\Uuid\Uuid; use Symfony\Component\Messenger\MessageBusInterface; -use Doctrine\ORM\EntityManagerInterface; readonly class ScrapeChapterHandler { @@ -33,151 +30,92 @@ readonly class ScrapeChapterHandler private MangaRepositoryInterface $mangaRepository, private SourceRepositoryInterface $sourceRepository, private MessageBusInterface $eventBus, - private EntityManagerInterface $entityManager ) { } public function handle(ScrapeChapter $command): void { - $job = null; - try { - // 1. Récupération du chapitre - /**@var Chapter $chapter */ - $chapter = $this->chapterRepository->getById($command->chapterId); - if (!$chapter) { - throw new \InvalidArgumentException("Chapter not found with ID: {$command->chapterId}"); - } + /** @var Chapter $chapter */ + $chapter = $this->chapterRepository->getById($command->chapterId); + $manga = $this->mangaRepository->getById($chapter->mangaId); - // 2. Récupération du manga - $manga = $this->mangaRepository->getById($chapter->mangaId); - if (!$manga) { - throw new \InvalidArgumentException("Manga not found with ID: {$chapter->mangaId}"); - } + $job = $this->jobRepository->get($command->jobId); + $job->context['chapterId'] = $command->chapterId; + $job->context['mangaTitle'] = $manga->getTitle(); + $job->start(); + $this->jobRepository->save($job); - // 3. Dispatch de l'événement de démarrage - $this->eventBus->dispatch(new ChapterScrapingStarted($manga->getTitle(), $chapter->chapterNumber)); + $this->eventBus->dispatch(new ChapterScrapingStarted($manga->getTitle(), $chapter->chapterNumber)); - // 4. Détermination des sources à utiliser - $sources = $this->getSourcesToTry($manga); - if (empty($sources)) { - throw new \InvalidArgumentException("No sources available for scraping"); - } + $sources = $this->getSourcesToTry($manga); + $slugsToTry = array_merge([$manga->getSlug()], $manga->getAlternativeSlugs()); + $success = false; + $lastException = null; - // 5. Essai de scraping sur chaque source jusqu'à succès - $success = false; - $lastException = null; - - foreach ($sources as $source) { - // Préparer la liste des slugs à essayer : slug principal + slugs alternatifs - $slugsToTry = array_merge([$manga->getSlug()], $manga->getAlternativeSlugs()); - - foreach ($slugsToTry as $slug) { - $job = new ScrapingJob( - Uuid::uuid4()->toString(), - $chapter->mangaId, - $chapter->chapterNumber, - $source->getId()->getValue() - ); - - // Ajout de l'ID du chapitre et du slug dans le contexte du job - $job->context['chapterId'] = $command->chapterId; + foreach ($sources as $source) { + foreach ($slugsToTry as $slug) { + try { + $job->context['sourceId'] = $source->getId()->getValue(); $job->context['slug'] = $slug; - $job->context['mangaTitle'] = $manga->getTitle(); - - $job->start(); $this->jobRepository->save($job); - try { - $this->entityManager->beginTransaction(); + $scrapingParameters = $source->getScrappingParameters(); + $scrapingParameters['chapterNumber'] = $chapter->chapterNumber; + $scrapingType = $scrapingParameters['scrapingType'] ?? 'html'; - // 5. Scraping des URLs avec le slug courant - $scrapingParameters = $source->getScrappingParameters(); - $scrapingParameters['chapterNumber'] = $chapter->chapterNumber; - $scrapingType = $scrapingParameters['scrapingType'] ?? 'html'; + $scrapingRequest = new ScrapingRequest( + $scrapingType, + $source->buildChapterUrl($slug, $chapter->chapterNumber), + $scrapingParameters + ); - $scrapingRequest = new ScrapingRequest( - $scrapingType, - $source->buildChapterUrl($slug, $chapter->chapterNumber), - $scrapingParameters - ); + $scraper = $this->scraperFactory->getScraperWithFallback($scrapingType); + $scrapingResult = $scraper->scrape($scrapingRequest); - // Sélection du scraper approprié selon le type - $scraper = $this->scraperFactory->getScraperWithFallback($scrapingType); - $scrapingResult = $scraper->scrape($scrapingRequest); + $tempDir = new TempDirectory(); + $downloadResults = $this->imageDownloader->downloadBatch( + $scrapingResult->getImageUrls(), + $tempDir, + $job->id + ); - // 6. Téléchargement des images - $tempDir = new TempDirectory(); - $downloadResults = $this->imageDownloader->downloadBatch( - $scrapingResult->getImageUrls(), - $tempDir, - $job->id - ); + $localPaths = array_map(fn ($r) => $r->getLocalPath(), $downloadResults); + $pagesDirectory = $this->imageStorage->storeChapterImages($command->chapterId, $localPaths); + $pageCount = count($downloadResults); - // 7. Stockage des images individuelles - $localPaths = array_map(fn ($r) => $r->getLocalPath(), $downloadResults); - $pagesDirectory = $this->imageStorage->storeChapterImages($command->chapterId, $localPaths); - $pageCount = count($downloadResults); + $job->complete(); + $this->jobRepository->save($job); - $job->complete(); - $this->jobRepository->save($job); + $this->eventBus->dispatch(new ChapterScraped($job->id, $command->chapterId, $pagesDirectory, $pageCount)); + $tempDir->cleanup(); - $this->entityManager->commit(); - - $this->eventBus->dispatch(new ChapterScraped($job->id, $command->chapterId, $pagesDirectory, $pageCount)); - - // 8. Nettoyage - $tempDir->cleanup(); - - // Scraping réussi, pas besoin d'essayer d'autres slugs ni d'autres sources - $success = true; - break; - - } catch (\Exception $e) { - dump('EXCEPTION for source ' . $source->getName() . ' with slug ' . $slug . ': ' . $e->getMessage()); - - $this->entityManager->rollback(); - - if (isset($job)) { - $job->fail($e->getMessage()); - $this->jobRepository->save($job); - } - - $lastException = $e; - - // Continuer avec le slug suivant pour cette source - } - } - - // Si le scraping a réussi avec un des slugs, sortir de la boucle des sources - if ($success) { + $success = true; break; + + } catch (\Exception $e) { + $lastException = $e; } } - // Si toutes les sources ont échoué - if (!$success) { - $errorMessage = $lastException ? $lastException->getMessage() : "Failed to scrape chapter from all available sources"; - $this->eventBus->dispatch(new ChapterScrapingFailed($chapter->mangaId, $chapter->chapterNumber, $errorMessage)); + if ($success) { + break; } + } - } catch (\Exception $e) { - if (isset($job)) { - $job->fail($e->getMessage()); - $this->jobRepository->save($job); - } - $this->eventBus->dispatch(new ChapterScrapingFailed($chapter->mangaId ?? 'unknown', $chapter->chapterNumber ?? 'unknown', $e->getMessage())); + if (!$success) { + $errorMessage = $lastException?->getMessage() ?? 'Failed to scrape chapter from all available sources'; + $job->fail($errorMessage); + $this->jobRepository->save($job); + $this->eventBus->dispatch(new ChapterScrapingFailed($chapter->mangaId, $chapter->chapterNumber, $errorMessage)); } } /** - * Détermine les sources à utiliser pour le scraping en fonction des préférences du manga - * * @param \App\Domain\Scraping\Domain\Model\Manga $manga * @return Source[] */ private function getSourcesToTry(\App\Domain\Scraping\Domain\Model\Manga $manga): array { - // Si le manga a des sources préférées, les utiliser if ($manga->hasPreferredSources()) { $preferredSources = []; foreach ($manga->getPreferredSources() as $sourceId) { @@ -186,7 +124,6 @@ readonly class ScrapeChapterHandler $preferredSources[] = $source; } - // Limiter à 3 sources préférées maximum if (count($preferredSources) >= 3) { break; } @@ -197,7 +134,6 @@ readonly class ScrapeChapterHandler } } - // Sinon, utiliser toutes les sources disponibles return $this->sourceRepository->getAll(); } } diff --git a/src/Domain/Scraping/Domain/Model/ScrapingJob.php b/src/Domain/Scraping/Domain/Model/ScrapingJob.php index 13ec881..6732d3b 100644 --- a/src/Domain/Scraping/Domain/Model/ScrapingJob.php +++ b/src/Domain/Scraping/Domain/Model/ScrapingJob.php @@ -8,9 +8,9 @@ class ScrapingJob extends Job { public function __construct( string $id, - string $mangaId, - float $chapterNumber, - string $sourceId + ?string $mangaId = null, + ?float $chapterNumber = null, + ?string $sourceId = null ) { parent::__construct($id, 'scraping_job'); $this->maxAttempts = 1; diff --git a/src/Domain/Scraping/Infrastructure/ApiPlatform/State/Processor/ScrapeChapterStateProcessor.php b/src/Domain/Scraping/Infrastructure/ApiPlatform/State/Processor/ScrapeChapterStateProcessor.php index 603ba6c..762ea1c 100644 --- a/src/Domain/Scraping/Infrastructure/ApiPlatform/State/Processor/ScrapeChapterStateProcessor.php +++ b/src/Domain/Scraping/Infrastructure/ApiPlatform/State/Processor/ScrapeChapterStateProcessor.php @@ -5,13 +5,17 @@ namespace App\Domain\Scraping\Infrastructure\ApiPlatform\State\Processor; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use App\Domain\Scraping\Application\Command\ScrapeChapter; +use App\Domain\Scraping\Domain\Model\ScrapingJob; use App\Domain\Scraping\Infrastructure\ApiPlatform\Dto\ScrapeChapterRequest; +use App\Domain\Shared\Domain\Contract\JobRepositoryInterface; +use Ramsey\Uuid\Uuid; use Symfony\Component\Messenger\MessageBusInterface; final class ScrapeChapterStateProcessor implements ProcessorInterface { public function __construct( - private readonly MessageBusInterface $commandBus + private readonly MessageBusInterface $commandBus, + private readonly JobRepositoryInterface $jobRepository, ) { } @@ -20,10 +24,11 @@ final class ScrapeChapterStateProcessor implements ProcessorInterface */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void { - $this->commandBus->dispatch( - new ScrapeChapter( - $data->chapterId - ) - ); + $jobId = Uuid::uuid4()->toString(); + $job = new ScrapingJob($jobId); + $job->context['chapterId'] = $data->chapterId; + $this->jobRepository->save($job); + + $this->commandBus->dispatch(new ScrapeChapter($data->chapterId, $jobId)); } } diff --git a/src/Domain/Scraping/Infrastructure/EventSubscriber/ScrapingEventSubscriber.php b/src/Domain/Scraping/Infrastructure/EventSubscriber/ScrapingEventSubscriber.php index a17f4a4..e861e1b 100644 --- a/src/Domain/Scraping/Infrastructure/EventSubscriber/ScrapingEventSubscriber.php +++ b/src/Domain/Scraping/Infrastructure/EventSubscriber/ScrapingEventSubscriber.php @@ -5,6 +5,7 @@ namespace App\Domain\Scraping\Infrastructure\EventSubscriber; use App\Domain\Shared\Domain\Event\ChapterScraped; use App\Domain\Scraping\Domain\Event\ChapterScrapingFailed; use App\Domain\Scraping\Domain\Event\ChapterScrapingStarted; +use App\Domain\Scraping\Domain\Event\PageScrapingProgressed; use App\Domain\Scraping\Domain\Contract\Repository\ChapterRepositoryInterface; use App\Domain\Shared\Domain\Contract\JobRepositoryInterface; use App\Domain\Shared\Domain\Contract\NotificationInterface; @@ -30,6 +31,22 @@ class ScrapingEventSubscriber implements EventSubscriberInterface return []; } + #[AsMessageHandler] + public function onPageScrapingProgressed(PageScrapingProgressed $event): void + { + $progress = (int) round($event->getProgress()->getPercentage()); + + $update = new Update( + 'jobs/activity', + json_encode([ + 'type' => 'job.progress_updated', + 'jobId' => $event->getJobId(), + 'progress' => $progress, + ]) + ); + $this->hub->publish($update); + } + #[AsMessageHandler] public function onChapterScrapingStarted(ChapterScrapingStarted $event): void { diff --git a/tests/Domain/Scraping/Application/CommandHandler/ScrapeChapterHandlerTest.php b/tests/Domain/Scraping/Application/CommandHandler/ScrapeChapterHandlerTest.php index 0c0fdec..e875548 100644 --- a/tests/Domain/Scraping/Application/CommandHandler/ScrapeChapterHandlerTest.php +++ b/tests/Domain/Scraping/Application/CommandHandler/ScrapeChapterHandlerTest.php @@ -7,6 +7,7 @@ use App\Domain\Scraping\Application\CommandHandler\ScrapeChapterHandler; 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\ScrapingJob; use App\Domain\Shared\Domain\Event\ChapterScraped; use App\Tests\Domain\Scraping\Adapter\InMemoryChapterRepository; use App\Tests\Domain\Scraping\Adapter\InMemoryEventBus; @@ -16,8 +17,6 @@ use App\Tests\Domain\Scraping\Adapter\InMemoryMangaRepository; use App\Tests\Domain\Scraping\Adapter\InMemoryScraperFactory; use App\Tests\Domain\Scraping\Adapter\InMemorySourceRepository; use App\Tests\Domain\Shared\Adapter\InMemoryJobRepository; -use Doctrine\ORM\EntityManagerInterface; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class ScrapeChapterHandlerTest extends TestCase @@ -30,7 +29,6 @@ class ScrapeChapterHandlerTest extends TestCase private InMemoryMangaRepository $mangaRepository; private InMemorySourceRepository $sourceRepository; private InMemoryEventBus $eventBus; - private EntityManagerInterface|MockObject $entityManager; private ScrapeChapterHandler $handler; protected function setUp(): void @@ -43,11 +41,6 @@ class ScrapeChapterHandlerTest extends TestCase $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', @@ -65,21 +58,21 @@ class ScrapeChapterHandlerTest extends TestCase $this->mangaRepository, $this->sourceRepository, $this->eventBus, - $this->entityManager ); } public function testHandleSuccessfully(): void { - $command = new ScrapeChapter( - chapterId: '1' - ); + $jobId = 'test-job-id'; + $job = new ScrapingJob($jobId, 'test-manga', 2); + $this->jobRepository->save($job); + $command = new ScrapeChapter(chapterId: '1', jobId: $jobId); $this->handler->handle($command); - $job = $this->jobRepository->findByType('scraping_job'); - $this->assertCount(1, $job); - $job = array_values($job)[0]; + $jobs = $this->jobRepository->findByType('scraping_job'); + $this->assertCount(1, $jobs); + $job = array_values($jobs)[0]; $dispatchedMessages = $this->eventBus->getDispatchedMessages(); $this->assertCount(2, $dispatchedMessages); diff --git a/tests/Feature/Scraping/ScrapeChapterTest.php b/tests/Feature/Scraping/ScrapeChapterTest.php index 3f2e884..a76e5f4 100644 --- a/tests/Feature/Scraping/ScrapeChapterTest.php +++ b/tests/Feature/Scraping/ScrapeChapterTest.php @@ -35,13 +35,14 @@ class ScrapeChapterTest extends AbstractApiTestCase // Then $this->assertResponseStatusCodeSame(202); - $messages = $this->messageBus->getDispatchedMessages(); + $messages = InMemoryMessageBus::$messages; $this->assertCount(1, $messages, 'Un message devrait être dispatché'); /** @var ScrapeChapter $message */ $message = $messages[0]; $this->assertInstanceOf(ScrapeChapter::class, $message); $this->assertEquals('chapter-123', $message->chapterId); + $this->assertNotEmpty($message->jobId); } public function testInitiateChapterScrapingWithInvalidPayload(): void @@ -72,6 +73,6 @@ class ScrapeChapterTest extends AbstractApiTestCase protected function tearDown(): void { parent::tearDown(); - $this->messageBus->clear(); + InMemoryMessageBus::$messages = []; } }