mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-24 05:38:06 +03:00
Created endpoint to list non-orphan visits
This commit is contained in:
parent
8b79eee081
commit
fe1fa7689a
9 changed files with 184 additions and 3 deletions
|
@ -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,
|
||||
));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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(),
|
||||
|
|
37
module/Rest/src/Action/Visit/NonOrphanVisitsAction.php
Normal file
37
module/Rest/src/Action/Visit/NonOrphanVisitsAction.php
Normal 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),
|
||||
]);
|
||||
}
|
||||
}
|
36
module/Rest/test-api/Action/NonOrphanVisitsTest.php
Normal file
36
module/Rest/test-api/Action/NonOrphanVisitsTest.php
Normal 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];
|
||||
}
|
||||
}
|
49
module/Rest/test/Action/Visit/NonOrphanVisitsActionTest.php
Normal file
49
module/Rest/test/Action/Visit/NonOrphanVisitsActionTest.php
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
Loading…
Reference in a new issue