From cb1705b6e801bf3b193970f1ebce8d9c87fcadf7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 11 Dec 2021 22:18:46 +0100 Subject: [PATCH] Created NotifyVisitToRabbitTest --- CHANGELOG.md | 1 - composer.json | 2 +- .../EventDispatcher/NotifyVisitToRabbit.php | 3 +- .../NotifyVisitToRabbitTest.php | 178 ++++++++++++++++++ 4 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 module/Core/test/EventDispatcher/NotifyVisitToRabbitTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d0cbf02b..2f6d10c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this * [#1218](https://github.com/shlinkio/shlink/issues/1218) Updated to symfony/mercure 0.6. * [#1223](https://github.com/shlinkio/shlink/issues/1223) Updated to phpstan 1.0. * Added `domain` field to `DeleteShortUrlException` exception. -* [#1001](https://github.com/shlinkio/shlink/issues/1001) Increased required MSI to 83%. ### Deprecated * [#1260](https://github.com/shlinkio/shlink/issues/1260) Deprecated `USE_HTTPS` env var that was added in previous release, in favor of the new `IS_HTTPS_ENABLED`. diff --git a/composer.json b/composer.json index 7a0a8542..e244fbfc 100644 --- a/composer.json +++ b/composer.json @@ -142,7 +142,7 @@ "test:api": "bin/test/run-api-tests.sh", "test:api:ci": "GENERATE_COVERAGE=yes composer test:api", "infect:ci:base": "infection --threads=4 --log-verbosity=default --only-covered --only-covering-test-cases --skip-initial-tests", - "infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=83", + "infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=80", "infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json", "infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json", "infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api", diff --git a/module/Core/src/EventDispatcher/NotifyVisitToRabbit.php b/module/Core/src/EventDispatcher/NotifyVisitToRabbit.php index 426b02bb..6ff79eb8 100644 --- a/module/Core/src/EventDispatcher/NotifyVisitToRabbit.php +++ b/module/Core/src/EventDispatcher/NotifyVisitToRabbit.php @@ -38,9 +38,8 @@ class NotifyVisitToRabbit } $visitId = $shortUrlLocated->visitId(); - - /** @var Visit|null $visit */ $visit = $this->em->find(Visit::class, $visitId); + if ($visit === null) { $this->logger->warning('Tried to notify RabbitMQ for visit with id "{visitId}", but it does not exist.', [ 'visitId' => $visitId, diff --git a/module/Core/test/EventDispatcher/NotifyVisitToRabbitTest.php b/module/Core/test/EventDispatcher/NotifyVisitToRabbitTest.php new file mode 100644 index 00000000..73a970fb --- /dev/null +++ b/module/Core/test/EventDispatcher/NotifyVisitToRabbitTest.php @@ -0,0 +1,178 @@ +channel = $this->prophesize(AMQPChannel::class); + + $this->connection = $this->prophesize(AMQPStreamConnection::class); + $this->connection->isConnected()->willReturn(false); + $this->connection->channel()->willReturn($this->channel->reveal()); + + $this->em = $this->prophesize(EntityManagerInterface::class); + $this->logger = $this->prophesize(LoggerInterface::class); + + $this->listener = new NotifyVisitToRabbit( + $this->connection->reveal(), + $this->em->reveal(), + $this->logger->reveal(), + new OrphanVisitDataTransformer(), + true, + ); + } + + /** @test */ + public function doesNothingWhenTheFeatureIsNotEnabled(): void + { + $listener = new NotifyVisitToRabbit( + $this->connection->reveal(), + $this->em->reveal(), + $this->logger->reveal(), + new OrphanVisitDataTransformer(), + false, + ); + + $listener(new VisitLocated('123')); + + $this->em->find(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->logger->warning(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->logger->debug(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->connection->isConnected()->shouldNotHaveBeenCalled(); + $this->connection->close()->shouldNotHaveBeenCalled(); + } + + /** @test */ + public function notificationsAreNotSentWhenVisitCannotBeFound(): void + { + $visitId = '123'; + $findVisit = $this->em->find(Visit::class, $visitId)->willReturn(null); + $logWarning = $this->logger->warning( + 'Tried to notify RabbitMQ for visit with id "{visitId}", but it does not exist.', + ['visitId' => $visitId], + ); + + ($this->listener)(new VisitLocated($visitId)); + + $findVisit->shouldHaveBeenCalledOnce(); + $logWarning->shouldHaveBeenCalledOnce(); + $this->logger->debug(Argument::cetera())->shouldNotHaveBeenCalled(); + $this->connection->isConnected()->shouldNotHaveBeenCalled(); + $this->connection->close()->shouldNotHaveBeenCalled(); + } + + /** + * @test + * @dataProvider provideVisits + */ + public function expectedChannelsAreNotifiedBasedOnTheVisitType(Visit $visit, array $expectedChannels): void + { + $visitId = '123'; + $findVisit = $this->em->find(Visit::class, $visitId)->willReturn($visit); + $argumentWithExpectedChannel = Argument::that(fn (string $channel) => contains($expectedChannels, $channel)); + + ($this->listener)(new VisitLocated($visitId)); + + $findVisit->shouldHaveBeenCalledOnce(); + $this->channel->exchange_declare($argumentWithExpectedChannel, Argument::cetera())->shouldHaveBeenCalledTimes( + count($expectedChannels), + ); + $this->channel->queue_declare($argumentWithExpectedChannel, Argument::cetera())->shouldHaveBeenCalledTimes( + count($expectedChannels), + ); + $this->channel->queue_bind( + $argumentWithExpectedChannel, + $argumentWithExpectedChannel, + )->shouldHaveBeenCalledTimes(count($expectedChannels)); + $this->channel->basic_publish(Argument::any(), $argumentWithExpectedChannel)->shouldHaveBeenCalledTimes( + count($expectedChannels), + ); + $this->channel->close()->shouldHaveBeenCalledOnce(); + $this->connection->reconnect()->shouldHaveBeenCalledOnce(); + $this->connection->close()->shouldHaveBeenCalledOnce(); + $this->logger->debug(Argument::cetera())->shouldNotHaveBeenCalled(); + } + + public function provideVisits(): iterable + { + $visitor = Visitor::emptyInstance(); + + yield 'orphan visit' => [Visit::forBasePath($visitor), ['https://shlink.io/new-orphan-visit']]; + yield 'non-orphan visit' => [ + Visit::forValidShortUrl( + ShortUrl::fromMeta(ShortUrlMeta::fromRawData([ + 'longUrl' => 'foo', + 'customSlug' => 'bar', + ])), + $visitor, + ), + ['https://shlink.io/new-visit', 'https://shlink.io/new-visit/bar'], + ]; + } + + /** + * @test + * @dataProvider provideExceptions + */ + public function printsDebugMessageInCaseOfError(Throwable $e): void + { + $visitId = '123'; + $findVisit = $this->em->find(Visit::class, $visitId)->willReturn(Visit::forBasePath(Visitor::emptyInstance())); + $channel = $this->connection->channel()->willThrow($e); + + ($this->listener)(new VisitLocated($visitId)); + + $this->logger->debug( + 'Error while trying to notify RabbitMQ with new visit. {e}', + ['e' => $e], + )->shouldHaveBeenCalledOnce(); + $this->connection->close()->shouldHaveBeenCalledOnce(); + $this->connection->reconnect()->shouldHaveBeenCalledOnce(); + $findVisit->shouldHaveBeenCalledOnce(); + $channel->shouldHaveBeenCalledOnce(); + $this->channel->close()->shouldNotHaveBeenCalled(); + } + + public function provideExceptions(): iterable + { + yield [new RuntimeException('RuntimeException Error')]; + yield [new Exception('Exception Error')]; + yield [new DomainException('DomainException Error')]; + } +}