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')]; } }