Updated visits to support pagination

This commit is contained in:
Alejandro Celaya 2018-11-28 20:39:08 +01:00
parent b0f250ed8a
commit d0e0aea0f1
11 changed files with 167 additions and 60 deletions

View File

@ -14,6 +14,7 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\Stdlib\ArrayUtils;
use function array_map; use function array_map;
use function Functional\select_keys; use function Functional\select_keys;
@ -73,7 +74,9 @@ class GetVisitsCommand extends Command
$startDate = $this->getDateOption($input, 'startDate'); $startDate = $this->getDateOption($input, 'startDate');
$endDate = $this->getDateOption($input, 'endDate'); $endDate = $this->getDateOption($input, 'endDate');
$visits = $this->visitsTracker->info($shortCode, new VisitsParams(new DateRange($startDate, $endDate))); $paginator = $this->visitsTracker->info($shortCode, new VisitsParams(new DateRange($startDate, $endDate)));
$visits = ArrayUtils::iteratorToArray($paginator->getCurrentItems());
$rows = array_map(function (Visit $visit) { $rows = array_map(function (Visit $visit) {
$rowData = $visit->jsonSerialize(); $rowData = $visit->jsonSerialize();
$rowData['country'] = $visit->getVisitLocation()->getCountryName(); $rowData['country'] = $visit->getVisitLocation()->getCountryName();

View File

@ -17,7 +17,8 @@ use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tester\CommandTester;
use function strpos; use Zend\Paginator\Adapter\ArrayAdapter;
use Zend\Paginator\Paginator;
class GetVisitsCommandTest extends TestCase class GetVisitsCommandTest extends TestCase
{ {
@ -41,8 +42,9 @@ class GetVisitsCommandTest extends TestCase
public function noDateFlagsTriesToListWithoutDateRange() public function noDateFlagsTriesToListWithoutDateRange()
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->visitsTracker->info($shortCode, new VisitsParams(new DateRange(null, null)))->willReturn([]) $this->visitsTracker->info($shortCode, new VisitsParams(new DateRange(null, null)))->willReturn(
->shouldBeCalledOnce(); new Paginator(new ArrayAdapter([]))
)->shouldBeCalledOnce();
$this->commandTester->execute([ $this->commandTester->execute([
'command' => 'shortcode:visits', 'command' => 'shortcode:visits',
@ -62,7 +64,7 @@ class GetVisitsCommandTest extends TestCase
$shortCode, $shortCode,
new VisitsParams(new DateRange(Chronos::parse($startDate), Chronos::parse($endDate))) new VisitsParams(new DateRange(Chronos::parse($startDate), Chronos::parse($endDate)))
) )
->willReturn([]) ->willReturn(new Paginator(new ArrayAdapter([])))
->shouldBeCalledOnce(); ->shouldBeCalledOnce();
$this->commandTester->execute([ $this->commandTester->execute([
@ -79,19 +81,21 @@ class GetVisitsCommandTest extends TestCase
public function outputIsProperlyGenerated() public function outputIsProperlyGenerated()
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->visitsTracker->info($shortCode, Argument::any())->willReturn([ $this->visitsTracker->info($shortCode, Argument::any())->willReturn(
(new Visit(new ShortUrl(''), new Visitor('bar', 'foo', '')))->locate( new Paginator(new ArrayAdapter([
new VisitLocation(['country_name' => 'Spain']) (new Visit(new ShortUrl(''), new Visitor('bar', 'foo', '')))->locate(
), new VisitLocation(['country_name' => 'Spain'])
])->shouldBeCalledOnce(); ),
]))
)->shouldBeCalledOnce();
$this->commandTester->execute([ $this->commandTester->execute([
'command' => 'shortcode:visits', 'command' => 'shortcode:visits',
'shortCode' => $shortCode, 'shortCode' => $shortCode,
]); ]);
$output = $this->commandTester->getDisplay(); $output = $this->commandTester->getDisplay();
$this->assertGreaterThan(0, strpos($output, 'foo')); $this->assertContains('foo', $output);
$this->assertGreaterThan(0, strpos($output, 'Spain')); $this->assertContains('Spain', $output);
$this->assertGreaterThan(0, strpos($output, 'bar')); $this->assertContains('bar', $output);
} }
} }

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Paginator\Adapter;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Zend\Paginator\Adapter\AdapterInterface;
class VisitsPaginatorAdapter implements AdapterInterface
{
/** @var VisitRepositoryInterface */
private $visitRepository;
/** @var string */
private $shortCode;
/** @var VisitsParams */
private $params;
public function __construct(VisitRepositoryInterface $visitRepository, string $shortCode, VisitsParams $params)
{
$this->visitRepository = $visitRepository;
$this->shortCode = $shortCode;
$this->params = $params;
}
public function getItems($offset, $itemCountPerPage): array
{
return $this->visitRepository->findVisitsByShortCode(
$this->shortCode,
$this->params->getDateRange(),
$itemCountPerPage,
$offset
);
}
public function count(): int
{
return $this->visitRepository->countVisitsByShortCode($this->shortCode, $this->params->getDateRange());
}
}

View File

@ -4,8 +4,8 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Repository; namespace Shlinkio\Shlink\Core\Repository;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
class VisitRepository extends EntityRepository implements VisitRepositoryInterface class VisitRepository extends EntityRepository implements VisitRepositoryInterface
@ -19,24 +19,42 @@ class VisitRepository extends EntityRepository implements VisitRepositoryInterfa
} }
/** /**
* @param ShortUrl|int $shortUrlOrId
* @param DateRange|null $dateRange
* @return Visit[] * @return Visit[]
*/ */
public function findVisitsByShortUrl($shortUrlOrId, DateRange $dateRange = null): array public function findVisitsByShortCode(
{ string $shortCode,
/** @var ShortUrl|null $shortUrl */ ?DateRange $dateRange = null,
$shortUrl = $shortUrlOrId instanceof ShortUrl ?int $limit = null,
? $shortUrlOrId ?int $offset = null
: $this->getEntityManager()->find(ShortUrl::class, $shortUrlOrId); ): array {
$qb = $this->createVisitsByShortCodeQueryBuilder($shortCode, $dateRange);
$qb->select('v');
if ($shortUrl === null) { if ($limit !== null) {
return []; $qb->setMaxResults($limit);
}
if ($offset !== null) {
$qb->setFirstResult($offset);
} }
$qb = $this->createQueryBuilder('v'); return $qb->getQuery()->getResult();
$qb->where($qb->expr()->eq('v.shortUrl', ':shortUrl')) }
->setParameter('shortUrl', $shortUrl)
public function countVisitsByShortCode(string $shortCode, ?DateRange $dateRange = null): int
{
$qb = $this->createVisitsByShortCodeQueryBuilder($shortCode, $dateRange);
$qb->select('COUNT(DISTINCT v.id)');
return (int) $qb->getQuery()->getSingleScalarResult();
}
private function createVisitsByShortCodeQueryBuilder(string $shortCode, ?DateRange $dateRange = null): QueryBuilder
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->from(Visit::class, 'v')
->join('v.shortUrl', 'su')
->where($qb->expr()->eq('su.shortCode', ':shortCode'))
->setParameter('shortCode', $shortCode)
->orderBy('v.date', 'DESC') ; ->orderBy('v.date', 'DESC') ;
// Apply date range filtering // Apply date range filtering
@ -49,6 +67,6 @@ class VisitRepository extends EntityRepository implements VisitRepositoryInterfa
->setParameter('endDate', $dateRange->getEndDate()); ->setParameter('endDate', $dateRange->getEndDate());
} }
return $qb->getQuery()->getResult(); return $qb;
} }
} }

View File

@ -5,7 +5,6 @@ namespace Shlinkio\Shlink\Core\Repository;
use Doctrine\Common\Persistence\ObjectRepository; use Doctrine\Common\Persistence\ObjectRepository;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
interface VisitRepositoryInterface extends ObjectRepository interface VisitRepositoryInterface extends ObjectRepository
@ -13,9 +12,14 @@ interface VisitRepositoryInterface extends ObjectRepository
public function findUnlocatedVisits(): iterable; public function findUnlocatedVisits(): iterable;
/** /**
* @param ShortUrl|int $shortUrl
* @param DateRange|null $dateRange
* @return Visit[] * @return Visit[]
*/ */
public function findVisitsByShortUrl($shortUrl, DateRange $dateRange = null): array; public function findVisitsByShortCode(
string $shortCode,
?DateRange $dateRange = null,
?int $limit = null,
?int $offset = null
): array;
public function countVisitsByShortCode(string $shortCode, ?DateRange $dateRange = null): int;
} }

View File

@ -9,7 +9,9 @@ use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException; use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Repository\VisitRepository; use Shlinkio\Shlink\Core\Repository\VisitRepository;
use Zend\Paginator\Paginator;
use function sprintf; use function sprintf;
class VisitsTracker implements VisitsTrackerInterface class VisitsTracker implements VisitsTrackerInterface
@ -43,21 +45,23 @@ class VisitsTracker implements VisitsTrackerInterface
/** /**
* Returns the visits on certain short code * Returns the visits on certain short code
* *
* @return Visit[] * @return Visit[]|Paginator
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function info(string $shortCode, VisitsParams $params): array public function info(string $shortCode, VisitsParams $params): Paginator
{ {
/** @var ShortUrl|null $shortUrl */ /** @var ORM\EntityRepository $repo */
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([ $repo = $this->em->getRepository(ShortUrl::class);
'shortCode' => $shortCode, if ($repo->count(['shortCode' => $shortCode]) < 1) {
]);
if ($shortUrl === null) {
throw new InvalidArgumentException(sprintf('Short code "%s" not found', $shortCode)); throw new InvalidArgumentException(sprintf('Short code "%s" not found', $shortCode));
} }
/** @var VisitRepository $repo */ /** @var VisitRepository $repo */
$repo = $this->em->getRepository(Visit::class); $repo = $this->em->getRepository(Visit::class);
return $repo->findVisitsByShortUrl($shortUrl, $params->getDateRange()); $paginator = new Paginator(new VisitsPaginatorAdapter($repo, $shortCode, $params));
$paginator->setItemCountPerPage($params->hasItemsPerPage() ? $params->getItemsPerPage() : -1)
->setCurrentPageNumber($params->getPage());
return $paginator;
} }
} }

View File

@ -7,6 +7,7 @@ use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException; use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Model\Visitor; use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Model\VisitsParams;
use Zend\Paginator\Paginator;
interface VisitsTrackerInterface interface VisitsTrackerInterface
{ {
@ -18,8 +19,8 @@ interface VisitsTrackerInterface
/** /**
* Returns the visits on certain short code * Returns the visits on certain short code
* *
* @return Visit[] * @return Visit[]|Paginator
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function info(string $shortCode, VisitsParams $params): array; public function info(string $shortCode, VisitsParams $params): Paginator;
} }

View File

@ -62,7 +62,7 @@ class VisitRepositoryTest extends DatabaseTestCase
/** /**
* @test * @test
*/ */
public function findVisitsByShortUrlReturnsProperData() public function findVisitsByShortCodeReturnsProperData()
{ {
$shortUrl = new ShortUrl(''); $shortUrl = new ShortUrl('');
$this->getEntityManager()->persist($shortUrl); $this->getEntityManager()->persist($shortUrl);
@ -73,13 +73,38 @@ class VisitRepositoryTest extends DatabaseTestCase
} }
$this->getEntityManager()->flush(); $this->getEntityManager()->flush();
$this->assertCount(0, $this->repo->findVisitsByShortUrl('invalid')); $this->assertCount(0, $this->repo->findVisitsByShortCode('invalid'));
$this->assertCount(6, $this->repo->findVisitsByShortUrl($shortUrl->getId())); $this->assertCount(6, $this->repo->findVisitsByShortCode($shortUrl->getShortCode()));
$this->assertCount(2, $this->repo->findVisitsByShortUrl($shortUrl->getId(), new DateRange( $this->assertCount(2, $this->repo->findVisitsByShortCode($shortUrl->getShortCode(), new DateRange(
Chronos::parse('2016-01-02'), Chronos::parse('2016-01-02'),
Chronos::parse('2016-01-03') Chronos::parse('2016-01-03')
))); )));
$this->assertCount(4, $this->repo->findVisitsByShortUrl($shortUrl->getId(), new DateRange( $this->assertCount(4, $this->repo->findVisitsByShortCode($shortUrl->getShortCode(), new DateRange(
Chronos::parse('2016-01-03')
)));
}
/**
* @test
*/
public function countVisitsByShortCodeReturnsProperData()
{
$shortUrl = new ShortUrl('');
$this->getEntityManager()->persist($shortUrl);
for ($i = 0; $i < 6; $i++) {
$visit = new Visit($shortUrl, Visitor::emptyInstance(), Chronos::parse(sprintf('2016-01-0%s', $i + 1)));
$this->getEntityManager()->persist($visit);
}
$this->getEntityManager()->flush();
$this->assertEquals(0, $this->repo->countVisitsByShortCode('invalid'));
$this->assertEquals(6, $this->repo->countVisitsByShortCode($shortUrl->getShortCode()));
$this->assertEquals(2, $this->repo->countVisitsByShortCode($shortUrl->getShortCode(), new DateRange(
Chronos::parse('2016-01-02'),
Chronos::parse('2016-01-03')
)));
$this->assertEquals(4, $this->repo->countVisitsByShortCode($shortUrl->getShortCode(), new DateRange(
Chronos::parse('2016-01-03') Chronos::parse('2016-01-03')
))); )));
} }

View File

@ -5,6 +5,7 @@ namespace ShlinkioTest\Shlink\Core\Service;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
@ -15,6 +16,7 @@ use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Repository\VisitRepository; use Shlinkio\Shlink\Core\Repository\VisitRepository;
use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Core\Service\VisitsTracker;
use Zend\Stdlib\ArrayUtils;
class VisitsTrackerTest extends TestCase class VisitsTrackerTest extends TestCase
{ {
@ -51,15 +53,14 @@ class VisitsTrackerTest extends TestCase
public function trackedIpAddressGetsObfuscated() public function trackedIpAddressGetsObfuscated()
{ {
$shortCode = '123ABC'; $shortCode = '123ABC';
$test = $this;
$repo = $this->prophesize(EntityRepository::class); $repo = $this->prophesize(EntityRepository::class);
$repo->findOneBy(['shortCode' => $shortCode])->willReturn(new ShortUrl('')); $repo->findOneBy(['shortCode' => $shortCode])->willReturn(new ShortUrl(''));
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce(); $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce();
$this->em->persist(Argument::any())->will(function ($args) use ($test) { $this->em->persist(Argument::any())->will(function ($args) {
/** @var Visit $visit */ /** @var Visit $visit */
$visit = $args[0]; $visit = $args[0];
$test->assertEquals('4.3.2.0', $visit->getRemoteAddr()); Assert::assertEquals('4.3.2.0', $visit->getRemoteAddr());
})->shouldBeCalledOnce(); })->shouldBeCalledOnce();
$this->em->flush(Argument::type(Visit::class))->shouldBeCalledOnce(); $this->em->flush(Argument::type(Visit::class))->shouldBeCalledOnce();
@ -72,9 +73,8 @@ class VisitsTrackerTest extends TestCase
public function infoReturnsVisistForCertainShortCode() public function infoReturnsVisistForCertainShortCode()
{ {
$shortCode = '123ABC'; $shortCode = '123ABC';
$shortUrl = new ShortUrl('http://domain.com/foo/bar');
$repo = $this->prophesize(EntityRepository::class); $repo = $this->prophesize(EntityRepository::class);
$repo->findOneBy(['shortCode' => $shortCode])->willReturn($shortUrl); $count = $repo->count(['shortCode' => $shortCode])->willReturn(1);
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce(); $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce();
$list = [ $list = [
@ -82,9 +82,13 @@ class VisitsTrackerTest extends TestCase
new Visit(new ShortUrl(''), Visitor::emptyInstance()), new Visit(new ShortUrl(''), Visitor::emptyInstance()),
]; ];
$repo2 = $this->prophesize(VisitRepository::class); $repo2 = $this->prophesize(VisitRepository::class);
$repo2->findVisitsByShortUrl($shortUrl, Argument::type(DateRange::class))->willReturn($list); $repo2->findVisitsByShortCode($shortCode, Argument::type(DateRange::class), 1, 0)->willReturn($list);
$repo2->countVisitsByShortCode($shortCode, Argument::type(DateRange::class))->willReturn(1);
$this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledOnce(); $this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledOnce();
$this->assertEquals($list, $this->visitsTracker->info($shortCode, new VisitsParams())); $paginator = $this->visitsTracker->info($shortCode, new VisitsParams());
$this->assertEquals($list, ArrayUtils::iteratorToArray($paginator->getCurrentItems()));
$count->shouldHaveBeenCalledOnce();
} }
} }

View File

@ -7,6 +7,7 @@ use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction; use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
@ -16,6 +17,8 @@ use function sprintf;
class GetVisitsAction extends AbstractRestAction class GetVisitsAction extends AbstractRestAction
{ {
use PaginatorUtilsTrait;
protected const ROUTE_PATH = '/short-urls/{shortCode}/visits'; protected const ROUTE_PATH = '/short-urls/{shortCode}/visits';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET]; protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
@ -41,9 +44,7 @@ class GetVisitsAction extends AbstractRestAction
$visits = $this->visitsTracker->info($shortCode, VisitsParams::fromRawData($request->getQueryParams())); $visits = $this->visitsTracker->info($shortCode, VisitsParams::fromRawData($request->getQueryParams()));
return new JsonResponse([ return new JsonResponse([
'visits' => [ 'visits' => $this->serializePaginator($visits),
'data' => $visits,
],
]); ]);
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
$this->logger->warning('Provided nonexistent short code {e}', ['e' => $e]); $this->logger->warning('Provided nonexistent short code {e}', ['e' => $e]);

View File

@ -13,6 +13,8 @@ use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Core\Service\VisitsTracker;
use Shlinkio\Shlink\Rest\Action\Visit\GetVisitsAction; use Shlinkio\Shlink\Rest\Action\Visit\GetVisitsAction;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\Paginator\Adapter\ArrayAdapter;
use Zend\Paginator\Paginator;
class GetVisitsActionTest extends TestCase class GetVisitsActionTest extends TestCase
{ {
@ -33,8 +35,9 @@ class GetVisitsActionTest extends TestCase
public function providingCorrectShortCodeReturnsVisits() public function providingCorrectShortCodeReturnsVisits()
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->visitsTracker->info($shortCode, Argument::type(VisitsParams::class))->willReturn([]) $this->visitsTracker->info($shortCode, Argument::type(VisitsParams::class))->willReturn(
->shouldBeCalledOnce(); new Paginator(new ArrayAdapter([]))
)->shouldBeCalledOnce();
$response = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode)); $response = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode));
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
@ -63,7 +66,7 @@ class GetVisitsActionTest extends TestCase
$this->visitsTracker->info($shortCode, new VisitsParams( $this->visitsTracker->info($shortCode, new VisitsParams(
new DateRange(null, Chronos::parse('2016-01-01 00:00:00')) new DateRange(null, Chronos::parse('2016-01-01 00:00:00'))
)) ))
->willReturn([]) ->willReturn(new Paginator(new ArrayAdapter([])))
->shouldBeCalledOnce(); ->shouldBeCalledOnce();
$response = $this->action->handle( $response = $this->action->handle(