LocateVisitTest.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. <?php
  2. declare(strict_types=1);
  3. namespace ShlinkioTest\Shlink\Core\EventDispatcher;
  4. use Doctrine\ORM\EntityManagerInterface;
  5. use PHPUnit\Framework\TestCase;
  6. use Prophecy\Argument;
  7. use Prophecy\PhpUnit\ProphecyTrait;
  8. use Prophecy\Prophecy\ObjectProphecy;
  9. use Psr\EventDispatcher\EventDispatcherInterface;
  10. use Psr\Log\LoggerInterface;
  11. use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
  12. use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
  13. use Shlinkio\Shlink\Common\Util\IpAddress;
  14. use Shlinkio\Shlink\Core\Entity\ShortUrl;
  15. use Shlinkio\Shlink\Core\Entity\Visit;
  16. use Shlinkio\Shlink\Core\Entity\VisitLocation;
  17. use Shlinkio\Shlink\Core\EventDispatcher\Event\UrlVisited;
  18. use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated;
  19. use Shlinkio\Shlink\Core\EventDispatcher\LocateVisit;
  20. use Shlinkio\Shlink\Core\Model\Visitor;
  21. use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
  22. use Shlinkio\Shlink\IpGeolocation\Model\Location;
  23. use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
  24. class LocateVisitTest extends TestCase
  25. {
  26. use ProphecyTrait;
  27. private LocateVisit $locateVisit;
  28. private ObjectProphecy $ipLocationResolver;
  29. private ObjectProphecy $em;
  30. private ObjectProphecy $logger;
  31. private ObjectProphecy $dbUpdater;
  32. private ObjectProphecy $eventDispatcher;
  33. public function setUp(): void
  34. {
  35. $this->ipLocationResolver = $this->prophesize(IpLocationResolverInterface::class);
  36. $this->em = $this->prophesize(EntityManagerInterface::class);
  37. $this->logger = $this->prophesize(LoggerInterface::class);
  38. $this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class);
  39. $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
  40. $this->locateVisit = new LocateVisit(
  41. $this->ipLocationResolver->reveal(),
  42. $this->em->reveal(),
  43. $this->logger->reveal(),
  44. $this->dbUpdater->reveal(),
  45. $this->eventDispatcher->reveal(),
  46. );
  47. }
  48. /** @test */
  49. public function invalidVisitLogsWarning(): void
  50. {
  51. $event = new UrlVisited('123');
  52. $findVisit = $this->em->find(Visit::class, '123')->willReturn(null);
  53. $logWarning = $this->logger->warning('Tried to locate visit with id "{visitId}", but it does not exist.', [
  54. 'visitId' => 123,
  55. ]);
  56. $dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
  57. });
  58. ($this->locateVisit)($event);
  59. $findVisit->shouldHaveBeenCalledOnce();
  60. $this->em->flush()->shouldNotHaveBeenCalled();
  61. $this->ipLocationResolver->resolveIpLocation(Argument::cetera())->shouldNotHaveBeenCalled();
  62. $logWarning->shouldHaveBeenCalled();
  63. $dispatch->shouldNotHaveBeenCalled();
  64. }
  65. /** @test */
  66. public function invalidAddressLogsWarning(): void
  67. {
  68. $event = new UrlVisited('123');
  69. $findVisit = $this->em->find(Visit::class, '123')->willReturn(
  70. Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', '1.2.3.4', '')),
  71. );
  72. $resolveLocation = $this->ipLocationResolver->resolveIpLocation(Argument::cetera())->willThrow(
  73. WrongIpException::class,
  74. );
  75. $logWarning = $this->logger->warning(
  76. Argument::containingString('Tried to locate visit with id "{visitId}", but its address seems to be wrong.'),
  77. Argument::type('array'),
  78. );
  79. $dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
  80. });
  81. ($this->locateVisit)($event);
  82. $findVisit->shouldHaveBeenCalledOnce();
  83. $resolveLocation->shouldHaveBeenCalledOnce();
  84. $logWarning->shouldHaveBeenCalled();
  85. $this->em->flush()->shouldNotHaveBeenCalled();
  86. $dispatch->shouldHaveBeenCalledOnce();
  87. }
  88. /**
  89. * @test
  90. * @dataProvider provideNonLocatableVisits
  91. */
  92. public function nonLocatableVisitsResolveToEmptyLocations(Visit $visit): void
  93. {
  94. $event = new UrlVisited('123');
  95. $findVisit = $this->em->find(Visit::class, '123')->willReturn($visit);
  96. $flush = $this->em->flush()->will(function (): void {
  97. });
  98. $resolveIp = $this->ipLocationResolver->resolveIpLocation(Argument::any());
  99. $dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
  100. });
  101. ($this->locateVisit)($event);
  102. self::assertEquals($visit->getVisitLocation(), new VisitLocation(Location::emptyInstance()));
  103. $findVisit->shouldHaveBeenCalledOnce();
  104. $flush->shouldHaveBeenCalledOnce();
  105. $resolveIp->shouldNotHaveBeenCalled();
  106. $this->logger->warning(Argument::cetera())->shouldNotHaveBeenCalled();
  107. $dispatch->shouldHaveBeenCalledOnce();
  108. }
  109. public function provideNonLocatableVisits(): iterable
  110. {
  111. $shortUrl = ShortUrl::createEmpty();
  112. yield 'null IP' => [Visit::forValidShortUrl($shortUrl, new Visitor('', '', null, ''))];
  113. yield 'empty IP' => [Visit::forValidShortUrl($shortUrl, new Visitor('', '', '', ''))];
  114. yield 'localhost' => [Visit::forValidShortUrl($shortUrl, new Visitor('', '', IpAddress::LOCALHOST, ''))];
  115. }
  116. /**
  117. * @test
  118. * @dataProvider provideIpAddresses
  119. */
  120. public function locatableVisitsResolveToLocation(Visit $visit, ?string $originalIpAddress): void
  121. {
  122. $ipAddr = $originalIpAddress ?? $visit->getRemoteAddr();
  123. $location = new Location('', '', '', '', 0.0, 0.0, '');
  124. $event = new UrlVisited('123', $originalIpAddress);
  125. $findVisit = $this->em->find(Visit::class, '123')->willReturn($visit);
  126. $flush = $this->em->flush()->will(function (): void {
  127. });
  128. $resolveIp = $this->ipLocationResolver->resolveIpLocation($ipAddr)->willReturn($location);
  129. $dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
  130. });
  131. ($this->locateVisit)($event);
  132. self::assertEquals($visit->getVisitLocation(), new VisitLocation($location));
  133. $findVisit->shouldHaveBeenCalledOnce();
  134. $flush->shouldHaveBeenCalledOnce();
  135. $resolveIp->shouldHaveBeenCalledOnce();
  136. $this->logger->warning(Argument::cetera())->shouldNotHaveBeenCalled();
  137. $dispatch->shouldHaveBeenCalledOnce();
  138. }
  139. public function provideIpAddresses(): iterable
  140. {
  141. yield 'no original IP address' => [
  142. Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', '1.2.3.4', '')),
  143. null,
  144. ];
  145. yield 'original IP address' => [
  146. Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', '1.2.3.4', '')),
  147. '1.2.3.4',
  148. ];
  149. yield 'base url' => [Visit::forBasePath(new Visitor('', '', '1.2.3.4', '')), '1.2.3.4'];
  150. yield 'invalid short url' => [Visit::forInvalidShortUrl(new Visitor('', '', '1.2.3.4', '')), '1.2.3.4'];
  151. yield 'regular not found' => [Visit::forRegularNotFound(new Visitor('', '', '1.2.3.4', '')), '1.2.3.4'];
  152. }
  153. /** @test */
  154. public function errorWhenUpdatingGeoLiteWithExistingCopyLogsWarning(): void
  155. {
  156. $e = GeolocationDbUpdateFailedException::withOlderDb();
  157. $ipAddr = '1.2.3.0';
  158. $visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', $ipAddr, ''));
  159. $location = new Location('', '', '', '', 0.0, 0.0, '');
  160. $event = new UrlVisited('123');
  161. $findVisit = $this->em->find(Visit::class, '123')->willReturn($visit);
  162. $flush = $this->em->flush()->will(function (): void {
  163. });
  164. $resolveIp = $this->ipLocationResolver->resolveIpLocation($ipAddr)->willReturn($location);
  165. $checkUpdateDb = $this->dbUpdater->checkDbUpdate(Argument::cetera())->willThrow($e);
  166. $dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
  167. });
  168. ($this->locateVisit)($event);
  169. self::assertEquals($visit->getVisitLocation(), new VisitLocation($location));
  170. $findVisit->shouldHaveBeenCalledOnce();
  171. $flush->shouldHaveBeenCalledOnce();
  172. $resolveIp->shouldHaveBeenCalledOnce();
  173. $checkUpdateDb->shouldHaveBeenCalledOnce();
  174. $this->logger->warning(
  175. 'GeoLite2 database update failed. Proceeding with old version. {e}',
  176. ['e' => $e],
  177. )->shouldHaveBeenCalledOnce();
  178. $dispatch->shouldHaveBeenCalledOnce();
  179. }
  180. /** @test */
  181. public function errorWhenDownloadingGeoLiteCancelsLocation(): void
  182. {
  183. $e = GeolocationDbUpdateFailedException::withoutOlderDb();
  184. $ipAddr = '1.2.3.0';
  185. $visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', $ipAddr, ''));
  186. $location = new Location('', '', '', '', 0.0, 0.0, '');
  187. $event = new UrlVisited('123');
  188. $findVisit = $this->em->find(Visit::class, '123')->willReturn($visit);
  189. $flush = $this->em->flush()->will(function (): void {
  190. });
  191. $resolveIp = $this->ipLocationResolver->resolveIpLocation($ipAddr)->willReturn($location);
  192. $checkUpdateDb = $this->dbUpdater->checkDbUpdate(Argument::cetera())->willThrow($e);
  193. $logError = $this->logger->error(
  194. 'GeoLite2 database download failed. It is not possible to locate visit with id {visitId}. {e}',
  195. ['e' => $e, 'visitId' => 123],
  196. );
  197. $dispatch = $this->eventDispatcher->dispatch(new VisitLocated('123'))->will(function (): void {
  198. });
  199. ($this->locateVisit)($event);
  200. self::assertNull($visit->getVisitLocation());
  201. $findVisit->shouldHaveBeenCalledOnce();
  202. $flush->shouldNotHaveBeenCalled();
  203. $resolveIp->shouldNotHaveBeenCalled();
  204. $checkUpdateDb->shouldHaveBeenCalledOnce();
  205. $logError->shouldHaveBeenCalledOnce();
  206. $dispatch->shouldHaveBeenCalledOnce();
  207. }
  208. }