LocateVisitsCommandTest.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. <?php
  2. declare(strict_types=1);
  3. namespace ShlinkioTest\Shlink\CLI\Command\Visit;
  4. use PHPUnit\Framework\TestCase;
  5. use Prophecy\Argument;
  6. use Prophecy\Prophecy\ObjectProphecy;
  7. use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand;
  8. use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
  9. use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
  10. use Shlinkio\Shlink\Common\Util\IpAddress;
  11. use Shlinkio\Shlink\Core\Entity\ShortUrl;
  12. use Shlinkio\Shlink\Core\Entity\Visit;
  13. use Shlinkio\Shlink\Core\Entity\VisitLocation;
  14. use Shlinkio\Shlink\Core\Model\Visitor;
  15. use Shlinkio\Shlink\Core\Service\VisitService;
  16. use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
  17. use Shlinkio\Shlink\IpGeolocation\Model\Location;
  18. use Shlinkio\Shlink\IpGeolocation\Resolver\IpApiLocationResolver;
  19. use Symfony\Component\Console\Application;
  20. use Symfony\Component\Console\Output\OutputInterface;
  21. use Symfony\Component\Console\Tester\CommandTester;
  22. use Symfony\Component\Lock;
  23. use function array_shift;
  24. use function sprintf;
  25. class LocateVisitsCommandTest extends TestCase
  26. {
  27. /** @var CommandTester */
  28. private $commandTester;
  29. /** @var ObjectProphecy */
  30. private $visitService;
  31. /** @var ObjectProphecy */
  32. private $ipResolver;
  33. /** @var ObjectProphecy */
  34. private $locker;
  35. /** @var ObjectProphecy */
  36. private $lock;
  37. /** @var ObjectProphecy */
  38. private $dbUpdater;
  39. public function setUp(): void
  40. {
  41. $this->visitService = $this->prophesize(VisitService::class);
  42. $this->ipResolver = $this->prophesize(IpApiLocationResolver::class);
  43. $this->dbUpdater = $this->prophesize(GeolocationDbUpdaterInterface::class);
  44. $this->locker = $this->prophesize(Lock\Factory::class);
  45. $this->lock = $this->prophesize(Lock\LockInterface::class);
  46. $this->lock->acquire(false)->willReturn(true);
  47. $this->lock->release()->will(function () {
  48. });
  49. $this->locker->createLock(Argument::type('string'), 90.0, false)->willReturn($this->lock->reveal());
  50. $command = new LocateVisitsCommand(
  51. $this->visitService->reveal(),
  52. $this->ipResolver->reveal(),
  53. $this->locker->reveal(),
  54. $this->dbUpdater->reveal()
  55. );
  56. $app = new Application();
  57. $app->add($command);
  58. $this->commandTester = new CommandTester($command);
  59. }
  60. /** @test */
  61. public function allPendingVisitsAreProcessed(): void
  62. {
  63. $visit = new Visit(new ShortUrl(''), new Visitor('', '', '1.2.3.4'));
  64. $location = new VisitLocation(Location::emptyInstance());
  65. $locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(
  66. function (array $args) use ($visit, $location) {
  67. $firstCallback = array_shift($args);
  68. $firstCallback($visit);
  69. $secondCallback = array_shift($args);
  70. $secondCallback($location, $visit);
  71. }
  72. );
  73. $resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn(
  74. Location::emptyInstance()
  75. );
  76. $this->commandTester->execute([]);
  77. $output = $this->commandTester->getDisplay();
  78. $this->assertStringContainsString('Processing IP 1.2.3.0', $output);
  79. $locateVisits->shouldHaveBeenCalledOnce();
  80. $resolveIpLocation->shouldHaveBeenCalledOnce();
  81. }
  82. /**
  83. * @test
  84. * @dataProvider provideIgnoredAddresses
  85. */
  86. public function localhostAndEmptyAddressesAreIgnored(?string $address, string $message): void
  87. {
  88. $visit = new Visit(new ShortUrl(''), new Visitor('', '', $address));
  89. $location = new VisitLocation(Location::emptyInstance());
  90. $locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(
  91. function (array $args) use ($visit, $location) {
  92. $firstCallback = array_shift($args);
  93. $firstCallback($visit);
  94. $secondCallback = array_shift($args);
  95. $secondCallback($location, $visit);
  96. }
  97. );
  98. $resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn(
  99. Location::emptyInstance()
  100. );
  101. $this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
  102. $output = $this->commandTester->getDisplay();
  103. $this->assertStringContainsString($message, $output);
  104. if (empty($address)) {
  105. $this->assertStringNotContainsString('Processing IP', $output);
  106. } else {
  107. $this->assertStringContainsString('Processing IP', $output);
  108. }
  109. $locateVisits->shouldHaveBeenCalledOnce();
  110. $resolveIpLocation->shouldNotHaveBeenCalled();
  111. }
  112. public function provideIgnoredAddresses(): iterable
  113. {
  114. yield 'with empty address' => ['', 'Ignored visit with no IP address'];
  115. yield 'with null address' => [null, 'Ignored visit with no IP address'];
  116. yield 'with localhost address' => [IpAddress::LOCALHOST, 'Ignored localhost address'];
  117. }
  118. /** @test */
  119. public function errorWhileLocatingIpIsDisplayed(): void
  120. {
  121. $visit = new Visit(new ShortUrl(''), new Visitor('', '', '1.2.3.4'));
  122. $location = new VisitLocation(Location::emptyInstance());
  123. $locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(
  124. function (array $args) use ($visit, $location) {
  125. $firstCallback = array_shift($args);
  126. $firstCallback($visit);
  127. $secondCallback = array_shift($args);
  128. $secondCallback($location, $visit);
  129. }
  130. );
  131. $resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willThrow(WrongIpException::class);
  132. $this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
  133. $output = $this->commandTester->getDisplay();
  134. $this->assertStringContainsString('An error occurred while locating IP. Skipped', $output);
  135. $locateVisits->shouldHaveBeenCalledOnce();
  136. $resolveIpLocation->shouldHaveBeenCalledOnce();
  137. }
  138. /** @test */
  139. public function noActionIsPerformedIfLockIsAcquired(): void
  140. {
  141. $this->lock->acquire(false)->willReturn(false);
  142. $locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function () {
  143. });
  144. $resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]);
  145. $this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
  146. $output = $this->commandTester->getDisplay();
  147. $this->assertStringContainsString(
  148. sprintf('Command "%s" is already in progress. Skipping.', LocateVisitsCommand::NAME),
  149. $output
  150. );
  151. $locateVisits->shouldNotHaveBeenCalled();
  152. $resolveIpLocation->shouldNotHaveBeenCalled();
  153. }
  154. /**
  155. * @test
  156. * @dataProvider provideParams
  157. */
  158. public function showsProperMessageWhenGeoLiteUpdateFails(bool $olderDbExists, string $expectedMessage): void
  159. {
  160. $locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function () {
  161. });
  162. $checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will(
  163. function (array $args) use ($olderDbExists) {
  164. [$mustBeUpdated, $handleProgress] = $args;
  165. $mustBeUpdated($olderDbExists);
  166. $handleProgress(100, 50);
  167. throw GeolocationDbUpdateFailedException::create($olderDbExists);
  168. }
  169. );
  170. $this->commandTester->execute([]);
  171. $output = $this->commandTester->getDisplay();
  172. $this->assertStringContainsString(
  173. sprintf('%s GeoLite2 database...', $olderDbExists ? 'Updating' : 'Downloading'),
  174. $output
  175. );
  176. $this->assertStringContainsString($expectedMessage, $output);
  177. $locateVisits->shouldHaveBeenCalledTimes((int) $olderDbExists);
  178. $checkDbUpdate->shouldHaveBeenCalledOnce();
  179. }
  180. public function provideParams(): iterable
  181. {
  182. yield [true, '[Warning] GeoLite2 database update failed. Proceeding with old version.'];
  183. yield [false, 'GeoLite2 database download failed. It is not possible to locate visits.'];
  184. }
  185. }