Applied API role specs to short URL visits

This commit is contained in:
Alejandro Celaya 2021-01-03 17:48:32 +01:00
parent 25ee9b5daf
commit 4a1e7b761a
13 changed files with 87 additions and 42 deletions

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Paginator\Adapter; namespace Shlinkio\Shlink\Core\Paginator\Adapter;
use Happyr\DoctrineSpecification\Specification\Specification;
use Laminas\Paginator\Adapter\AdapterInterface; use Laminas\Paginator\Adapter\AdapterInterface;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface; use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
@ -31,7 +32,7 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
$this->params->tags(), $this->params->tags(),
$this->params->orderBy(), $this->params->orderBy(),
$this->params->dateRange(), $this->params->dateRange(),
$this->apiKey !== null ? $this->apiKey->spec() : null, $this->resolveSpec(),
); );
} }
@ -41,7 +42,12 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
$this->params->searchTerm(), $this->params->searchTerm(),
$this->params->tags(), $this->params->tags(),
$this->params->dateRange(), $this->params->dateRange(),
$this->apiKey !== null ? $this->apiKey->spec() : null, $this->resolveSpec(),
); );
} }
private function resolveSpec(): ?Specification
{
return $this->apiKey !== null ? $this->apiKey->spec() : null;
}
} }

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Paginator\Adapter; namespace Shlinkio\Shlink\Core\Paginator\Adapter;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface; use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
@ -13,15 +14,18 @@ class VisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
private VisitRepositoryInterface $visitRepository; private VisitRepositoryInterface $visitRepository;
private ShortUrlIdentifier $identifier; private ShortUrlIdentifier $identifier;
private VisitsParams $params; private VisitsParams $params;
private ?Specification $spec;
public function __construct( public function __construct(
VisitRepositoryInterface $visitRepository, VisitRepositoryInterface $visitRepository,
ShortUrlIdentifier $identifier, ShortUrlIdentifier $identifier,
VisitsParams $params VisitsParams $params,
?Specification $spec
) { ) {
$this->visitRepository = $visitRepository; $this->visitRepository = $visitRepository;
$this->params = $params; $this->params = $params;
$this->identifier = $identifier; $this->identifier = $identifier;
$this->spec = $spec;
} }
public function getItems($offset, $itemCountPerPage): array // phpcs:ignore public function getItems($offset, $itemCountPerPage): array // phpcs:ignore
@ -32,6 +36,7 @@ class VisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
$this->params->getDateRange(), $this->params->getDateRange(),
$itemCountPerPage, $itemCountPerPage,
$offset, $offset,
$this->spec,
); );
} }
@ -41,6 +46,7 @@ class VisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
$this->identifier->shortCode(), $this->identifier->shortCode(),
$this->identifier->domain(), $this->identifier->domain(),
$this->params->getDateRange(), $this->params->getDateRange(),
$this->spec,
); );
} }
} }

View file

@ -177,9 +177,9 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
return $qb->getQuery()->getOneOrNullResult(); return $qb->getQuery()->getOneOrNullResult();
} }
public function shortCodeIsInUse(string $slug, ?string $domain = null): bool public function shortCodeIsInUse(string $slug, ?string $domain = null, ?Specification $spec = null): bool
{ {
$qb = $this->createFindOneQueryBuilder($slug, $domain, null); $qb = $this->createFindOneQueryBuilder($slug, $domain, $spec);
$qb->select('COUNT(DISTINCT s.id)'); $qb->select('COUNT(DISTINCT s.id)');
return ((int) $qb->getQuery()->getSingleScalarResult()) > 0; return ((int) $qb->getQuery()->getSingleScalarResult()) > 0;

View file

@ -36,7 +36,7 @@ interface ShortUrlRepositoryInterface extends ObjectRepository, EntitySpecificat
public function findOne(string $shortCode, ?string $domain = null, ?Specification $spec = null): ?ShortUrl; public function findOne(string $shortCode, ?string $domain = null, ?Specification $spec = null): ?ShortUrl;
public function shortCodeIsInUse(string $slug, ?string $domain): bool; public function shortCodeIsInUse(string $slug, ?string $domain, ?Specification $spec = null): bool;
public function findOneMatching(string $url, array $tags, ShortUrlMeta $meta): ?ShortUrl; public function findOneMatching(string $url, array $tags, ShortUrlMeta $meta): ?ShortUrl;

View file

@ -4,9 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Repository; namespace Shlinkio\Shlink\Core\Repository;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Happyr\DoctrineSpecification\EntitySpecificationRepository;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
@ -14,7 +15,7 @@ use Shlinkio\Shlink\Core\Entity\VisitLocation;
use const PHP_INT_MAX; use const PHP_INT_MAX;
class VisitRepository extends EntityRepository implements VisitRepositoryInterface class VisitRepository extends EntitySpecificationRepository implements VisitRepositoryInterface
{ {
/** /**
* @return iterable|Visit[] * @return iterable|Visit[]
@ -84,15 +85,20 @@ class VisitRepository extends EntityRepository implements VisitRepositoryInterfa
?string $domain = null, ?string $domain = null,
?DateRange $dateRange = null, ?DateRange $dateRange = null,
?int $limit = null, ?int $limit = null,
?int $offset = null ?int $offset = null,
?Specification $spec = null
): array { ): array {
$qb = $this->createVisitsByShortCodeQueryBuilder($shortCode, $domain, $dateRange); $qb = $this->createVisitsByShortCodeQueryBuilder($shortCode, $domain, $dateRange, $spec);
return $this->resolveVisitsWithNativeQuery($qb, $limit, $offset); return $this->resolveVisitsWithNativeQuery($qb, $limit, $offset);
} }
public function countVisitsByShortCode(string $shortCode, ?string $domain = null, ?DateRange $dateRange = null): int public function countVisitsByShortCode(
{ string $shortCode,
$qb = $this->createVisitsByShortCodeQueryBuilder($shortCode, $domain, $dateRange); ?string $domain = null,
?DateRange $dateRange = null,
?Specification $spec = null
): int {
$qb = $this->createVisitsByShortCodeQueryBuilder($shortCode, $domain, $dateRange, $spec);
$qb->select('COUNT(v.id)'); $qb->select('COUNT(v.id)');
return (int) $qb->getQuery()->getSingleScalarResult(); return (int) $qb->getQuery()->getSingleScalarResult();
@ -101,11 +107,12 @@ class VisitRepository extends EntityRepository implements VisitRepositoryInterfa
private function createVisitsByShortCodeQueryBuilder( private function createVisitsByShortCodeQueryBuilder(
string $shortCode, string $shortCode,
?string $domain, ?string $domain,
?DateRange $dateRange ?DateRange $dateRange,
?Specification $spec = null
): QueryBuilder { ): QueryBuilder {
/** @var ShortUrlRepositoryInterface $shortUrlRepo */ /** @var ShortUrlRepositoryInterface $shortUrlRepo */
$shortUrlRepo = $this->getEntityManager()->getRepository(ShortUrl::class); $shortUrlRepo = $this->getEntityManager()->getRepository(ShortUrl::class);
$shortUrl = $shortUrlRepo->findOne($shortCode, $domain); $shortUrl = $shortUrlRepo->findOne($shortCode, $domain, $spec);
$shortUrlId = $shortUrl !== null ? $shortUrl->getId() : -1; $shortUrlId = $shortUrl !== null ? $shortUrl->getId() : -1;
// Parameters in this query need to be part of the query itself, as we need to use it a sub-query later // Parameters in this query need to be part of the query itself, as we need to use it a sub-query later

View file

@ -5,10 +5,12 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Repository; namespace Shlinkio\Shlink\Core\Repository;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
use Happyr\DoctrineSpecification\EntitySpecificationRepositoryInterface;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
interface VisitRepositoryInterface extends ObjectRepository interface VisitRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface
{ {
public const DEFAULT_BLOCK_SIZE = 10000; public const DEFAULT_BLOCK_SIZE = 10000;
@ -35,13 +37,15 @@ interface VisitRepositoryInterface extends ObjectRepository
?string $domain = null, ?string $domain = null,
?DateRange $dateRange = null, ?DateRange $dateRange = null,
?int $limit = null, ?int $limit = null,
?int $offset = null ?int $offset = null,
?Specification $spec = null
): array; ): array;
public function countVisitsByShortCode( public function countVisitsByShortCode(
string $shortCode, string $shortCode,
?string $domain = null, ?string $domain = null,
?DateRange $dateRange = null ?DateRange $dateRange = null,
?Specification $spec = null
): int; ): int;
/** /**

View file

@ -21,6 +21,7 @@ use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface; use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
use Shlinkio\Shlink\Core\Repository\TagRepository; use Shlinkio\Shlink\Core\Repository\TagRepository;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface; use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class VisitsTracker implements VisitsTrackerInterface class VisitsTracker implements VisitsTrackerInterface
{ {
@ -52,17 +53,19 @@ class VisitsTracker implements VisitsTrackerInterface
* @return Visit[]|Paginator * @return Visit[]|Paginator
* @throws ShortUrlNotFoundException * @throws ShortUrlNotFoundException
*/ */
public function info(ShortUrlIdentifier $identifier, VisitsParams $params): Paginator public function info(ShortUrlIdentifier $identifier, VisitsParams $params, ?ApiKey $apiKey = null): Paginator
{ {
$spec = $apiKey !== null ? $apiKey->spec() : null;
/** @var ShortUrlRepositoryInterface $repo */ /** @var ShortUrlRepositoryInterface $repo */
$repo = $this->em->getRepository(ShortUrl::class); $repo = $this->em->getRepository(ShortUrl::class);
if (! $repo->shortCodeIsInUse($identifier->shortCode(), $identifier->domain())) { if (! $repo->shortCodeIsInUse($identifier->shortCode(), $identifier->domain(), $spec)) {
throw ShortUrlNotFoundException::fromNotFound($identifier); throw ShortUrlNotFoundException::fromNotFound($identifier);
} }
/** @var VisitRepositoryInterface $repo */ /** @var VisitRepositoryInterface $repo */
$repo = $this->em->getRepository(Visit::class); $repo = $this->em->getRepository(Visit::class);
$paginator = new Paginator(new VisitsPaginatorAdapter($repo, $identifier, $params)); $paginator = new Paginator(new VisitsPaginatorAdapter($repo, $identifier, $params, $spec));
$paginator->setItemCountPerPage($params->getItemsPerPage()) $paginator->setItemCountPerPage($params->getItemsPerPage())
->setCurrentPageNumber($params->getPage()); ->setCurrentPageNumber($params->getPage());

View file

@ -12,6 +12,7 @@ use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
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\Rest\Entity\ApiKey;
interface VisitsTrackerInterface interface VisitsTrackerInterface
{ {
@ -21,7 +22,7 @@ interface VisitsTrackerInterface
* @return Visit[]|Paginator * @return Visit[]|Paginator
* @throws ShortUrlNotFoundException * @throws ShortUrlNotFoundException
*/ */
public function info(ShortUrlIdentifier $identifier, VisitsParams $params): Paginator; public function info(ShortUrlIdentifier $identifier, VisitsParams $params, ?ApiKey $apiKey = null): Paginator;
/** /**
* @return Visit[]|Paginator * @return Visit[]|Paginator

View file

@ -16,8 +16,8 @@ class BelongsToApiKey extends BaseSpecification
public function __construct(ApiKey $apiKey, ?string $dqlAlias = null) public function __construct(ApiKey $apiKey, ?string $dqlAlias = null)
{ {
$this->dqlAlias = $dqlAlias ?? 's';
$this->apiKey = $apiKey; $this->apiKey = $apiKey;
$this->dqlAlias = $dqlAlias ?? 's';
parent::__construct($this->dqlAlias); parent::__construct($this->dqlAlias);
} }

View file

@ -27,6 +27,7 @@ class VisitsPaginatorAdapterTest extends TestCase
$this->repo->reveal(), $this->repo->reveal(),
new ShortUrlIdentifier(''), new ShortUrlIdentifier(''),
VisitsParams::fromRawData([]), VisitsParams::fromRawData([]),
null,
); );
} }
@ -36,7 +37,9 @@ class VisitsPaginatorAdapterTest extends TestCase
$count = 3; $count = 3;
$limit = 1; $limit = 1;
$offset = 5; $offset = 5;
$findVisits = $this->repo->findVisitsByShortCode('', null, new DateRange(), $limit, $offset)->willReturn([]); $findVisits = $this->repo->findVisitsByShortCode('', null, new DateRange(), $limit, $offset, null)->willReturn(
[],
);
for ($i = 0; $i < $count; $i++) { for ($i = 0; $i < $count; $i++) {
$this->adapter->getItems($offset, $limit); $this->adapter->getItems($offset, $limit);
@ -49,7 +52,7 @@ class VisitsPaginatorAdapterTest extends TestCase
public function repoIsCalledOnlyOnceForCount(): void public function repoIsCalledOnlyOnceForCount(): void
{ {
$count = 3; $count = 3;
$countVisits = $this->repo->countVisitsByShortCode('', null, new DateRange())->willReturn(3); $countVisits = $this->repo->countVisitsByShortCode('', null, new DateRange(), null)->willReturn(3);
for ($i = 0; $i < $count; $i++) { for ($i = 0; $i < $count; $i++) {
$this->adapter->count(); $this->adapter->count();

View file

@ -63,13 +63,15 @@ class VisitsTrackerTest extends TestCase
{ {
$shortCode = '123ABC'; $shortCode = '123ABC';
$repo = $this->prophesize(ShortUrlRepositoryInterface::class); $repo = $this->prophesize(ShortUrlRepositoryInterface::class);
$count = $repo->shortCodeIsInUse($shortCode, null)->willReturn(true); $count = $repo->shortCodeIsInUse($shortCode, null, null)->willReturn(true);
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce(); $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce();
$list = map(range(0, 1), fn () => new Visit(new ShortUrl(''), Visitor::emptyInstance())); $list = map(range(0, 1), fn () => new Visit(new ShortUrl(''), Visitor::emptyInstance()));
$repo2 = $this->prophesize(VisitRepository::class); $repo2 = $this->prophesize(VisitRepository::class);
$repo2->findVisitsByShortCode($shortCode, null, Argument::type(DateRange::class), 1, 0)->willReturn($list); $repo2->findVisitsByShortCode($shortCode, null, Argument::type(DateRange::class), 1, 0, null)->willReturn(
$repo2->countVisitsByShortCode($shortCode, null, Argument::type(DateRange::class))->willReturn(1); $list,
);
$repo2->countVisitsByShortCode($shortCode, null, Argument::type(DateRange::class), null)->willReturn(1);
$this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledOnce(); $this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledOnce();
$paginator = $this->visitsTracker->info(new ShortUrlIdentifier($shortCode), new VisitsParams()); $paginator = $this->visitsTracker->info(new ShortUrlIdentifier($shortCode), new VisitsParams());
@ -83,7 +85,7 @@ class VisitsTrackerTest extends TestCase
{ {
$shortCode = '123ABC'; $shortCode = '123ABC';
$repo = $this->prophesize(ShortUrlRepositoryInterface::class); $repo = $this->prophesize(ShortUrlRepositoryInterface::class);
$count = $repo->shortCodeIsInUse($shortCode, null)->willReturn(false); $count = $repo->shortCodeIsInUse($shortCode, null, null)->willReturn(false);
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce(); $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledOnce();
$this->expectException(ShortUrlNotFoundException::class); $this->expectException(ShortUrlNotFoundException::class);

View file

@ -12,6 +12,7 @@ use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
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;
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
class ShortUrlVisitsAction extends AbstractRestAction class ShortUrlVisitsAction extends AbstractRestAction
{ {
@ -30,7 +31,9 @@ class ShortUrlVisitsAction extends AbstractRestAction
public function handle(Request $request): Response public function handle(Request $request): Response
{ {
$identifier = ShortUrlIdentifier::fromApiRequest($request); $identifier = ShortUrlIdentifier::fromApiRequest($request);
$visits = $this->visitsTracker->info($identifier, VisitsParams::fromRawData($request->getQueryParams())); $params = VisitsParams::fromRawData($request->getQueryParams());
$apiKey = AuthenticationMiddleware::apiKeyFromRequest($request);
$visits = $this->visitsTracker->info($identifier, $params, $apiKey);
return new JsonResponse([ return new JsonResponse([
'visits' => $this->serializePaginator($visits), 'visits' => $this->serializePaginator($visits),

View file

@ -5,18 +5,20 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action\Visit; namespace ShlinkioTest\Shlink\Rest\Action\Visit;
use Cake\Chronos\Chronos; use Cake\Chronos\Chronos;
use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Paginator\Adapter\ArrayAdapter; use Laminas\Paginator\Adapter\ArrayAdapter;
use Laminas\Paginator\Paginator; use Laminas\Paginator\Paginator;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\VisitsParams; use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Core\Service\VisitsTracker;
use Shlinkio\Shlink\Rest\Action\Visit\ShortUrlVisitsAction; use Shlinkio\Shlink\Rest\Action\Visit\ShortUrlVisitsAction;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class ShortUrlVisitsActionTest extends TestCase class ShortUrlVisitsActionTest extends TestCase
{ {
@ -35,11 +37,14 @@ class ShortUrlVisitsActionTest extends TestCase
public function providingCorrectShortCodeReturnsVisits(): void public function providingCorrectShortCodeReturnsVisits(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->visitsTracker->info(new ShortUrlIdentifier($shortCode), Argument::type(VisitsParams::class))->willReturn( $this->visitsTracker->info(
new Paginator(new ArrayAdapter([])), new ShortUrlIdentifier($shortCode),
)->shouldBeCalledOnce(); Argument::type(VisitsParams::class),
Argument::type(ApiKey::class),
)->willReturn(new Paginator(new ArrayAdapter([])))
->shouldBeCalledOnce();
$response = $this->action->handle((new ServerRequest())->withAttribute('shortCode', $shortCode)); $response = $this->action->handle($this->requestWithApiKey()->withAttribute('shortCode', $shortCode));
self::assertEquals(200, $response->getStatusCode()); self::assertEquals(200, $response->getStatusCode());
} }
@ -51,12 +56,12 @@ class ShortUrlVisitsActionTest extends TestCase
new DateRange(null, Chronos::parse('2016-01-01 00:00:00')), new DateRange(null, Chronos::parse('2016-01-01 00:00:00')),
3, 3,
10, 10,
)) ), Argument::type(ApiKey::class))
->willReturn(new Paginator(new ArrayAdapter([]))) ->willReturn(new Paginator(new ArrayAdapter([])))
->shouldBeCalledOnce(); ->shouldBeCalledOnce();
$response = $this->action->handle( $response = $this->action->handle(
(new ServerRequest())->withAttribute('shortCode', $shortCode) $this->requestWithApiKey()->withAttribute('shortCode', $shortCode)
->withQueryParams([ ->withQueryParams([
'endDate' => '2016-01-01 00:00:00', 'endDate' => '2016-01-01 00:00:00',
'page' => '3', 'page' => '3',
@ -65,4 +70,9 @@ class ShortUrlVisitsActionTest extends TestCase
); );
self::assertEquals(200, $response->getStatusCode()); self::assertEquals(200, $response->getStatusCode());
} }
private function requestWithApiKey(): ServerRequestInterface
{
return ServerRequestFactory::fromGlobals()->withAttribute(ApiKey::class, new ApiKey());
}
} }