From 1b4e62b823228ce473b4af87b3a4f1f2f9e385cd Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 8 Feb 2021 19:46:51 +0100 Subject: [PATCH] Separated methods to track visits and list visits --- module/CLI/config/dependencies.config.php | 2 +- .../src/Command/ShortUrl/GetVisitsCommand.php | 13 ++- .../Command/ShortUrl/GetVisitsCommandTest.php | 20 ++-- module/Core/src/Service/VisitsTracker.php | 56 ----------- .../src/Service/VisitsTrackerInterface.php | 19 ---- module/Core/src/Visit/VisitsStatsHelper.php | 59 +++++++++++ .../src/Visit/VisitsStatsHelperInterface.php | 22 +++++ .../Core/test/Service/VisitsTrackerTest.php | 97 ------------------- .../Core/test/Visit/VisitsStatsHelperTest.php | 96 ++++++++++++++++++ module/Rest/config/dependencies.config.php | 4 +- .../src/Action/Visit/ShortUrlVisitsAction.php | 10 +- .../Rest/src/Action/Visit/TagVisitsAction.php | 10 +- .../Action/Visit/ShortUrlVisitsActionTest.php | 12 +-- .../test/Action/Visit/TagVisitsActionTest.php | 10 +- 14 files changed, 220 insertions(+), 210 deletions(-) diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 685dc9fd..7e224c33 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -74,7 +74,7 @@ return [ Service\ShortUrlService::class, ShortUrlDataTransformer::class, ], - Command\ShortUrl\GetVisitsCommand::class => [Service\VisitsTracker::class], + Command\ShortUrl\GetVisitsCommand::class => [Visit\VisitsStatsHelper::class], Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class], Command\Visit\LocateVisitsCommand::class => [ diff --git a/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php b/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php index 0b7de663..7b020356 100644 --- a/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php +++ b/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php @@ -11,8 +11,8 @@ use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\VisitsParams; -use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Shlinkio\Shlink\Core\Visit\Model\UnknownVisitLocation; +use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -27,11 +27,11 @@ class GetVisitsCommand extends AbstractWithDateRangeCommand { public const NAME = 'short-url:visits'; - private VisitsTrackerInterface $visitsTracker; + private VisitsStatsHelperInterface $visitsHelper; - public function __construct(VisitsTrackerInterface $visitsTracker) + public function __construct(VisitsStatsHelperInterface $visitsHelper) { - $this->visitsTracker = $visitsTracker; + $this->visitsHelper = $visitsHelper; parent::__construct(); } @@ -74,7 +74,10 @@ class GetVisitsCommand extends AbstractWithDateRangeCommand $startDate = $this->getStartDateOption($input, $output); $endDate = $this->getEndDateOption($input, $output); - $paginator = $this->visitsTracker->info($identifier, new VisitsParams(new DateRange($startDate, $endDate))); + $paginator = $this->visitsHelper->visitsForShortUrl( + $identifier, + new VisitsParams(new DateRange($startDate, $endDate)), + ); $rows = map($paginator->getCurrentPageResults(), function (Visit $visit) { $rowData = $visit->jsonSerialize(); diff --git a/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php b/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php index 4d5e5832..d25d5763 100644 --- a/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php @@ -19,7 +19,7 @@ use Shlinkio\Shlink\Core\Entity\VisitLocation; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Model\VisitsParams; -use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; +use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Shlinkio\Shlink\IpGeolocation\Model\Location; use Symfony\Component\Console\Application; use Symfony\Component\Console\Tester\CommandTester; @@ -31,12 +31,12 @@ class GetVisitsCommandTest extends TestCase use ProphecyTrait; private CommandTester $commandTester; - private ObjectProphecy $visitsTracker; + private ObjectProphecy $visitsHelper; public function setUp(): void { - $this->visitsTracker = $this->prophesize(VisitsTrackerInterface::class); - $command = new GetVisitsCommand($this->visitsTracker->reveal()); + $this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class); + $command = new GetVisitsCommand($this->visitsHelper->reveal()); $app = new Application(); $app->add($command); $this->commandTester = new CommandTester($command); @@ -46,7 +46,7 @@ class GetVisitsCommandTest extends TestCase public function noDateFlagsTriesToListWithoutDateRange(): void { $shortCode = 'abc123'; - $this->visitsTracker->info( + $this->visitsHelper->visitsForShortUrl( new ShortUrlIdentifier($shortCode), new VisitsParams(new DateRange(null, null)), ) @@ -62,7 +62,7 @@ class GetVisitsCommandTest extends TestCase $shortCode = 'abc123'; $startDate = '2016-01-01'; $endDate = '2016-02-01'; - $this->visitsTracker->info( + $this->visitsHelper->visitsForShortUrl( new ShortUrlIdentifier($shortCode), new VisitsParams(new DateRange(Chronos::parse($startDate), Chronos::parse($endDate))), ) @@ -81,8 +81,10 @@ class GetVisitsCommandTest extends TestCase { $shortCode = 'abc123'; $startDate = 'foo'; - $info = $this->visitsTracker->info(new ShortUrlIdentifier($shortCode), new VisitsParams(new DateRange())) - ->willReturn(new Paginator(new ArrayAdapter([]))); + $info = $this->visitsHelper->visitsForShortUrl( + new ShortUrlIdentifier($shortCode), + new VisitsParams(new DateRange()), + )->willReturn(new Paginator(new ArrayAdapter([]))); $this->commandTester->execute([ 'shortCode' => $shortCode, @@ -101,7 +103,7 @@ class GetVisitsCommandTest extends TestCase public function outputIsProperlyGenerated(): void { $shortCode = 'abc123'; - $this->visitsTracker->info(new ShortUrlIdentifier($shortCode), Argument::any())->willReturn( + $this->visitsHelper->visitsForShortUrl(new ShortUrlIdentifier($shortCode), Argument::any())->willReturn( new Paginator(new ArrayAdapter([ Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('bar', 'foo', '', ''))->locate( new VisitLocation(new Location('', 'Spain', '', '', 0, 0, '')), diff --git a/module/Core/src/Service/VisitsTracker.php b/module/Core/src/Service/VisitsTracker.php index bda33a01..daf4aa24 100644 --- a/module/Core/src/Service/VisitsTracker.php +++ b/module/Core/src/Service/VisitsTracker.php @@ -6,22 +6,10 @@ namespace Shlinkio\Shlink\Core\Service; use Doctrine\ORM; use Psr\EventDispatcher\EventDispatcherInterface; -use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Entity\Tag; use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlVisited; -use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; -use Shlinkio\Shlink\Core\Exception\TagNotFoundException; -use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\Visitor; -use Shlinkio\Shlink\Core\Model\VisitsParams; -use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsForTagPaginatorAdapter; -use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsPaginatorAdapter; -use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface; -use Shlinkio\Shlink\Core\Repository\TagRepository; -use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface; -use Shlinkio\Shlink\Rest\Entity\ApiKey; class VisitsTracker implements VisitsTrackerInterface { @@ -48,48 +36,4 @@ class VisitsTracker implements VisitsTrackerInterface $this->eventDispatcher->dispatch(new ShortUrlVisited($visit->getId(), $visitor->getRemoteAddress())); } - - /** - * @return Visit[]|Paginator - * @throws ShortUrlNotFoundException - */ - public function info(ShortUrlIdentifier $identifier, VisitsParams $params, ?ApiKey $apiKey = null): Paginator - { - $spec = $apiKey !== null ? $apiKey->spec() : null; - - /** @var ShortUrlRepositoryInterface $repo */ - $repo = $this->em->getRepository(ShortUrl::class); - if (! $repo->shortCodeIsInUse($identifier->shortCode(), $identifier->domain(), $spec)) { - throw ShortUrlNotFoundException::fromNotFound($identifier); - } - - /** @var VisitRepositoryInterface $repo */ - $repo = $this->em->getRepository(Visit::class); - $paginator = new Paginator(new VisitsPaginatorAdapter($repo, $identifier, $params, $spec)); - $paginator->setMaxPerPage($params->getItemsPerPage()) - ->setCurrentPage($params->getPage()); - - return $paginator; - } - - /** - * @return Visit[]|Paginator - * @throws TagNotFoundException - */ - public function visitsForTag(string $tag, VisitsParams $params, ?ApiKey $apiKey = null): Paginator - { - /** @var TagRepository $tagRepo */ - $tagRepo = $this->em->getRepository(Tag::class); - if (! $tagRepo->tagExists($tag, $apiKey)) { - throw TagNotFoundException::fromTag($tag); - } - - /** @var VisitRepositoryInterface $repo */ - $repo = $this->em->getRepository(Visit::class); - $paginator = new Paginator(new VisitsForTagPaginatorAdapter($repo, $tag, $params, $apiKey)); - $paginator->setMaxPerPage($params->getItemsPerPage()) - ->setCurrentPage($params->getPage()); - - return $paginator; - } } diff --git a/module/Core/src/Service/VisitsTrackerInterface.php b/module/Core/src/Service/VisitsTrackerInterface.php index 0814d986..c468cf0e 100644 --- a/module/Core/src/Service/VisitsTrackerInterface.php +++ b/module/Core/src/Service/VisitsTrackerInterface.php @@ -4,29 +4,10 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Service; -use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Entity\Visit; -use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; -use Shlinkio\Shlink\Core\Exception\TagNotFoundException; -use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\Visitor; -use Shlinkio\Shlink\Core\Model\VisitsParams; -use Shlinkio\Shlink\Rest\Entity\ApiKey; interface VisitsTrackerInterface { public function track(ShortUrl $shortUrl, Visitor $visitor): void; - - /** - * @return Visit[]|Paginator - * @throws ShortUrlNotFoundException - */ - public function info(ShortUrlIdentifier $identifier, VisitsParams $params, ?ApiKey $apiKey = null): Paginator; - - /** - * @return Visit[]|Paginator - * @throws TagNotFoundException - */ - public function visitsForTag(string $tag, VisitsParams $params, ?ApiKey $apiKey = null): Paginator; } diff --git a/module/Core/src/Visit/VisitsStatsHelper.php b/module/Core/src/Visit/VisitsStatsHelper.php index ab06079a..7c4efd23 100644 --- a/module/Core/src/Visit/VisitsStatsHelper.php +++ b/module/Core/src/Visit/VisitsStatsHelper.php @@ -5,8 +5,20 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Visit; use Doctrine\ORM\EntityManagerInterface; +use Shlinkio\Shlink\Common\Paginator\Paginator; +use Shlinkio\Shlink\Core\Entity\ShortUrl; +use Shlinkio\Shlink\Core\Entity\Tag; use Shlinkio\Shlink\Core\Entity\Visit; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; +use Shlinkio\Shlink\Core\Exception\TagNotFoundException; +use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; +use Shlinkio\Shlink\Core\Model\VisitsParams; +use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsForTagPaginatorAdapter; +use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsPaginatorAdapter; +use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface; +use Shlinkio\Shlink\Core\Repository\TagRepository; use Shlinkio\Shlink\Core\Repository\VisitRepository; +use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface; use Shlinkio\Shlink\Core\Visit\Model\VisitsStats; use Shlinkio\Shlink\Rest\Entity\ApiKey; @@ -30,4 +42,51 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface $visitsRepo = $this->em->getRepository(Visit::class); return $visitsRepo->countVisits($apiKey); } + + /** + * @return Visit[]|Paginator + * @throws ShortUrlNotFoundException + */ + public function visitsForShortUrl( + ShortUrlIdentifier $identifier, + VisitsParams $params, + ?ApiKey $apiKey = null + ): Paginator { + $spec = $apiKey !== null ? $apiKey->spec() : null; + + /** @var ShortUrlRepositoryInterface $repo */ + $repo = $this->em->getRepository(ShortUrl::class); + if (! $repo->shortCodeIsInUse($identifier->shortCode(), $identifier->domain(), $spec)) { + throw ShortUrlNotFoundException::fromNotFound($identifier); + } + + /** @var VisitRepositoryInterface $repo */ + $repo = $this->em->getRepository(Visit::class); + $paginator = new Paginator(new VisitsPaginatorAdapter($repo, $identifier, $params, $spec)); + $paginator->setMaxPerPage($params->getItemsPerPage()) + ->setCurrentPage($params->getPage()); + + return $paginator; + } + + /** + * @return Visit[]|Paginator + * @throws TagNotFoundException + */ + public function visitsForTag(string $tag, VisitsParams $params, ?ApiKey $apiKey = null): Paginator + { + /** @var TagRepository $tagRepo */ + $tagRepo = $this->em->getRepository(Tag::class); + if (! $tagRepo->tagExists($tag, $apiKey)) { + throw TagNotFoundException::fromTag($tag); + } + + /** @var VisitRepositoryInterface $repo */ + $repo = $this->em->getRepository(Visit::class); + $paginator = new Paginator(new VisitsForTagPaginatorAdapter($repo, $tag, $params, $apiKey)); + $paginator->setMaxPerPage($params->getItemsPerPage()) + ->setCurrentPage($params->getPage()); + + return $paginator; + } } diff --git a/module/Core/src/Visit/VisitsStatsHelperInterface.php b/module/Core/src/Visit/VisitsStatsHelperInterface.php index ca044d4b..a67c8dcd 100644 --- a/module/Core/src/Visit/VisitsStatsHelperInterface.php +++ b/module/Core/src/Visit/VisitsStatsHelperInterface.php @@ -4,10 +4,32 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Visit; +use Shlinkio\Shlink\Common\Paginator\Paginator; +use Shlinkio\Shlink\Core\Entity\Visit; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; +use Shlinkio\Shlink\Core\Exception\TagNotFoundException; +use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; +use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Visit\Model\VisitsStats; use Shlinkio\Shlink\Rest\Entity\ApiKey; interface VisitsStatsHelperInterface { public function getVisitsStats(?ApiKey $apiKey = null): VisitsStats; + + /** + * @return Visit[]|Paginator + * @throws ShortUrlNotFoundException + */ + public function visitsForShortUrl( + ShortUrlIdentifier $identifier, + VisitsParams $params, + ?ApiKey $apiKey = null + ): Paginator; + + /** + * @return Visit[]|Paginator + * @throws TagNotFoundException + */ + public function visitsForTag(string $tag, VisitsParams $params, ?ApiKey $apiKey = null): Paginator; } diff --git a/module/Core/test/Service/VisitsTrackerTest.php b/module/Core/test/Service/VisitsTrackerTest.php index 821db275..4807dc41 100644 --- a/module/Core/test/Service/VisitsTrackerTest.php +++ b/module/Core/test/Service/VisitsTrackerTest.php @@ -5,35 +5,19 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Core\Service; use Doctrine\ORM\EntityManager; -use Laminas\Stdlib\ArrayUtils; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\EventDispatcher\EventDispatcherInterface; -use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Entity\ShortUrl; -use Shlinkio\Shlink\Core\Entity\Tag; use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlVisited; -use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; -use Shlinkio\Shlink\Core\Exception\TagNotFoundException; -use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\Visitor; -use Shlinkio\Shlink\Core\Model\VisitsParams; -use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface; -use Shlinkio\Shlink\Core\Repository\TagRepository; -use Shlinkio\Shlink\Core\Repository\VisitRepository; use Shlinkio\Shlink\Core\Service\VisitsTracker; -use Shlinkio\Shlink\Rest\Entity\ApiKey; -use ShlinkioTest\Shlink\Core\Util\ApiKeyHelpersTrait; - -use function Functional\map; -use function range; class VisitsTrackerTest extends TestCase { - use ApiKeyHelpersTrait; use ProphecyTrait; private VisitsTracker $visitsTracker; @@ -60,85 +44,4 @@ class VisitsTrackerTest extends TestCase $this->eventDispatcher->dispatch(Argument::type(ShortUrlVisited::class))->shouldHaveBeenCalled(); } - - /** - * @test - * @dataProvider provideAdminApiKeys - */ - public function infoReturnsVisitsForCertainShortCode(?ApiKey $apiKey): void - { - $shortCode = '123ABC'; - $spec = $apiKey === null ? null : $apiKey->spec(); - $repo = $this->prophesize(ShortUrlRepositoryInterface::class); - $count = $repo->shortCodeIsInUse($shortCode, null, $spec)->willReturn(true); - $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce(); - - $list = map(range(0, 1), fn () => Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance())); - $repo2 = $this->prophesize(VisitRepository::class); - $repo2->findVisitsByShortCode($shortCode, null, Argument::type(DateRange::class), 1, 0, $spec)->willReturn( - $list, - ); - $repo2->countVisitsByShortCode($shortCode, null, Argument::type(DateRange::class), $spec)->willReturn(1); - $this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledOnce(); - - $paginator = $this->visitsTracker->info(new ShortUrlIdentifier($shortCode), new VisitsParams(), $apiKey); - - self::assertEquals($list, ArrayUtils::iteratorToArray($paginator->getCurrentPageResults())); - $count->shouldHaveBeenCalledOnce(); - } - - /** @test */ - public function throwsExceptionWhenRequestingVisitsForInvalidShortCode(): void - { - $shortCode = '123ABC'; - $repo = $this->prophesize(ShortUrlRepositoryInterface::class); - $count = $repo->shortCodeIsInUse($shortCode, null, null)->willReturn(false); - $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce(); - - $this->expectException(ShortUrlNotFoundException::class); - $count->shouldBeCalledOnce(); - - $this->visitsTracker->info(new ShortUrlIdentifier($shortCode), new VisitsParams()); - } - - /** @test */ - public function throwsExceptionWhenRequestingVisitsForInvalidTag(): void - { - $tag = 'foo'; - $apiKey = new ApiKey(); - $repo = $this->prophesize(TagRepository::class); - $tagExists = $repo->tagExists($tag, $apiKey)->willReturn(false); - $getRepo = $this->em->getRepository(Tag::class)->willReturn($repo->reveal()); - - $this->expectException(TagNotFoundException::class); - $tagExists->shouldBeCalledOnce(); - $getRepo->shouldBeCalledOnce(); - - $this->visitsTracker->visitsForTag($tag, new VisitsParams(), $apiKey); - } - - /** - * @test - * @dataProvider provideAdminApiKeys - */ - public function visitsForTagAreReturnedAsExpected(?ApiKey $apiKey): void - { - $tag = 'foo'; - $repo = $this->prophesize(TagRepository::class); - $tagExists = $repo->tagExists($tag, $apiKey)->willReturn(true); - $getRepo = $this->em->getRepository(Tag::class)->willReturn($repo->reveal()); - - $spec = $apiKey === null ? null : $apiKey->spec(); - $list = map(range(0, 1), fn () => Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance())); - $repo2 = $this->prophesize(VisitRepository::class); - $repo2->findVisitsByTag($tag, Argument::type(DateRange::class), 1, 0, $spec)->willReturn($list); - $repo2->countVisitsByTag($tag, Argument::type(DateRange::class), $spec)->willReturn(1); - $this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledOnce(); - - $paginator = $this->visitsTracker->visitsForTag($tag, new VisitsParams(), $apiKey); - - self::assertEquals($list, ArrayUtils::iteratorToArray($paginator->getCurrentPageResults())); - $tagExists->shouldHaveBeenCalledOnce(); - $getRepo->shouldHaveBeenCalledOnce(); - } } diff --git a/module/Core/test/Visit/VisitsStatsHelperTest.php b/module/Core/test/Visit/VisitsStatsHelperTest.php index cdc76bd4..20a39316 100644 --- a/module/Core/test/Visit/VisitsStatsHelperTest.php +++ b/module/Core/test/Visit/VisitsStatsHelperTest.php @@ -5,19 +5,34 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Core\Visit; use Doctrine\ORM\EntityManagerInterface; +use Laminas\Stdlib\ArrayUtils; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; +use Shlinkio\Shlink\Common\Util\DateRange; +use Shlinkio\Shlink\Core\Entity\ShortUrl; +use Shlinkio\Shlink\Core\Entity\Tag; use Shlinkio\Shlink\Core\Entity\Visit; +use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; +use Shlinkio\Shlink\Core\Exception\TagNotFoundException; +use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; +use Shlinkio\Shlink\Core\Model\Visitor; +use Shlinkio\Shlink\Core\Model\VisitsParams; +use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface; +use Shlinkio\Shlink\Core\Repository\TagRepository; use Shlinkio\Shlink\Core\Repository\VisitRepository; use Shlinkio\Shlink\Core\Visit\Model\VisitsStats; use Shlinkio\Shlink\Core\Visit\VisitsStatsHelper; +use Shlinkio\Shlink\Rest\Entity\ApiKey; +use ShlinkioTest\Shlink\Core\Util\ApiKeyHelpersTrait; use function Functional\map; use function range; class VisitsStatsHelperTest extends TestCase { + use ApiKeyHelpersTrait; use ProphecyTrait; private VisitsStatsHelper $helper; @@ -50,4 +65,85 @@ class VisitsStatsHelperTest extends TestCase { return map(range(0, 50, 5), fn (int $value) => [$value]); } + + /** + * @test + * @dataProvider provideAdminApiKeys + */ + public function infoReturnsVisitsForCertainShortCode(?ApiKey $apiKey): void + { + $shortCode = '123ABC'; + $spec = $apiKey === null ? null : $apiKey->spec(); + $repo = $this->prophesize(ShortUrlRepositoryInterface::class); + $count = $repo->shortCodeIsInUse($shortCode, null, $spec)->willReturn(true); + $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce(); + + $list = map(range(0, 1), fn () => Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance())); + $repo2 = $this->prophesize(VisitRepository::class); + $repo2->findVisitsByShortCode($shortCode, null, Argument::type(DateRange::class), 1, 0, $spec)->willReturn( + $list, + ); + $repo2->countVisitsByShortCode($shortCode, null, Argument::type(DateRange::class), $spec)->willReturn(1); + $this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledOnce(); + + $paginator = $this->helper->visitsForShortUrl(new ShortUrlIdentifier($shortCode), new VisitsParams(), $apiKey); + + self::assertEquals($list, ArrayUtils::iteratorToArray($paginator->getCurrentPageResults())); + $count->shouldHaveBeenCalledOnce(); + } + + /** @test */ + public function throwsExceptionWhenRequestingVisitsForInvalidShortCode(): void + { + $shortCode = '123ABC'; + $repo = $this->prophesize(ShortUrlRepositoryInterface::class); + $count = $repo->shortCodeIsInUse($shortCode, null, null)->willReturn(false); + $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce(); + + $this->expectException(ShortUrlNotFoundException::class); + $count->shouldBeCalledOnce(); + + $this->helper->visitsForShortUrl(new ShortUrlIdentifier($shortCode), new VisitsParams()); + } + + /** @test */ + public function throwsExceptionWhenRequestingVisitsForInvalidTag(): void + { + $tag = 'foo'; + $apiKey = new ApiKey(); + $repo = $this->prophesize(TagRepository::class); + $tagExists = $repo->tagExists($tag, $apiKey)->willReturn(false); + $getRepo = $this->em->getRepository(Tag::class)->willReturn($repo->reveal()); + + $this->expectException(TagNotFoundException::class); + $tagExists->shouldBeCalledOnce(); + $getRepo->shouldBeCalledOnce(); + + $this->helper->visitsForTag($tag, new VisitsParams(), $apiKey); + } + + /** + * @test + * @dataProvider provideAdminApiKeys + */ + public function visitsForTagAreReturnedAsExpected(?ApiKey $apiKey): void + { + $tag = 'foo'; + $repo = $this->prophesize(TagRepository::class); + $tagExists = $repo->tagExists($tag, $apiKey)->willReturn(true); + $getRepo = $this->em->getRepository(Tag::class)->willReturn($repo->reveal()); + + $spec = $apiKey === null ? null : $apiKey->spec(); + $list = map(range(0, 1), fn () => Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance())); + $repo2 = $this->prophesize(VisitRepository::class); + $repo2->findVisitsByTag($tag, Argument::type(DateRange::class), 1, 0, $spec)->willReturn($list); + $repo2->countVisitsByTag($tag, Argument::type(DateRange::class), $spec)->willReturn(1); + $this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledOnce(); + + $paginator = $this->helper->visitsForTag($tag, new VisitsParams(), $apiKey); + + self::assertEquals($list, ArrayUtils::iteratorToArray($paginator->getCurrentPageResults())); + $tagExists->shouldHaveBeenCalledOnce(); + $getRepo->shouldHaveBeenCalledOnce(); + } } diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index cfb97320..5b68ada7 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -66,8 +66,8 @@ return [ Service\ShortUrl\ShortUrlResolver::class, ShortUrlDataTransformer::class, ], - Action\Visit\ShortUrlVisitsAction::class => [Service\VisitsTracker::class], - Action\Visit\TagVisitsAction::class => [Service\VisitsTracker::class], + Action\Visit\ShortUrlVisitsAction::class => [Visit\VisitsStatsHelper::class], + Action\Visit\TagVisitsAction::class => [Visit\VisitsStatsHelper::class], Action\Visit\GlobalVisitsAction::class => [Visit\VisitsStatsHelper::class], Action\ShortUrl\ListShortUrlsAction::class => [Service\ShortUrlService::class, ShortUrlDataTransformer::class], Action\ShortUrl\EditShortUrlTagsAction::class => [Service\ShortUrlService::class], diff --git a/module/Rest/src/Action/Visit/ShortUrlVisitsAction.php b/module/Rest/src/Action/Visit/ShortUrlVisitsAction.php index 7b7c1055..8175d1c7 100644 --- a/module/Rest/src/Action/Visit/ShortUrlVisitsAction.php +++ b/module/Rest/src/Action/Visit/ShortUrlVisitsAction.php @@ -10,7 +10,7 @@ use Psr\Http\Message\ServerRequestInterface as Request; use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\VisitsParams; -use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; +use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware; @@ -21,11 +21,11 @@ class ShortUrlVisitsAction extends AbstractRestAction protected const ROUTE_PATH = '/short-urls/{shortCode}/visits'; protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET]; - private VisitsTrackerInterface $visitsTracker; + private VisitsStatsHelperInterface $visitsHelper; - public function __construct(VisitsTrackerInterface $visitsTracker) + public function __construct(VisitsStatsHelperInterface $visitsHelper) { - $this->visitsTracker = $visitsTracker; + $this->visitsHelper = $visitsHelper; } public function handle(Request $request): Response @@ -33,7 +33,7 @@ class ShortUrlVisitsAction extends AbstractRestAction $identifier = ShortUrlIdentifier::fromApiRequest($request); $params = VisitsParams::fromRawData($request->getQueryParams()); $apiKey = AuthenticationMiddleware::apiKeyFromRequest($request); - $visits = $this->visitsTracker->info($identifier, $params, $apiKey); + $visits = $this->visitsHelper->visitsForShortUrl($identifier, $params, $apiKey); return new JsonResponse([ 'visits' => $this->serializePaginator($visits), diff --git a/module/Rest/src/Action/Visit/TagVisitsAction.php b/module/Rest/src/Action/Visit/TagVisitsAction.php index aec42ebb..8d981c82 100644 --- a/module/Rest/src/Action/Visit/TagVisitsAction.php +++ b/module/Rest/src/Action/Visit/TagVisitsAction.php @@ -9,7 +9,7 @@ use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait; use Shlinkio\Shlink\Core\Model\VisitsParams; -use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; +use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware; @@ -20,11 +20,11 @@ class TagVisitsAction extends AbstractRestAction protected const ROUTE_PATH = '/tags/{tag}/visits'; protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET]; - private VisitsTrackerInterface $visitsTracker; + private VisitsStatsHelperInterface $visitsHelper; - public function __construct(VisitsTrackerInterface $visitsTracker) + public function __construct(VisitsStatsHelperInterface $visitsHelper) { - $this->visitsTracker = $visitsTracker; + $this->visitsHelper = $visitsHelper; } public function handle(Request $request): Response @@ -32,7 +32,7 @@ class TagVisitsAction extends AbstractRestAction $tag = $request->getAttribute('tag', ''); $params = VisitsParams::fromRawData($request->getQueryParams()); $apiKey = AuthenticationMiddleware::apiKeyFromRequest($request); - $visits = $this->visitsTracker->visitsForTag($tag, $params, $apiKey); + $visits = $this->visitsHelper->visitsForTag($tag, $params, $apiKey); return new JsonResponse([ 'visits' => $this->serializePaginator($visits), diff --git a/module/Rest/test/Action/Visit/ShortUrlVisitsActionTest.php b/module/Rest/test/Action/Visit/ShortUrlVisitsActionTest.php index 9c751214..6b149877 100644 --- a/module/Rest/test/Action/Visit/ShortUrlVisitsActionTest.php +++ b/module/Rest/test/Action/Visit/ShortUrlVisitsActionTest.php @@ -16,7 +16,7 @@ use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\VisitsParams; -use Shlinkio\Shlink\Core\Service\VisitsTracker; +use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Shlinkio\Shlink\Rest\Action\Visit\ShortUrlVisitsAction; use Shlinkio\Shlink\Rest\Entity\ApiKey; @@ -25,19 +25,19 @@ class ShortUrlVisitsActionTest extends TestCase use ProphecyTrait; private ShortUrlVisitsAction $action; - private ObjectProphecy $visitsTracker; + private ObjectProphecy $visitsHelper; public function setUp(): void { - $this->visitsTracker = $this->prophesize(VisitsTracker::class); - $this->action = new ShortUrlVisitsAction($this->visitsTracker->reveal()); + $this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class); + $this->action = new ShortUrlVisitsAction($this->visitsHelper->reveal()); } /** @test */ public function providingCorrectShortCodeReturnsVisits(): void { $shortCode = 'abc123'; - $this->visitsTracker->info( + $this->visitsHelper->visitsForShortUrl( new ShortUrlIdentifier($shortCode), Argument::type(VisitsParams::class), Argument::type(ApiKey::class), @@ -52,7 +52,7 @@ class ShortUrlVisitsActionTest extends TestCase public function paramsAreReadFromQuery(): void { $shortCode = 'abc123'; - $this->visitsTracker->info(new ShortUrlIdentifier($shortCode), new VisitsParams( + $this->visitsHelper->visitsForShortUrl(new ShortUrlIdentifier($shortCode), new VisitsParams( new DateRange(null, Chronos::parse('2016-01-01 00:00:00')), 3, 10, diff --git a/module/Rest/test/Action/Visit/TagVisitsActionTest.php b/module/Rest/test/Action/Visit/TagVisitsActionTest.php index c9097d07..da046f26 100644 --- a/module/Rest/test/Action/Visit/TagVisitsActionTest.php +++ b/module/Rest/test/Action/Visit/TagVisitsActionTest.php @@ -12,7 +12,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Core\Model\VisitsParams; -use Shlinkio\Shlink\Core\Service\VisitsTracker; +use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Shlinkio\Shlink\Rest\Action\Visit\TagVisitsAction; use Shlinkio\Shlink\Rest\Entity\ApiKey; @@ -21,12 +21,12 @@ class TagVisitsActionTest extends TestCase use ProphecyTrait; private TagVisitsAction $action; - private ObjectProphecy $visitsTracker; + private ObjectProphecy $visitsHelper; protected function setUp(): void { - $this->visitsTracker = $this->prophesize(VisitsTracker::class); - $this->action = new TagVisitsAction($this->visitsTracker->reveal()); + $this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class); + $this->action = new TagVisitsAction($this->visitsHelper->reveal()); } /** @test */ @@ -34,7 +34,7 @@ class TagVisitsActionTest extends TestCase { $tag = 'foo'; $apiKey = new ApiKey(); - $getVisits = $this->visitsTracker->visitsForTag($tag, Argument::type(VisitsParams::class), $apiKey)->willReturn( + $getVisits = $this->visitsHelper->visitsForTag($tag, Argument::type(VisitsParams::class), $apiKey)->willReturn( new Paginator(new ArrayAdapter([])), );