diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results new file mode 100644 index 0000000..71831d1 --- /dev/null +++ b/.phpunit.cache/test-results @@ -0,0 +1 @@ +{"version":1,"defects":{"App\\Tests\\Domain\\Scraping\\Application\\CommandHandler\\ScrapeChapterHandlerTest::testHandleSuccessfully":8,"App\\Tests\\Domain\\Scraping\\Application\\CommandHandler\\ScrapeChapterHandlerTest::testHandleThrowsException":8,"App\\Tests\\Feature\\Scraping\\ScrapeChapterTest::testInitiateChapterScraping":8,"App\\Tests\\Feature\\Scraping\\ScrapeChapterTest::testInitiateChapterScrapingWithInvalidPayload":8,"App\\Tests\\Feature\\Scraping\\ScrapingStatusTest::testGetScrapingStatus":7,"App\\Tests\\Feature\\Scraping\\ScrapingStatusTest::testGetScrapingStatusForNonExistentJob":7},"times":{"App\\Tests\\Domain\\Scraping\\Application\\CommandHandler\\ScrapeChapterHandlerTest::testHandleSuccessfully":0.003,"App\\Tests\\Domain\\Scraping\\Application\\CommandHandler\\ScrapeChapterHandlerTest::testHandleThrowsException":0,"App\\Tests\\Feature\\Scraping\\ScrapeChapterTest::testInitiateChapterScraping":0.038,"App\\Tests\\Feature\\Scraping\\ScrapeChapterTest::testInitiateChapterScrapingWithInvalidPayload":0.008,"App\\Tests\\Feature\\Scraping\\ScrapingStatusTest::testGetScrapingStatus":0.005,"App\\Tests\\Feature\\Scraping\\ScrapingStatusTest::testGetScrapingStatusForNonExistentJob":0.006}} \ No newline at end of file diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index 74f86b9..0cb406f 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -2,8 +2,8 @@ api_platform: title: Mangarr API version: 1.0.0 formats: - jsonld: ['application/ld+json'] json: ['application/json'] + jsonld: ['application/ld+json'] html: ['text/html'] jsonhal: ['application/hal+json'] swagger: @@ -23,3 +23,8 @@ api_platform: rfc_7807_compliant_errors: true event_listeners_backward_compatibility_layer: false keep_legacy_inflector: false + mapping: + paths: + - '%kernel.project_dir%/src/Domain/Scraping/Infrastructure/ApiPlatform/Dto' + patch_formats: + json: ['application/merge-patch+json'] diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 9fe3f61..8b92ef4 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -21,6 +21,13 @@ doctrine: dir: '%kernel.project_dir%/src/Entity' prefix: 'App\Entity' alias: App + # Ajout du mapping pour le domaine Scraping + Scraping: + type: attribute + is_bundle: false + dir: '%kernel.project_dir%/src/Domain/Scraping/Infrastructure/Persistence/Entity' + prefix: 'App\Domain\Scraping\Infrastructure\Persistence\Entity' + alias: Scraping when@test: doctrine: diff --git a/config/services_test.yaml b/config/services_test.yaml new file mode 100644 index 0000000..d626944 --- /dev/null +++ b/config/services_test.yaml @@ -0,0 +1,15 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: true + + Symfony\Component\Messenger\MessageBusInterface: + class: 'App\Tests\Domain\Scraping\Adapter\InMemoryMessageBus' + public: true + + App\Domain\Scraping\Domain\Contract\Repository\ScrapingJobRepositoryInterface: + class: 'App\Tests\Domain\Scraping\Adapter\InMemoryScrapingJobRepository' + public: true + + diff --git a/phpmd.xml b/phpmd.xml index 22855b2..a0b8d00 100644 --- a/phpmd.xml +++ b/phpmd.xml @@ -15,10 +15,10 @@ - + - - + + diff --git a/src/Domain/Scraping/Domain/Contract/Repository/SourceRepositoryInterface.php b/src/Domain/Scraping/Domain/Contract/Repository/SourceRepositoryInterface.php index b2d3637..ccbafe0 100644 --- a/src/Domain/Scraping/Domain/Contract/Repository/SourceRepositoryInterface.php +++ b/src/Domain/Scraping/Domain/Contract/Repository/SourceRepositoryInterface.php @@ -1,10 +1,10 @@ pages; } + public function getTotalPages(): int + { + return $this->totalPages; + } + public function getStatus(): ScrapingStatus { return $this->status; diff --git a/src/Domain/Scraping/Infrastructure/ApiPlatform/Dto/ScrapeChapterRequest.php b/src/Domain/Scraping/Infrastructure/ApiPlatform/Dto/ScrapeChapterRequest.php new file mode 100644 index 0000000..afb9ffd --- /dev/null +++ b/src/Domain/Scraping/Infrastructure/ApiPlatform/Dto/ScrapeChapterRequest.php @@ -0,0 +1,37 @@ + new Link( + fromProperty: 'jobId', + toProperty: 'id', + fromClass: ScrapingStatusResponse::class, + toClass: ScrapingJob::class + ) + ] + ), + ], +)] +readonly class ScrapingStatusResponse +{ + public function __construct( + #[ApiProperty(identifier: true)] + public string $jobId, + + #[ApiProperty] + public string $status, + + #[ApiProperty] + public ?float $progress = null, + + #[ApiProperty] + public ?string $error = null + ) { + } +} diff --git a/src/Domain/Scraping/Infrastructure/ApiPlatform/State/Processor/ScrapeChapterStateProcessor.php b/src/Domain/Scraping/Infrastructure/ApiPlatform/State/Processor/ScrapeChapterStateProcessor.php new file mode 100644 index 0000000..3c49577 --- /dev/null +++ b/src/Domain/Scraping/Infrastructure/ApiPlatform/State/Processor/ScrapeChapterStateProcessor.php @@ -0,0 +1,30 @@ +commandBus->dispatch( + new ScrapeChapter( + $data->chapterId, + $data->sourceId, + $data->mangaId + ) + ); + } +} diff --git a/src/Domain/Scraping/Infrastructure/ApiPlatform/State/Provider/ScrapingStatusStateProvider.php b/src/Domain/Scraping/Infrastructure/ApiPlatform/State/Provider/ScrapingStatusStateProvider.php new file mode 100644 index 0000000..7ad5037 --- /dev/null +++ b/src/Domain/Scraping/Infrastructure/ApiPlatform/State/Provider/ScrapingStatusStateProvider.php @@ -0,0 +1,37 @@ +scrapingJobRepository->findById($uriVariables['jobId']); + + if (!$job) { + throw new NotFoundHttpException('Job de scraping non trouvé'); + } + + $progress = 0; + if ($job->getTotalPages() > 0) { + $progress = (count($job->getPages()) / $job->getTotalPages()) * 100; + } + + return new ScrapingStatusResponse( + jobId: $job->getId(), + status: $job->getStatus()->value, + progress: $progress + ); + } +} diff --git a/src/Domain/Scraping/Infrastructure/Persistence/DoctrineScrapingJobRepository.php b/src/Domain/Scraping/Infrastructure/Persistence/DoctrineScrapingJobRepository.php index 0b569f0..f0f3266 100644 --- a/src/Domain/Scraping/Infrastructure/Persistence/DoctrineScrapingJobRepository.php +++ b/src/Domain/Scraping/Infrastructure/Persistence/DoctrineScrapingJobRepository.php @@ -2,17 +2,18 @@ namespace App\Domain\Scraping\Infrastructure\Persistence; +use App\Domain\Scraping\Domain\Contract\Repository\ScrapingJobRepositoryInterface; use App\Domain\Scraping\Domain\Model\ScrapingJob; use App\Domain\Scraping\Domain\Model\ScrapingStatus; -use App\Domain\Scraping\Domain\Repository\ScrapingJobRepositoryInterface; use App\Domain\Scraping\Infrastructure\Persistence\Entity\ScrapingJobEntity; use Doctrine\ORM\EntityManagerInterface; -class DoctrineScrapingJobRepository implements ScrapingJobRepositoryInterface +readonly class DoctrineScrapingJobRepository implements ScrapingJobRepositoryInterface { public function __construct( - private readonly EntityManagerInterface $entityManager - ) {} + private EntityManagerInterface $entityManager + ) { + } public function save(ScrapingJob $job): void { @@ -46,7 +47,7 @@ class DoctrineScrapingJobRepository implements ScrapingJobRepositoryInterface ->getQuery() ->getResult(); - return array_map(fn(ScrapingJobEntity $entity) => $entity->toDomain(), $entities); + return array_map(fn (ScrapingJobEntity $entity) => $entity->toDomain(), $entities); } public function findInProgressJobs(): array @@ -58,6 +59,6 @@ class DoctrineScrapingJobRepository implements ScrapingJobRepositoryInterface ->getQuery() ->getResult(); - return array_map(fn(ScrapingJobEntity $entity) => $entity->toDomain(), $entities); + return array_map(fn (ScrapingJobEntity $entity) => $entity->toDomain(), $entities); } -} \ No newline at end of file +} diff --git a/src/Entity/User.php b/src/Entity/User.php.old similarity index 100% rename from src/Entity/User.php rename to src/Entity/User.php.old diff --git a/tests/Domain/Scraping/Adapter/InMemoryMessageBus.php b/tests/Domain/Scraping/Adapter/InMemoryMessageBus.php new file mode 100644 index 0000000..bd13dd6 --- /dev/null +++ b/tests/Domain/Scraping/Adapter/InMemoryMessageBus.php @@ -0,0 +1,38 @@ + */ + public static array $messages = []; + + public function dispatch(object $message, array $stamps = []): Envelope + { + self::$messages[] = $message; + return new Envelope($message); + } + + public function getDispatchedMessages(): array + { + return self::$messages; + } + + public function clear(): void + { + self::$messages = []; + } + + public function hasMessageOfType(string $messageClass): bool + { + foreach (self::$messages as $message) { + if ($message instanceof $messageClass) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/tests/Domain/Scraping/Adapter/InMemoryScrapingJobRepository.php b/tests/Domain/Scraping/Adapter/InMemoryScrapingJobRepository.php index bbb5219..8068b07 100644 --- a/tests/Domain/Scraping/Adapter/InMemoryScrapingJobRepository.php +++ b/tests/Domain/Scraping/Adapter/InMemoryScrapingJobRepository.php @@ -8,21 +8,21 @@ use App\Domain\Scraping\Domain\Model\ScrapingJob; class InMemoryScrapingJobRepository implements ScrapingJobRepositoryInterface { /** @var ScrapingJob[] */ - private array $jobs = []; + private static array $jobs = []; public function save(ScrapingJob $job): void { - $this->jobs[] = $job; + self::$jobs[] = $job; } public function getJobs(): array { - return $this->jobs; + return self::$jobs; } public function findById(string $id): ?ScrapingJob { - foreach ($this->jobs as $job) { + foreach (self::$jobs as $job) { if ($job->getId() === $id) { return $job; } @@ -33,7 +33,7 @@ class InMemoryScrapingJobRepository implements ScrapingJobRepositoryInterface public function findByChapterId(string $chapterId): ?ScrapingJob { - foreach ($this->jobs as $job) { + foreach (self::$jobs as $job) { if ($job->getChapterId() === $chapterId) { return $job; } @@ -41,4 +41,9 @@ class InMemoryScrapingJobRepository implements ScrapingJobRepositoryInterface return null; } -} + + public function clear(): void + { + self::$jobs = []; + } +} \ No newline at end of file diff --git a/tests/Domain/Scraping/Application/CommandHandler/ScrapeChapterHandlerTest.php b/tests/Domain/Scraping/Application/CommandHandler/ScrapeChapterHandlerTest.php index 11088ad..3fc5269 100644 --- a/tests/Domain/Scraping/Application/CommandHandler/ScrapeChapterHandlerTest.php +++ b/tests/Domain/Scraping/Application/CommandHandler/ScrapeChapterHandlerTest.php @@ -40,17 +40,14 @@ class ScrapeChapterHandlerTest extends TestCase $this->handler->handle($command); - // Vérifier que le job a été créé $scrapingJobs = $this->scraper->getJobs(); $this->assertCount(1, $scrapingJobs); $job = $scrapingJobs[0]; - // Vérifier que le job a été sauvegardé $savedJobs = $this->repository->getJobs(); $this->assertCount(1, $savedJobs); $this->assertSame($job, $savedJobs[0]); - // Vérifier que l'événement a été dispatché $dispatchedMessages = $this->eventBus->getDispatchedMessages(); $this->assertCount(1, $dispatchedMessages); $this->assertInstanceOf(ChapterScrapingStarted::class, $dispatchedMessages[0]); @@ -74,7 +71,6 @@ class ScrapeChapterHandlerTest extends TestCase try { $this->handler->handle($command); } finally { - // Vérifier que l'événement d'échec a été dispatché $dispatchedMessages = $this->eventBus->getDispatchedMessages(); $this->assertCount(1, $dispatchedMessages); $this->assertInstanceOf(ChapterScrapingFailed::class, $dispatchedMessages[0]); diff --git a/tests/Feature/Scraping/AbstractApiTestCase.php b/tests/Feature/Scraping/AbstractApiTestCase.php new file mode 100644 index 0000000..4245ce8 --- /dev/null +++ b/tests/Feature/Scraping/AbstractApiTestCase.php @@ -0,0 +1,18 @@ +toString(), + $attributes['mangaId'] ?? 'manga-'.Uuid::uuid4()->toString(), + $attributes['chapterId'] ?? 'chapter-'.Uuid::uuid4()->toString(), + $attributes['sourceId'] ?? 'source-'.Uuid::uuid4()->toString() + ); + + if (isset($attributes['status'])) { + $this->setJobStatus($job, $attributes['status']); + } + + if (isset($attributes['pages'])) { + foreach ($attributes['pages'] as $index => $page) { + $job->addPage(new PageNumber($index + 1), new ImageUrl($page)); + } + } + + $this->repository->save($job); + + return $job; + } + + private function setJobStatus(ScrapingJob $job, ScrapingStatus $status): void + { + // Cette méthode nécessite peut-être d'ajouter des méthodes protégées dans ScrapingJob + // pour permettre la modification du statut dans les tests + // Ou utiliser de la réflexion si nécessaire + $reflection = new \ReflectionProperty($job, 'status'); + $reflection->setAccessible(true); + $reflection->setValue($job, $status); + } +} \ No newline at end of file diff --git a/tests/Feature/Scraping/ScrapeChapterTest.php b/tests/Feature/Scraping/ScrapeChapterTest.php new file mode 100644 index 0000000..1d99acd --- /dev/null +++ b/tests/Feature/Scraping/ScrapeChapterTest.php @@ -0,0 +1,71 @@ +messageBus = self::getContainer()->get(MessageBusInterface::class); + } + + public function testInitiateChapterScraping(): void + { + // Given + $payload = [ + 'chapterId' => 'chapter-123', + 'sourceId' => 'source-456', + 'mangaId' => 'manga-789', + ]; + + // When + $response = static::createClient()->request('POST', '/api/scraping/chapters', [ + 'json' => $payload, + 'headers' => ['Accept' => 'application/json'], + ]); + + // Then + $this->assertResponseStatusCodeSame(202); + + $messages = $this->messageBus::$messages; + $this->assertCount(1, $messages, 'Un message devrait être dispatché'); + + /** @var ScrapeChapter $message */ + $message = $messages[0]; + $this->assertInstanceOf(ScrapeChapter::class, $message); + } + + public function testInitiateChapterScrapingWithInvalidPayload(): void + { + // Given + $payload = [ + 'chapterId' => '', + 'sourceId' => 'source-456', + 'mangaId' => 'manga-789', + ]; + + // When + $response = static::createClient()->request('POST', '/api/scraping/chapters', [ + 'json' => $payload, + 'headers' => ['Accept' => 'application/json'], + ]); + + // Then + $this->assertResponseStatusCodeSame(422); + $this->assertJsonContains([ + 'violations' => [ + [ + 'propertyPath' => 'chapterId', + 'message' => 'This value should not be blank.', + ], + ], + ]); + } +} diff --git a/tests/Feature/Scraping/ScrapingStatusTest.php b/tests/Feature/Scraping/ScrapingStatusTest.php new file mode 100644 index 0000000..b277347 --- /dev/null +++ b/tests/Feature/Scraping/ScrapingStatusTest.php @@ -0,0 +1,69 @@ +messageBus = self::getContainer()->get(MessageBusInterface::class); + $this->repository = self::getContainer()->get(ScrapingJobRepositoryInterface::class); + } + + public function testGetScrapingStatus(): void + { + // Given + $jobId = Uuid::uuid4()->toString(); + $job = new ScrapingJob($jobId, 'manga-123', 'chapter-456', 'source-789'); + + $job->addPage(new PageNumber(1), new ImageUrl('http://example.com/page1.jpg')); + $job->addPage(new PageNumber(2), new ImageUrl('http://example.com/page2.jpg')); + + $this->repository->save($job); + + // When + $response = static::createClient()->request('GET', '/api/scraping/jobs/'.$jobId.'/status'); + + // Then + $this->assertResponseIsSuccessful(); + $this->assertJsonContains([ + 'jobId' => $jobId, + 'status' => ScrapingStatus::IN_PROGRESS->value, + 'progress' => 0 + ]); + } + + public function testGetScrapingStatusForNonExistentJob(): void + { + // When + $response = static::createClient()->request('GET', '/api/scraping/jobs/non-existent-id/status', [ + 'headers' => ['Accept' => 'application/json'] + ]); + + // Then + $this->assertResponseStatusCodeSame(404); + } + + protected function tearDown(): void + { + parent::tearDown(); + + self::ensureKernelShutdown(); + } +} \ No newline at end of file