diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 16c91a8..0b7dc4d 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -8,3 +8,11 @@ parameters: - config/ - public/ - src/ + + ignoreErrors: + - + message: '#^Undefined variable: \$this$#' + paths: + - tests/* + + reportUnmatchedIgnoredErrors: false diff --git a/src/Application/CommandHandler/Game/CreateDeveloperCommandHandler.php b/src/Application/CommandHandler/Game/CreateDeveloperCommandHandler.php index ab530b9..fa71ac0 100644 --- a/src/Application/CommandHandler/Game/CreateDeveloperCommandHandler.php +++ b/src/Application/CommandHandler/Game/CreateDeveloperCommandHandler.php @@ -5,6 +5,7 @@ namespace App\Application\CommandHandler\Game; use App\Application\Command\Game\CreateDeveloperCommand; +use App\Application\Fetcher\Game\DeveloperFetcher; use App\Domain\Factory\Game\DeveloperFactory; use App\Domain\Repository\Game\DeveloperRepositoryInterface; use Psr\Log\LoggerInterface; @@ -15,13 +16,14 @@ class CreateDeveloperCommandHandler { public function __construct( private readonly DeveloperRepositoryInterface $developerRepository, + private readonly DeveloperFetcher $developerFetcher, private readonly LoggerInterface $logger, ) { } public function __invoke(CreateDeveloperCommand $command): void { - $developer = $this->developerRepository->findByName($command->name); + $developer = $this->developerFetcher->findOneByName($command->name); if (null !== $developer) { return; @@ -35,6 +37,10 @@ public function __invoke(CreateDeveloperCommand $command): void try { $this->developerRepository->save($developer); + + /** @var string $developerName */ + $developerName = $developer->getName(); + $this->developerFetcher->deleteCacheName($developerName); } catch (\Exception $e) { $this->logger->error($e->getMessage()); } diff --git a/src/Application/CommandHandler/Game/CreateGameCommandHandler.php b/src/Application/CommandHandler/Game/CreateGameCommandHandler.php index 866fec2..28ef549 100644 --- a/src/Application/CommandHandler/Game/CreateGameCommandHandler.php +++ b/src/Application/CommandHandler/Game/CreateGameCommandHandler.php @@ -7,6 +7,7 @@ use App\Application\Command\Game\CreateDeveloperCommand; use App\Application\Command\Game\CreateGameCommand; use App\Application\Command\Game\CreatePublisherCommand; +use App\Application\Fetcher\Game\GameFetcher; use App\Application\Helper\MessageBusHelper; use App\Application\Query\Game\GetDeveloperByNameQuery; use App\Application\Query\Game\GetPublisherByNameQuery; @@ -25,6 +26,7 @@ class CreateGameCommandHandler { public function __construct( private readonly GameRepositoryInterface $gameRepository, + private readonly GameFetcher $gameFetcher, private readonly PublisherRepositoryInterface $publisherRepository, private readonly DeveloperRepositoryInterface $developerRepository, private readonly MessageBusInterface $messageBus, @@ -35,27 +37,30 @@ public function __construct( public function __invoke(CreateGameCommand $command): void { + $game = $this->gameRepository->getOneBySlugEnabledGame($command->slug); + + if (null !== $game) { + return; + } + $publisher = null; $developer = null; if (null !== $command->publisher) { /** @var ?Publisher $publisher */ - $publisher = $this->publisherRepository->findByName( + $publisher = $this->publisherRepository->findOneByName( $command->publisher->name, ); } if (null !== $command->developer) { /** @var ?Developer $developer */ - $developer = $this->developerRepository->findByName( + $developer = $this->developerRepository->findOneByName( $command->developer->name, ); } - if ( - null !== $command->publisher - && null === $publisher - ) { + if (null !== $command->publisher && null === $publisher) { $this->messageBus->dispatch( new CreatePublisherCommand( name: $command->publisher->name, @@ -76,10 +81,7 @@ public function __invoke(CreateGameCommand $command): void ); } - if ( - null !== $command->developer - && null === $developer - ) { + if (null !== $command->developer && null === $developer) { $this->messageBus->dispatch( new CreateDeveloperCommand( name: $command->developer->name, @@ -112,6 +114,7 @@ public function __invoke(CreateGameCommand $command): void try { $this->gameRepository->save($game); + $this->gameFetcher->deleteCacheForSlug($command->slug); } catch (\Exception $e) { $this->logger->error($e->getMessage()); } diff --git a/src/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandler.php b/src/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandler.php index 094bb23..a5a73ff 100644 --- a/src/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandler.php +++ b/src/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandler.php @@ -51,6 +51,7 @@ function (): ?ApiGame { $apiGame = $this->messageBusHelper->getContentFromEnvelope( $gameEnvelope, 'Failed to fetch game from API', + ApiGame::class, ); } diff --git a/src/Application/CommandHandler/Game/CreatePublisherCommandHandler.php b/src/Application/CommandHandler/Game/CreatePublisherCommandHandler.php index b4f2f43..b204c92 100644 --- a/src/Application/CommandHandler/Game/CreatePublisherCommandHandler.php +++ b/src/Application/CommandHandler/Game/CreatePublisherCommandHandler.php @@ -6,6 +6,7 @@ use App\Application\Command\Game\CreatePublisherCommand; use App\Application\Exception\Game\CannotCreatePublisherException; +use App\Application\Fetcher\Game\PublisherFetcher; use App\Domain\Factory\Game\GamePublisherFactory; use App\Domain\Repository\Game\PublisherRepositoryInterface; use Psr\Log\LoggerInterface; @@ -16,6 +17,7 @@ class CreatePublisherCommandHandler { public function __construct( private readonly PublisherRepositoryInterface $publisherRepository, + private readonly PublisherFetcher $publisherFetcher, private readonly LoggerInterface $logger, ) { } @@ -25,7 +27,7 @@ public function __construct( */ public function __invoke(CreatePublisherCommand $command): void { - $publisher = $this->publisherRepository->findByName($command->name); + $publisher = $this->publisherFetcher->findOneByName($command->name); if (null !== $publisher) { return; @@ -39,6 +41,7 @@ public function __invoke(CreatePublisherCommand $command): void try { $this->publisherRepository->save($publisher); + $this->publisherFetcher->deleteCacheName($command->name); } catch (\Exception $e) { $this->logger->error($e->getMessage()); throw new CannotCreatePublisherException(); diff --git a/src/Application/CommandHandler/Game/UpdateDeveloperCommandHandler.php b/src/Application/CommandHandler/Game/UpdateDeveloperCommandHandler.php index cb721d8..3abccd3 100644 --- a/src/Application/CommandHandler/Game/UpdateDeveloperCommandHandler.php +++ b/src/Application/CommandHandler/Game/UpdateDeveloperCommandHandler.php @@ -5,6 +5,7 @@ namespace App\Application\CommandHandler\Game; use App\Application\Command\Game\UpdateDeveloperCommand; +use App\Application\Fetcher\Game\DeveloperFetcher; use App\Domain\Model\Game\Developer; use App\Domain\Repository\Game\DeveloperRepositoryInterface; use Psr\Log\LoggerInterface; @@ -15,6 +16,7 @@ class UpdateDeveloperCommandHandler { public function __construct( private readonly DeveloperRepositoryInterface $developerRepository, + private readonly DeveloperFetcher $developerFetcher, private readonly LoggerInterface $logger, ) { } @@ -22,7 +24,10 @@ public function __construct( public function __invoke(UpdateDeveloperCommand $command): void { /** @var Developer $developer */ - $developer = $this->developerRepository->findByApiId($command->developerApiId); + $developer = $this->developerFetcher->findOneByApiId( + $command->developerApiId, + $command->name, + ); if (null !== $developer) { $developer->setName($command->name); @@ -31,6 +36,7 @@ public function __invoke(UpdateDeveloperCommand $command): void try { $this->developerRepository->update($developer); + $this->developerFetcher->deleteCacheName($command->name); } catch (\Exception $e) { $this->logger->error($e->getMessage()); } diff --git a/src/Application/CommandHandler/Game/UpdateGameFromApiCommandHandler.php b/src/Application/CommandHandler/Game/UpdateGameFromApiCommandHandler.php index cfb9a4e..69b6878 100644 --- a/src/Application/CommandHandler/Game/UpdateGameFromApiCommandHandler.php +++ b/src/Application/CommandHandler/Game/UpdateGameFromApiCommandHandler.php @@ -48,11 +48,13 @@ public function __invoke(UpdateGameFromApiCommand $command): void if (null !== $command->apiGame->getPublisher()) { if (null === $publisher) { try { - $this->messageBus->dispatch(new CreatePublisherCommand( - name: $command->apiGame->getPublisher()->name, - website: $command->apiGame->getPublisher()->website, - apiId: $command->apiGame->getPublisher()->apiId, - )); + $this->messageBus->dispatch( + new CreatePublisherCommand( + name: $command->apiGame->getPublisher()->name, + website: $command->apiGame->getPublisher()->website, + apiId: $command->apiGame->getPublisher()->apiId, + ), + ); } catch (ExceptionInterface) { throw new CannotCreatePublisherException(); } @@ -61,27 +63,33 @@ public function __invoke(UpdateGameFromApiCommand $command): void $publisherApiId = $publisher->getApiId(); try { - $this->messageBus->dispatch(new UpdatePublisherCommand( - publisherApiId: $publisherApiId, - name: $command->apiGame->getPublisher()->name, - website: $command->apiGame->getPublisher()->website, - )); + $this->messageBus->dispatch( + new UpdatePublisherCommand( + publisherApiId: $publisherApiId, + name: $command->apiGame->getPublisher()->name, + website: $command->apiGame->getPublisher()->website, + ), + ); } catch (ExceptionInterface) { throw new CannotUpdatePublisherException(); } } - $publisher = $this->publisherRepository->findByApiId($command->apiGame->getPublisher()->apiId); + $publisher = $this->publisherRepository->findOneByApiId( + $command->apiGame->getPublisher()->apiId, + ); } if (null !== $command->apiGame->getDeveloper()) { if (null === $developer) { try { - $this->messageBus->dispatch(new CreateDeveloperCommand( - name: $command->apiGame->getDeveloper()->name, - website: $command->apiGame->getDeveloper()->website, - apiId: $command->apiGame->getDeveloper()->apiId, - )); + $this->messageBus->dispatch( + new CreateDeveloperCommand( + name: $command->apiGame->getDeveloper()->name, + website: $command->apiGame->getDeveloper()->website, + apiId: $command->apiGame->getDeveloper()->apiId, + ), + ); } catch (ExceptionInterface) { throw new CannotCreateDeveloperException(); } @@ -90,17 +98,21 @@ public function __invoke(UpdateGameFromApiCommand $command): void $developerApiId = $developer->getApiId(); try { - $this->messageBus->dispatch(new UpdateDeveloperCommand( - developerApiId: $developerApiId, - name: $command->apiGame->getDeveloper()->name, - website: $command->apiGame->getDeveloper()->website, - )); + $this->messageBus->dispatch( + new UpdateDeveloperCommand( + developerApiId: $developerApiId, + name: $command->apiGame->getDeveloper()->name, + website: $command->apiGame->getDeveloper()->website, + ), + ); } catch (ExceptionInterface) { throw new CannotUpdateDeveloperException(); } } - $developer = $this->developerRepository->findByApiId($command->apiGame->getDeveloper()->apiId); + $developer = $this->developerRepository->findOneByApiId( + $command->apiGame->getDeveloper()->apiId, + ); } $command->game->setName($command->apiGame->getName()); diff --git a/src/Application/CommandHandler/Game/UpdatePublisherCommandHandler.php b/src/Application/CommandHandler/Game/UpdatePublisherCommandHandler.php index 84ed372..a309fb6 100644 --- a/src/Application/CommandHandler/Game/UpdatePublisherCommandHandler.php +++ b/src/Application/CommandHandler/Game/UpdatePublisherCommandHandler.php @@ -5,6 +5,7 @@ namespace App\Application\CommandHandler\Game; use App\Application\Command\Game\UpdatePublisherCommand; +use App\Application\Fetcher\Game\PublisherFetcher; use App\Domain\Model\Game\Publisher; use App\Domain\Repository\Game\PublisherRepositoryInterface; use Psr\Log\LoggerInterface; @@ -15,6 +16,7 @@ class UpdatePublisherCommandHandler { public function __construct( private readonly PublisherRepositoryInterface $publisherRepository, + private readonly PublisherFetcher $publisherFetcher, private readonly LoggerInterface $logger, ) { } @@ -22,7 +24,10 @@ public function __construct( public function __invoke(UpdatePublisherCommand $command): void { /** @var Publisher $publisher */ - $publisher = $this->publisherRepository->findByApiId($command->publisherApiId); + $publisher = $this->publisherFetcher->findOneByApiId( + $command->publisherApiId, + $command->name, + ); if (null !== $publisher) { $publisher->setName($command->name); @@ -31,6 +36,7 @@ public function __invoke(UpdatePublisherCommand $command): void try { $this->publisherRepository->update($publisher); + $this->publisherFetcher->deleteCacheName($command->name); } catch (\Exception $e) { $this->logger->error($e->getMessage()); } diff --git a/src/Application/CommandHandler/Report/CreateReportFromDtoCommandHandler.php b/src/Application/CommandHandler/Report/CreateReportFromDtoCommandHandler.php index d263967..281c290 100644 --- a/src/Application/CommandHandler/Report/CreateReportFromDtoCommandHandler.php +++ b/src/Application/CommandHandler/Report/CreateReportFromDtoCommandHandler.php @@ -5,6 +5,8 @@ namespace App\Application\CommandHandler\Report; use App\Application\Command\Report\CreateReportFromDtoCommand; +use App\Application\Fetcher\Report\ReportFetcher; +use App\Application\Helper\CacheKeyBuilderHelper; use App\Domain\Factory\Report\ReportFactory; use App\Domain\Model\Game\Game; use App\Domain\Repository\Game\GameRepositoryInterface; @@ -18,6 +20,7 @@ class CreateReportFromDtoCommandHandler public function __construct( private readonly ReportRepositoryInterface $reportRepository, private readonly GameRepositoryInterface $gameRepository, + private readonly ReportFetcher $reportFetcher, ) { } @@ -46,5 +49,8 @@ public function __invoke(CreateReportFromDtoCommand $command): void ); $this->reportRepository->save($newReport); + $this->reportFetcher->deleteCache( + CacheKeyBuilderHelper::build(ReportFetcher::REPORTS_CACHE_KEY, $command->formInputDto->gameSlug) + ); } } diff --git a/src/Application/Enum/CacheDurationEnum.php b/src/Application/Enum/CacheDurationEnum.php new file mode 100644 index 0000000..7c09a2b --- /dev/null +++ b/src/Application/Enum/CacheDurationEnum.php @@ -0,0 +1,13 @@ +cache->get( + self::DEVELOPER_CACHE_KEY.$sluggedDeveloperName, + function (ItemInterface $item) use ($developerName) { + $developer = $this->developerRepository->findOneByName( + $developerName, + ); + + if (null === $developer) { + return null; + } + + $item->expiresAfter(CacheDurationEnum::THREE_DAYS->value); + + return $developer; + }, + ); + } + + public function findOneByApiId( + int $developerApiId, + string $developerName, + ): ?Developer { + /** @var ?Developer */ + return $this->cache->get( + self::DEVELOPER_CACHE_KEY.$developerName, + function (ItemInterface $item) use ($developerApiId) { + $developer = $this->developerRepository->findOneByApiId($developerApiId); + + if (null === $developer) { + return null; + } + + $item->expiresAfter(CacheDurationEnum::THREE_DAYS->value); + + return $developer; + }, + ); + } + + public function deleteCacheName(string $developerName): void + { + $this->cache->delete( + self::DEVELOPER_CACHE_KEY.SlugHelper::slugify($developerName), + ); + } +} diff --git a/src/Application/Fetcher/Game/GameFetcher.php b/src/Application/Fetcher/Game/GameFetcher.php new file mode 100644 index 0000000..f71d256 --- /dev/null +++ b/src/Application/Fetcher/Game/GameFetcher.php @@ -0,0 +1,82 @@ +cache->get($cacheKey, function ( + ItemInterface $item, + ) use ($slug) { + $game = $this->gameRepository->getOneBySlugEnabledGame($slug); + + if (null === $game) { + $item->expiresAfter(10); + + return null; + } + + $item->expiresAfter(CacheDurationEnum::THREE_DAYS->value); + + return $game; + }); + + return $game; + } + + public function getOneByIdEnabledGame(string $gameId): ?Game + { + return $this->cache->get( + CacheKeyBuilderHelper::build( + self::GAME_CACHE_KEY.'getOneByIdEnabledGame', + $gameId, + ), + function (ItemInterface $item) use ($gameId) { + $game = $this->gameRepository->getOneByIdEnabledGame( + gameId: $gameId, + ); + + if (null === $game) { + return null; + } + + $item->expiresAfter(CacheDurationEnum::THREE_DAYS->value); + + return $game; + }, + ); + } + + public function deleteCacheForSlug(string $slug): bool + { + return $this->cache->delete( + CacheKeyBuilderHelper::build( + self::GAME_CACHE_KEY.'getOneBySlugEnabledGame', + $slug, + ), + ); + } +} diff --git a/src/Application/Fetcher/Game/PublisherFetcher.php b/src/Application/Fetcher/Game/PublisherFetcher.php new file mode 100644 index 0000000..8b37a75 --- /dev/null +++ b/src/Application/Fetcher/Game/PublisherFetcher.php @@ -0,0 +1,67 @@ +cache->get( + self::PUBLISHER_CACHE_KEY.SlugHelper::slugify($name), + function (ItemInterface $item) use ($name) { + $publisher = $this->publisherRepository->findOneByName($name); + + if (null === $publisher) { + return null; + } + + $item->expiresAfter(CacheDurationEnum::THREE_DAYS->value); + + return $publisher; + }, + ); + } + + public function findOneByApiId(int $apiId, string $name): ?Publisher + { + /** @var ?Publisher */ + return $this->cache->get(self::PUBLISHER_CACHE_KEY.$name, function ( + ItemInterface $item, + ) use ($apiId) { + $publisher = $this->publisherRepository->findOneByApiId($apiId); + + if (null === $publisher) { + return null; + } + + $item->expiresAfter(CacheDurationEnum::THREE_DAYS->value); + + return $publisher; + }); + } + + public function deleteCacheName(string $name): void + { + $this->cache->delete( + self::PUBLISHER_CACHE_KEY.SlugHelper::slugify($name), + ); + } +} diff --git a/src/Application/Fetcher/Report/ReportFetcher.php b/src/Application/Fetcher/Report/ReportFetcher.php new file mode 100644 index 0000000..613647a --- /dev/null +++ b/src/Application/Fetcher/Report/ReportFetcher.php @@ -0,0 +1,54 @@ +cache->get( + CacheKeyBuilderHelper::build(self::REPORTS_CACHE_KEY, $gameSlug), + function (ItemInterface $item) use ($gameId) { + $reports = $this->repository->getAllVisibleReportsForGame( + $gameId, + ); + + if (empty($reports)) { + return []; + } + + $item->expiresAfter(CacheDurationEnum::THREE_DAYS->value); + + return $reports; + }, + ); + } + + public function deleteCache(string $key): bool + { + return $this->cache->delete($key); + } +} diff --git a/src/Application/Helper/CacheKeyBuilderHelper.php b/src/Application/Helper/CacheKeyBuilderHelper.php new file mode 100644 index 0000000..8adc498 --- /dev/null +++ b/src/Application/Helper/CacheKeyBuilderHelper.php @@ -0,0 +1,13 @@ +logger->error('Get data from envelope failed: class and type cannot be null together'); + if (null === $class && (null === $type || 'array' !== $type)) { + $this->logger->error( + 'Get data from envelope failed: class and type cannot be null together', + ); throw new ClassAndTypeCannotBeNullTogetherException('Get data from envelope failed: class and type cannot be null together'); } @@ -47,9 +45,13 @@ public function getContentFromEnvelope( } if ( - null !== $type - && gettype($result) !== $type + 'array' === $type + && empty($result) ) { + return []; + } + + if (null !== $type && gettype($result) !== $type) { $this->logger->error($logErrorMessage); throw new WrongTypeInMessageBusEnvelopeException(message: sprintf('Expected "%s", returned "%s"', $type, gettype($result))); } @@ -67,10 +69,7 @@ public function getContentFromEnvelope( return $result; } - if ( - !empty($result) - && null !== $class - ) { + if (!empty($result) && null !== $class) { $this->logger->error($logErrorMessage); /** @var object $result */ @@ -78,10 +77,7 @@ public function getContentFromEnvelope( } } - if ( - null !== $class - && !$result instanceof $class - ) { + if (null !== $class && !$result instanceof $class) { $this->logger->error($logErrorMessage); /** @var object $result */ diff --git a/src/Application/Helper/SlugHelper.php b/src/Application/Helper/SlugHelper.php new file mode 100644 index 0000000..37ec0e1 --- /dev/null +++ b/src/Application/Helper/SlugHelper.php @@ -0,0 +1,13 @@ +developerRepository->findByName($query->name); + return $this->developerFetcher->findOneByName($query->name); } } diff --git a/src/Application/QueryHandler/Game/GetGameBySlugQueryHandler.php b/src/Application/QueryHandler/Game/GetGameBySlugQueryHandler.php index 6dcab6b..99b1168 100644 --- a/src/Application/QueryHandler/Game/GetGameBySlugQueryHandler.php +++ b/src/Application/QueryHandler/Game/GetGameBySlugQueryHandler.php @@ -4,21 +4,20 @@ namespace App\Application\QueryHandler\Game; +use App\Application\Fetcher\Game\GameFetcher; use App\Application\Query\Game\GetGameBySlugQuery; use App\Domain\Model\Game\Game; -use App\Domain\Repository\Game\GameRepositoryInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; #[AsMessageHandler] class GetGameBySlugQueryHandler { - public function __construct( - private readonly GameRepositoryInterface $gameRepository, - ) { + public function __construct(private readonly GameFetcher $gameFetcher) + { } public function __invoke(GetGameBySlugQuery $query): ?Game { - return $this->gameRepository->getOneBySlugEnabledGame($query->gameSlug); + return $this->gameFetcher->getOneBySlugEnabledGame($query->gameSlug); } } diff --git a/src/Application/QueryHandler/Game/GetOrCreateGameQueryHandler.php b/src/Application/QueryHandler/Game/GetOrCreateGameQueryHandler.php index 23f5331..54de61a 100644 --- a/src/Application/QueryHandler/Game/GetOrCreateGameQueryHandler.php +++ b/src/Application/QueryHandler/Game/GetOrCreateGameQueryHandler.php @@ -6,9 +6,9 @@ use App\Application\Command\Game\CreateGameCommand; use App\Application\Exception\Game\CannotCreateGameException; +use App\Application\Fetcher\Game\GameFetcher; use App\Application\Query\Game\GetOrCreateGameQuery; use App\Domain\Model\Game\Game; -use App\Domain\Repository\Game\GameRepositoryInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Exception\ExceptionInterface; @@ -19,7 +19,7 @@ class GetOrCreateGameQueryHandler { public function __construct( private readonly MessageBusInterface $messageBus, - private readonly GameRepositoryInterface $gameRepository, + private readonly GameFetcher $gameFetcher, private readonly LoggerInterface $logger, ) { } @@ -29,19 +29,21 @@ public function __construct( */ public function __invoke(GetOrCreateGameQuery $query): Game { - $game = $this->gameRepository->getOneBySlugEnabledGame($query->gameSlug); + $game = $this->gameFetcher->getOneBySlugEnabledGame($query->gameSlug); if (null === $game) { try { - $this->messageBus->dispatch(new CreateGameCommand( - name: $query->apiGame->getName(), - slug: $query->apiGame->getSlug(), - releaseDate: $query->apiGame->getReleaseDate(), - imageCover: $query->apiGame->getImageCover(), - publisher: $query->apiGame->getPublisher(), - developer: $query->apiGame->getDeveloper(), - description: $query->apiGame->getDescription(), - )); + $this->messageBus->dispatch( + new CreateGameCommand( + name: $query->apiGame->getName(), + slug: $query->apiGame->getSlug(), + releaseDate: $query->apiGame->getReleaseDate(), + imageCover: $query->apiGame->getImageCover(), + publisher: $query->apiGame->getPublisher(), + developer: $query->apiGame->getDeveloper(), + description: $query->apiGame->getDescription(), + ), + ); } catch (\Exception|ExceptionInterface $e) { $this->logger->error($e->getMessage()); @@ -49,7 +51,9 @@ public function __invoke(GetOrCreateGameQuery $query): Game } /** @var Game $game */ - $game = $this->gameRepository->getOneBySlugEnabledGame($query->gameSlug); + $game = $this->gameFetcher->getOneBySlugEnabledGame( + $query->gameSlug, + ); } return $game; diff --git a/src/Application/QueryHandler/Game/GetPublisherByNameQueryHandler.php b/src/Application/QueryHandler/Game/GetPublisherByNameQueryHandler.php index dd3d2fe..d83f2fa 100644 --- a/src/Application/QueryHandler/Game/GetPublisherByNameQueryHandler.php +++ b/src/Application/QueryHandler/Game/GetPublisherByNameQueryHandler.php @@ -4,21 +4,21 @@ namespace App\Application\QueryHandler\Game; +use App\Application\Fetcher\Game\PublisherFetcher; use App\Application\Query\Game\GetPublisherByNameQuery; use App\Domain\Model\Game\Publisher; -use App\Domain\Repository\Game\PublisherRepositoryInterface as GamePublisherRepositoryInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; #[AsMessageHandler] class GetPublisherByNameQueryHandler { public function __construct( - private GamePublisherRepositoryInterface $publisherRepository, + private PublisherFetcher $publisherFetcher, ) { } public function __invoke(GetPublisherByNameQuery $query): ?Publisher { - return $this->publisherRepository->findByName($query->name); + return $this->publisherFetcher->findOneByName($query->name); } } diff --git a/src/Application/QueryHandler/Report/GetReportsForGameIdQueryHandler.php b/src/Application/QueryHandler/Report/GetReportsForGameIdQueryHandler.php new file mode 100644 index 0000000..8df260c --- /dev/null +++ b/src/Application/QueryHandler/Report/GetReportsForGameIdQueryHandler.php @@ -0,0 +1,29 @@ +reportFetcher->getAllVisibleReportsForGame( + $query->gameId, + $query->gameSlug, + ); + } +} diff --git a/src/Domain/Repository/Game/DeveloperRepositoryInterface.php b/src/Domain/Repository/Game/DeveloperRepositoryInterface.php index b98d633..b2bec1b 100644 --- a/src/Domain/Repository/Game/DeveloperRepositoryInterface.php +++ b/src/Domain/Repository/Game/DeveloperRepositoryInterface.php @@ -19,7 +19,7 @@ */ interface DeveloperRepositoryInterface { - public function findByName(string $name): ?Developer; + public function findOneByName(string $name): ?Developer; - public function findByApiId(int $apiId): ?Developer; + public function findOneByApiId(int $apiId): ?Developer; } diff --git a/src/Domain/Repository/Game/PublisherRepositoryInterface.php b/src/Domain/Repository/Game/PublisherRepositoryInterface.php index bbb3755..82a133f 100644 --- a/src/Domain/Repository/Game/PublisherRepositoryInterface.php +++ b/src/Domain/Repository/Game/PublisherRepositoryInterface.php @@ -19,7 +19,7 @@ */ interface PublisherRepositoryInterface { - public function findByName(string $name): ?Publisher; + public function findOneByName(string $name): ?Publisher; - public function findByApiId(int $apiId): ?Publisher; + public function findOneByApiId(int $apiId): ?Publisher; } diff --git a/src/Infrastructure/Client/IgdbClient.php b/src/Infrastructure/Client/IgdbClient.php index 3ee8d3a..34751ab 100644 --- a/src/Infrastructure/Client/IgdbClient.php +++ b/src/Infrastructure/Client/IgdbClient.php @@ -19,6 +19,7 @@ use Symfony\Component\Serializer\Exception\ExceptionInterface; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; @@ -85,23 +86,29 @@ public function getGameBySlug(string $slug, int $limit = 3): ?ApiGame { $cacheKey = $this->generateGetGameSlugCacheKey($slug, $limit); - $game = $this->cache->get( - $cacheKey, - function () use ($slug, $limit) { - return $this->performApiRequest( - query: $slug, - type: ApiTypeRequestEnum::SLUG, - limit: $limit, - ); - }, - self::API_RESPONSE_CACHE_TTL, - ); + $game = $this->cache->get($cacheKey, function ( + ItemInterface $item, + ) use ($slug, $limit) { + $games = $this->performApiRequest( + query: $slug, + type: ApiTypeRequestEnum::SLUG, + limit: $limit, + ); - if (!empty($game)) { - return $game[0]; + if (empty($games)) { + return null; + } + + $item->expiresAfter(self::API_RESPONSE_CACHE_TTL); + + return $games[0]; + }); + + if (null === $game) { + return null; } - return null; + return $game; } private function generateSearchCacheKey(string $query, int $limit): string @@ -158,18 +165,10 @@ private function performApiRequest( return []; } - $apiQuery = ''; - - if (ApiTypeRequestEnum::SLUG === $type) { - $apiQuery = sprintf('slug ~ *"%s";', $query); - } else { - foreach ($queryTerms as $term) { - $apiQuery .= - $term === $queryTerms[0] - ? sprintf('name ~ *"%s"*', $term) - : sprintf(' & name ~ *"%s"*', $term); - } - } + $apiQuery = match ($type) { + ApiTypeRequestEnum::SLUG => $this->buildSlugBodyWhereClause($query), + default => $this->buildSearchBodyWhereClause($queryTerms), + }; $body = sprintf( ' @@ -184,7 +183,7 @@ private function performApiRequest( summary, websites.url, updated_at; - where (%s) + where %s & platforms = (%d) & %s & version_parent = null @@ -224,6 +223,28 @@ private function performApiRequest( return $this->formatResponseData($deserializedResponseData); } + private function buildSlugBodyWhereClause(string $slug): string + { + return sprintf('slug ~ *"%s"* ', $slug); + } + + /** + * @param string[] $queryTerms + */ + private function buildSearchBodyWhereClause(array $queryTerms): string + { + $apiQuery = ''; + + foreach ($queryTerms as $term) { + $apiQuery .= + $term === $queryTerms[0] + ? sprintf('name ~ *"%s"*', $term) + : sprintf(' & name ~ *"%s"*', $term); + } + + return '('.$apiQuery.')'; + } + /** * @param IgdbSearchResponseDto[] $igdbSearchResponseDto * diff --git a/src/Infrastructure/Persistence/Doctrine/Game/Repository/DoctrineDeveloperRepository.php b/src/Infrastructure/Persistence/Doctrine/Game/Repository/DoctrineDeveloperRepository.php index ae2ddbc..b6731e5 100644 --- a/src/Infrastructure/Persistence/Doctrine/Game/Repository/DoctrineDeveloperRepository.php +++ b/src/Infrastructure/Persistence/Doctrine/Game/Repository/DoctrineDeveloperRepository.php @@ -16,13 +16,13 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, Developer::class); } - public function findByName(string $name): ?Developer + public function findOneByName(string $name): ?Developer { /** @var ?Developer */ return $this->findOneBy(['name' => $name]); } - public function findByApiId(int $apiId): ?Developer + public function findOneByApiId(int $apiId): ?Developer { /** @var ?Developer */ return $this->findOneBy(['apiId' => $apiId]); diff --git a/src/Infrastructure/Persistence/Doctrine/Game/Repository/DoctrinePublisherRepository.php b/src/Infrastructure/Persistence/Doctrine/Game/Repository/DoctrinePublisherRepository.php index 1159ab7..23fac9c 100644 --- a/src/Infrastructure/Persistence/Doctrine/Game/Repository/DoctrinePublisherRepository.php +++ b/src/Infrastructure/Persistence/Doctrine/Game/Repository/DoctrinePublisherRepository.php @@ -16,13 +16,13 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, Publisher::class); } - public function findByName(string $name): ?Publisher + public function findOneByName(string $name): ?Publisher { /** @var ?Publisher */ return $this->findOneBy(['name' => $name]); } - public function findByApiId(int $apiId): ?Publisher + public function findOneByApiId(int $apiId): ?Publisher { /** @var ?Publisher */ return $this->findOneBy(['apiId' => $apiId]); diff --git a/src/Infrastructure/Persistence/Doctrine/Report/Repository/DoctrineReportRepository.php b/src/Infrastructure/Persistence/Doctrine/Report/Repository/DoctrineReportRepository.php index 207bd67..cb5ebbf 100644 --- a/src/Infrastructure/Persistence/Doctrine/Report/Repository/DoctrineReportRepository.php +++ b/src/Infrastructure/Persistence/Doctrine/Report/Repository/DoctrineReportRepository.php @@ -22,8 +22,10 @@ public function __construct(ManagerRegistry $registry) public function getAllVisibleReportsForGame(string $gameId): array { $qb = $this->createQueryBuilder('r') + ->select('r', 'g') ->where('r.isVisible = true') ->andWhere('r.game = :game') + ->innerJoin('r.game', 'g') ->setParameter('game', $gameId) ->orderBy('r.upvoteCount', 'DESC') ->addOrderBy('r.createdAt', 'ASC') @@ -36,8 +38,10 @@ public function getAllVisibleReportsForGame(string $gameId): array public function findMostUpvotedReportForGame(string $gameId): ?Report { $qb = $this->createQueryBuilder('r') + ->select('r', 'g') ->where('r.isVisible = true') ->andWhere('r.game = :game') + ->innerJoin('r.game', 'g') ->setParameter('game', $gameId) ->orderBy('r.upvoteCount', 'DESC') ->addOrderBy('r.createdAt', 'ASC') diff --git a/src/Presentation/Controller/ReportIncreaseUpvoteCountController.php b/src/Presentation/Controller/ReportIncreaseUpvoteCountController.php index a5d9f07..b530498 100644 --- a/src/Presentation/Controller/ReportIncreaseUpvoteCountController.php +++ b/src/Presentation/Controller/ReportIncreaseUpvoteCountController.php @@ -2,6 +2,8 @@ namespace App\Presentation\Controller; +use App\Application\Fetcher\Report\ReportFetcher; +use App\Application\Helper\CacheKeyBuilderHelper; use App\Domain\Model\Report\Report; use App\Domain\Repository\Report\ReportRepositoryInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -25,16 +27,29 @@ public function __invoke( Report $report, Request $request, ReportRepositoryInterface $reportRepository, + ReportFetcher $reportFetcher, ): Response { $report->increaseUpvoteCount(); $reportRepository->save($report); + /** @var string $gameSlug */ + $gameSlug = $report->getGame()->getSlug(); + + $reportFetcher->deleteCache( + CacheKeyBuilderHelper::build( + ReportFetcher::REPORTS_CACHE_KEY, + $gameSlug, + ), + ); + if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) { $request->setRequestFormat(TurboBundle::STREAM_FORMAT); /** @var string $currentReportGameId */ $currentReportGameId = $report->getGame()->getId(); - $mostUpvotedReport = $reportRepository->findMostUpvotedReportForGame($currentReportGameId); + $mostUpvotedReport = $reportRepository->findMostUpvotedReportForGame( + $currentReportGameId, + ); /** @var string $mostUpvotedReportId */ $mostUpvotedReportId = $mostUpvotedReport?->getId(); diff --git a/src/Presentation/Controller/ReportsController.php b/src/Presentation/Controller/ReportsController.php index 0162e60..76e2b88 100644 --- a/src/Presentation/Controller/ReportsController.php +++ b/src/Presentation/Controller/ReportsController.php @@ -7,6 +7,7 @@ use App\Application\Command\Game\CreateGameWithCacheCheckCommand; use App\Application\Helper\MessageBusHelper; use App\Application\Query\Game\GetGameBySlugQuery; +use App\Application\Query\Report\GetReportsForGameIdQuery; use App\Domain\Model\Game\Game; use App\Domain\Model\Report\Report; use App\Presentation\Form\CreateReportCommentForm; @@ -76,7 +77,22 @@ class: Game::class, ]); } - $reports = $game->getReports()->toArray(); + /** @var string $gameId */ + $gameId = $game->getId(); + /** @var string $gameSlug */ + $gameSlug = $game->getSlug(); + + $reportsEnvelope = $messageBus->dispatch( + new GetReportsForGameIdQuery($gameId, $gameSlug), + ); + + $reports = $messageBusHelper->getContentFromEnvelope( + $reportsEnvelope, + 'Reports not found', + Report::class, + 'array', + ); + $topReport = null; if (!empty($reports)) { diff --git a/tests/Unit/Application/CommandHandler/Game/CreateDeveloperCommandHandlerTest.php b/tests/Unit/Application/CommandHandler/Game/CreateDeveloperCommandHandlerTest.php index 510f9a9..40c5289 100644 --- a/tests/Unit/Application/CommandHandler/Game/CreateDeveloperCommandHandlerTest.php +++ b/tests/Unit/Application/CommandHandler/Game/CreateDeveloperCommandHandlerTest.php @@ -6,6 +6,7 @@ use App\Application\Command\Game\CreateDeveloperCommand; use App\Application\CommandHandler\Game\CreateDeveloperCommandHandler; +use App\Application\Fetcher\Game\DeveloperFetcher; use App\Domain\Factory\Game\DeveloperFactory; use App\Domain\Model\Game\Developer; use App\Infrastructure\Persistence\Doctrine\Game\Repository\DoctrineDeveloperRepository; @@ -15,7 +16,10 @@ beforeEach(function () { $this->faker = Factory::create(); - $this->developerRepository = $this->createMock(DoctrineDeveloperRepository::class); + $this->developerRepository = $this->createMock( + DoctrineDeveloperRepository::class, + ); + $this->developerFetcher = $this->createMock(DeveloperFetcher::class); $this->logger = $this->createMock(LoggerInterface::class); $this->developerName = $this->faker->company(); @@ -25,69 +29,77 @@ $this->existingDeveloper = DeveloperFactory::create( name: $this->developerName, apiId: $this->developerApiId, - website: $this->developerWebsite - ); -}); - -it('successfully creates and saves developer when developer does not exist', function () { - // Given - $this->developerRepository - ->expects($this->once()) - ->method('findByName') - ->with($this->developerName) - ->willReturn(null); - - $this->developerRepository - ->expects($this->once()) - ->method('save') - ->with($this->callback(function ($developer) { - return $developer instanceof Developer - && $developer->getName() === $this->developerName - && $developer->getWebsite() === $this->developerWebsite - && $developer->getApiId() === $this->developerApiId; - })); - - $this->logger - ->expects($this->never()) - ->method('error'); - - $handler = new CreateDeveloperCommandHandler( - $this->developerRepository, - $this->logger - ); - - $command = new CreateDeveloperCommand( - name: $this->developerName, website: $this->developerWebsite, - apiId: $this->developerApiId, ); - - // When - $handler->__invoke($command); - - // Then - expect(true)->toBeTrue(); }); +it( + 'successfully creates and saves developer when developer does not exist', + function () { + // Given + $this->developerFetcher + ->expects($this->once()) + ->method('findOneByName') + ->with($this->developerName) + ->willReturn(null); + + $this->developerRepository + ->expects($this->once()) + ->method('save') + ->with( + $this->callback(function ($developer) { + return $developer instanceof Developer && + $developer->getName() === $this->developerName && + $developer->getWebsite() === $this->developerWebsite && + $developer->getApiId() === $this->developerApiId; + }), + ); + + $this->developerFetcher + ->expects($this->once()) + ->method('deleteCacheName') + ->with($this->developerName); + + $this->logger->expects($this->never())->method('error'); + + $handler = new CreateDeveloperCommandHandler( + $this->developerRepository, + $this->developerFetcher, + $this->logger, + ); + + $command = new CreateDeveloperCommand( + name: $this->developerName, + website: $this->developerWebsite, + apiId: $this->developerApiId, + ); + + // When + $handler->__invoke($command); + + // Then + expect(true)->toBeTrue(); + }, +); + it('returns early when developer already exists', function () { // Given - $this->developerRepository + $this->developerFetcher ->expects($this->once()) - ->method('findByName') + ->method('findOneByName') ->with($this->developerName) ->willReturn($this->existingDeveloper); - $this->developerRepository - ->expects($this->never()) - ->method('save'); + $this->developerRepository->expects($this->never())->method('save'); - $this->logger - ->expects($this->never()) - ->method('error'); + $this->developerFetcher->expects($this->never())->method('deleteCacheName'); + + $this->logger->expects($this->never())->method('error'); $handler = new CreateDeveloperCommandHandler( $this->developerRepository, - $this->logger + $this->developerFetcher, + $this->logger, ); $command = new CreateDeveloperCommand( @@ -105,9 +117,9 @@ it('handles exception during save operation', function () { // Given - $this->developerRepository + $this->developerFetcher ->expects($this->once()) - ->method('findByName') + ->method('findOneByName') ->with($this->developerName) ->willReturn(null); @@ -118,6 +130,8 @@ ->method('save') ->willThrowException($exception); + $this->developerFetcher->expects($this->never())->method('deleteCacheName'); + $this->logger ->expects($this->once()) ->method('error') @@ -125,7 +139,8 @@ $handler = new CreateDeveloperCommandHandler( $this->developerRepository, - $this->logger + $this->developerFetcher, + $this->logger, ); $command = new CreateDeveloperCommand( @@ -143,9 +158,9 @@ it('handles different types of exceptions during save', function () { // Given - $this->developerRepository + $this->developerFetcher ->expects($this->once()) - ->method('findByName') + ->method('findOneByName') ->with($this->developerName) ->willReturn(null); @@ -156,6 +171,8 @@ ->method('save') ->willThrowException($exception); + $this->developerFetcher->expects($this->never())->method('deleteCacheName'); + $this->logger ->expects($this->once()) ->method('error') @@ -163,7 +180,8 @@ $handler = new CreateDeveloperCommandHandler( $this->developerRepository, - $this->logger + $this->developerFetcher, + $this->logger, ); $command = new CreateDeveloperCommand( @@ -179,70 +197,96 @@ expect(true)->toBeTrue(); }); -it('creates developer using DeveloperFactory with correct parameters', function () { - // Given - $developerName = 'Valve Corporation'; - $developerWebsite = 'https://www.valvesoftware.com'; - $developerApiId = 123456; - - $this->developerRepository - ->expects($this->once()) - ->method('findByName') - ->with($developerName) - ->willReturn(null); - - $this->developerRepository - ->expects($this->once()) - ->method('save') - ->with($this->callback(function ($developer) use ($developerName, $developerWebsite, $developerApiId) { - return $developer instanceof Developer - && $developer->getName() === $developerName - && $developer->getWebsite() === $developerWebsite - && $developer->getApiId() === $developerApiId; - })); - - $handler = new CreateDeveloperCommandHandler( - $this->developerRepository, - $this->logger - ); - - $command = new CreateDeveloperCommand( - name: $developerName, - website: $developerWebsite, - apiId: $developerApiId, - ); - - // When - $handler->__invoke($command); - - // Then - expect(true)->toBeTrue(); -}); +it( + 'creates developer using DeveloperFactory with correct parameters', + function () { + // Given + $developerName = 'Valve Corporation'; + $developerWebsite = 'https://www.valvesoftware.com'; + $developerApiId = 123456; + + $this->developerFetcher + ->expects($this->once()) + ->method('findOneByName') + ->with($developerName) + ->willReturn(null); + + $this->developerRepository + ->expects($this->once()) + ->method('save') + ->with( + $this->callback(function ($developer) use ( + $developerName, + $developerWebsite, + $developerApiId, + ) { + return $developer instanceof Developer && + $developer->getName() === $developerName && + $developer->getWebsite() === $developerWebsite && + $developer->getApiId() === $developerApiId; + }), + ); + + $this->developerFetcher + ->expects($this->once()) + ->method('deleteCacheName') + ->with($developerName); + + $handler = new CreateDeveloperCommandHandler( + $this->developerRepository, + $this->developerFetcher, + $this->logger, + ); + + $command = new CreateDeveloperCommand( + name: $developerName, + website: $developerWebsite, + apiId: $developerApiId, + ); + + // When + $handler->__invoke($command); + + // Then + expect(true)->toBeTrue(); + }, +); it('handles command with null website', function () { // Given $developerName = 'Indie Developer'; $developerApiId = 789012; - $this->developerRepository + $this->developerFetcher ->expects($this->once()) - ->method('findByName') + ->method('findOneByName') ->with($developerName) ->willReturn(null); $this->developerRepository ->expects($this->once()) ->method('save') - ->with($this->callback(function ($developer) use ($developerName, $developerApiId) { - return $developer instanceof Developer - && $developer->getName() === $developerName - && $developer->getWebsite() === null - && $developer->getApiId() === $developerApiId; - })); + ->with( + $this->callback(function ($developer) use ( + $developerName, + $developerApiId, + ) { + return $developer instanceof Developer && + $developer->getName() === $developerName && + $developer->getWebsite() === null && + $developer->getApiId() === $developerApiId; + }), + ); + + $this->developerFetcher + ->expects($this->once()) + ->method('deleteCacheName') + ->with($developerName); $handler = new CreateDeveloperCommandHandler( $this->developerRepository, - $this->logger + $this->developerFetcher, + $this->logger, ); $command = new CreateDeveloperCommand( diff --git a/tests/Unit/Application/CommandHandler/Game/CreateGameCommandHandlerTest.php b/tests/Unit/Application/CommandHandler/Game/CreateGameCommandHandlerTest.php index 094c76e..009234c 100644 --- a/tests/Unit/Application/CommandHandler/Game/CreateGameCommandHandlerTest.php +++ b/tests/Unit/Application/CommandHandler/Game/CreateGameCommandHandlerTest.php @@ -7,6 +7,7 @@ use App\Application\Command\Game\CreateGameCommand; use App\Application\Command\Game\CreatePublisherCommand; use App\Application\CommandHandler\Game\CreateGameCommandHandler; +use App\Application\Fetcher\Game\GameFetcher; use App\Application\Helper\MessageBusHelper; use App\Domain\Factory\Game\GamePublisherFactory; use App\Domain\Model\Game\Game; @@ -25,8 +26,13 @@ $this->faker = Factory::create(); $this->gameRepository = $this->createMock(DoctrineGameRepository::class); - $this->publisherRepository = $this->createMock(DoctrinePublisherRepository::class); - $this->developerRepository = $this->createMock(DoctrineDeveloperRepository::class); + $this->gameFetcher = $this->createMock(GameFetcher::class); + $this->publisherRepository = $this->createMock( + DoctrinePublisherRepository::class, + ); + $this->developerRepository = $this->createMock( + DoctrineDeveloperRepository::class, + ); $this->messageBus = $this->createMock(MessageBusInterface::class); $this->messageBusHelper = $this->createMock(MessageBusHelper::class); $this->logger = $this->createMock(LoggerInterface::class); @@ -35,7 +41,11 @@ $this->publisherWebsite = $this->faker->url(); $this->publisherApiId = $this->faker->randomNumber(5); - $this->createdPublisher = GamePublisherFactory::create($this->publisherName, $this->publisherApiId, $this->publisherWebsite); + $this->createdPublisher = GamePublisherFactory::create( + $this->publisherName, + $this->publisherApiId, + $this->publisherWebsite, + ); $this->publisherDto = new GameCompanyDataDto( $this->publisherName, @@ -46,19 +56,26 @@ it('successfully creates and saves game', function () { // Given + $this->gameRepository + ->expects($this->once()) + ->method('getOneBySlugEnabledGame') + ->with('zelda-breath-of-the-wild') + ->willReturn(null); + $this->publisherRepository ->expects($this->once()) - ->method('findByName') + ->method('findOneByName') ->with($this->publisherName) ->willReturn($this->createdPublisher); $handler = new CreateGameCommandHandler( $this->gameRepository, + $this->gameFetcher, $this->publisherRepository, $this->developerRepository, $this->messageBus, $this->messageBusHelper, - $this->logger + $this->logger, ); $command = new CreateGameCommand( @@ -73,14 +90,63 @@ $this->gameRepository ->expects($this->once()) ->method('save') - ->with($this->callback(function ($game) use ($command) { - return $game instanceof Game - && $game->getName() === $command->getName() - && $game->getSlug() === $command->getSlug() - && $game->getDescription() === $command->getDescription() - && $game->getReleaseDate() === $command->getReleaseDate() - && $game->getImageCover() === $command->getImageCover(); - })); + ->with( + $this->callback(function ($game) use ($command) { + return $game instanceof Game && + $game->getName() === $command->getName() && + $game->getSlug() === $command->getSlug() && + $game->getDescription() === $command->getDescription() && + $game->getReleaseDate() === $command->getReleaseDate() && + $game->getImageCover() === $command->getImageCover(); + }), + ); + + $this->gameFetcher + ->expects($this->once()) + ->method('deleteCacheForSlug') + ->with('zelda-breath-of-the-wild'); + + // When + $handler->__invoke($command); + + // Then + expect(true)->toBeTrue(); +}); + +it('returns early when game already exists', function () { + // Given + $existingGame = $this->createMock(Game::class); + + $this->gameRepository + ->expects($this->once()) + ->method('getOneBySlugEnabledGame') + ->with('zelda-breath-of-the-wild') + ->willReturn($existingGame); + + $this->publisherRepository + ->expects($this->never()) + ->method('findOneByName'); + $this->gameRepository->expects($this->never())->method('save'); + $this->gameFetcher->expects($this->never())->method('deleteCacheForSlug'); + + $handler = new CreateGameCommandHandler( + $this->gameRepository, + $this->gameFetcher, + $this->publisherRepository, + $this->developerRepository, + $this->messageBus, + $this->messageBusHelper, + $this->logger, + ); + + $command = new CreateGameCommand( + name: 'The Legend of Zelda: Breath of the Wild', + slug: 'zelda-breath-of-the-wild', + releaseDate: '2017-03-03', + imageCover: 'https://example.com/zelda.jpg', + publisher: $this->publisherDto, + description: 'An open-world adventure game', + ); // When $handler->__invoke($command); @@ -91,19 +157,26 @@ it('handles exception during save operation', function () { // Given + $this->gameRepository + ->expects($this->once()) + ->method('getOneBySlugEnabledGame') + ->with('mario-odyssey') + ->willReturn(null); + $this->publisherRepository ->expects($this->once()) - ->method('findByName') + ->method('findOneByName') ->with($this->publisherName) ->willReturn($this->createdPublisher); $handler = new CreateGameCommandHandler( $this->gameRepository, + $this->gameFetcher, $this->publisherRepository, $this->developerRepository, $this->messageBus, $this->messageBusHelper, - $this->logger + $this->logger, ); $command = new CreateGameCommand( @@ -112,7 +185,7 @@ releaseDate: '2017-10-27', imageCover: 'https://example.com/mario.jpg', publisher: $this->publisherDto, - description: 'A 3D platform game' + description: 'A 3D platform game', ); $exception = new \Exception('Database connection failed'); @@ -122,6 +195,8 @@ ->method('save') ->willThrowException($exception); + $this->gameFetcher->expects($this->never())->method('deleteCacheForSlug'); + $this->logger ->expects($this->once()) ->method('error') @@ -136,14 +211,21 @@ it('properly handles command with null description', function () { // Given + $this->gameRepository + ->expects($this->once()) + ->method('getOneBySlugEnabledGame') + ->with('metroid-dread') + ->willReturn(null); + $this->publisherRepository ->expects($this->once()) - ->method('findByName') + ->method('findOneByName') ->with($this->publisherName) ->willReturn($this->createdPublisher); $handler = new CreateGameCommandHandler( $this->gameRepository, + $this->gameFetcher, $this->publisherRepository, $this->developerRepository, $this->messageBus, @@ -163,14 +245,22 @@ $this->gameRepository ->expects($this->once()) ->method('save') - ->with($this->callback(function ($game) { - return $game instanceof Game - && $game->getName() === 'Metroid Dread' - && $game->getSlug() === 'metroid-dread' - && $game->getDescription() === '' - && $game->getReleaseDate() === '2021-10-08' - && $game->getImageCover() === 'https://example.com/metroid.jpg'; - })); + ->with( + $this->callback(function ($game) { + return $game instanceof Game && + $game->getName() === 'Metroid Dread' && + $game->getSlug() === 'metroid-dread' && + $game->getDescription() === '' && + $game->getReleaseDate() === '2021-10-08' && + $game->getImageCover() === + 'https://example.com/metroid.jpg'; + }), + ); + + $this->gameFetcher + ->expects($this->once()) + ->method('deleteCacheForSlug') + ->with('metroid-dread'); // When $handler->__invoke($command); @@ -181,14 +271,21 @@ it('properly handles command with non-null description', function () { // Given + $this->gameRepository + ->expects($this->once()) + ->method('getOneBySlugEnabledGame') + ->with('hollow-knight') + ->willReturn(null); + $this->publisherRepository ->expects($this->once()) - ->method('findByName') + ->method('findOneByName') ->with($this->publisherName) ->willReturn($this->createdPublisher); $handler = new CreateGameCommandHandler( $this->gameRepository, + $this->gameFetcher, $this->publisherRepository, $this->developerRepository, $this->messageBus, @@ -202,24 +299,31 @@ releaseDate: '2017-02-24', imageCover: 'https://example.com/hollow-knight.jpg', publisher: $this->publisherDto, - description: 'A challenging 2D Metroidvania' + description: 'A challenging 2D Metroidvania', ); $this->gameRepository ->expects($this->once()) ->method('save') - ->with($this->callback(function ($game) { - return $game instanceof Game - && $game->getName() === 'Hollow Knight' - && $game->getSlug() === 'hollow-knight' - && $game->getDescription() === 'A challenging 2D Metroidvania' - && $game->getReleaseDate() === '2017-02-24' - && $game->getImageCover() === 'https://example.com/hollow-knight.jpg'; - })); + ->with( + $this->callback(function ($game) { + return $game instanceof Game && + $game->getName() === 'Hollow Knight' && + $game->getSlug() === 'hollow-knight' && + $game->getDescription() === + 'A challenging 2D Metroidvania' && + $game->getReleaseDate() === '2017-02-24' && + $game->getImageCover() === + 'https://example.com/hollow-knight.jpg'; + }), + ); - $this->logger - ->expects($this->never()) - ->method('error'); + $this->gameFetcher + ->expects($this->once()) + ->method('deleteCacheForSlug') + ->with('hollow-knight'); + + $this->logger->expects($this->never())->method('error'); // When $handler->__invoke($command); @@ -231,13 +335,28 @@ it('handles different types of exceptions during save', function () { // Given $gameRepository = $this->createMock(DoctrineGameRepository::class); - $publisherRepository = $this->createMock(DoctrinePublisherRepository::class); + $gameFetcher = $this->createMock(GameFetcher::class); + $publisherRepository = $this->createMock( + DoctrinePublisherRepository::class, + ); $messageBus = $this->createMock(MessageBusInterface::class); $messageBusHelper = $this->createMock(MessageBusHelper::class); $logger = $this->createMock(LoggerInterface::class); + $gameRepository + ->expects($this->once()) + ->method('getOneBySlugEnabledGame') + ->with('celeste') + ->willReturn(null); + + $publisherRepository + ->expects($this->once()) + ->method('findOneByName') + ->willReturn(null); + $handler = new CreateGameCommandHandler( $gameRepository, + $gameFetcher, $publisherRepository, $this->developerRepository, $messageBus, @@ -245,23 +364,37 @@ $logger, ); - $publisherEnvelope = new Envelope($this->createdPublisher, [new HandledStamp($this->createdPublisher, 'handler.service_id')]); + $publisherEnvelope = new Envelope($this->createdPublisher, [ + new HandledStamp($this->createdPublisher, 'handler.service_id'), + ]); $messageBus ->expects($this->exactly(2)) ->method('dispatch') ->willReturnOnConsecutiveCalls( - new Envelope(new CreatePublisherCommand($this->publisherName, $this->publisherWebsite, $this->publisherApiId)), + new Envelope( + new CreatePublisherCommand( + $this->publisherName, + $this->publisherWebsite, + $this->publisherApiId, + ), + ), $publisherEnvelope, ); + $messageBusHelper + ->expects($this->once()) + ->method('getContentFromEnvelope') + ->with($publisherEnvelope, 'Publisher not found', Publisher::class) + ->willReturn($this->createdPublisher); + $command = new CreateGameCommand( name: 'Celeste', slug: 'celeste', releaseDate: '2018-01-25', imageCover: 'https://example.com/celeste.jpg', publisher: $this->publisherDto, - description: 'A challenging platformer' + description: 'A challenging platformer', ); $exception = new \RuntimeException('Runtime error occurred'); @@ -271,6 +404,8 @@ ->method('save') ->willThrowException($exception); + $gameFetcher->expects($this->never())->method('deleteCacheForSlug'); + $logger ->expects($this->once()) ->method('error') @@ -285,14 +420,21 @@ it('creates game using GameFactory with correct parameters', function () { // Given + $this->gameRepository + ->expects($this->once()) + ->method('getOneBySlugEnabledGame') + ->with('hades') + ->willReturn(null); + $this->publisherRepository ->expects($this->once()) - ->method('findByName') + ->method('findOneByName') ->with($this->publisherName) ->willReturn($this->createdPublisher); $handler = new CreateGameCommandHandler( $this->gameRepository, + $this->gameFetcher, $this->publisherRepository, $this->developerRepository, $this->messageBus, @@ -306,74 +448,32 @@ releaseDate: '2020-09-17', imageCover: 'https://example.com/hades.jpg', publisher: $this->publisherDto, - description: 'A rogue-like dungeon crawler' + description: 'A rogue-like dungeon crawler', ); $this->gameRepository ->expects($this->once()) ->method('save') - ->with($this->callback(function ($game) { - return $game instanceof Game - && $game->getName() === 'Hades' - && $game->getSlug() === 'hades' - && $game->getDescription() === 'A rogue-like dungeon crawler' - && $game->getReleaseDate() === '2020-09-17' - && $game->getImageCover() === 'https://example.com/hades.jpg' - && $game->isActive() === true - && $game->getPublisher() === $this->createdPublisher - && $game->getReports()->isEmpty(); - })); - - // When - $handler->__invoke($command); - - // Then - expect(true)->toBeTrue(); -}); - -it('handles command properties validation through CreateGameCommand', function () { - // Given - $this->publisherRepository - ->expects($this->once()) - ->method('findByName') - ->with($this->publisherName) - ->willReturn($this->createdPublisher); - - $handler = new CreateGameCommandHandler( - $this->gameRepository, - $this->publisherRepository, - $this->developerRepository, - $this->messageBus, - $this->messageBusHelper, - $this->logger, - ); - - $command = new CreateGameCommand( - name: 'Ori and the Will of the Wisps', - slug: 'ori-will-of-wisps', - releaseDate: '2020-03-11', - imageCover: 'https://example.com/ori.jpg', - publisher: $this->publisherDto, - ); - - expect($command->getName())->toBe('Ori and the Will of the Wisps') - ->and($command->getSlug())->toBe('ori-will-of-wisps') - ->and($command->getReleaseDate())->toBe('2020-03-11') - ->and($command->getImageCover())->toBe('https://example.com/ori.jpg') - ->and($command->getDescription())->toBeNull() - ->and($command->isActive())->toBeTrue(); + ->with( + $this->callback(function ($game) { + return $game instanceof Game && + $game->getName() === 'Hades' && + $game->getSlug() === 'hades' && + $game->getDescription() === + 'A rogue-like dungeon crawler' && + $game->getReleaseDate() === '2020-09-17' && + $game->getImageCover() === + 'https://example.com/hades.jpg' && + $game->isActive() === true && + $game->getPublisher() === $this->createdPublisher && + $game->getReports()->isEmpty(); + }), + ); - $this->gameRepository + $this->gameFetcher ->expects($this->once()) - ->method('save') - ->with($this->callback(function ($game) { - return $game instanceof Game - && $game->getName() === 'Ori and the Will of the Wisps' - && $game->getSlug() === 'ori-will-of-wisps' - && $game->getDescription() === '' // null becomes empty string - && $game->getReleaseDate() === '2020-03-11' - && $game->getImageCover() === 'https://example.com/ori.jpg'; - })); + ->method('deleteCacheForSlug') + ->with('hades'); // When $handler->__invoke($command); @@ -382,70 +482,174 @@ expect(true)->toBeTrue(); }); -it('creates game and publisher using message bus when publisher does not exist', function () { - // Given - $gameRepository = $this->createMock(DoctrineGameRepository::class); - $publisherRepository = $this->createMock(DoctrinePublisherRepository::class); - $messageBus = $this->createMock(MessageBusInterface::class); - $messageBusHelper = $this->createMock(MessageBusHelper::class); - - $publisherRepository - ->expects($this->once()) - ->method('findByName') - ->with($this->publisherName) - ->willReturn(null); - - - $handler = new CreateGameCommandHandler( - $gameRepository, - $publisherRepository, - $this->developerRepository, - $messageBus, - $messageBusHelper, - $this->logger, - ); - - $command = new CreateGameCommand( - name: 'The Witcher 3: Wild Hunt', - slug: 'witcher-3-wild-hunt', - releaseDate: '2015-05-19', - imageCover: 'https://example.com/witcher3.jpg', - publisher: $this->publisherDto, - description: 'An open world RPG', - ); - - $publisherEnvelope = new Envelope($this->createdPublisher, [new HandledStamp($this->createdPublisher, 'handler.service_id')]); - - $messageBus - ->expects($this->exactly(2)) - ->method('dispatch') - ->willReturnOnConsecutiveCalls( - new Envelope(new CreatePublisherCommand($this->publisherName, $this->publisherWebsite, $this->publisherApiId)), - $publisherEnvelope, +it( + 'handles command properties validation through CreateGameCommand', + function () { + // Given + $this->gameRepository + ->expects($this->once()) + ->method('getOneBySlugEnabledGame') + ->with('ori-will-of-wisps') + ->willReturn(null); + + $this->publisherRepository + ->expects($this->once()) + ->method('findOneByName') + ->with($this->publisherName) + ->willReturn($this->createdPublisher); + + $handler = new CreateGameCommandHandler( + $this->gameRepository, + $this->gameFetcher, + $this->publisherRepository, + $this->developerRepository, + $this->messageBus, + $this->messageBusHelper, + $this->logger, ); - $messageBusHelper - ->expects($this->once()) - ->method('getContentFromEnvelope') - ->with($publisherEnvelope, 'Publisher not found', Publisher::class) - ->willReturn($this->createdPublisher); + $command = new CreateGameCommand( + name: 'Ori and the Will of the Wisps', + slug: 'ori-will-of-wisps', + releaseDate: '2020-03-11', + imageCover: 'https://example.com/ori.jpg', + publisher: $this->publisherDto, + ); - $gameRepository - ->expects($this->once()) - ->method('save') - ->with($this->callback(function ($game) use ($command) { - return $game instanceof Game - && $game->getName() === $command->getName() - && $game->getSlug() === $command->getSlug() - && $game->getDescription() === $command->getDescription() - && $game->getReleaseDate() === $command->getReleaseDate() - && $game->getImageCover() === $command->getImageCover() - && $game->getPublisher() === $this->createdPublisher; - })); + expect($command->getName()) + ->toBe('Ori and the Will of the Wisps') + ->and($command->getSlug()) + ->toBe('ori-will-of-wisps') + ->and($command->getReleaseDate()) + ->toBe('2020-03-11') + ->and($command->getImageCover()) + ->toBe('https://example.com/ori.jpg') + ->and($command->getDescription()) + ->toBeNull() + ->and($command->isActive()) + ->toBeTrue(); + + $this->gameRepository + ->expects($this->once()) + ->method('save') + ->with( + $this->callback(function ($game) { + return $game instanceof Game && + $game->getName() === 'Ori and the Will of the Wisps' && + $game->getSlug() === 'ori-will-of-wisps' && + $game->getDescription() === '' && // null becomes empty string + $game->getReleaseDate() === '2020-03-11' && + $game->getImageCover() === + 'https://example.com/ori.jpg'; + }), + ); + + $this->gameFetcher + ->expects($this->once()) + ->method('deleteCacheForSlug') + ->with('ori-will-of-wisps'); + + // When + $handler->__invoke($command); + + // Then + expect(true)->toBeTrue(); + }, +); + +it( + 'creates game and publisher using message bus when publisher does not exist', + function () { + // Given + $gameRepository = $this->createMock(DoctrineGameRepository::class); + $gameFetcher = $this->createMock(GameFetcher::class); + $publisherRepository = $this->createMock( + DoctrinePublisherRepository::class, + ); + $messageBus = $this->createMock(MessageBusInterface::class); + $messageBusHelper = $this->createMock(MessageBusHelper::class); + + $gameRepository + ->expects($this->once()) + ->method('getOneBySlugEnabledGame') + ->with('witcher-3-wild-hunt') + ->willReturn(null); + + $publisherRepository + ->expects($this->once()) + ->method('findOneByName') + ->with($this->publisherName) + ->willReturn(null); + + $handler = new CreateGameCommandHandler( + $gameRepository, + $gameFetcher, + $publisherRepository, + $this->developerRepository, + $messageBus, + $messageBusHelper, + $this->logger, + ); - // When - $handler->__invoke($command); + $command = new CreateGameCommand( + name: 'The Witcher 3: Wild Hunt', + slug: 'witcher-3-wild-hunt', + releaseDate: '2015-05-19', + imageCover: 'https://example.com/witcher3.jpg', + publisher: $this->publisherDto, + description: 'An open world RPG', + ); - // Then - expect(true)->toBeTrue(); -}); + $publisherEnvelope = new Envelope($this->createdPublisher, [ + new HandledStamp($this->createdPublisher, 'handler.service_id'), + ]); + + $messageBus + ->expects($this->exactly(2)) + ->method('dispatch') + ->willReturnOnConsecutiveCalls( + new Envelope( + new CreatePublisherCommand( + $this->publisherName, + $this->publisherWebsite, + $this->publisherApiId, + ), + ), + $publisherEnvelope, + ); + + $messageBusHelper + ->expects($this->once()) + ->method('getContentFromEnvelope') + ->with($publisherEnvelope, 'Publisher not found', Publisher::class) + ->willReturn($this->createdPublisher); + + $gameRepository + ->expects($this->once()) + ->method('save') + ->with( + $this->callback(function ($game) use ($command) { + return $game instanceof Game && + $game->getName() === $command->getName() && + $game->getSlug() === $command->getSlug() && + $game->getDescription() === + $command->getDescription() && + $game->getReleaseDate() === + $command->getReleaseDate() && + $game->getImageCover() === $command->getImageCover() && + $game->getPublisher() === $this->createdPublisher; + }), + ); + + $gameFetcher + ->expects($this->once()) + ->method('deleteCacheForSlug') + ->with('witcher-3-wild-hunt'); + + // When + $handler->__invoke($command); + + // Then + expect(true)->toBeTrue(); + }, +); diff --git a/tests/Unit/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandlerTest.php b/tests/Unit/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandlerTest.php index b487a57..5ac6379 100644 --- a/tests/Unit/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandlerTest.php +++ b/tests/Unit/Application/CommandHandler/Game/CreateGameWithCacheCheckCommandHandlerTest.php @@ -167,6 +167,7 @@ $envelope->getMessage() instanceof GetGameBySlugOnApiQuery; }), 'Failed to fetch game from API', + ApiGame::class, ) ->willReturn($this->apiGame); @@ -217,6 +218,7 @@ function () { GetGameBySlugOnApiQuery; }), 'Failed to fetch game from API', + ApiGame::class, ) ->willReturn(null); diff --git a/tests/Unit/Application/CommandHandler/Game/CreatePublisherCommandHandlerTest.php b/tests/Unit/Application/CommandHandler/Game/CreatePublisherCommandHandlerTest.php index 48024eb..6af41ae 100644 --- a/tests/Unit/Application/CommandHandler/Game/CreatePublisherCommandHandlerTest.php +++ b/tests/Unit/Application/CommandHandler/Game/CreatePublisherCommandHandlerTest.php @@ -7,6 +7,7 @@ use App\Application\Command\Game\CreatePublisherCommand; use App\Application\CommandHandler\Game\CreatePublisherCommandHandler; use App\Application\Exception\Game\CannotCreatePublisherException; +use App\Application\Fetcher\Game\PublisherFetcher; use App\Domain\Factory\Game\GamePublisherFactory; use App\Domain\Model\Game\Publisher; use App\Infrastructure\Persistence\Doctrine\Game\Repository\DoctrinePublisherRepository; @@ -16,7 +17,10 @@ beforeEach(function () { $this->faker = Factory::create(); - $this->publisherRepository = $this->createMock(DoctrinePublisherRepository::class); + $this->publisherRepository = $this->createMock( + DoctrinePublisherRepository::class, + ); + $this->publisherFetcher = $this->createMock(PublisherFetcher::class); $this->logger = $this->createMock(LoggerInterface::class); $this->publisherName = $this->faker->company(); @@ -26,69 +30,77 @@ $this->existingPublisher = GamePublisherFactory::create( $this->publisherName, $this->publisherApiId, - $this->publisherWebsite + $this->publisherWebsite, ); }); -it('successfully creates and saves publisher when publisher does not exist', function () { - // Given - $this->publisherRepository - ->expects($this->once()) - ->method('findByName') - ->with($this->publisherName) - ->willReturn(null); - - $this->publisherRepository - ->expects($this->once()) - ->method('save') - ->with($this->callback(function ($publisher) { - return $publisher instanceof Publisher - && $publisher->getName() === $this->publisherName - && $publisher->getWebsite() === $this->publisherWebsite - && $publisher->getApiId() === $this->publisherApiId; - })); - - $this->logger - ->expects($this->never()) - ->method('error'); - - $handler = new CreatePublisherCommandHandler( - $this->publisherRepository, - $this->logger - ); - - $command = new CreatePublisherCommand( - name: $this->publisherName, - website: $this->publisherWebsite, - apiId: $this->publisherApiId, - ); - - // When - $handler->__invoke($command); - - // Then - expect(true)->toBeTrue(); -}); +it( + 'successfully creates and saves publisher when publisher does not exist', + function () { + // Given + $this->publisherFetcher + ->expects($this->once()) + ->method('findOneByName') + ->with($this->publisherName) + ->willReturn(null); + + $this->publisherRepository + ->expects($this->once()) + ->method('save') + ->with( + $this->callback(function ($publisher) { + return $publisher instanceof Publisher && + $publisher->getName() === $this->publisherName && + $publisher->getWebsite() === $this->publisherWebsite && + $publisher->getApiId() === $this->publisherApiId; + }), + ); + + $this->publisherFetcher + ->expects($this->once()) + ->method('deleteCacheName') + ->with($this->publisherName); + + $this->logger->expects($this->never())->method('error'); + + $handler = new CreatePublisherCommandHandler( + $this->publisherRepository, + $this->publisherFetcher, + $this->logger, + ); + + $command = new CreatePublisherCommand( + name: $this->publisherName, + website: $this->publisherWebsite, + apiId: $this->publisherApiId, + ); + + // When + $handler->__invoke($command); + + // Then + expect(true)->toBeTrue(); + }, +); it('returns early when publisher already exists', function () { // Given - $this->publisherRepository + $this->publisherFetcher ->expects($this->once()) - ->method('findByName') + ->method('findOneByName') ->with($this->publisherName) ->willReturn($this->existingPublisher); - $this->publisherRepository - ->expects($this->never()) - ->method('save'); + $this->publisherRepository->expects($this->never())->method('save'); - $this->logger - ->expects($this->never()) - ->method('error'); + $this->publisherFetcher->expects($this->never())->method('deleteCacheName'); + + $this->logger->expects($this->never())->method('error'); $handler = new CreatePublisherCommandHandler( $this->publisherRepository, - $this->logger + $this->publisherFetcher, + $this->logger, ); $command = new CreatePublisherCommand( @@ -106,9 +118,9 @@ it('handles exception during save operation', function () { // Given - $this->publisherRepository + $this->publisherFetcher ->expects($this->once()) - ->method('findByName') + ->method('findOneByName') ->with($this->publisherName) ->willReturn(null); @@ -119,6 +131,8 @@ ->method('save') ->willThrowException($exception); + $this->publisherFetcher->expects($this->never())->method('deleteCacheName'); + $this->logger ->expects($this->once()) ->method('error') @@ -126,6 +140,7 @@ $handler = new CreatePublisherCommandHandler( $this->publisherRepository, + $this->publisherFetcher, $this->logger, ); @@ -144,9 +159,9 @@ it('handles different types of exceptions during save', function () { // Given - $this->publisherRepository + $this->publisherFetcher ->expects($this->once()) - ->method('findByName') + ->method('findOneByName') ->with($this->publisherName) ->willReturn(null); @@ -157,6 +172,8 @@ ->method('save') ->willThrowException($exception); + $this->publisherFetcher->expects($this->never())->method('deleteCacheName'); + $this->logger ->expects($this->once()) ->method('error') @@ -164,7 +181,8 @@ $handler = new CreatePublisherCommandHandler( $this->publisherRepository, - $this->logger + $this->publisherFetcher, + $this->logger, ); $command = new CreatePublisherCommand( @@ -180,70 +198,96 @@ expect(true)->toBeTrue(); })->throws(CannotCreatePublisherException::class); -it('creates publisher using GamePublisherFactory with correct parameters', function () { - // Given - $publisherName = 'Nintendo'; - $publisherWebsite = 'https://www.nintendo.com'; - $publisherApiId = 123456; - - $this->publisherRepository - ->expects($this->once()) - ->method('findByName') - ->with($publisherName) - ->willReturn(null); - - $this->publisherRepository - ->expects($this->once()) - ->method('save') - ->with($this->callback(function ($publisher) use ($publisherName, $publisherWebsite, $publisherApiId) { - return $publisher instanceof Publisher - && $publisher->getName() === $publisherName - && $publisher->getWebsite() === $publisherWebsite - && $publisher->getApiId() === $publisherApiId; - })); - - $handler = new CreatePublisherCommandHandler( - $this->publisherRepository, - $this->logger - ); - - $command = new CreatePublisherCommand( - name: $publisherName, - website: $publisherWebsite, - apiId: $publisherApiId, - ); - - // When - $handler->__invoke($command); - - // Then - expect(true)->toBeTrue(); -}); +it( + 'creates publisher using GamePublisherFactory with correct parameters', + function () { + // Given + $publisherName = 'Nintendo'; + $publisherWebsite = 'https://www.nintendo.com'; + $publisherApiId = 123456; + + $this->publisherFetcher + ->expects($this->once()) + ->method('findOneByName') + ->with($publisherName) + ->willReturn(null); + + $this->publisherRepository + ->expects($this->once()) + ->method('save') + ->with( + $this->callback(function ($publisher) use ( + $publisherName, + $publisherWebsite, + $publisherApiId, + ) { + return $publisher instanceof Publisher && + $publisher->getName() === $publisherName && + $publisher->getWebsite() === $publisherWebsite && + $publisher->getApiId() === $publisherApiId; + }), + ); + + $this->publisherFetcher + ->expects($this->once()) + ->method('deleteCacheName') + ->with($publisherName); + + $handler = new CreatePublisherCommandHandler( + $this->publisherRepository, + $this->publisherFetcher, + $this->logger, + ); + + $command = new CreatePublisherCommand( + name: $publisherName, + website: $publisherWebsite, + apiId: $publisherApiId, + ); + + // When + $handler->__invoke($command); + + // Then + expect(true)->toBeTrue(); + }, +); it('handles command with null website', function () { // Given $publisherName = 'Indie Developer'; $publisherApiId = 789012; - $this->publisherRepository + $this->publisherFetcher ->expects($this->once()) - ->method('findByName') + ->method('findOneByName') ->with($publisherName) ->willReturn(null); $this->publisherRepository ->expects($this->once()) ->method('save') - ->with($this->callback(function ($publisher) use ($publisherName, $publisherApiId) { - return $publisher instanceof Publisher - && $publisher->getName() === $publisherName - && $publisher->getWebsite() === null - && $publisher->getApiId() === $publisherApiId; - })); + ->with( + $this->callback(function ($publisher) use ( + $publisherName, + $publisherApiId, + ) { + return $publisher instanceof Publisher && + $publisher->getName() === $publisherName && + $publisher->getWebsite() === null && + $publisher->getApiId() === $publisherApiId; + }), + ); + + $this->publisherFetcher + ->expects($this->once()) + ->method('deleteCacheName') + ->with($publisherName); $handler = new CreatePublisherCommandHandler( $this->publisherRepository, - $this->logger + $this->publisherFetcher, + $this->logger, ); $command = new CreatePublisherCommand( diff --git a/tests/Unit/Application/CommandHandler/Game/UpdateGameFromApiCommandHandlerTest.php b/tests/Unit/Application/CommandHandler/Game/UpdateGameFromApiCommandHandlerTest.php index 9b68eeb..3a9cf11 100644 --- a/tests/Unit/Application/CommandHandler/Game/UpdateGameFromApiCommandHandlerTest.php +++ b/tests/Unit/Application/CommandHandler/Game/UpdateGameFromApiCommandHandlerTest.php @@ -33,8 +33,12 @@ $this->faker = Factory::create(); $this->gameRepository = $this->createMock(DoctrineGameRepository::class); - $this->publisherRepository = $this->createMock(DoctrinePublisherRepository::class); - $this->developerRepository = $this->createMock(DoctrineDeveloperRepository::class); + $this->publisherRepository = $this->createMock( + DoctrinePublisherRepository::class, + ); + $this->developerRepository = $this->createMock( + DoctrineDeveloperRepository::class, + ); $this->messageBus = $this->createMock(MessageBusInterface::class); $this->game = $this->createMock(Game::class); @@ -65,703 +69,772 @@ ); }); -it('successfully updates game when publisher and developer need to be created', function () { - // Given - $this->game - ->expects($this->once()) - ->method('getPublisher') - ->willReturn(null); - - $this->game - ->expects($this->once()) - ->method('getDeveloper') - ->willReturn(null); - - $createPublisherEnvelope = new Envelope( - new CreatePublisherCommand($this->publisherDto->name, $this->publisherDto->website, $this->publisherDto->apiId), - [new HandledStamp(true, 'handler.service_id')] - ); - - $createDeveloperEnvelope = new Envelope( - new CreateDeveloperCommand($this->developerDto->name, $this->developerDto->website, $this->developerDto->apiId), - [new HandledStamp(true, 'handler.service_id')] - ); - - $this->messageBus - ->expects($this->exactly(2)) - ->method('dispatch') - ->willReturnOnConsecutiveCalls( - $createPublisherEnvelope, - $createDeveloperEnvelope +it( + 'successfully updates game when publisher and developer need to be created', + function () { + // Given + $this->game + ->expects($this->once()) + ->method('getPublisher') + ->willReturn(null); + + $this->game + ->expects($this->once()) + ->method('getDeveloper') + ->willReturn(null); + + $createPublisherEnvelope = new Envelope( + new CreatePublisherCommand( + $this->publisherDto->name, + $this->publisherDto->website, + $this->publisherDto->apiId, + ), + [new HandledStamp(true, 'handler.service_id')], ); - $this->publisherRepository - ->expects($this->once()) - ->method('findByApiId') - ->with($this->publisherDto->apiId) - ->willReturn($this->publisher); - - $this->developerRepository - ->expects($this->once()) - ->method('findByApiId') - ->with($this->developerDto->apiId) - ->willReturn($this->developer); - - $this->game - ->expects($this->once()) - ->method('setName') - ->with($this->apiGame->getName()); - - $this->game - ->expects($this->once()) - ->method('setDescription') - ->with($this->apiGame->getDescription()); - - $this->game - ->expects($this->once()) - ->method('setImageCover') - ->with($this->apiGame->getImageCover()); - - $this->game - ->expects($this->once()) - ->method('setReleaseDate') - ->with($this->apiGame->getReleaseDate()); - - $this->game - ->expects($this->once()) - ->method('setPublisher') - ->with($this->publisher); - - $this->game - ->expects($this->once()) - ->method('setDeveloper') - ->with($this->developer); - - $this->gameRepository - ->expects($this->once()) - ->method('update') - ->with($this->game); - - $handler = new UpdateGameFromApiCommandHandler( - $this->gameRepository, - $this->publisherRepository, - $this->developerRepository, - $this->messageBus, - ); - - $command = new UpdateGameFromApiCommand($this->game, $this->apiGame); - - // When - $handler->__invoke($command); - - // Then - expect(true)->toBeTrue(); -}); - -it('successfully updates game when publisher and developer need to be updated', function () { - // Given - $this->game - ->expects($this->once()) - ->method('getPublisher') - ->willReturn($this->publisher); - - $this->game - ->expects($this->once()) - ->method('getDeveloper') - ->willReturn($this->developer); - - $this->publisher - ->expects($this->once()) - ->method('getApiId') - ->willReturn($this->publisherDto->apiId); - - $this->developer - ->expects($this->once()) - ->method('getApiId') - ->willReturn($this->developerDto->apiId); - - $updatePublisherEnvelope = new Envelope( - new UpdatePublisherCommand($this->publisherDto->apiId, $this->publisherDto->name, $this->publisherDto->website), - [new HandledStamp(true, 'handler.service_id')] - ); - - $updateDeveloperEnvelope = new Envelope( - new UpdateDeveloperCommand($this->developerDto->apiId, $this->developerDto->name, $this->developerDto->website), - [new HandledStamp(true, 'handler.service_id')] - ); - - $this->messageBus - ->expects($this->exactly(2)) - ->method('dispatch') - ->willReturnOnConsecutiveCalls( - $updatePublisherEnvelope, - $updateDeveloperEnvelope + $createDeveloperEnvelope = new Envelope( + new CreateDeveloperCommand( + $this->developerDto->name, + $this->developerDto->website, + $this->developerDto->apiId, + ), + [new HandledStamp(true, 'handler.service_id')], ); - $this->publisherRepository - ->expects($this->once()) - ->method('findByApiId') - ->with($this->publisherDto->apiId) - ->willReturn($this->publisher); - - $this->developerRepository - ->expects($this->once()) - ->method('findByApiId') - ->with($this->developerDto->apiId) - ->willReturn($this->developer); - - $this->game - ->expects($this->once()) - ->method('setName') - ->with($this->apiGame->getName()); - - $this->game - ->expects($this->once()) - ->method('setDescription') - ->with($this->apiGame->getDescription()); - - $this->game - ->expects($this->once()) - ->method('setImageCover') - ->with($this->apiGame->getImageCover()); - - $this->game - ->expects($this->once()) - ->method('setReleaseDate') - ->with($this->apiGame->getReleaseDate()); - - $this->game - ->expects($this->once()) - ->method('setPublisher') - ->with($this->publisher); - - $this->game - ->expects($this->once()) - ->method('setDeveloper') - ->with($this->developer); - - $this->gameRepository - ->expects($this->once()) - ->method('update') - ->with($this->game); - - $handler = new UpdateGameFromApiCommandHandler( - $this->gameRepository, - $this->publisherRepository, - $this->developerRepository, - $this->messageBus, - ); - - $command = new UpdateGameFromApiCommand($this->game, $this->apiGame); - - // When - $handler->__invoke($command); - - // Then - expect(true)->toBeTrue(); -}); - -it('successfully updates game when apiGame has null publisher and developer', function () { - // Given - $apiGameWithNullCompanies = new ApiGame( - name: $this->faker->words(3, true), - slug: $this->faker->slug(), - description: $this->faker->paragraph(), - imageCover: $this->faker->imageUrl(), - releaseDate: $this->faker->date(), - updatedAt: new \DateTimeImmutable(), - publisher: null, - developer: null, - ); - - $this->game - ->expects($this->once()) - ->method('getPublisher') - ->willReturn($this->publisher); - - $this->game - ->expects($this->once()) - ->method('getDeveloper') - ->willReturn($this->developer); - - $this->messageBus - ->expects($this->never()) - ->method('dispatch'); - - $this->publisherRepository - ->expects($this->never()) - ->method('findByApiId'); - - $this->developerRepository - ->expects($this->never()) - ->method('findByApiId'); - - $this->game - ->expects($this->once()) - ->method('setName') - ->with($apiGameWithNullCompanies->getName()); - - $this->game - ->expects($this->once()) - ->method('setDescription') - ->with($apiGameWithNullCompanies->getDescription()); - - $this->game - ->expects($this->once()) - ->method('setImageCover') - ->with($apiGameWithNullCompanies->getImageCover()); - - $this->game - ->expects($this->once()) - ->method('setReleaseDate') - ->with($apiGameWithNullCompanies->getReleaseDate()); - - $this->game - ->expects($this->once()) - ->method('setPublisher') - ->with($this->publisher); - - $this->game - ->expects($this->once()) - ->method('setDeveloper') - ->with($this->developer); - - $this->gameRepository - ->expects($this->once()) - ->method('update') - ->with($this->game); - - $handler = new UpdateGameFromApiCommandHandler( - $this->gameRepository, - $this->publisherRepository, - $this->developerRepository, - $this->messageBus, - ); - - $command = new UpdateGameFromApiCommand($this->game, $apiGameWithNullCompanies); - - // When - $handler->__invoke($command); - - // Then - expect(true)->toBeTrue(); -}); - -it('throws CannotCreatePublisherException when publisher creation fails', function () { - // Given - $this->game - ->expects($this->once()) - ->method('getPublisher') - ->willReturn(null); - - $exception = $this->createMock(ExceptionInterface::class); - - $this->messageBus - ->expects($this->once()) - ->method('dispatch') - ->willThrowException($exception); - - $handler = new UpdateGameFromApiCommandHandler( - $this->gameRepository, - $this->publisherRepository, - $this->developerRepository, - $this->messageBus, - ); + $this->messageBus + ->expects($this->exactly(2)) + ->method('dispatch') + ->willReturnOnConsecutiveCalls( + $createPublisherEnvelope, + $createDeveloperEnvelope, + ); + + $this->publisherRepository + ->expects($this->once()) + ->method('findOneByApiId') + ->with($this->publisherDto->apiId) + ->willReturn($this->publisher); + + $this->developerRepository + ->expects($this->once()) + ->method('findOneByApiId') + ->with($this->developerDto->apiId) + ->willReturn($this->developer); + + $this->game + ->expects($this->once()) + ->method('setName') + ->with($this->apiGame->getName()); + + $this->game + ->expects($this->once()) + ->method('setDescription') + ->with($this->apiGame->getDescription()); + + $this->game + ->expects($this->once()) + ->method('setImageCover') + ->with($this->apiGame->getImageCover()); + + $this->game + ->expects($this->once()) + ->method('setReleaseDate') + ->with($this->apiGame->getReleaseDate()); + + $this->game + ->expects($this->once()) + ->method('setPublisher') + ->with($this->publisher); + + $this->game + ->expects($this->once()) + ->method('setDeveloper') + ->with($this->developer); + + $this->gameRepository + ->expects($this->once()) + ->method('update') + ->with($this->game); + + $handler = new UpdateGameFromApiCommandHandler( + $this->gameRepository, + $this->publisherRepository, + $this->developerRepository, + $this->messageBus, + ); - $command = new UpdateGameFromApiCommand($this->game, $this->apiGame); + $command = new UpdateGameFromApiCommand($this->game, $this->apiGame); - // When & Then - expect(function () use ($handler, $command) { + // When $handler->__invoke($command); - })->toThrow(CannotCreatePublisherException::class); -}); -it('throws CannotUpdatePublisherException when publisher update fails', function () { - // Given - $this->game - ->expects($this->once()) - ->method('getPublisher') - ->willReturn($this->publisher); - - $this->publisher - ->expects($this->once()) - ->method('getApiId') - ->willReturn($this->publisherDto->apiId); - - $exception = $this->createMock(ExceptionInterface::class); - - $this->messageBus - ->expects($this->once()) - ->method('dispatch') - ->willThrowException($exception); - - $handler = new UpdateGameFromApiCommandHandler( - $this->gameRepository, - $this->publisherRepository, - $this->developerRepository, - $this->messageBus, - ); + // Then + expect(true)->toBeTrue(); + }, +); + +it( + 'successfully updates game when publisher and developer need to be updated', + function () { + // Given + $this->game + ->expects($this->once()) + ->method('getPublisher') + ->willReturn($this->publisher); + + $this->game + ->expects($this->once()) + ->method('getDeveloper') + ->willReturn($this->developer); + + $this->publisher + ->expects($this->once()) + ->method('getApiId') + ->willReturn($this->publisherDto->apiId); + + $this->developer + ->expects($this->once()) + ->method('getApiId') + ->willReturn($this->developerDto->apiId); + + $updatePublisherEnvelope = new Envelope( + new UpdatePublisherCommand( + $this->publisherDto->apiId, + $this->publisherDto->name, + $this->publisherDto->website, + ), + [new HandledStamp(true, 'handler.service_id')], + ); - $command = new UpdateGameFromApiCommand($this->game, $this->apiGame); + $updateDeveloperEnvelope = new Envelope( + new UpdateDeveloperCommand( + $this->developerDto->apiId, + $this->developerDto->name, + $this->developerDto->website, + ), + [new HandledStamp(true, 'handler.service_id')], + ); - // When & Then - expect(function () use ($handler, $command) { - $handler->__invoke($command); - })->toThrow(CannotUpdatePublisherException::class); -}); + $this->messageBus + ->expects($this->exactly(2)) + ->method('dispatch') + ->willReturnOnConsecutiveCalls( + $updatePublisherEnvelope, + $updateDeveloperEnvelope, + ); + + $this->publisherRepository + ->expects($this->once()) + ->method('findOneByApiId') + ->with($this->publisherDto->apiId) + ->willReturn($this->publisher); + + $this->developerRepository + ->expects($this->once()) + ->method('findOneByApiId') + ->with($this->developerDto->apiId) + ->willReturn($this->developer); + + $this->game + ->expects($this->once()) + ->method('setName') + ->with($this->apiGame->getName()); + + $this->game + ->expects($this->once()) + ->method('setDescription') + ->with($this->apiGame->getDescription()); + + $this->game + ->expects($this->once()) + ->method('setImageCover') + ->with($this->apiGame->getImageCover()); + + $this->game + ->expects($this->once()) + ->method('setReleaseDate') + ->with($this->apiGame->getReleaseDate()); + + $this->game + ->expects($this->once()) + ->method('setPublisher') + ->with($this->publisher); + + $this->game + ->expects($this->once()) + ->method('setDeveloper') + ->with($this->developer); + + $this->gameRepository + ->expects($this->once()) + ->method('update') + ->with($this->game); + + $handler = new UpdateGameFromApiCommandHandler( + $this->gameRepository, + $this->publisherRepository, + $this->developerRepository, + $this->messageBus, + ); -it('throws CannotCreateDeveloperException when developer creation fails', function () { - // Given - $this->game - ->expects($this->once()) - ->method('getPublisher') - ->willReturn(null); - - $this->game - ->expects($this->once()) - ->method('getDeveloper') - ->willReturn(null); - - $createPublisherEnvelope = new Envelope( - new CreatePublisherCommand($this->publisherDto->name, $this->publisherDto->website, $this->publisherDto->apiId), - [new HandledStamp(true, 'handler.service_id')] - ); + $command = new UpdateGameFromApiCommand($this->game, $this->apiGame); - $exception = $this->createMock(ExceptionInterface::class); + // When + $handler->__invoke($command); - $this->messageBus - ->expects($this->exactly(2)) - ->method('dispatch') - ->willReturnOnConsecutiveCalls( - $createPublisherEnvelope, - $this->throwException($exception) + // Then + expect(true)->toBeTrue(); + }, +); + +it( + 'successfully updates game when apiGame has null publisher and developer', + function () { + // Given + $apiGameWithNullCompanies = new ApiGame( + name: $this->faker->words(3, true), + slug: $this->faker->slug(), + description: $this->faker->paragraph(), + imageCover: $this->faker->imageUrl(), + releaseDate: $this->faker->date(), + updatedAt: new \DateTimeImmutable(), + publisher: null, + developer: null, ); - $this->publisherRepository - ->expects($this->once()) - ->method('findByApiId') - ->with($this->publisherDto->apiId) - ->willReturn($this->publisher); - - $handler = new UpdateGameFromApiCommandHandler( - $this->gameRepository, - $this->publisherRepository, - $this->developerRepository, - $this->messageBus, - ); + $this->game + ->expects($this->once()) + ->method('getPublisher') + ->willReturn($this->publisher); + + $this->game + ->expects($this->once()) + ->method('getDeveloper') + ->willReturn($this->developer); + + $this->messageBus->expects($this->never())->method('dispatch'); + + $this->publisherRepository + ->expects($this->never()) + ->method('findOneByApiId'); + + $this->developerRepository + ->expects($this->never()) + ->method('findOneByApiId'); + + $this->game + ->expects($this->once()) + ->method('setName') + ->with($apiGameWithNullCompanies->getName()); + + $this->game + ->expects($this->once()) + ->method('setDescription') + ->with($apiGameWithNullCompanies->getDescription()); + + $this->game + ->expects($this->once()) + ->method('setImageCover') + ->with($apiGameWithNullCompanies->getImageCover()); + + $this->game + ->expects($this->once()) + ->method('setReleaseDate') + ->with($apiGameWithNullCompanies->getReleaseDate()); + + $this->game + ->expects($this->once()) + ->method('setPublisher') + ->with($this->publisher); + + $this->game + ->expects($this->once()) + ->method('setDeveloper') + ->with($this->developer); + + $this->gameRepository + ->expects($this->once()) + ->method('update') + ->with($this->game); + + $handler = new UpdateGameFromApiCommandHandler( + $this->gameRepository, + $this->publisherRepository, + $this->developerRepository, + $this->messageBus, + ); - $command = new UpdateGameFromApiCommand($this->game, $this->apiGame); + $command = new UpdateGameFromApiCommand( + $this->game, + $apiGameWithNullCompanies, + ); - // When & Then - expect(function () use ($handler, $command) { + // When $handler->__invoke($command); - })->toThrow(CannotCreateDeveloperException::class); -}); - -it('throws CannotUpdateDeveloperException when developer update fails', function () { - // Given - $this->game - ->expects($this->once()) - ->method('getPublisher') - ->willReturn($this->publisher); - - $this->game - ->expects($this->once()) - ->method('getDeveloper') - ->willReturn($this->developer); - - $this->publisher - ->expects($this->once()) - ->method('getApiId') - ->willReturn($this->publisherDto->apiId); - - $this->developer - ->expects($this->once()) - ->method('getApiId') - ->willReturn($this->developerDto->apiId); - - $updatePublisherEnvelope = new Envelope( - new UpdatePublisherCommand($this->publisherDto->apiId, $this->publisherDto->name, $this->publisherDto->website), - [new HandledStamp(true, 'handler.service_id')] - ); - - $exception = $this->createMock(ExceptionInterface::class); - $this->messageBus - ->expects($this->exactly(2)) - ->method('dispatch') - ->willReturnOnConsecutiveCalls( - $updatePublisherEnvelope, - $this->throwException($exception) + // Then + expect(true)->toBeTrue(); + }, +); + +it( + 'throws CannotCreatePublisherException when publisher creation fails', + function () { + // Given + $this->game + ->expects($this->once()) + ->method('getPublisher') + ->willReturn(null); + + $exception = $this->createMock(ExceptionInterface::class); + + $this->messageBus + ->expects($this->once()) + ->method('dispatch') + ->willThrowException($exception); + + $handler = new UpdateGameFromApiCommandHandler( + $this->gameRepository, + $this->publisherRepository, + $this->developerRepository, + $this->messageBus, ); - $this->publisherRepository - ->expects($this->once()) - ->method('findByApiId') - ->with($this->publisherDto->apiId) - ->willReturn($this->publisher); - - $handler = new UpdateGameFromApiCommandHandler( - $this->gameRepository, - $this->publisherRepository, - $this->developerRepository, - $this->messageBus, - ); - - $command = new UpdateGameFromApiCommand($this->game, $this->apiGame); + $command = new UpdateGameFromApiCommand($this->game, $this->apiGame); + + // When & Then + expect(function () use ($handler, $command) { + $handler->__invoke($command); + })->toThrow(CannotCreatePublisherException::class); + }, +); + +it( + 'throws CannotUpdatePublisherException when publisher update fails', + function () { + // Given + $this->game + ->expects($this->once()) + ->method('getPublisher') + ->willReturn($this->publisher); + + $this->publisher + ->expects($this->once()) + ->method('getApiId') + ->willReturn($this->publisherDto->apiId); + + $exception = $this->createMock(ExceptionInterface::class); + + $this->messageBus + ->expects($this->once()) + ->method('dispatch') + ->willThrowException($exception); + + $handler = new UpdateGameFromApiCommandHandler( + $this->gameRepository, + $this->publisherRepository, + $this->developerRepository, + $this->messageBus, + ); - // When & Then - expect(function () use ($handler, $command) { - $handler->__invoke($command); - })->toThrow(CannotUpdateDeveloperException::class); -}); + $command = new UpdateGameFromApiCommand($this->game, $this->apiGame); + + // When & Then + expect(function () use ($handler, $command) { + $handler->__invoke($command); + })->toThrow(CannotUpdatePublisherException::class); + }, +); + +it( + 'throws CannotCreateDeveloperException when developer creation fails', + function () { + // Given + $this->game + ->expects($this->once()) + ->method('getPublisher') + ->willReturn(null); + + $this->game + ->expects($this->once()) + ->method('getDeveloper') + ->willReturn(null); + + $createPublisherEnvelope = new Envelope( + new CreatePublisherCommand( + $this->publisherDto->name, + $this->publisherDto->website, + $this->publisherDto->apiId, + ), + [new HandledStamp(true, 'handler.service_id')], + ); -it('throws CannotUpdateGameException when game repository update fails', function () { - // Given - $apiGameWithNullCompanies = new ApiGame( - name: $this->faker->words(3, true), - slug: $this->faker->slug(), - description: $this->faker->paragraph(), - imageCover: $this->faker->imageUrl(), - releaseDate: $this->faker->date(), - updatedAt: new \DateTimeImmutable(), - publisher: null, - developer: null, - ); + $exception = $this->createMock(ExceptionInterface::class); + + $this->messageBus + ->expects($this->exactly(2)) + ->method('dispatch') + ->willReturnOnConsecutiveCalls( + $createPublisherEnvelope, + $this->throwException($exception), + ); + + $this->publisherRepository + ->expects($this->once()) + ->method('findOneByApiId') + ->with($this->publisherDto->apiId) + ->willReturn($this->publisher); + + $handler = new UpdateGameFromApiCommandHandler( + $this->gameRepository, + $this->publisherRepository, + $this->developerRepository, + $this->messageBus, + ); - $this->game - ->expects($this->once()) - ->method('getPublisher') - ->willReturn($this->publisher); - - $this->game - ->expects($this->once()) - ->method('getDeveloper') - ->willReturn($this->developer); - - $this->game - ->expects($this->once()) - ->method('setName') - ->with($apiGameWithNullCompanies->getName()); - - $this->game - ->expects($this->once()) - ->method('setDescription') - ->with($apiGameWithNullCompanies->getDescription()); - - $this->game - ->expects($this->once()) - ->method('setImageCover') - ->with($apiGameWithNullCompanies->getImageCover()); - - $this->game - ->expects($this->once()) - ->method('setReleaseDate') - ->with($apiGameWithNullCompanies->getReleaseDate()); - - $this->game - ->expects($this->once()) - ->method('setPublisher') - ->with($this->publisher); - - $this->game - ->expects($this->once()) - ->method('setDeveloper') - ->with($this->developer); - - $exception = new \Exception('Database connection failed'); - - $this->gameRepository - ->expects($this->once()) - ->method('update') - ->with($this->game) - ->willThrowException($exception); - - $handler = new UpdateGameFromApiCommandHandler( - $this->gameRepository, - $this->publisherRepository, - $this->developerRepository, - $this->messageBus, - ); + $command = new UpdateGameFromApiCommand($this->game, $this->apiGame); + + // When & Then + expect(function () use ($handler, $command) { + $handler->__invoke($command); + })->toThrow(CannotCreateDeveloperException::class); + }, +); + +it( + 'throws CannotUpdateDeveloperException when developer update fails', + function () { + // Given + $this->game + ->expects($this->once()) + ->method('getPublisher') + ->willReturn($this->publisher); + + $this->game + ->expects($this->once()) + ->method('getDeveloper') + ->willReturn($this->developer); + + $this->publisher + ->expects($this->once()) + ->method('getApiId') + ->willReturn($this->publisherDto->apiId); + + $this->developer + ->expects($this->once()) + ->method('getApiId') + ->willReturn($this->developerDto->apiId); + + $updatePublisherEnvelope = new Envelope( + new UpdatePublisherCommand( + $this->publisherDto->apiId, + $this->publisherDto->name, + $this->publisherDto->website, + ), + [new HandledStamp(true, 'handler.service_id')], + ); - $command = new UpdateGameFromApiCommand($this->game, $apiGameWithNullCompanies); + $exception = $this->createMock(ExceptionInterface::class); + + $this->messageBus + ->expects($this->exactly(2)) + ->method('dispatch') + ->willReturnOnConsecutiveCalls( + $updatePublisherEnvelope, + $this->throwException($exception), + ); + + $this->publisherRepository + ->expects($this->once()) + ->method('findOneByApiId') + ->with($this->publisherDto->apiId) + ->willReturn($this->publisher); + + $handler = new UpdateGameFromApiCommandHandler( + $this->gameRepository, + $this->publisherRepository, + $this->developerRepository, + $this->messageBus, + ); - // When & Then - expect(function () use ($handler, $command) { - $handler->__invoke($command); - })->toThrow(CannotUpdateGameException::class); -}); + $command = new UpdateGameFromApiCommand($this->game, $this->apiGame); + + // When & Then + expect(function () use ($handler, $command) { + $handler->__invoke($command); + })->toThrow(CannotUpdateDeveloperException::class); + }, +); + +it( + 'throws CannotUpdateGameException when game repository update fails', + function () { + // Given + $apiGameWithNullCompanies = new ApiGame( + name: $this->faker->words(3, true), + slug: $this->faker->slug(), + description: $this->faker->paragraph(), + imageCover: $this->faker->imageUrl(), + releaseDate: $this->faker->date(), + updatedAt: new \DateTimeImmutable(), + publisher: null, + developer: null, + ); -it('handles different types of exceptions during game repository update', function () { - // Given - $apiGameWithNullCompanies = new ApiGame( - name: $this->faker->words(3, true), - slug: $this->faker->slug(), - description: $this->faker->paragraph(), - imageCover: $this->faker->imageUrl(), - releaseDate: $this->faker->date(), - updatedAt: new \DateTimeImmutable(), - publisher: null, - developer: null, - ); + $this->game + ->expects($this->once()) + ->method('getPublisher') + ->willReturn($this->publisher); + + $this->game + ->expects($this->once()) + ->method('getDeveloper') + ->willReturn($this->developer); + + $this->game + ->expects($this->once()) + ->method('setName') + ->with($apiGameWithNullCompanies->getName()); + + $this->game + ->expects($this->once()) + ->method('setDescription') + ->with($apiGameWithNullCompanies->getDescription()); + + $this->game + ->expects($this->once()) + ->method('setImageCover') + ->with($apiGameWithNullCompanies->getImageCover()); + + $this->game + ->expects($this->once()) + ->method('setReleaseDate') + ->with($apiGameWithNullCompanies->getReleaseDate()); + + $this->game + ->expects($this->once()) + ->method('setPublisher') + ->with($this->publisher); + + $this->game + ->expects($this->once()) + ->method('setDeveloper') + ->with($this->developer); + + $exception = new \Exception('Database connection failed'); + + $this->gameRepository + ->expects($this->once()) + ->method('update') + ->with($this->game) + ->willThrowException($exception); + + $handler = new UpdateGameFromApiCommandHandler( + $this->gameRepository, + $this->publisherRepository, + $this->developerRepository, + $this->messageBus, + ); - $this->game - ->expects($this->once()) - ->method('getPublisher') - ->willReturn($this->publisher); - - $this->game - ->expects($this->once()) - ->method('getDeveloper') - ->willReturn($this->developer); - - $this->game - ->expects($this->once()) - ->method('setName') - ->with($apiGameWithNullCompanies->getName()); - - $this->game - ->expects($this->once()) - ->method('setDescription') - ->with($apiGameWithNullCompanies->getDescription()); - - $this->game - ->expects($this->once()) - ->method('setImageCover') - ->with($apiGameWithNullCompanies->getImageCover()); - - $this->game - ->expects($this->once()) - ->method('setReleaseDate') - ->with($apiGameWithNullCompanies->getReleaseDate()); - - $this->game - ->expects($this->once()) - ->method('setPublisher') - ->with($this->publisher); - - $this->game - ->expects($this->once()) - ->method('setDeveloper') - ->with($this->developer); - - $exception = new \RuntimeException('Runtime error occurred'); - - $this->gameRepository - ->expects($this->once()) - ->method('update') - ->with($this->game) - ->willThrowException($exception); - - $handler = new UpdateGameFromApiCommandHandler( - $this->gameRepository, - $this->publisherRepository, - $this->developerRepository, - $this->messageBus, - ); + $command = new UpdateGameFromApiCommand( + $this->game, + $apiGameWithNullCompanies, + ); - $command = new UpdateGameFromApiCommand($this->game, $apiGameWithNullCompanies); + // When & Then + expect(function () use ($handler, $command) { + $handler->__invoke($command); + })->toThrow(CannotUpdateGameException::class); + }, +); + +it( + 'handles different types of exceptions during game repository update', + function () { + // Given + $apiGameWithNullCompanies = new ApiGame( + name: $this->faker->words(3, true), + slug: $this->faker->slug(), + description: $this->faker->paragraph(), + imageCover: $this->faker->imageUrl(), + releaseDate: $this->faker->date(), + updatedAt: new \DateTimeImmutable(), + publisher: null, + developer: null, + ); - // When & Then - expect(function () use ($handler, $command) { - $handler->__invoke($command); - })->toThrow(CannotUpdateGameException::class); -}); + $this->game + ->expects($this->once()) + ->method('getPublisher') + ->willReturn($this->publisher); + + $this->game + ->expects($this->once()) + ->method('getDeveloper') + ->willReturn($this->developer); + + $this->game + ->expects($this->once()) + ->method('setName') + ->with($apiGameWithNullCompanies->getName()); + + $this->game + ->expects($this->once()) + ->method('setDescription') + ->with($apiGameWithNullCompanies->getDescription()); + + $this->game + ->expects($this->once()) + ->method('setImageCover') + ->with($apiGameWithNullCompanies->getImageCover()); + + $this->game + ->expects($this->once()) + ->method('setReleaseDate') + ->with($apiGameWithNullCompanies->getReleaseDate()); + + $this->game + ->expects($this->once()) + ->method('setPublisher') + ->with($this->publisher); + + $this->game + ->expects($this->once()) + ->method('setDeveloper') + ->with($this->developer); + + $exception = new \RuntimeException('Runtime error occurred'); + + $this->gameRepository + ->expects($this->once()) + ->method('update') + ->with($this->game) + ->willThrowException($exception); + + $handler = new UpdateGameFromApiCommandHandler( + $this->gameRepository, + $this->publisherRepository, + $this->developerRepository, + $this->messageBus, + ); -it('handles mixed scenarios with publisher creation and developer update', function () { - // Given - $this->game - ->expects($this->once()) - ->method('getPublisher') - ->willReturn(null); - - $this->game - ->expects($this->once()) - ->method('getDeveloper') - ->willReturn($this->developer); - - $this->developer - ->expects($this->once()) - ->method('getApiId') - ->willReturn($this->developerDto->apiId); - - $createPublisherEnvelope = new Envelope( - new CreatePublisherCommand($this->publisherDto->name, $this->publisherDto->website, $this->publisherDto->apiId), - [new HandledStamp(true, 'handler.service_id')] - ); + $command = new UpdateGameFromApiCommand( + $this->game, + $apiGameWithNullCompanies, + ); - $updateDeveloperEnvelope = new Envelope( - new UpdateDeveloperCommand($this->developerDto->apiId, $this->developerDto->name, $this->developerDto->website), - [new HandledStamp(true, 'handler.service_id')] - ); + // When & Then + expect(function () use ($handler, $command) { + $handler->__invoke($command); + })->toThrow(CannotUpdateGameException::class); + }, +); + +it( + 'handles mixed scenarios with publisher creation and developer update', + function () { + // Given + $this->game + ->expects($this->once()) + ->method('getPublisher') + ->willReturn(null); + + $this->game + ->expects($this->once()) + ->method('getDeveloper') + ->willReturn($this->developer); + + $this->developer + ->expects($this->once()) + ->method('getApiId') + ->willReturn($this->developerDto->apiId); + + $createPublisherEnvelope = new Envelope( + new CreatePublisherCommand( + $this->publisherDto->name, + $this->publisherDto->website, + $this->publisherDto->apiId, + ), + [new HandledStamp(true, 'handler.service_id')], + ); - $this->messageBus - ->expects($this->exactly(2)) - ->method('dispatch') - ->willReturnOnConsecutiveCalls( - $createPublisherEnvelope, - $updateDeveloperEnvelope + $updateDeveloperEnvelope = new Envelope( + new UpdateDeveloperCommand( + $this->developerDto->apiId, + $this->developerDto->name, + $this->developerDto->website, + ), + [new HandledStamp(true, 'handler.service_id')], ); - $this->publisherRepository - ->expects($this->once()) - ->method('findByApiId') - ->with($this->publisherDto->apiId) - ->willReturn($this->publisher); - - $this->developerRepository - ->expects($this->once()) - ->method('findByApiId') - ->with($this->developerDto->apiId) - ->willReturn($this->developer); - - $this->game - ->expects($this->once()) - ->method('setName') - ->with($this->apiGame->getName()); - - $this->game - ->expects($this->once()) - ->method('setDescription') - ->with($this->apiGame->getDescription()); - - $this->game - ->expects($this->once()) - ->method('setImageCover') - ->with($this->apiGame->getImageCover()); - - $this->game - ->expects($this->once()) - ->method('setReleaseDate') - ->with($this->apiGame->getReleaseDate()); - - $this->game - ->expects($this->once()) - ->method('setPublisher') - ->with($this->publisher); - - $this->game - ->expects($this->once()) - ->method('setDeveloper') - ->with($this->developer); - - $this->gameRepository - ->expects($this->once()) - ->method('update') - ->with($this->game); - - $handler = new UpdateGameFromApiCommandHandler( - $this->gameRepository, - $this->publisherRepository, - $this->developerRepository, - $this->messageBus, - ); + $this->messageBus + ->expects($this->exactly(2)) + ->method('dispatch') + ->willReturnOnConsecutiveCalls( + $createPublisherEnvelope, + $updateDeveloperEnvelope, + ); + + $this->publisherRepository + ->expects($this->once()) + ->method('findOneByApiId') + ->with($this->publisherDto->apiId) + ->willReturn($this->publisher); + + $this->developerRepository + ->expects($this->once()) + ->method('findOneByApiId') + ->with($this->developerDto->apiId) + ->willReturn($this->developer); + + $this->game + ->expects($this->once()) + ->method('setName') + ->with($this->apiGame->getName()); + + $this->game + ->expects($this->once()) + ->method('setDescription') + ->with($this->apiGame->getDescription()); + + $this->game + ->expects($this->once()) + ->method('setImageCover') + ->with($this->apiGame->getImageCover()); + + $this->game + ->expects($this->once()) + ->method('setReleaseDate') + ->with($this->apiGame->getReleaseDate()); + + $this->game + ->expects($this->once()) + ->method('setPublisher') + ->with($this->publisher); + + $this->game + ->expects($this->once()) + ->method('setDeveloper') + ->with($this->developer); + + $this->gameRepository + ->expects($this->once()) + ->method('update') + ->with($this->game); + + $handler = new UpdateGameFromApiCommandHandler( + $this->gameRepository, + $this->publisherRepository, + $this->developerRepository, + $this->messageBus, + ); - $command = new UpdateGameFromApiCommand($this->game, $this->apiGame); + $command = new UpdateGameFromApiCommand($this->game, $this->apiGame); - // When - $handler->__invoke($command); + // When + $handler->__invoke($command); - // Then - expect(true)->toBeTrue(); -}); + // Then + expect(true)->toBeTrue(); + }, +); diff --git a/tests/Unit/Application/CommandHandler/Report/CreateReportFromDtoCommandHandlerTest.php b/tests/Unit/Application/CommandHandler/Report/CreateReportFromDtoCommandHandlerTest.php index fe60e16..c90a558 100644 --- a/tests/Unit/Application/CommandHandler/Report/CreateReportFromDtoCommandHandlerTest.php +++ b/tests/Unit/Application/CommandHandler/Report/CreateReportFromDtoCommandHandlerTest.php @@ -6,6 +6,7 @@ use App\Application\Command\Report\CreateReportFromDtoCommand; use App\Application\CommandHandler\Report\CreateReportFromDtoCommandHandler; +use App\Application\Fetcher\Report\ReportFetcher; use App\Domain\Model\Game\Game; use App\Domain\Model\Report\Report; use App\Infrastructure\Enum\ReportGameStatusEnum; @@ -17,8 +18,11 @@ beforeEach(function () { $this->faker = Factory::create(); - $this->reportRepository = $this->createMock(DoctrineReportRepository::class); + $this->reportRepository = $this->createMock( + DoctrineReportRepository::class, + ); $this->gameRepository = $this->createMock(DoctrineGameRepository::class); + $this->reportFetcher = $this->createMock(ReportFetcher::class); $this->gameId = $this->faker->uuid(); $this->gameSlug = $this->faker->slug(); @@ -55,25 +59,41 @@ $this->reportRepository ->expects($this->once()) ->method('save') - ->with($this->callback(function ($report) { - return $report instanceof Report - && $report->is60FpsPortable() === $this->createReportDto->is60FpsPortable - && $report->isHasStableFrameratePortable() === $this->createReportDto->hasStableFrameratePortable - && $report->isHasResolutionImprovedPortable() === $this->createReportDto->hasResolutionImprovedPortable - && $report->isNativeResolutionPortable() === $this->createReportDto->isNativeResolutionPortable - && $report->is60FpsDocked() === $this->createReportDto->is60FpsDocked - && $report->isHasStableFramerateDocked() === $this->createReportDto->hasStableFramerateDocked - && $report->isHasResolutionImprovedDocked() === $this->createReportDto->hasResolutionImprovedDocked - && $report->isNativeResolutionDocked() === $this->createReportDto->isNativeResolutionDocked - && $report->isHasImprovedLoadingTimes() === $this->createReportDto->hasImprovedLoadingTimes - && $report->isSwitch2Edition() === $this->createReportDto->isSwitch2Edition - && $report->getGameStatus() === $this->createReportDto->gameStatus - && $report->isPatched() === $this->createReportDto->isPatched; - })); + ->with( + $this->callback(function ($report) { + return $report instanceof Report && + $report->is60FpsPortable() === + $this->createReportDto->is60FpsPortable && + $report->isHasStableFrameratePortable() === + $this->createReportDto->hasStableFrameratePortable && + $report->isHasResolutionImprovedPortable() === + $this->createReportDto->hasResolutionImprovedPortable && + $report->isNativeResolutionPortable() === + $this->createReportDto->isNativeResolutionPortable && + $report->is60FpsDocked() === + $this->createReportDto->is60FpsDocked && + $report->isHasStableFramerateDocked() === + $this->createReportDto->hasStableFramerateDocked && + $report->isHasResolutionImprovedDocked() === + $this->createReportDto->hasResolutionImprovedDocked && + $report->isNativeResolutionDocked() === + $this->createReportDto->isNativeResolutionDocked && + $report->isHasImprovedLoadingTimes() === + $this->createReportDto->hasImprovedLoadingTimes && + $report->isSwitch2Edition() === + $this->createReportDto->isSwitch2Edition && + $report->getGameStatus() === + $this->createReportDto->gameStatus && + $report->isPatched() === $this->createReportDto->isPatched; + }), + ); + + $this->reportFetcher->expects($this->once())->method('deleteCache'); $handler = new CreateReportFromDtoCommandHandler( $this->reportRepository, $this->gameRepository, + $this->reportFetcher, ); $command = new CreateReportFromDtoCommand($this->createReportDto); @@ -93,13 +113,14 @@ ->with($this->gameId) ->willReturn(null); - $this->reportRepository - ->expects($this->never()) - ->method('save'); + $this->reportRepository->expects($this->never())->method('save'); + + $this->reportFetcher->expects($this->never())->method('deleteCache'); $handler = new CreateReportFromDtoCommandHandler( $this->reportRepository, $this->gameRepository, + $this->reportFetcher, ); $command = new CreateReportFromDtoCommand($this->createReportDto); @@ -125,9 +146,12 @@ ->method('save') ->willThrowException($exception); + $this->reportFetcher->expects($this->never())->method('deleteCache'); + $handler = new CreateReportFromDtoCommandHandler( $this->reportRepository, - $this->gameRepository + $this->gameRepository, + $this->reportFetcher, ); $command = new CreateReportFromDtoCommand($this->createReportDto); @@ -166,25 +190,39 @@ $this->reportRepository ->expects($this->once()) ->method('save') - ->with($this->callback(function ($report) use ($customDto) { - return $report instanceof Report - && $report->is60FpsPortable() === $customDto->is60FpsPortable - && $report->isHasStableFrameratePortable() === $customDto->hasStableFrameratePortable - && $report->isHasResolutionImprovedPortable() === $customDto->hasResolutionImprovedPortable - && $report->isNativeResolutionPortable() === $customDto->isNativeResolutionPortable - && $report->is60FpsDocked() === $customDto->is60FpsDocked - && $report->isHasStableFramerateDocked() === $customDto->hasStableFramerateDocked - && $report->isHasResolutionImprovedDocked() === $customDto->hasResolutionImprovedDocked - && $report->isNativeResolutionDocked() === $customDto->isNativeResolutionDocked - && $report->isHasImprovedLoadingTimes() === $customDto->hasImprovedLoadingTimes - && $report->isSwitch2Edition() === $customDto->isSwitch2Edition - && $report->getGameStatus() === $customDto->gameStatus - && $report->isPatched() === $customDto->isPatched; - })); + ->with( + $this->callback(function ($report) use ($customDto) { + return $report instanceof Report && + $report->is60FpsPortable() === + $customDto->is60FpsPortable && + $report->isHasStableFrameratePortable() === + $customDto->hasStableFrameratePortable && + $report->isHasResolutionImprovedPortable() === + $customDto->hasResolutionImprovedPortable && + $report->isNativeResolutionPortable() === + $customDto->isNativeResolutionPortable && + $report->is60FpsDocked() === $customDto->is60FpsDocked && + $report->isHasStableFramerateDocked() === + $customDto->hasStableFramerateDocked && + $report->isHasResolutionImprovedDocked() === + $customDto->hasResolutionImprovedDocked && + $report->isNativeResolutionDocked() === + $customDto->isNativeResolutionDocked && + $report->isHasImprovedLoadingTimes() === + $customDto->hasImprovedLoadingTimes && + $report->isSwitch2Edition() === + $customDto->isSwitch2Edition && + $report->getGameStatus() === $customDto->gameStatus && + $report->isPatched() === $customDto->isPatched; + }), + ); + + $this->reportFetcher->expects($this->once())->method('deleteCache'); $handler = new CreateReportFromDtoCommandHandler( $this->reportRepository, - $this->gameRepository + $this->gameRepository, + $this->reportFetcher, ); $command = new CreateReportFromDtoCommand($customDto); @@ -224,25 +262,41 @@ $this->reportRepository ->expects($this->once()) ->method('save') - ->with($this->callback(function ($report) use ($dtoWithBadStatus) { - return $report instanceof Report - && $report->is60FpsPortable() === $dtoWithBadStatus->is60FpsPortable - && $report->isHasStableFrameratePortable() === $dtoWithBadStatus->hasStableFrameratePortable - && $report->isHasResolutionImprovedPortable() === $dtoWithBadStatus->hasResolutionImprovedPortable - && $report->isNativeResolutionPortable() === $dtoWithBadStatus->isNativeResolutionPortable - && $report->is60FpsDocked() === $dtoWithBadStatus->is60FpsDocked - && $report->isHasStableFramerateDocked() === $dtoWithBadStatus->hasStableFramerateDocked - && $report->isHasResolutionImprovedDocked() === $dtoWithBadStatus->hasResolutionImprovedDocked - && $report->isNativeResolutionDocked() === $dtoWithBadStatus->isNativeResolutionDocked - && $report->isHasImprovedLoadingTimes() === $dtoWithBadStatus->hasImprovedLoadingTimes - && $report->isSwitch2Edition() === $dtoWithBadStatus->isSwitch2Edition - && $report->getGameStatus() === $dtoWithBadStatus->gameStatus - && $report->isPatched() === $dtoWithBadStatus->isPatched; - })); + ->with( + $this->callback(function ($report) use ($dtoWithBadStatus) { + return $report instanceof Report && + $report->is60FpsPortable() === + $dtoWithBadStatus->is60FpsPortable && + $report->isHasStableFrameratePortable() === + $dtoWithBadStatus->hasStableFrameratePortable && + $report->isHasResolutionImprovedPortable() === + $dtoWithBadStatus->hasResolutionImprovedPortable && + $report->isNativeResolutionPortable() === + $dtoWithBadStatus->isNativeResolutionPortable && + $report->is60FpsDocked() === + $dtoWithBadStatus->is60FpsDocked && + $report->isHasStableFramerateDocked() === + $dtoWithBadStatus->hasStableFramerateDocked && + $report->isHasResolutionImprovedDocked() === + $dtoWithBadStatus->hasResolutionImprovedDocked && + $report->isNativeResolutionDocked() === + $dtoWithBadStatus->isNativeResolutionDocked && + $report->isHasImprovedLoadingTimes() === + $dtoWithBadStatus->hasImprovedLoadingTimes && + $report->isSwitch2Edition() === + $dtoWithBadStatus->isSwitch2Edition && + $report->getGameStatus() === + $dtoWithBadStatus->gameStatus && + $report->isPatched() === $dtoWithBadStatus->isPatched; + }), + ); + + $this->reportFetcher->expects($this->once())->method('deleteCache'); $handler = new CreateReportFromDtoCommandHandler( $this->reportRepository, - $this->gameRepository + $this->gameRepository, + $this->reportFetcher, ); $command = new CreateReportFromDtoCommand($dtoWithBadStatus); diff --git a/tests/Unit/Application/QueryHandler/Game/GetOrCreateGameQueryHandlerTest.php b/tests/Unit/Application/QueryHandler/Game/GetOrCreateGameQueryHandlerTest.php index 0be594e..984cb31 100644 --- a/tests/Unit/Application/QueryHandler/Game/GetOrCreateGameQueryHandlerTest.php +++ b/tests/Unit/Application/QueryHandler/Game/GetOrCreateGameQueryHandlerTest.php @@ -6,6 +6,7 @@ use App\Application\Command\Game\CreateGameCommand; use App\Application\Exception\Game\CannotCreateGameException; +use App\Application\Fetcher\Game\GameFetcher; use App\Application\Helper\MessageBusHelper; use App\Application\Query\Game\GetOrCreateGameQuery; use App\Application\QueryHandler\Game\GetOrCreateGameQueryHandler; @@ -26,7 +27,10 @@ $this->faker = Factory::create(); $this->gameRepository = $this->createMock(DoctrineGameRepository::class); - $this->publisherRepository = $this->createMock(DoctrinePublisherRepository::class); + $this->gameFetcher = $this->createMock(GameFetcher::class); + $this->publisherRepository = $this->createMock( + DoctrinePublisherRepository::class, + ); $this->messageBus = $this->createMock(MessageBusInterface::class); $this->messageBusHelper = $this->createMock(MessageBusHelper::class); $this->logger = $this->createMock(LoggerInterface::class); @@ -41,7 +45,11 @@ $this->publisherApiId, ); - $this->createdPublisher = GamePublisherFactory::create($this->publisherName, $this->publisherApiId, $this->publisherWebsite); + $this->createdPublisher = GamePublisherFactory::create( + $this->publisherName, + $this->publisherApiId, + $this->publisherWebsite, + ); $this->developerName = $this->faker->company(); $this->developerWebsite = $this->faker->url(); @@ -58,7 +66,7 @@ // Given $handler = new GetOrCreateGameQueryHandler( $this->messageBus, - $this->gameRepository, + $this->gameFetcher, $this->logger, ); @@ -75,16 +83,14 @@ $existingGame = new Game(slug: $gameSlug); - $this->gameRepository + $this->gameFetcher ->expects($this->once()) ->method('getOneBySlugEnabledGame') ->with($gameSlug) ->willReturn($existingGame); // Message bus should not be called when game exists - $this->messageBus - ->expects($this->never()) - ->method('dispatch'); + $this->messageBus->expects($this->never())->method('dispatch'); $query = new GetOrCreateGameQuery($gameSlug, $apiGame); @@ -99,8 +105,8 @@ // Given $handler = new GetOrCreateGameQueryHandler( $this->messageBus, - $this->gameRepository, - $this->logger + $this->gameFetcher, + $this->logger, ); $gameSlug = 'zelda-breath-of-the-wild'; @@ -119,7 +125,7 @@ $handledStamp = new HandledStamp($createdGame, 'handler.service_id'); $envelope = new Envelope($handler, [$handledStamp]); - $this->gameRepository + $this->gameFetcher ->expects($this->exactly(2)) ->method('getOneBySlugEnabledGame') ->with($gameSlug) @@ -128,14 +134,16 @@ $this->messageBus ->expects($this->once()) ->method('dispatch') - ->with($this->callback(function ($message) use ($apiGame) { - return $message instanceof CreateGameCommand - && $message->name === $apiGame->getName() - && $message->slug === $apiGame->getSlug() - && $message->releaseDate === $apiGame->getReleaseDate() - && $message->imageCover === $apiGame->getImageCover() - && $message->description === $apiGame->getDescription(); - })) + ->with( + $this->callback(function ($message) use ($apiGame) { + return $message instanceof CreateGameCommand && + $message->name === $apiGame->getName() && + $message->slug === $apiGame->getSlug() && + $message->releaseDate === $apiGame->getReleaseDate() && + $message->imageCover === $apiGame->getImageCover() && + $message->description === $apiGame->getDescription(); + }), + ) ->willReturn($envelope); $query = new GetOrCreateGameQuery($gameSlug, $apiGame); @@ -147,105 +155,112 @@ expect($result)->toBe($createdGame); }); -it('throws CannotCreateGameException when message bus dispatch fails with Exception', function () { - // Given - $this->gameRepository = $this->createMock(GameRepositoryInterface::class); - $this->messageBus = $this->createMock(MessageBusInterface::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $handler = new GetOrCreateGameQueryHandler( - $this->messageBus, - $this->gameRepository, - $this->logger - ); - - $gameSlug = 'zelda-breath-of-the-wild'; - $apiGame = new ApiGame( - name: 'The Legend of Zelda: Breath of the Wild', - slug: $gameSlug, - description: 'An open-world adventure game', - imageCover: 'https://example.com/image.jpg', - releaseDate: '2017-03-03', - updatedAt: new \DateTimeImmutable('now'), - publisher: $this->publisherDto, - ); - - $this->gameRepository - ->expects($this->once()) - ->method('getOneBySlugEnabledGame') - ->with($gameSlug) - ->willReturn(null); - - $exception = new \Exception('Database error'); - - $this->messageBus - ->expects($this->once()) - ->method('dispatch') - ->willThrowException($exception); - - $this->logger - ->expects($this->once()) - ->method('error') - ->with('Database error'); - - $query = new GetOrCreateGameQuery($gameSlug, $apiGame); - - // When & Then - expect(fn() => $handler->__invoke($query)) - ->toThrow(CannotCreateGameException::class); -}); - -it('throws CannotCreateGameException when message bus dispatch fails with ExceptionInterface', function () { - // Given - $this->gameRepository = $this->createMock(GameRepositoryInterface::class); - $this->messageBus = $this->createMock(MessageBusInterface::class); - $this->logger = $this->createMock(LoggerInterface::class); - $exception = new CannotCreateGameException(); - - $handler = new GetOrCreateGameQueryHandler( - $this->messageBus, - $this->gameRepository, - $this->logger - ); - - $gameSlug = 'zelda-breath-of-the-wild'; - $apiGame = new ApiGame( - name: 'The Legend of Zelda: Breath of the Wild', - slug: $gameSlug, - description: 'An open-world adventure game', - imageCover: 'https://example.com/image.jpg', - releaseDate: '2017-03-03', - updatedAt: new \DateTimeImmutable('now'), - publisher: $this->publisherDto, - ); - - $this->gameRepository - ->expects($this->once()) - ->method('getOneBySlugEnabledGame') - ->with($gameSlug) - ->willReturn(null); - - $this->messageBus - ->expects($this->once()) - ->method('dispatch') - ->willThrowException($exception); - - $query = new GetOrCreateGameQuery($gameSlug, $apiGame); - - // When & Then - $handler->__invoke($query); -})->throws(CannotCreateGameException::class); +it( + 'throws CannotCreateGameException when message bus dispatch fails with Exception', + function () { + // Given + $gameFetcher = $this->createMock(GameFetcher::class); + $messageBus = $this->createMock(MessageBusInterface::class); + $logger = $this->createMock(LoggerInterface::class); + + $handler = new GetOrCreateGameQueryHandler( + $messageBus, + $gameFetcher, + $logger, + ); + + $gameSlug = 'zelda-breath-of-the-wild'; + $apiGame = new ApiGame( + name: 'The Legend of Zelda: Breath of the Wild', + slug: $gameSlug, + description: 'An open-world adventure game', + imageCover: 'https://example.com/image.jpg', + releaseDate: '2017-03-03', + updatedAt: new \DateTimeImmutable('now'), + publisher: $this->publisherDto, + ); + + $gameFetcher + ->expects($this->once()) + ->method('getOneBySlugEnabledGame') + ->with($gameSlug) + ->willReturn(null); + + $exception = new \Exception('Database error'); + + $messageBus + ->expects($this->once()) + ->method('dispatch') + ->willThrowException($exception); + + $logger + ->expects($this->once()) + ->method('error') + ->with('Database error'); + + $query = new GetOrCreateGameQuery($gameSlug, $apiGame); + + // When & Then + expect(fn() => $handler->__invoke($query))->toThrow( + CannotCreateGameException::class, + ); + }, +); + +it( + 'throws CannotCreateGameException when message bus dispatch fails with ExceptionInterface', + function () { + // Given + $gameFetcher = $this->createMock(GameFetcher::class); + $messageBus = $this->createMock(MessageBusInterface::class); + $logger = $this->createMock(LoggerInterface::class); + $exception = new CannotCreateGameException(); + + $handler = new GetOrCreateGameQueryHandler( + $messageBus, + $gameFetcher, + $logger, + ); + + $gameSlug = 'zelda-breath-of-the-wild'; + $apiGame = new ApiGame( + name: 'The Legend of Zelda: Breath of the Wild', + slug: $gameSlug, + description: 'An open-world adventure game', + imageCover: 'https://example.com/image.jpg', + releaseDate: '2017-03-03', + updatedAt: new \DateTimeImmutable('now'), + publisher: $this->publisherDto, + ); + + $gameFetcher + ->expects($this->once()) + ->method('getOneBySlugEnabledGame') + ->with($gameSlug) + ->willReturn(null); + + $messageBus + ->expects($this->once()) + ->method('dispatch') + ->willThrowException($exception); + + $query = new GetOrCreateGameQuery($gameSlug, $apiGame); + + // When & Then + $handler->__invoke($query); + }, +)->throws(CannotCreateGameException::class); it('properly handles different ApiGame properties', function () { // Given - $this->gameRepository = $this->createMock(GameRepositoryInterface::class); - $this->messageBus = $this->createMock(MessageBusInterface::class); - $this->logger = $this->createMock(LoggerInterface::class); + $gameFetcher = $this->createMock(GameFetcher::class); + $messageBus = $this->createMock(MessageBusInterface::class); + $logger = $this->createMock(LoggerInterface::class); $handler = new GetOrCreateGameQueryHandler( - $this->messageBus, - $this->gameRepository, - $this->logger + $messageBus, + $gameFetcher, + $logger, ); $gameSlug = 'mario-odyssey'; @@ -261,28 +276,30 @@ $createdGame = new Game(slug: $gameSlug); - // Game not found initially, then found after creation - $this->gameRepository + $handledStamp = new HandledStamp($createdGame, 'handler.service_id'); + $envelope = new Envelope($handler, [$handledStamp]); + + $gameFetcher ->expects($this->exactly(2)) ->method('getOneBySlugEnabledGame') ->with($gameSlug) ->willReturnOnConsecutiveCalls(null, $createdGame); - $handledStamp = new HandledStamp($createdGame, 'handler.service_id'); - $envelope = new Envelope($handler, [$handledStamp]); - - $this->messageBus + $messageBus ->expects($this->once()) ->method('dispatch') - ->with($this->callback(function ($command) { - return $command instanceof CreateGameCommand - && $command->getName() === 'Super Mario Odyssey' - && $command->getSlug() === 'mario-odyssey' - && $command->getReleaseDate() === '2017-10-27' - && $command->getImageCover() === 'https://example.com/mario.jpg' - && $command->getDescription() === 'A 3D platform game'; - })) - ->willReturn($envelope); + ->with( + $this->callback(function ($command) { + return $command instanceof CreateGameCommand && + $command->getName() === 'Super Mario Odyssey' && + $command->getSlug() === 'mario-odyssey' && + $command->getReleaseDate() === '2017-10-27' && + $command->getImageCover() === + 'https://example.com/mario.jpg' && + $command->getDescription() === 'A 3D platform game'; + }), + ) + ->willReturn($envelope); $query = new GetOrCreateGameQuery($gameSlug, $apiGame); @@ -297,16 +314,16 @@ // Given $handler = new GetOrCreateGameQueryHandler( $this->messageBus, - $this->gameRepository, - $this->logger + $this->gameFetcher, + $this->logger, ); $gameSlug = 'witcher-3-wild-hunt'; $apiGame = new ApiGame( name: 'The Witcher 3: Wild Hunt', slug: $gameSlug, - description: 'An open-world RPG game', - imageCover: 'https://example.com/witcher.jpg', + description: 'An open world RPG', + imageCover: 'https://example.com/witcher3.jpg', releaseDate: '2015-05-19', updatedAt: new \DateTimeImmutable('now'), publisher: $this->publisherDto, @@ -318,7 +335,7 @@ $handledStamp = new HandledStamp($createdGame, 'handler.service_id'); $envelope = new Envelope($handler, [$handledStamp]); - $this->gameRepository + $this->gameFetcher ->expects($this->exactly(2)) ->method('getOneBySlugEnabledGame') ->with($gameSlug) @@ -327,16 +344,18 @@ $this->messageBus ->expects($this->once()) ->method('dispatch') - ->with($this->callback(function ($command) use ($apiGame) { - return $command instanceof CreateGameCommand - && $command->name === $apiGame->getName() - && $command->slug === $apiGame->getSlug() - && $command->releaseDate === $apiGame->getReleaseDate() - && $command->imageCover === $apiGame->getImageCover() - && $command->description === $apiGame->getDescription() - && $command->publisher === $apiGame->getPublisher() - && $command->developer === $apiGame->getDeveloper(); - })) + ->with( + $this->callback(function ($command) use ($apiGame) { + return $command instanceof CreateGameCommand && + $command->name === $apiGame->getName() && + $command->slug === $apiGame->getSlug() && + $command->releaseDate === $apiGame->getReleaseDate() && + $command->imageCover === $apiGame->getImageCover() && + $command->description === $apiGame->getDescription() && + $command->publisher === $apiGame->getPublisher() && + $command->developer === $apiGame->getDeveloper(); + }), + ) ->willReturn($envelope); $query = new GetOrCreateGameQuery($gameSlug, $apiGame); diff --git a/tests/Unit/Infrastructure/Client/IgdbClientTest.php b/tests/Unit/Infrastructure/Client/IgdbClientTest.php index 39afeda..7715597 100644 --- a/tests/Unit/Infrastructure/Client/IgdbClientTest.php +++ b/tests/Unit/Infrastructure/Client/IgdbClientTest.php @@ -30,7 +30,7 @@ clientSecret: 'test-client-secret', cache: $this->cache, serializer: $this->serializer, - baseUrl: 'https://api.igdb.com/v4/' + baseUrl: 'https://api.igdb.com/v4/', ); $reflection = new \ReflectionClass($this->igdbClient); @@ -67,23 +67,25 @@ $this->cache ->expects($this->exactly(2)) ->method('get') - ->with($this->callback(function ($cacheKey) use ( - $cacheSearchKey, - $cacheGameKey, - ) { - return in_array($cacheKey, [$cacheSearchKey, $cacheGameKey]); - })) + ->with( + $this->callback(function ($cacheKey) use ( + $cacheSearchKey, + $cacheGameKey, + ) { + return in_array($cacheKey, [$cacheSearchKey, $cacheGameKey]); + }), + ) ->willReturnCallback(function ($cacheKey, $callback) use ( $expectedGames, $cacheSearchKey, $cacheGameKey, ) { - return match($cacheKey) { + return match ($cacheKey) { $cacheSearchKey => $expectedGames, $cacheGameKey => $expectedGames[0], - default => throw new \RuntimeException('Unexpected envelope') + default => throw new \RuntimeException('Unexpected envelope'), }; - }); + }); // When $result = $this->igdbClient->searchGames($query, $limit); @@ -91,8 +93,10 @@ // Then expect($result) ->toHaveCount(1) - ->and($result[0])->toBeInstanceOf(ApiGame::class) - ->and($result[0])->toBe($expectedGames[0]); + ->and($result[0]) + ->toBeInstanceOf(ApiGame::class) + ->and($result[0]) + ->toBe($expectedGames[0]); }); it('searches games with custom limit', function () { @@ -106,7 +110,10 @@ 'platform' => IgdbGamePlatformEnum::NINTENDO_SWITCH->value, ]; - $expectedGames = [createApiGame($this->faker, $this->publisherDto), createApiGame($this->faker, $this->publisherDto)]; + $expectedGames = [ + createApiGame($this->faker, $this->publisherDto), + createApiGame($this->faker, $this->publisherDto), + ]; $cacheSearchKey = 'igdb_api_search_' . md5(json_encode($cacheKeyData)); $cacheFirstGameKey = 'api_game_' . $expectedGames[0]->getSlug(); $cacheSecondGameKey = 'api_game_' . $expectedGames[1]->getSlug(); @@ -114,24 +121,30 @@ $this->cache ->expects($this->exactly(3)) ->method('get') - ->with($this->callback(function ($cacheKey) use ( - $cacheSearchKey, - $cacheFirstGameKey, - $cacheSecondGameKey, - ) { - return in_array($cacheKey, [$cacheSearchKey, $cacheFirstGameKey, $cacheSecondGameKey]);; - })) + ->with( + $this->callback(function ($cacheKey) use ( + $cacheSearchKey, + $cacheFirstGameKey, + $cacheSecondGameKey, + ) { + return in_array($cacheKey, [ + $cacheSearchKey, + $cacheFirstGameKey, + $cacheSecondGameKey, + ]); + }), + ) ->willReturnCallback(function ($cacheKey, $callback) use ( $expectedGames, $cacheSearchKey, $cacheFirstGameKey, $cacheSecondGameKey, ) { - return match($cacheKey) { + return match ($cacheKey) { $cacheSearchKey => $expectedGames, $cacheFirstGameKey => $expectedGames[0], $cacheSecondGameKey => $expectedGames[1], - default => throw new \RuntimeException('Unexpected envelope') + default => throw new \RuntimeException('Unexpected envelope'), }; }); @@ -139,10 +152,7 @@ $result = $this->igdbClient->searchGames($query, $limit); // Then - expect($result) - ->toHaveCount(2) - ->toBe($expectedGames) - ; + expect($result)->toHaveCount(2)->toBe($expectedGames); }); it('returns cached results when available', function () { @@ -163,21 +173,23 @@ $this->cache ->expects($this->exactly(2)) ->method('get') - ->with($this->callback(function ($cacheKey) use ( - $cacheSearchKey, - $cacheGameKey, - ) { - return in_array($cacheKey, [$cacheSearchKey, $cacheGameKey]); - })) + ->with( + $this->callback(function ($cacheKey) use ( + $cacheSearchKey, + $cacheGameKey, + ) { + return in_array($cacheKey, [$cacheSearchKey, $cacheGameKey]); + }), + ) ->willReturnCallback(function ($cacheKey, $callback) use ( $expectedGames, $cacheSearchKey, $cacheGameKey, ) { - return match($cacheKey) { + return match ($cacheKey) { $cacheSearchKey => $expectedGames, $cacheGameKey => $expectedGames[0], - default => throw new \RuntimeException('Unexpected envelope') + default => throw new \RuntimeException('Unexpected envelope'), }; }); @@ -196,8 +208,10 @@ $this->cache ->expects($this->once()) ->method('get') - ->willReturnCallback(function ($cacheKey, $callback) use ($expectedGame) { - return [$expectedGame]; + ->willReturnCallback(function ($cacheKey, $callback) use ( + $expectedGame, + ) { + return $expectedGame; }); // When @@ -215,7 +229,7 @@ ->expects($this->once()) ->method('get') ->willReturnCallback(function ($cacheKey, $callback) { - return []; + return null; }); // When @@ -233,16 +247,23 @@ $this->cache ->expects($this->once()) ->method('get') - ->willReturnCallback(function ($cacheKey, $callback) use ($query, $limit) { + ->willReturnCallback(function ($cacheKey, $callback) use ( + $query, + $limit, + ) { // Verify cache key format expect($cacheKey) ->toStartWith('igdb_api_search_') - ->and($cacheKey)->toContain( - md5(json_encode([ - 'query' => trim(strtolower($query)), - 'limit' => $limit, - 'platform' => IgdbGamePlatformEnum::NINTENDO_SWITCH->value, - ])) + ->and($cacheKey) + ->toContain( + md5( + json_encode([ + 'query' => trim(strtolower($query)), + 'limit' => $limit, + 'platform' => + IgdbGamePlatformEnum::NINTENDO_SWITCH->value, + ]), + ), ); return []; }); @@ -259,17 +280,24 @@ $this->cache ->expects($this->once()) ->method('get') - ->willReturnCallback(function ($cacheKey, $callback) use ($slug, $limit) { + ->willReturnCallback(function ($cacheKey, $callback) use ( + $slug, + $limit, + ) { expect($cacheKey) ->toStartWith('igdb_api_slug_') - ->and($cacheKey)->toContain( - md5(json_encode([ - 'query' => trim(strtolower($slug)), - 'limit' => $limit, - 'platform' => IgdbGamePlatformEnum::NINTENDO_SWITCH->value, - ])) + ->and($cacheKey) + ->toContain( + md5( + json_encode([ + 'query' => trim(strtolower($slug)), + 'limit' => $limit, + 'platform' => + IgdbGamePlatformEnum::NINTENDO_SWITCH->value, + ]), + ), ); - return []; + return null; }); // When @@ -305,24 +333,33 @@ $initialTokenResponse = $this->createMock(ResponseInterface::class); $initialTokenResponse->method('getStatusCode')->willReturn(200); - $initialTokenResponse->method('toArray')->willReturn(['access_token' => $initialToken]); - - $unauthorizedException = new class($unauthorizedResponse) extends \Exception implements ClientExceptionInterface { + $initialTokenResponse + ->method('toArray') + ->willReturn(['access_token' => $initialToken]); + + $unauthorizedException = new class ($unauthorizedResponse) + extends \Exception + implements ClientExceptionInterface + { private ResponseInterface $response; - public function __construct(ResponseInterface $response) { + public function __construct(ResponseInterface $response) + { parent::__construct('Unauthorized'); $this->response = $response; } - public function getResponse(): ResponseInterface { + public function getResponse(): ResponseInterface + { return $this->response; } }; $newTokenResponse = $this->createMock(ResponseInterface::class); $newTokenResponse->method('getStatusCode')->willReturn(200); - $newTokenResponse->method('toArray')->willReturn(['access_token' => $newToken]); + $newTokenResponse + ->method('toArray') + ->willReturn(['access_token' => $newToken]); $successResponse = $this->createMock(ResponseInterface::class); $successResponse->method('getStatusCode')->willReturn(200); @@ -371,7 +408,9 @@ public function getResponse(): ResponseInterface { return $callback(); }); - $transportException = new class('Network error') extends \Exception implements TransportExceptionInterface {}; + $transportException = new class ('Network error') + extends \Exception + implements TransportExceptionInterface {}; $this->httpClient ->expects($this->once()) @@ -379,8 +418,9 @@ public function getResponse(): ResponseInterface { ->willThrowException($transportException); // When & Then - expect(fn() => $this->igdbClient->searchGames('test')) - ->toThrow(IgdbAccessTokenRetrievalException::class); + expect(fn() => $this->igdbClient->searchGames('test'))->toThrow( + IgdbAccessTokenRetrievalException::class, + ); }); function createApiGame( @@ -391,7 +431,9 @@ function createApiGame( name: $faker->words(3, true), slug: $faker->slug(), description: $faker->paragraph(), - imageCover: 'https://images.igdb.com/igdb/image/upload/t_thumb/' . $faker->sha1() . '.jpg', + imageCover: 'https://images.igdb.com/igdb/image/upload/t_thumb/' . + $faker->sha1() . + '.jpg', releaseDate: $faker->dateTime()->format(DATE_ATOM), updatedAt: new \DateTimeImmutable('now'), publisher: $publisherDto, @@ -407,16 +449,17 @@ function createIgdbSearchResponseDto(Generator $faker): IgdbSearchResponseDto involved_companies: [ [ 'company' => ['name' => $faker->company()], - 'publisher' => true - ] + 'publisher' => true, + ], ], cover: [ - 'url' => '//images.igdb.com/igdb/image/upload/t_thumb/' . $faker->sha1() . '.jpg' + 'url' => + '//images.igdb.com/igdb/image/upload/t_thumb/' . + $faker->sha1() . + '.jpg', ], first_release_date: $faker->unixTime(), summary: $faker->paragraph(), - websites: [ - ['url' => $faker->url()] - ] + websites: [['url' => $faker->url()]], ); }