Created endpoint to list non-orphan visits

This commit is contained in:
Alejandro Celaya 2022-01-16 12:24:02 +01:00
parent 8b79eee081
commit fe1fa7689a
9 changed files with 184 additions and 3 deletions

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

@ -19,6 +19,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\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;
@ -95,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

@ -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());