Merge pull request #1333 from acelaya-forks/feature/all-visits-endpoint

Feature/all visits endpoint
This commit is contained in:
Alejandro Celaya 2022-01-16 12:49:01 +01:00 committed by GitHub
commit fb43885d85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 645 additions and 118 deletions

View file

@ -21,6 +21,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
Additionally, the endpoint also supports filtering by `searchTerm` query param. When provided, only tags matching it will be returned.
* [#1063](https://github.com/shlinkio/shlink/issues/1063) Added new endpoint that allows fetching all existing non-orphan visits, in case you need a global view of all visits received by your Shlink instance.
This can be achieved using the `GET /visits/non-orphan` endpoint.
### Changed
* [#1277](https://github.com/shlinkio/shlink/issues/1277) Reduced docker image size to 45% of the original size.
* [#1268](https://github.com/shlinkio/shlink/issues/1268) Updated dependencies, including symfony/console 6 and mezzio/mezzio-swoole 4.

View file

@ -0,0 +1,146 @@
{
"get": {
"operationId": "getNonOrphanVisits",
"tags": [
"Visits"
],
"summary": "List non-orphan visits",
"description": "Get the list of visits to any short URL.",
"parameters": [
{
"$ref": "../parameters/version.json"
},
{
"name": "startDate",
"in": "query",
"description": "The date (in ISO-8601 format) from which we want to get visits.",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "endDate",
"in": "query",
"description": "The date (in ISO-8601 format) until which we want to get visits.",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "page",
"in": "query",
"description": "The page to display. Defaults to 1",
"required": false,
"schema": {
"type": "number"
}
},
{
"name": "itemsPerPage",
"in": "query",
"description": "The amount of items to return on every page. Defaults to all the items",
"required": false,
"schema": {
"type": "number"
}
},
{
"name": "excludeBots",
"in": "query",
"description": "Tells if visits from potential bots should be excluded from the result set",
"required": false,
"schema": {
"type": "string",
"enum": ["true"]
}
}
],
"security": [
{
"ApiKey": []
}
],
"responses": {
"200": {
"description": "List of visits.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"visits": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "../definitions/Visit.json"
}
},
"pagination": {
"$ref": "../definitions/Pagination.json"
}
}
}
}
},
"example": {
"visits": {
"data": [
{
"referer": "https://twitter.com",
"date": "2015-08-20T05:05:03+04:00",
"userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0",
"visitLocation": null,
"potentialBot": false
},
{
"referer": "https://t.co",
"date": "2015-08-20T05:05:03+04:00",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
"visitLocation": {
"cityName": "Cupertino",
"countryCode": "US",
"countryName": "United States",
"latitude": 37.3042,
"longitude": -122.0946,
"regionName": "California",
"timezone": "America/Los_Angeles"
},
"potentialBot": false
},
{
"referer": null,
"date": "2015-08-20T05:05:03+04:00",
"userAgent": "some_web_crawler/1.4",
"visitLocation": null,
"potentialBot": true
}
],
"pagination": {
"currentPage": 5,
"pagesCount": 12,
"itemsPerPage": 10,
"itemsInCurrentPage": 10,
"totalItems": 115
}
}
}
}
}
},
"default": {
"description": "Unexpected error.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View file

@ -98,6 +98,9 @@
"/rest/v{version}/visits/orphan": {
"$ref": "paths/v2_visits_orphan.json"
},
"/rest/v{version}/visits/non-orphan": {
"$ref": "paths/v2_visits_non-orphan.json"
},
"/rest/v{version}/domains": {
"$ref": "paths/v2_domains.json"

View file

@ -18,7 +18,7 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
) {
}
public function getSlice($offset, $length): array // phpcs:ignore
public function getSlice(int $offset, int $length): iterable
{
return $this->repository->findList(
$length,

View file

@ -60,9 +60,7 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
}
$apiKey = $filtering?->apiKey();
if ($apiKey !== null) {
$this->applySpecification($subQb, $apiKey->spec(false, 'shortUrls'), 't');
}
$this->applySpecification($subQb, $apiKey?->spec(false, 'shortUrls'), 't');
$subQuery = $subQb->getQuery();
$subQuerySql = $subQuery->getSQL();

View file

@ -14,9 +14,8 @@ use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
use Shlinkio\Shlink\Core\Visit\Spec\CountOfNonOrphanVisits;
use Shlinkio\Shlink\Core\Visit\Spec\CountOfOrphanVisits;
use Shlinkio\Shlink\Core\Visit\Spec\CountOfShortUrlVisits;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use const PHP_INT_MAX;
@ -53,10 +52,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
public function findAllVisits(int $blockSize = self::DEFAULT_BLOCK_SIZE): iterable
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('v')
->from(Visit::class, 'v');
$qb = $this->createQueryBuilder('v');
return $this->visitsIterableForQuery($qb, $blockSize);
}
@ -107,11 +103,10 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
): QueryBuilder {
/** @var ShortUrlRepositoryInterface $shortUrlRepo */
$shortUrlRepo = $this->getEntityManager()->getRepository(ShortUrl::class);
$shortUrl = $shortUrlRepo->findOne($identifier, $filtering->spec());
$shortUrlId = $shortUrl?->getId() ?? '-1';
$shortUrlId = $shortUrlRepo->findOne($identifier, $filtering->apiKey()?->spec())?->getId() ?? '-1';
// Parameters in this query need to be part of the query itself, as we need to use it a sub-query later
// Since they are not strictly provided by the caller, it's reasonably safe
// Since they are not provided by the caller, it's reasonably safe
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->from(Visit::class, 'v')
->where($qb->expr()->eq('v.shortUrl', $shortUrlId));
@ -142,8 +137,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
private function createVisitsByTagQueryBuilder(string $tag, VisitsCountFiltering $filtering): QueryBuilder
{
// Parameters in this query need to be inlined, not bound, as we need to use it as sub-query later
// Since they are not strictly provided by the caller, it's reasonably safe
// Parameters in this query need to be inlined, not bound, as we need to use it as sub-query later.
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->from(Visit::class, 'v')
->join('v.shortUrl', 's')
@ -155,25 +149,15 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
}
$this->applyDatesInline($qb, $filtering->dateRange());
$this->applySpecification($qb, $filtering->spec(), 'v');
$this->applySpecification($qb, $filtering->apiKey()?->spec(true), 'v');
return $qb;
}
public function findOrphanVisits(VisitsListFiltering $filtering): array
{
// Parameters in this query need to be inlined, not bound, as we need to use it as sub-query later
// Since they are not strictly provided by the caller, it's reasonably safe
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->from(Visit::class, 'v')
->where($qb->expr()->isNull('v.shortUrl'));
if ($filtering->excludeBots()) {
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
}
$this->applyDatesInline($qb, $filtering->dateRange());
$qb = $this->createAllVisitsQueryBuilder($filtering);
$qb->andWhere($qb->expr()->isNull('v.shortUrl'));
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset());
}
@ -182,18 +166,49 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
return (int) $this->matchSingleScalarResult(new CountOfOrphanVisits($filtering));
}
public function countVisits(?ApiKey $apiKey = null): int
/**
* @return Visit[]
*/
public function findNonOrphanVisits(VisitsListFiltering $filtering): array
{
return (int) $this->matchSingleScalarResult(new CountOfShortUrlVisits($apiKey));
$qb = $this->createAllVisitsQueryBuilder($filtering);
$qb->andWhere($qb->expr()->isNotNull('v.shortUrl'));
$this->applySpecification($qb, $filtering->apiKey()?->spec(true));
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset());
}
public function countNonOrphanVisits(VisitsCountFiltering $filtering): int
{
return (int) $this->matchSingleScalarResult(new CountOfNonOrphanVisits($filtering));
}
private function createAllVisitsQueryBuilder(VisitsListFiltering $filtering): QueryBuilder
{
// Parameters in this query need to be inlined, not bound, as we need to use it as sub-query later
// Since they are not provided by the caller, it's reasonably safe
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->from(Visit::class, 'v');
if ($filtering->excludeBots()) {
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
}
$this->applyDatesInline($qb, $filtering->dateRange());
return $qb;
}
private function applyDatesInline(QueryBuilder $qb, ?DateRange $dateRange): void
{
$conn = $this->getEntityManager()->getConnection();
if ($dateRange?->startDate() !== null) {
$qb->andWhere($qb->expr()->gte('v.date', '\'' . $dateRange->startDate()->toDateTimeString() . '\''));
$qb->andWhere($qb->expr()->gte('v.date', $conn->quote($dateRange->startDate()->toDateTimeString())));
}
if ($dateRange?->endDate() !== null) {
$qb->andWhere($qb->expr()->lte('v.date', '\'' . $dateRange->endDate()->toDateTimeString() . '\''));
$qb->andWhere($qb->expr()->lte('v.date', $conn->quote($dateRange->endDate()->toDateTimeString())));
}
}

View file

@ -10,8 +10,8 @@ use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
// TODO Split into VisitsListsRepository and VisitsLocationRepository
interface VisitRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface
{
public const DEFAULT_BLOCK_SIZE = 10000;
@ -52,5 +52,10 @@ interface VisitRepositoryInterface extends ObjectRepository, EntitySpecification
public function countOrphanVisits(VisitsCountFiltering $filtering): int;
public function countVisits(?ApiKey $apiKey = null): int;
/**
* @return Visit[]
*/
public function findNonOrphanVisits(VisitsListFiltering $filtering): array;
public function countNonOrphanVisits(VisitsCountFiltering $filtering): int;
}

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit\Paginator\Adapter;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\AbstractCacheableCountPaginatorAdapter;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class NonOrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
{
public function __construct(
private VisitRepositoryInterface $repo,
private VisitsParams $params,
private ?ApiKey $apiKey,
) {
}
protected function doCount(): int
{
return $this->repo->countNonOrphanVisits(new VisitsCountFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->apiKey,
));
}
public function getSlice(int $offset, int $length): iterable
{
return $this->repo->findNonOrphanVisits(new VisitsListFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->apiKey,
$length,
$offset,
));
}
}

View file

@ -2,9 +2,10 @@
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Paginator\Adapter;
namespace Shlinkio\Shlink\Core\Visit\Paginator\Adapter;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\AbstractCacheableCountPaginatorAdapter;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
@ -23,7 +24,7 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
));
}
public function getSlice($offset, $length): iterable // phpcs:ignore
public function getSlice(int $offset, int $length): iterable
{
return $this->repo->findOrphanVisits(new VisitsListFiltering(
$this->params->getDateRange(),

View file

@ -2,33 +2,34 @@
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Paginator\Adapter;
namespace Shlinkio\Shlink\Core\Visit\Paginator\Adapter;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\AbstractCacheableCountPaginatorAdapter;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class VisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
class ShortUrlVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
{
public function __construct(
private VisitRepositoryInterface $visitRepository,
private ShortUrlIdentifier $identifier,
private VisitsParams $params,
private ?Specification $spec,
private ?ApiKey $apiKey,
) {
}
public function getSlice($offset, $length): array // phpcs:ignore
public function getSlice(int $offset, int $length): iterable
{
return $this->visitRepository->findVisitsByShortCode(
$this->identifier,
new VisitsListFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->spec,
$this->apiKey,
$length,
$offset,
),
@ -42,7 +43,7 @@ class VisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
new VisitsCountFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->spec,
$this->apiKey,
),
);
}

View file

@ -2,15 +2,16 @@
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Paginator\Adapter;
namespace Shlinkio\Shlink\Core\Visit\Paginator\Adapter;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\AbstractCacheableCountPaginatorAdapter;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class VisitsForTagPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
class TagVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
{
public function __construct(
private VisitRepositoryInterface $visitRepository,
@ -20,14 +21,14 @@ class VisitsForTagPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
) {
}
public function getSlice($offset, $length): array // phpcs:ignore
public function getSlice(int $offset, int $length): iterable
{
return $this->visitRepository->findVisitsByTag(
$this->tag,
new VisitsListFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->apiKey?->spec(true),
$this->apiKey,
$length,
$offset,
),
@ -41,7 +42,7 @@ class VisitsForTagPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
new VisitsCountFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->apiKey?->spec(true),
$this->apiKey,
),
);
}

View file

@ -4,18 +4,23 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit\Persistence;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class VisitsCountFiltering
{
public function __construct(
private ?DateRange $dateRange = null,
private bool $excludeBots = false,
private ?Specification $spec = null,
private ?ApiKey $apiKey = null,
) {
}
public static function withApiKey(?ApiKey $apiKey): self
{
return new self(null, false, $apiKey);
}
public function dateRange(): ?DateRange
{
return $this->dateRange;
@ -26,8 +31,8 @@ class VisitsCountFiltering
return $this->excludeBots;
}
public function spec(): ?Specification
public function apiKey(): ?ApiKey
{
return $this->spec;
return $this->apiKey;
}
}

View file

@ -4,19 +4,19 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit\Persistence;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
final class VisitsListFiltering extends VisitsCountFiltering
{
public function __construct(
?DateRange $dateRange = null,
bool $excludeBots = false,
?Specification $spec = null,
?ApiKey $apiKey = null,
private ?int $limit = null,
private ?int $offset = null,
) {
parent::__construct($dateRange, $excludeBots, $spec);
parent::__construct($dateRange, $excludeBots, $apiKey);
}
public function limit(): ?int

View file

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit\Spec;
use Happyr\DoctrineSpecification\Spec;
use Happyr\DoctrineSpecification\Specification\BaseSpecification;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Core\Spec\InDateRange;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Rest\ApiKey\Spec\WithApiKeySpecsEnsuringJoin;
class CountOfNonOrphanVisits extends BaseSpecification
{
public function __construct(private VisitsCountFiltering $filtering)
{
parent::__construct();
}
protected function getSpec(): Specification
{
$conditions = [
Spec::isNotNull('shortUrl'),
new InDateRange($this->filtering->dateRange()),
];
if ($this->filtering->excludeBots()) {
$conditions[] = Spec::eq('potentialBot', false);
}
$apiKey = $this->filtering->apiKey();
if ($apiKey !== null) {
$conditions[] = new WithApiKeySpecsEnsuringJoin($apiKey, 'shortUrl');
}
return Spec::countOf(Spec::andX(...$conditions));
}
}

View file

@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit\Spec;
use Happyr\DoctrineSpecification\Spec;
use Happyr\DoctrineSpecification\Specification\BaseSpecification;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Rest\ApiKey\Spec\WithApiKeySpecsEnsuringJoin;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class CountOfShortUrlVisits extends BaseSpecification
{
public function __construct(private ?ApiKey $apiKey)
{
parent::__construct();
}
protected function getSpec(): Specification
{
return Spec::countOf(Spec::andX(
Spec::isNotNull('shortUrl'),
new WithApiKeySpecsEnsuringJoin($this->apiKey, 'shortUrl'),
));
}
}

View file

@ -14,14 +14,15 @@ 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\OrphanVisitsPaginatorAdapter;
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\Core\Visit\Paginator\Adapter\NonOrphanVisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\OrphanVisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\ShortUrlVisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\TagVisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@ -37,7 +38,7 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface
$visitsRepo = $this->em->getRepository(Visit::class);
return new VisitsStats(
$visitsRepo->countVisits($apiKey),
$visitsRepo->countNonOrphanVisits(VisitsCountFiltering::withApiKey($apiKey)),
$visitsRepo->countOrphanVisits(new VisitsCountFiltering()),
);
}
@ -51,18 +52,19 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface
VisitsParams $params,
?ApiKey $apiKey = null,
): Paginator {
$spec = $apiKey?->spec();
/** @var ShortUrlRepositoryInterface $repo */
$repo = $this->em->getRepository(ShortUrl::class);
if (! $repo->shortCodeIsInUse($identifier, $spec)) {
if (! $repo->shortCodeIsInUse($identifier, $apiKey?->spec())) {
throw ShortUrlNotFoundException::fromNotFound($identifier);
}
/** @var VisitRepositoryInterface $repo */
$repo = $this->em->getRepository(Visit::class);
return $this->createPaginator(new VisitsPaginatorAdapter($repo, $identifier, $params, $spec), $params);
return $this->createPaginator(
new ShortUrlVisitsPaginatorAdapter($repo, $identifier, $params, $apiKey),
$params,
);
}
/**
@ -80,7 +82,7 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface
/** @var VisitRepositoryInterface $repo */
$repo = $this->em->getRepository(Visit::class);
return $this->createPaginator(new VisitsForTagPaginatorAdapter($repo, $tag, $params, $apiKey), $params);
return $this->createPaginator(new TagVisitsPaginatorAdapter($repo, $tag, $params, $apiKey), $params);
}
/**
@ -94,6 +96,14 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface
return $this->createPaginator(new OrphanVisitsPaginatorAdapter($repo, $params), $params);
}
public function nonOrphanVisits(VisitsParams $params, ?ApiKey $apiKey = null): Paginator
{
/** @var VisitRepositoryInterface $repo */
$repo = $this->em->getRepository(Visit::class);
return $this->createPaginator(new NonOrphanVisitsPaginatorAdapter($repo, $params, $apiKey), $params);
}
private function createPaginator(AdapterInterface $adapter, VisitsParams $params): Paginator
{
$paginator = new Paginator($adapter);

View file

@ -37,4 +37,9 @@ interface VisitsStatsHelperInterface
* @return Visit[]|Paginator
*/
public function orphanVisits(VisitsParams $params): Paginator;
/**
* @return Visit[]|Paginator
*/
public function nonOrphanVisits(VisitsParams $params, ?ApiKey $apiKey = null): Paginator;
}

View file

@ -29,6 +29,9 @@ use function Functional\map;
use function is_string;
use function range;
use function sprintf;
use function str_pad;
use const STR_PAD_LEFT;
class VisitRepositoryTest extends DatabaseTestCase
{
@ -189,19 +192,19 @@ class VisitRepositoryTest extends DatabaseTestCase
self::assertNotEmpty($this->repo->findVisitsByShortCode(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode1),
new VisitsListFiltering(null, false, $adminApiKey->spec()),
new VisitsListFiltering(null, false, $adminApiKey),
));
self::assertNotEmpty($this->repo->findVisitsByShortCode(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode2),
new VisitsListFiltering(null, false, $adminApiKey->spec()),
new VisitsListFiltering(null, false, $adminApiKey),
));
self::assertEmpty($this->repo->findVisitsByShortCode(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode1),
new VisitsListFiltering(null, false, $restrictedApiKey->spec()),
new VisitsListFiltering(null, false, $restrictedApiKey),
));
self::assertNotEmpty($this->repo->findVisitsByShortCode(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode2),
new VisitsListFiltering(null, false, $restrictedApiKey->spec()),
new VisitsListFiltering(null, false, $restrictedApiKey),
));
}
@ -294,10 +297,20 @@ class VisitRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->flush();
self::assertEquals(4 + 5 + 7, $this->repo->countVisits());
self::assertEquals(4, $this->repo->countVisits($apiKey1));
self::assertEquals(5 + 7, $this->repo->countVisits($apiKey2));
self::assertEquals(4 + 7, $this->repo->countVisits($domainApiKey));
self::assertEquals(4 + 5 + 7, $this->repo->countNonOrphanVisits(new VisitsCountFiltering()));
self::assertEquals(4, $this->repo->countNonOrphanVisits(VisitsCountFiltering::withApiKey($apiKey1)));
self::assertEquals(5 + 7, $this->repo->countNonOrphanVisits(VisitsCountFiltering::withApiKey($apiKey2)));
self::assertEquals(4 + 7, $this->repo->countNonOrphanVisits(VisitsCountFiltering::withApiKey($domainApiKey)));
self::assertEquals(4, $this->repo->countNonOrphanVisits(new VisitsCountFiltering(DateRange::withStartDate(
Chronos::parse('2016-01-05')->startOfDay(),
))));
self::assertEquals(2, $this->repo->countNonOrphanVisits(new VisitsCountFiltering(DateRange::withStartDate(
Chronos::parse('2016-01-03')->startOfDay(),
), false, $apiKey1)));
self::assertEquals(1, $this->repo->countNonOrphanVisits(new VisitsCountFiltering(DateRange::withStartDate(
Chronos::parse('2016-01-07')->startOfDay(),
), false, $apiKey2)));
self::assertEquals(3 + 5, $this->repo->countNonOrphanVisits(new VisitsCountFiltering(null, true, $apiKey2)));
self::assertEquals(4, $this->repo->countOrphanVisits(new VisitsCountFiltering()));
self::assertEquals(3, $this->repo->countOrphanVisits(new VisitsCountFiltering(null, true)));
}
@ -388,6 +401,49 @@ class VisitRepositoryTest extends DatabaseTestCase
));
}
/** @test */
public function findNonOrphanVisitsReturnsExpectedResult(): void
{
$shortUrl = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['longUrl' => '1']));
$this->getEntityManager()->persist($shortUrl);
$this->createVisitsForShortUrl($shortUrl, 7);
$shortUrl2 = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['longUrl' => '2']));
$this->getEntityManager()->persist($shortUrl2);
$this->createVisitsForShortUrl($shortUrl2, 4);
$shortUrl3 = ShortUrl::fromMeta(ShortUrlMeta::fromRawData(['longUrl' => '3']));
$this->getEntityManager()->persist($shortUrl3);
$this->createVisitsForShortUrl($shortUrl3, 10);
$this->getEntityManager()->flush();
self::assertCount(21, $this->repo->findNonOrphanVisits(new VisitsListFiltering()));
self::assertCount(21, $this->repo->findNonOrphanVisits(new VisitsListFiltering(DateRange::emptyInstance())));
self::assertCount(7, $this->repo->findNonOrphanVisits(new VisitsListFiltering(DateRange::withStartDate(
Chronos::parse('2016-01-05')->endOfDay(),
))));
self::assertCount(12, $this->repo->findNonOrphanVisits(new VisitsListFiltering(DateRange::withEndDate(
Chronos::parse('2016-01-04')->endOfDay(),
))));
self::assertCount(6, $this->repo->findNonOrphanVisits(new VisitsListFiltering(DateRange::withStartAndEndDate(
Chronos::parse('2016-01-03')->startOfDay(),
Chronos::parse('2016-01-04')->endOfDay(),
))));
self::assertCount(13, $this->repo->findNonOrphanVisits(new VisitsListFiltering(DateRange::withStartAndEndDate(
Chronos::parse('2016-01-03')->startOfDay(),
Chronos::parse('2016-01-08')->endOfDay(),
))));
self::assertCount(3, $this->repo->findNonOrphanVisits(new VisitsListFiltering(DateRange::withStartAndEndDate(
Chronos::parse('2016-01-03')->startOfDay(),
Chronos::parse('2016-01-08')->endOfDay(),
), false, null, 10, 10)));
self::assertCount(15, $this->repo->findNonOrphanVisits(new VisitsListFiltering(null, true)));
self::assertCount(10, $this->repo->findNonOrphanVisits(new VisitsListFiltering(null, false, null, 10)));
self::assertCount(1, $this->repo->findNonOrphanVisits(new VisitsListFiltering(null, false, null, 10, 20)));
self::assertCount(5, $this->repo->findNonOrphanVisits(new VisitsListFiltering(null, false, null, 5, 5)));
}
/**
* @return array{string, string, ShortUrl}
*/
@ -429,7 +485,7 @@ class VisitRepositoryTest extends DatabaseTestCase
$shortUrl,
$botsAmount < 1 ? Visitor::emptyInstance() : Visitor::botInstance(),
),
Chronos::parse(sprintf('2016-01-0%s', $i + 1)),
Chronos::parse(sprintf('2016-01-%s', str_pad((string) ($i + 1), 2, '0', STR_PAD_LEFT)))->startOfDay(),
);
$botsAmount--;

View file

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Visit\Paginator\Adapter;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\NonOrphanVisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class NonOrphanVisitsPaginatorAdapterTest extends TestCase
{
use ProphecyTrait;
private NonOrphanVisitsPaginatorAdapter $adapter;
private ObjectProphecy $repo;
private VisitsParams $params;
private ApiKey $apiKey;
protected function setUp(): void
{
$this->repo = $this->prophesize(VisitRepositoryInterface::class);
$this->params = VisitsParams::fromRawData([]);
$this->apiKey = ApiKey::create();
$this->adapter = new NonOrphanVisitsPaginatorAdapter($this->repo->reveal(), $this->params, $this->apiKey);
}
/** @test */
public function countDelegatesToRepository(): void
{
$expectedCount = 5;
$repoCount = $this->repo->countNonOrphanVisits(
new VisitsCountFiltering($this->params->getDateRange(), $this->params->excludeBots(), $this->apiKey),
)->willReturn($expectedCount);
$result = $this->adapter->getNbResults();
self::assertEquals($expectedCount, $result);
$repoCount->shouldHaveBeenCalledOnce();
}
/**
* @test
* @dataProvider provideLimitAndOffset
*/
public function getSliceDelegatesToRepository(int $limit, int $offset): void
{
$visitor = Visitor::emptyInstance();
$list = [Visit::forRegularNotFound($visitor), Visit::forInvalidShortUrl($visitor)];
$repoFind = $this->repo->findNonOrphanVisits(new VisitsListFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->apiKey,
$limit,
$offset,
))->willReturn($list);
$result = $this->adapter->getSlice($offset, $limit);
self::assertEquals($list, $result);
$repoFind->shouldHaveBeenCalledOnce();
}
public function provideLimitAndOffset(): iterable
{
yield [1, 5];
yield [10, 4];
yield [30, 18];
}
}

View file

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Paginator\Adapter;
namespace ShlinkioTest\Shlink\Core\Visit\Paginator\Adapter;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
@ -10,8 +10,8 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\OrphanVisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\OrphanVisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;

View file

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Paginator\Adapter;
namespace ShlinkioTest\Shlink\Core\Visit\Paginator\Adapter;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
@ -10,13 +10,13 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\ShortUrlVisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class VisitsPaginatorAdapterTest extends TestCase
class ShortUrlVisitsPaginatorAdapterTest extends TestCase
{
use ProphecyTrait;
@ -54,7 +54,7 @@ class VisitsPaginatorAdapterTest extends TestCase
$adapter = $this->createAdapter($apiKey);
$countVisits = $this->repo->countVisitsByShortCode(
ShortUrlIdentifier::fromShortCodeAndDomain(''),
new VisitsCountFiltering(DateRange::emptyInstance(), false, $apiKey->spec()),
new VisitsCountFiltering(DateRange::emptyInstance(), false, $apiKey),
)->willReturn(3);
for ($i = 0; $i < $count; $i++) {
@ -64,13 +64,13 @@ class VisitsPaginatorAdapterTest extends TestCase
$countVisits->shouldHaveBeenCalledOnce();
}
private function createAdapter(?ApiKey $apiKey): VisitsPaginatorAdapter
private function createAdapter(?ApiKey $apiKey): ShortUrlVisitsPaginatorAdapter
{
return new VisitsPaginatorAdapter(
return new ShortUrlVisitsPaginatorAdapter(
$this->repo->reveal(),
new ShortUrlIdentifier(''),
VisitsParams::fromRawData([]),
$apiKey?->spec(),
$apiKey,
);
}
}

View file

@ -2,15 +2,15 @@
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Paginator\Adapter;
namespace ShlinkioTest\Shlink\Core\Visit\Paginator\Adapter;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsForTagPaginatorAdapter;
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\TagVisitsPaginatorAdapter;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@ -53,7 +53,7 @@ class VisitsForTagPaginatorAdapterTest extends TestCase
$adapter = $this->createAdapter($apiKey);
$countVisits = $this->repo->countVisitsByTag(
'foo',
new VisitsCountFiltering(DateRange::emptyInstance(), false, $apiKey->spec()),
new VisitsCountFiltering(DateRange::emptyInstance(), false, $apiKey),
)->willReturn(3);
for ($i = 0; $i < $count; $i++) {
@ -63,9 +63,9 @@ class VisitsForTagPaginatorAdapterTest extends TestCase
$countVisits->shouldHaveBeenCalledOnce();
}
private function createAdapter(?ApiKey $apiKey): VisitsForTagPaginatorAdapter
private function createAdapter(?ApiKey $apiKey): TagVisitsPaginatorAdapter
{
return new VisitsForTagPaginatorAdapter(
return new TagVisitsPaginatorAdapter(
$this->repo->reveal(),
'foo',
VisitsParams::fromRawData([]),

View file

@ -53,7 +53,7 @@ class VisitsStatsHelperTest extends TestCase
public function returnsExpectedVisitsStats(int $expectedCount): void
{
$repo = $this->prophesize(VisitRepository::class);
$count = $repo->countVisits(null)->willReturn($expectedCount * 3);
$count = $repo->countNonOrphanVisits(new VisitsCountFiltering())->willReturn($expectedCount * 3);
$countOrphan = $repo->countOrphanVisits(Argument::type(VisitsCountFiltering::class))->willReturn(
$expectedCount,
);
@ -174,4 +174,23 @@ class VisitsStatsHelperTest extends TestCase
$countVisits->shouldHaveBeenCalledOnce();
$getRepo->shouldHaveBeenCalledOnce();
}
/** @test */
public function nonOrphanVisitsAreReturnedAsExpected(): void
{
$list = map(range(0, 3), fn () => Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance()));
$repo = $this->prophesize(VisitRepository::class);
$countVisits = $repo->countNonOrphanVisits(Argument::type(VisitsCountFiltering::class))->willReturn(
count($list),
);
$listVisits = $repo->findNonOrphanVisits(Argument::type(VisitsListFiltering::class))->willReturn($list);
$getRepo = $this->em->getRepository(Visit::class)->willReturn($repo->reveal());
$paginator = $this->helper->nonOrphanVisits(new VisitsParams());
self::assertEquals($list, ArrayUtils::iteratorToArray($paginator->getCurrentPageResults()));
$listVisits->shouldHaveBeenCalledOnce();
$countVisits->shouldHaveBeenCalledOnce();
$getRepo->shouldHaveBeenCalledOnce();
}
}

View file

@ -34,6 +34,7 @@ return [
Action\Visit\TagVisitsAction::class => ConfigAbstractFactory::class,
Action\Visit\GlobalVisitsAction::class => ConfigAbstractFactory::class,
Action\Visit\OrphanVisitsAction::class => ConfigAbstractFactory::class,
Action\Visit\NonOrphanVisitsAction::class => ConfigAbstractFactory::class,
Action\Tag\ListTagsAction::class => ConfigAbstractFactory::class,
Action\Tag\TagsStatsAction::class => ConfigAbstractFactory::class,
Action\Tag\DeleteTagsAction::class => ConfigAbstractFactory::class,
@ -74,6 +75,7 @@ return [
Visit\VisitsStatsHelper::class,
Visit\Transformer\OrphanVisitDataTransformer::class,
],
Action\Visit\NonOrphanVisitsAction::class => [Visit\VisitsStatsHelper::class],
Action\ShortUrl\ListShortUrlsAction::class => [Service\ShortUrlService::class, ShortUrlDataTransformer::class],
Action\Tag\ListTagsAction::class => [TagService::class],
Action\Tag\TagsStatsAction::class => [TagService::class],

View file

@ -34,6 +34,7 @@ return [
Action\Visit\TagVisitsAction::getRouteDef(),
Action\Visit\GlobalVisitsAction::getRouteDef(),
Action\Visit\OrphanVisitsAction::getRouteDef(),
Action\Visit\NonOrphanVisitsAction::getRouteDef(),
// Tags
Action\Tag\ListTagsAction::getRouteDef(),

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\Visit;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
class NonOrphanVisitsAction extends AbstractRestAction
{
use PagerfantaUtilsTrait;
protected const ROUTE_PATH = '/visits/non-orphan';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
public function __construct(private VisitsStatsHelperInterface $visitsHelper)
{
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$params = VisitsParams::fromRawData($request->getQueryParams());
$apiKey = AuthenticationMiddleware::apiKeyFromRequest($request);
$visits = $this->visitsHelper->nonOrphanVisits($params, $apiKey);
return new JsonResponse([
'visits' => $this->serializePaginator($visits),
]);
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace ShlinkioApiTest\Shlink\Rest\Action;
use Cake\Chronos\Chronos;
use GuzzleHttp\RequestOptions;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
class NonOrphanVisitsTest extends ApiTestCase
{
/**
* @test
* @dataProvider provideQueries
*/
public function properVisitsAreReturnedBasedInQuery(array $query, int $totalItems, int $returnedItems): void
{
$resp = $this->callApiWithKey(self::METHOD_GET, '/visits/non-orphan', [RequestOptions::QUERY => $query]);
$payload = $this->getJsonResponsePayload($resp);
self::assertEquals($totalItems, $payload['visits']['pagination']['totalItems'] ?? Paginator::ALL_ITEMS);
self::assertCount($returnedItems, $payload['visits']['data'] ?? []);
}
public function provideQueries(): iterable
{
yield 'all data' => [[], 7, 7];
yield 'middle page' => [['page' => 2, 'itemsPerPage' => 3], 7, 3];
yield 'last page' => [['page' => 3, 'itemsPerPage' => 3], 7, 1];
yield 'bots excluded' => [['excludeBots' => 'true'], 6, 6];
yield 'bots excluded and pagination' => [['excludeBots' => 'true', 'page' => 1, 'itemsPerPage' => 4], 6, 4];
yield 'date filter' => [['startDate' => Chronos::now()->addDay()->toAtomString()], 0, 0];
}
}

View file

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action\Visit;
use Laminas\Diactoros\Response\JsonResponse;
use Laminas\Diactoros\ServerRequestFactory;
use Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Core\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Shlinkio\Shlink\Rest\Action\Visit\NonOrphanVisitsAction;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class NonOrphanVisitsActionTest extends TestCase
{
use ProphecyTrait;
private NonOrphanVisitsAction $action;
private ObjectProphecy $visitsHelper;
public function setUp(): void
{
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
$this->action = new NonOrphanVisitsAction($this->visitsHelper->reveal());
}
/** @test */
public function requestIsHandled(): void
{
$apiKey = ApiKey::create();
$getVisits = $this->visitsHelper->nonOrphanVisits(Argument::type(VisitsParams::class), $apiKey)->willReturn(
new Paginator(new ArrayAdapter([])),
);
/** @var JsonResponse $response */
$response = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute(ApiKey::class, $apiKey));
$payload = $response->getPayload();
self::assertEquals(200, $response->getStatusCode());
self::assertArrayHasKey('visits', $payload);
$getVisits->shouldHaveBeenCalledOnce();
}
}

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action\Visit;
use Laminas\Diactoros\ServerRequest;
use Laminas\Diactoros\ServerRequestFactory;
use Pagerfanta\Adapter\ArrayAdapter;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
@ -30,7 +30,7 @@ class TagVisitsActionTest extends TestCase
}
/** @test */
public function providingCorrectShortCodeReturnsVisits(): void
public function providingCorrectTagReturnsVisits(): void
{
$tag = 'foo';
$apiKey = ApiKey::create();
@ -39,7 +39,7 @@ class TagVisitsActionTest extends TestCase
);
$response = $this->action->handle(
(new ServerRequest())->withAttribute('tag', $tag)->withAttribute(ApiKey::class, $apiKey),
ServerRequestFactory::fromGlobals()->withAttribute('tag', $tag)->withAttribute(ApiKey::class, $apiKey),
);
self::assertEquals(200, $response->getStatusCode());