mirror of
https://github.com/shlinkio/shlink.git
synced 2025-03-14 04:00:57 +03:00
Merge pull request #1432 from acelaya-forks/feature/domain-visits
Feature/domain visits
This commit is contained in:
commit
ca9726c997
25 changed files with 683 additions and 51 deletions
|
@ -10,6 +10,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
|||
* [#1416](https://github.com/shlinkio/shlink/issues/1416) Added support to import URLs from Kutt.it.
|
||||
* [#1418](https://github.com/shlinkio/shlink/issues/1418) Added support to customize the timezone used by Shlink, falling back to the default one set in PHP config.
|
||||
* [#1309](https://github.com/shlinkio/shlink/issues/1309) Improved URL importing, ensuring individual errors do not make the whole process fail, and instead, failing URLs are skipped.
|
||||
* [#1162](https://github.com/shlinkio/shlink/issues/1162) Added new endpoint to get visits by domain.
|
||||
|
||||
The endpoint is `GET /domains/{domain}/visits`, and it has the same capabilities as any other visits endpoint, allowing pagination and filtering.
|
||||
|
||||
### Changed
|
||||
* [#1359](https://github.com/shlinkio/shlink/issues/1359) Hidden database commands.
|
||||
|
|
|
@ -6,7 +6,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
|
|||
|
||||
use const Shlinkio\Shlink\MIN_TASK_WORKERS;
|
||||
|
||||
return (static function () {
|
||||
return (static function (): array {
|
||||
$taskWorkers = (int) EnvVars::TASK_WORKER_NUM()->loadFromEnv(16);
|
||||
|
||||
return [
|
||||
|
|
|
@ -16,7 +16,7 @@ return (static function (): array {
|
|||
return [
|
||||
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'domain' => [ // TODO Refactor this structure to url_shortener.schema and url_shortener.default_domain
|
||||
'schema' => ((bool) EnvVars::IS_HTTPS_ENABLED()->loadFromEnv(true)) ? 'https' : 'http',
|
||||
'hostname' => EnvVars::DEFAULT_DOMAIN()->loadFromEnv(''),
|
||||
],
|
||||
|
|
|
@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Application as CliApp;
|
||||
|
||||
return (static function () {
|
||||
return (static function (): CliApp {
|
||||
/** @var ContainerInterface $container */
|
||||
$container = include __DIR__ . '/container.php';
|
||||
return $container->get(CliApp::class);
|
||||
|
|
|
@ -22,7 +22,7 @@ if (! class_exists(LOCAL_LOCK_FACTORY)) {
|
|||
}
|
||||
|
||||
// Build container
|
||||
return (static function () {
|
||||
return (static function (): ServiceManager {
|
||||
$config = require __DIR__ . '/config.php';
|
||||
$container = new ServiceManager($config['dependencies']);
|
||||
$container->setService('config', $config);
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
return (static function () {
|
||||
return (static function (): EntityManagerInterface {
|
||||
/** @var ContainerInterface $container */
|
||||
$container = include __DIR__ . '/container.php';
|
||||
return $container->get(EntityManager::class);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"value": {
|
||||
"detail":"No URL found with short code \"abc123\"",
|
||||
"title":"Short URL not found",
|
||||
"detail": "No URL found with short code \"abc123\"",
|
||||
"title": "Short URL not found",
|
||||
"type": "INVALID_SHORTCODE",
|
||||
"status": 404,
|
||||
"shortCode": "abc123"
|
||||
|
|
172
docs/swagger/paths/v2_domains_{domain}_visits.json
Normal file
172
docs/swagger/paths/v2_domains_{domain}_visits.json
Normal file
|
@ -0,0 +1,172 @@
|
|||
{
|
||||
"get": {
|
||||
"operationId": "getDomainVisits",
|
||||
"tags": [
|
||||
"Visits"
|
||||
],
|
||||
"summary": "List visits for domain",
|
||||
"description": "Get the list of visits on any short URL which belongs to provided domain.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "domain",
|
||||
"in": "path",
|
||||
"description": "The domain from which we want to get the visits, or **DEFAULT** keyword for default domain.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The domain does not exist.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"example": {
|
||||
"detail": "Domain with authority \"example.com\" could not be found",
|
||||
"title": "Domain not found",
|
||||
"type": "DOMAIN_NOT_FOUND",
|
||||
"status": 404,
|
||||
"authority": "example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -95,6 +95,9 @@
|
|||
"/rest/v{version}/tags/{tag}/visits": {
|
||||
"$ref": "paths/v2_tags_{tag}_visits.json"
|
||||
},
|
||||
"/rest/v{version}/domains/{domain}/visits": {
|
||||
"$ref": "paths/v2_domains_{domain}_visits.json"
|
||||
},
|
||||
"/rest/v{version}/visits/orphan": {
|
||||
"$ref": "paths/v2_visits_orphan.json"
|
||||
},
|
||||
|
|
|
@ -66,9 +66,8 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||
{
|
||||
$buildTimestamp = $this->resolveBuildTimestamp($meta);
|
||||
$buildDate = Chronos::createFromTimestamp($buildTimestamp);
|
||||
$now = Chronos::now();
|
||||
|
||||
return $now->gt($buildDate->addDays(35));
|
||||
return Chronos::now()->gt($buildDate->addDays(35));
|
||||
}
|
||||
|
||||
private function resolveBuildTimestamp(Metadata $meta): int
|
||||
|
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink\Core\Domain\Repository;
|
||||
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
|
||||
use Happyr\DoctrineSpecification\Spec;
|
||||
use Shlinkio\Shlink\Core\Domain\Spec\IsDomain;
|
||||
|
@ -40,8 +41,25 @@ class DomainRepository extends EntitySpecificationRepository implements DomainRe
|
|||
|
||||
public function findOneByAuthority(string $authority, ?ApiKey $apiKey = null): ?Domain
|
||||
{
|
||||
$qb = $this->createQueryBuilder('d');
|
||||
$qb->leftJoin(ShortUrl::class, 's', Join::WITH, 's.domain = d')
|
||||
$qb = $this->createDomainQueryBuilder($authority, $apiKey);
|
||||
$qb->select('d');
|
||||
|
||||
return $qb->getQuery()->getOneOrNullResult();
|
||||
}
|
||||
|
||||
public function domainExists(string $authority, ?ApiKey $apiKey = null): bool
|
||||
{
|
||||
$qb = $this->createDomainQueryBuilder($authority, $apiKey);
|
||||
$qb->select('COUNT(d.id)');
|
||||
|
||||
return ((int) $qb->getQuery()->getSingleScalarResult()) > 0;
|
||||
}
|
||||
|
||||
private function createDomainQueryBuilder(string $authority, ?ApiKey $apiKey): QueryBuilder
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->from(Domain::class, 'd')
|
||||
->leftJoin(ShortUrl::class, 's', Join::WITH, 's.domain = d')
|
||||
->where($qb->expr()->eq('d.authority', ':authority'))
|
||||
->setParameter('authority', $authority)
|
||||
->setMaxResults(1);
|
||||
|
@ -51,7 +69,7 @@ class DomainRepository extends EntitySpecificationRepository implements DomainRe
|
|||
$this->applySpecification($qb, $spec, $alias);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getOneOrNullResult();
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function determineExtraSpecs(?ApiKey $apiKey): iterable
|
||||
|
|
|
@ -17,4 +17,6 @@ interface DomainRepositoryInterface extends ObjectRepository, EntitySpecificatio
|
|||
public function findDomains(?ApiKey $apiKey = null): array;
|
||||
|
||||
public function findOneByAuthority(string $authority, ?ApiKey $apiKey = null): ?Domain;
|
||||
|
||||
public function domainExists(string $authority, ?ApiKey $apiKey = null): bool;
|
||||
}
|
||||
|
|
|
@ -154,6 +154,47 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
|
|||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Visit[]
|
||||
*/
|
||||
public function findVisitsByDomain(string $domain, VisitsListFiltering $filtering): array
|
||||
{
|
||||
$qb = $this->createVisitsByDomainQueryBuilder($domain, $filtering);
|
||||
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset());
|
||||
}
|
||||
|
||||
public function countVisitsByDomain(string $domain, VisitsCountFiltering $filtering): int
|
||||
{
|
||||
$qb = $this->createVisitsByDomainQueryBuilder($domain, $filtering);
|
||||
$qb->select('COUNT(v.id)');
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
private function createVisitsByDomainQueryBuilder(string $domain, VisitsCountFiltering $filtering): QueryBuilder
|
||||
{
|
||||
// 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');
|
||||
|
||||
if ($domain === 'DEFAULT') {
|
||||
$qb->where($qb->expr()->isNull('s.domain'));
|
||||
} else {
|
||||
$qb->join('s.domain', 'd')
|
||||
->where($qb->expr()->eq('d.authority', $this->getEntityManager()->getConnection()->quote($domain)));
|
||||
}
|
||||
|
||||
if ($filtering->excludeBots()) {
|
||||
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
|
||||
}
|
||||
|
||||
$this->applyDatesInline($qb, $filtering->dateRange());
|
||||
$this->applySpecification($qb, $filtering->apiKey()?->inlinedSpec(), 'v');
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function findOrphanVisits(VisitsListFiltering $filtering): array
|
||||
{
|
||||
$qb = $this->createAllVisitsQueryBuilder($filtering);
|
||||
|
|
|
@ -45,6 +45,13 @@ interface VisitRepositoryInterface extends ObjectRepository, EntitySpecification
|
|||
|
||||
public function countVisitsByTag(string $tag, VisitsCountFiltering $filtering): int;
|
||||
|
||||
/**
|
||||
* @return Visit[]
|
||||
*/
|
||||
public function findVisitsByDomain(string $domain, VisitsListFiltering $filtering): array;
|
||||
|
||||
public function countVisitsByDomain(string $domain, VisitsCountFiltering $filtering): int;
|
||||
|
||||
/**
|
||||
* @return Visit[]
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
<?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 DomainVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
|
||||
{
|
||||
public function __construct(
|
||||
private VisitRepositoryInterface $visitRepository,
|
||||
private string $domain,
|
||||
private VisitsParams $params,
|
||||
private ?ApiKey $apiKey,
|
||||
) {
|
||||
}
|
||||
|
||||
protected function doCount(): int
|
||||
{
|
||||
return $this->visitRepository->countVisitsByDomain(
|
||||
$this->domain,
|
||||
new VisitsCountFiltering(
|
||||
$this->params->getDateRange(),
|
||||
$this->params->excludeBots(),
|
||||
$this->apiKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getSlice(int $offset, int $length): iterable
|
||||
{
|
||||
return $this->visitRepository->findVisitsByDomain(
|
||||
$this->domain,
|
||||
new VisitsListFiltering(
|
||||
$this->params->getDateRange(),
|
||||
$this->params->excludeBots(),
|
||||
$this->apiKey,
|
||||
$length,
|
||||
$offset,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -7,9 +7,12 @@ namespace Shlinkio\Shlink\Core\Visit;
|
|||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Pagerfanta\Adapter\AdapterInterface;
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Exception\DomainNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
|
@ -19,6 +22,7 @@ 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\DomainVisitsPaginatorAdapter;
|
||||
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\NonOrphanVisitsPaginatorAdapter;
|
||||
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\OrphanVisitsPaginatorAdapter;
|
||||
use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\ShortUrlVisitsPaginatorAdapter;
|
||||
|
@ -85,6 +89,24 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface
|
|||
return $this->createPaginator(new TagVisitsPaginatorAdapter($repo, $tag, $params, $apiKey), $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Visit[]|Paginator
|
||||
* @throws DomainNotFoundException
|
||||
*/
|
||||
public function visitsForDomain(string $domain, VisitsParams $params, ?ApiKey $apiKey = null): Paginator
|
||||
{
|
||||
/** @var DomainRepository $domainRepo */
|
||||
$domainRepo = $this->em->getRepository(Domain::class);
|
||||
if ($domain !== 'DEFAULT' && ! $domainRepo->domainExists($domain, $apiKey)) {
|
||||
throw DomainNotFoundException::fromAuthority($domain);
|
||||
}
|
||||
|
||||
/** @var VisitRepositoryInterface $repo */
|
||||
$repo = $this->em->getRepository(Visit::class);
|
||||
|
||||
return $this->createPaginator(new DomainVisitsPaginatorAdapter($repo, $domain, $params, $apiKey), $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Visit[]|Paginator
|
||||
*/
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Visit;
|
|||
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Exception\DomainNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
|
@ -33,6 +34,12 @@ interface VisitsStatsHelperInterface
|
|||
*/
|
||||
public function visitsForTag(string $tag, VisitsParams $params, ?ApiKey $apiKey = null): Paginator;
|
||||
|
||||
/**
|
||||
* @return Visit[]|Paginator
|
||||
* @throws DomainNotFoundException
|
||||
*/
|
||||
public function visitsForDomain(string $domain, VisitsParams $params, ?ApiKey $apiKey = null): Paginator;
|
||||
|
||||
/**
|
||||
* @return Visit[]|Paginator
|
||||
*/
|
||||
|
|
|
@ -55,6 +55,10 @@ class DomainRepositoryTest extends DatabaseTestCase
|
|||
self::assertEquals($detachedWithRedirects, $this->repo->findOneByAuthority('detached-with-redirects.com'));
|
||||
self::assertNull($this->repo->findOneByAuthority('does-not-exist.com'));
|
||||
self::assertEquals($detachedDomain, $this->repo->findOneByAuthority('detached.com'));
|
||||
self::assertTrue($this->repo->domainExists('bar.com'));
|
||||
self::assertTrue($this->repo->domainExists('detached-with-redirects.com'));
|
||||
self::assertFalse($this->repo->domainExists('does-not-exist.com'));
|
||||
self::assertTrue($this->repo->domainExists('detached.com'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -115,6 +119,12 @@ class DomainRepositoryTest extends DatabaseTestCase
|
|||
$this->repo->findOneByAuthority('detached-with-redirects.com', $detachedWithRedirectsApiKey),
|
||||
);
|
||||
self::assertNull($this->repo->findOneByAuthority('foo.com', $detachedWithRedirectsApiKey));
|
||||
|
||||
self::assertTrue($this->repo->domainExists('foo.com', $authorApiKey));
|
||||
self::assertFalse($this->repo->domainExists('bar.com', $authorApiKey));
|
||||
self::assertTrue($this->repo->domainExists('bar.com', $barDomainApiKey));
|
||||
self::assertTrue($this->repo->domainExists('detached-with-redirects.com', $detachedWithRedirectsApiKey));
|
||||
self::assertFalse($this->repo->domainExists('foo.com', $detachedWithRedirectsApiKey));
|
||||
}
|
||||
|
||||
private function createShortUrl(Domain $domain, ?ApiKey $apiKey = null): ShortUrl
|
||||
|
|
|
@ -52,7 +52,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||
{
|
||||
$shortUrl = ShortUrl::createEmpty();
|
||||
$this->getEntityManager()->persist($shortUrl);
|
||||
$countIterable = function (iterable $results): int {
|
||||
$countIterable = static function (iterable $results): int {
|
||||
$resultsCount = 0;
|
||||
foreach ($results as $value) {
|
||||
$resultsCount++;
|
||||
|
@ -256,6 +256,54 @@ class VisitRepositoryTest extends DatabaseTestCase
|
|||
)));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function findVisitsByDomainReturnsProperData(): void
|
||||
{
|
||||
$this->createShortUrlsAndVisits('doma.in');
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
self::assertCount(0, $this->repo->findVisitsByDomain('invalid', new VisitsListFiltering()));
|
||||
self::assertCount(6, $this->repo->findVisitsByDomain('DEFAULT', new VisitsListFiltering()));
|
||||
self::assertCount(3, $this->repo->findVisitsByDomain('doma.in', new VisitsListFiltering()));
|
||||
self::assertCount(1, $this->repo->findVisitsByDomain('doma.in', new VisitsListFiltering(null, true)));
|
||||
self::assertCount(2, $this->repo->findVisitsByDomain('doma.in', new VisitsListFiltering(
|
||||
DateRange::withStartAndEndDate(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')),
|
||||
)));
|
||||
self::assertCount(1, $this->repo->findVisitsByDomain('doma.in', new VisitsListFiltering(
|
||||
DateRange::withStartDate(Chronos::parse('2016-01-03')),
|
||||
)));
|
||||
self::assertCount(2, $this->repo->findVisitsByDomain('DEFAULT', new VisitsListFiltering(
|
||||
DateRange::withStartAndEndDate(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')),
|
||||
)));
|
||||
self::assertCount(4, $this->repo->findVisitsByDomain('DEFAULT', new VisitsListFiltering(
|
||||
DateRange::withStartDate(Chronos::parse('2016-01-03')),
|
||||
)));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function countVisitsByDomainReturnsProperData(): void
|
||||
{
|
||||
$this->createShortUrlsAndVisits('doma.in');
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
self::assertEquals(0, $this->repo->countVisitsByDomain('invalid', new VisitsListFiltering()));
|
||||
self::assertEquals(6, $this->repo->countVisitsByDomain('DEFAULT', new VisitsListFiltering()));
|
||||
self::assertEquals(3, $this->repo->countVisitsByDomain('doma.in', new VisitsListFiltering()));
|
||||
self::assertEquals(1, $this->repo->countVisitsByDomain('doma.in', new VisitsListFiltering(null, true)));
|
||||
self::assertEquals(2, $this->repo->countVisitsByDomain('doma.in', new VisitsListFiltering(
|
||||
DateRange::withStartAndEndDate(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')),
|
||||
)));
|
||||
self::assertEquals(1, $this->repo->countVisitsByDomain('doma.in', new VisitsListFiltering(
|
||||
DateRange::withStartDate(Chronos::parse('2016-01-03')),
|
||||
)));
|
||||
self::assertEquals(2, $this->repo->countVisitsByDomain('DEFAULT', new VisitsListFiltering(
|
||||
DateRange::withStartAndEndDate(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')),
|
||||
)));
|
||||
self::assertEquals(4, $this->repo->countVisitsByDomain('DEFAULT', new VisitsListFiltering(
|
||||
DateRange::withStartDate(Chronos::parse('2016-01-03')),
|
||||
)));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function countVisitsReturnsExpectedResultBasedOnApiKey(): void
|
||||
{
|
||||
|
|
|
@ -10,9 +10,12 @@ use PHPUnit\Framework\TestCase;
|
|||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Exception\DomainNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
|
@ -158,6 +161,69 @@ class VisitsStatsHelperTest extends TestCase
|
|||
$getRepo->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function throwsExceptionWhenRequestingVisitsForInvalidDomain(): void
|
||||
{
|
||||
$domain = 'foo.com';
|
||||
$apiKey = ApiKey::create();
|
||||
$repo = $this->prophesize(DomainRepository::class);
|
||||
$domainExists = $repo->domainExists($domain, $apiKey)->willReturn(false);
|
||||
$getRepo = $this->em->getRepository(Domain::class)->willReturn($repo->reveal());
|
||||
|
||||
$this->expectException(DomainNotFoundException::class);
|
||||
$domainExists->shouldBeCalledOnce();
|
||||
$getRepo->shouldBeCalledOnce();
|
||||
|
||||
$this->helper->visitsForDomain($domain, new VisitsParams(), $apiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideAdminApiKeys
|
||||
*/
|
||||
public function visitsForNonDefaultDomainAreReturnedAsExpected(?ApiKey $apiKey): void
|
||||
{
|
||||
$domain = 'foo.com';
|
||||
$repo = $this->prophesize(DomainRepository::class);
|
||||
$domainExists = $repo->domainExists($domain, $apiKey)->willReturn(true);
|
||||
$getRepo = $this->em->getRepository(Domain::class)->willReturn($repo->reveal());
|
||||
|
||||
$list = map(range(0, 1), fn () => Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance()));
|
||||
$repo2 = $this->prophesize(VisitRepository::class);
|
||||
$repo2->findVisitsByDomain($domain, Argument::type(VisitsListFiltering::class))->willReturn($list);
|
||||
$repo2->countVisitsByDomain($domain, Argument::type(VisitsCountFiltering::class))->willReturn(1);
|
||||
$this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledOnce();
|
||||
|
||||
$paginator = $this->helper->visitsForDomain($domain, new VisitsParams(), $apiKey);
|
||||
|
||||
self::assertEquals($list, ArrayUtils::iteratorToArray($paginator->getCurrentPageResults()));
|
||||
$domainExists->shouldHaveBeenCalledOnce();
|
||||
$getRepo->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideAdminApiKeys
|
||||
*/
|
||||
public function visitsForDefaultDomainAreReturnedAsExpected(?ApiKey $apiKey): void
|
||||
{
|
||||
$repo = $this->prophesize(DomainRepository::class);
|
||||
$domainExists = $repo->domainExists(Argument::cetera());
|
||||
$getRepo = $this->em->getRepository(Domain::class)->willReturn($repo->reveal());
|
||||
|
||||
$list = map(range(0, 1), fn () => Visit::forValidShortUrl(ShortUrl::createEmpty(), Visitor::emptyInstance()));
|
||||
$repo2 = $this->prophesize(VisitRepository::class);
|
||||
$repo2->findVisitsByDomain('DEFAULT', Argument::type(VisitsListFiltering::class))->willReturn($list);
|
||||
$repo2->countVisitsByDomain('DEFAULT', Argument::type(VisitsCountFiltering::class))->willReturn(1);
|
||||
$this->em->getRepository(Visit::class)->willReturn($repo2->reveal())->shouldBeCalledOnce();
|
||||
|
||||
$paginator = $this->helper->visitsForDomain('DEFAULT', new VisitsParams(), $apiKey);
|
||||
|
||||
self::assertEquals($list, ArrayUtils::iteratorToArray($paginator->getCurrentPageResults()));
|
||||
$domainExists->shouldNotHaveBeenCalled();
|
||||
$getRepo->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function orphanVisitsAreReturnedAsExpected(): void
|
||||
{
|
||||
|
|
|
@ -34,6 +34,7 @@ return [
|
|||
Action\ShortUrl\ListShortUrlsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\ShortUrlVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\TagVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\DomainVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\GlobalVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\OrphanVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\NonOrphanVisitsAction::class => ConfigAbstractFactory::class,
|
||||
|
@ -73,6 +74,10 @@ return [
|
|||
],
|
||||
Action\Visit\ShortUrlVisitsAction::class => [Visit\VisitsStatsHelper::class],
|
||||
Action\Visit\TagVisitsAction::class => [Visit\VisitsStatsHelper::class],
|
||||
Action\Visit\DomainVisitsAction::class => [
|
||||
Visit\VisitsStatsHelper::class,
|
||||
'config.url_shortener.domain.hostname',
|
||||
],
|
||||
Action\Visit\GlobalVisitsAction::class => [Visit\VisitsStatsHelper::class],
|
||||
Action\Visit\OrphanVisitsAction::class => [
|
||||
Visit\VisitsStatsHelper::class,
|
||||
|
|
|
@ -6,49 +6,52 @@ namespace Shlinkio\Shlink\Rest;
|
|||
|
||||
use Shlinkio\Shlink\Rest\Middleware\Mercure\NotConfiguredMercureErrorHandler;
|
||||
|
||||
$contentNegotiationMiddleware = Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class;
|
||||
$dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class;
|
||||
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
|
||||
return (static function (): array {
|
||||
$contentNegotiationMiddleware = Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class;
|
||||
$dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class;
|
||||
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
|
||||
|
||||
return [
|
||||
return [
|
||||
|
||||
'routes' => [
|
||||
Action\HealthAction::getRouteDef(),
|
||||
'routes' => [
|
||||
Action\HealthAction::getRouteDef(),
|
||||
|
||||
// Short URLs
|
||||
Action\ShortUrl\CreateShortUrlAction::getRouteDef([
|
||||
$contentNegotiationMiddleware,
|
||||
$dropDomainMiddleware,
|
||||
$overrideDomainMiddleware,
|
||||
Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class,
|
||||
]),
|
||||
Action\ShortUrl\SingleStepCreateShortUrlAction::getRouteDef([
|
||||
$contentNegotiationMiddleware,
|
||||
$overrideDomainMiddleware,
|
||||
]),
|
||||
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\DeleteShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ResolveShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ListShortUrlsAction::getRouteDef(),
|
||||
// Short URLs
|
||||
Action\ShortUrl\CreateShortUrlAction::getRouteDef([
|
||||
$contentNegotiationMiddleware,
|
||||
$dropDomainMiddleware,
|
||||
$overrideDomainMiddleware,
|
||||
Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class,
|
||||
]),
|
||||
Action\ShortUrl\SingleStepCreateShortUrlAction::getRouteDef([
|
||||
$contentNegotiationMiddleware,
|
||||
$overrideDomainMiddleware,
|
||||
]),
|
||||
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\DeleteShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ResolveShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ListShortUrlsAction::getRouteDef(),
|
||||
|
||||
// Visits
|
||||
Action\Visit\ShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\Visit\TagVisitsAction::getRouteDef(),
|
||||
Action\Visit\GlobalVisitsAction::getRouteDef(),
|
||||
Action\Visit\OrphanVisitsAction::getRouteDef(),
|
||||
Action\Visit\NonOrphanVisitsAction::getRouteDef(),
|
||||
// Visits
|
||||
Action\Visit\ShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\Visit\TagVisitsAction::getRouteDef(),
|
||||
Action\Visit\DomainVisitsAction::getRouteDef(),
|
||||
Action\Visit\GlobalVisitsAction::getRouteDef(),
|
||||
Action\Visit\OrphanVisitsAction::getRouteDef(),
|
||||
Action\Visit\NonOrphanVisitsAction::getRouteDef(),
|
||||
|
||||
// Tags
|
||||
Action\Tag\ListTagsAction::getRouteDef(),
|
||||
Action\Tag\TagsStatsAction::getRouteDef(),
|
||||
Action\Tag\DeleteTagsAction::getRouteDef(),
|
||||
Action\Tag\UpdateTagAction::getRouteDef(),
|
||||
// Tags
|
||||
Action\Tag\ListTagsAction::getRouteDef(),
|
||||
Action\Tag\TagsStatsAction::getRouteDef(),
|
||||
Action\Tag\DeleteTagsAction::getRouteDef(),
|
||||
Action\Tag\UpdateTagAction::getRouteDef(),
|
||||
|
||||
// Domains
|
||||
Action\Domain\ListDomainsAction::getRouteDef(),
|
||||
Action\Domain\DomainRedirectsAction::getRouteDef(),
|
||||
// Domains
|
||||
Action\Domain\ListDomainsAction::getRouteDef(),
|
||||
Action\Domain\DomainRedirectsAction::getRouteDef(),
|
||||
|
||||
Action\MercureInfoAction::getRouteDef([NotConfiguredMercureErrorHandler::class]),
|
||||
],
|
||||
Action\MercureInfoAction::getRouteDef([NotConfiguredMercureErrorHandler::class]),
|
||||
],
|
||||
|
||||
];
|
||||
];
|
||||
})();
|
||||
|
|
48
module/Rest/src/Action/Visit/DomainVisitsAction.php
Normal file
48
module/Rest/src/Action/Visit/DomainVisitsAction.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Action\Visit;
|
||||
|
||||
use Laminas\Diactoros\Response\JsonResponse;
|
||||
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\Visit\VisitsStatsHelperInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
|
||||
|
||||
class DomainVisitsAction extends AbstractRestAction
|
||||
{
|
||||
use PagerfantaUtilsTrait;
|
||||
|
||||
protected const ROUTE_PATH = '/domains/{domain}/visits';
|
||||
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
|
||||
|
||||
public function __construct(private VisitsStatsHelperInterface $visitsHelper, private string $defaultDomain)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$domain = $this->resolveDomainParam($request);
|
||||
$params = VisitsParams::fromRawData($request->getQueryParams());
|
||||
$apiKey = AuthenticationMiddleware::apiKeyFromRequest($request);
|
||||
$visits = $this->visitsHelper->visitsForDomain($domain, $params, $apiKey);
|
||||
|
||||
return new JsonResponse([
|
||||
'visits' => $this->serializePaginator($visits),
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolveDomainParam(Request $request): string
|
||||
{
|
||||
$domainParam = $request->getAttribute('domain', '');
|
||||
if ($domainParam === $this->defaultDomain) {
|
||||
return 'DEFAULT';
|
||||
}
|
||||
|
||||
return $domainParam;
|
||||
}
|
||||
}
|
68
module/Rest/test-api/Action/DomainVisitsTest.php
Normal file
68
module/Rest/test-api/Action/DomainVisitsTest.php
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioApiTest\Shlink\Rest\Action;
|
||||
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DomainVisitsTest extends ApiTestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideDomains
|
||||
*/
|
||||
public function expectedVisitsAreReturned(
|
||||
string $apiKey,
|
||||
string $domain,
|
||||
bool $excludeBots,
|
||||
int $expectedVisitsAmount,
|
||||
): void {
|
||||
$resp = $this->callApiWithKey(self::METHOD_GET, sprintf('/domains/%s/visits', $domain), [
|
||||
RequestOptions::QUERY => $excludeBots ? ['excludeBots' => true] : [],
|
||||
], $apiKey);
|
||||
$payload = $this->getJsonResponsePayload($resp);
|
||||
|
||||
self::assertEquals(self::STATUS_OK, $resp->getStatusCode());
|
||||
self::assertArrayHasKey('visits', $payload);
|
||||
self::assertArrayHasKey('data', $payload['visits']);
|
||||
self::assertCount($expectedVisitsAmount, $payload['visits']['data']);
|
||||
}
|
||||
|
||||
public function provideDomains(): iterable
|
||||
{
|
||||
yield 'example.com with admin API key' => ['valid_api_key', 'example.com', false, 0];
|
||||
yield 'DEFAULT with admin API key' => ['valid_api_key', 'DEFAULT', false, 7];
|
||||
yield 'DEFAULT with admin API key and no bots' => ['valid_api_key', 'DEFAULT', true, 6];
|
||||
yield 'DEFAULT with domain API key' => ['domain_api_key', 'DEFAULT', false, 0];
|
||||
yield 'DEFAULT with author API key' => ['author_api_key', 'DEFAULT', false, 5];
|
||||
yield 'DEFAULT with author API key and no bots' => ['author_api_key', 'DEFAULT', true, 4];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideApiKeysAndTags
|
||||
*/
|
||||
public function notFoundErrorIsReturnedForInvalidTags(string $apiKey, string $domain): void
|
||||
{
|
||||
$resp = $this->callApiWithKey(self::METHOD_GET, sprintf('/domains/%s/visits', $domain), [], $apiKey);
|
||||
$payload = $this->getJsonResponsePayload($resp);
|
||||
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
|
||||
self::assertEquals('DOMAIN_NOT_FOUND', $payload['type']);
|
||||
self::assertEquals(sprintf('Domain with authority "%s" could not be found', $domain), $payload['detail']);
|
||||
self::assertEquals('Domain not found', $payload['title']);
|
||||
self::assertEquals($domain, $payload['authority']);
|
||||
}
|
||||
|
||||
public function provideApiKeysAndTags(): iterable
|
||||
{
|
||||
yield 'admin API key with invalid domain' => ['valid_api_key', 'invalid_domain.com'];
|
||||
yield 'domain API key with not-owned valid domain' => ['domain_api_key', 'this_domain_is_detached.com'];
|
||||
yield 'author API key with valid domain not used in URLs' => ['author_api_key', 'this_domain_is_detached.com'];
|
||||
}
|
||||
}
|
60
module/Rest/test/Action/Visit/DomainVisitsActionTest.php
Normal file
60
module/Rest/test/Action/Visit/DomainVisitsActionTest.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Rest\Action\Visit;
|
||||
|
||||
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\DomainVisitsAction;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
class DomainVisitsActionTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private DomainVisitsAction $action;
|
||||
private ObjectProphecy $visitsHelper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
|
||||
$this->action = new DomainVisitsAction($this->visitsHelper->reveal(), 'the_default.com');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideDomainAuthorities
|
||||
*/
|
||||
public function providingCorrectDomainReturnsVisits(string $providedDomain, string $expectedDomain): void
|
||||
{
|
||||
$apiKey = ApiKey::create();
|
||||
$getVisits = $this->visitsHelper->visitsForDomain(
|
||||
$expectedDomain,
|
||||
Argument::type(VisitsParams::class),
|
||||
$apiKey,
|
||||
)->willReturn(new Paginator(new ArrayAdapter([])));
|
||||
|
||||
$response = $this->action->handle(
|
||||
ServerRequestFactory::fromGlobals()->withAttribute('domain', $providedDomain)
|
||||
->withAttribute(ApiKey::class, $apiKey),
|
||||
);
|
||||
|
||||
self::assertEquals(200, $response->getStatusCode());
|
||||
$getVisits->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideDomainAuthorities(): iterable
|
||||
{
|
||||
yield 'no default domain' => ['foo.com', 'foo.com'];
|
||||
yield 'default domain' => ['the_default.com', 'DEFAULT'];
|
||||
yield 'DEFAULT keyword' => ['DEFAULT', 'DEFAULT'];
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue