mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-24 09:50:17 -06:00
Merge pull request #572 from alesub/date-filters
Date filters for short urls list endpoint
This commit is contained in:
commit
685b3f86b3
@ -17,6 +17,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||
|
||||
> After Shlink v2 is released, both API versions will behave like API v2.
|
||||
|
||||
* [#575](https://github.com/shlinkio/shlink/issues/575) Added support to filter short URL lists by date ranges.
|
||||
|
||||
* The `GET /short-urls` endpoint now accepts the `startDate` and `endDate` query params.
|
||||
* The `short-urls:list` command now allows `--startDate` and `--endDate` flags to be optionally provided.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#492](https://github.com/shlinkio/shlink/issues/492) Updated to monolog 2, together with other dependencies, like Symfony 5 and infection-php.
|
||||
|
@ -54,6 +54,24 @@
|
||||
"visits"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "startDate",
|
||||
"in": "query",
|
||||
"description": "The date (in ISO-8601 format) from which we want to get short URLs.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "endDate",
|
||||
"in": "query",
|
||||
"description": "The date (in ISO-8601 format) until which we want to get short URLs.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
|
@ -36,7 +36,7 @@ class GenerateKeyCommand extends Command
|
||||
->addOption(
|
||||
'expirationDate',
|
||||
'e',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The date in which the API key should expire. Use any valid PHP format.'
|
||||
);
|
||||
}
|
||||
|
@ -4,25 +4,22 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\Stdlib\ArrayUtils;
|
||||
|
||||
use function array_map;
|
||||
use function Functional\map;
|
||||
use function Functional\select_keys;
|
||||
|
||||
class GetVisitsCommand extends Command
|
||||
class GetVisitsCommand extends AbstractWithDateRangeCommand
|
||||
{
|
||||
public const NAME = 'short-url:visits';
|
||||
private const ALIASES = ['shortcode:visits', 'short-code:visits'];
|
||||
@ -36,25 +33,23 @@ class GetVisitsCommand extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
protected function doConfigure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription('Returns the detailed visits information for provided short code')
|
||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get')
|
||||
->addOption(
|
||||
'startDate',
|
||||
's',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Allows to filter visits, returning only those older than start date'
|
||||
)
|
||||
->addOption(
|
||||
'endDate',
|
||||
'e',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Allows to filter visits, returning only those newer than end date'
|
||||
);
|
||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get');
|
||||
}
|
||||
|
||||
protected function getStartDateDesc(): string
|
||||
{
|
||||
return 'Allows to filter visits, returning only those older than start date';
|
||||
}
|
||||
|
||||
protected function getEndDateDesc(): string
|
||||
{
|
||||
return 'Allows to filter visits, returning only those newer than end date';
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
@ -74,24 +69,18 @@ class GetVisitsCommand extends Command
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
$startDate = $this->getDateOption($input, 'startDate');
|
||||
$endDate = $this->getDateOption($input, 'endDate');
|
||||
$startDate = $this->getDateOption($input, $output, 'startDate');
|
||||
$endDate = $this->getDateOption($input, $output, 'endDate');
|
||||
|
||||
$paginator = $this->visitsTracker->info($shortCode, new VisitsParams(new DateRange($startDate, $endDate)));
|
||||
$visits = ArrayUtils::iteratorToArray($paginator->getCurrentItems());
|
||||
|
||||
$rows = array_map(function (Visit $visit) {
|
||||
$rows = map($paginator->getCurrentItems(), function (Visit $visit) {
|
||||
$rowData = $visit->jsonSerialize();
|
||||
$rowData['country'] = $visit->getVisitLocation()->getCountryName();
|
||||
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country']);
|
||||
}, $visits);
|
||||
});
|
||||
ShlinkTable::fromOutput($output)->render(['Referer', 'Date', 'User agent', 'Country'], $rows);
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function getDateOption(InputInterface $input, $key)
|
||||
{
|
||||
$value = $input->getOption($key);
|
||||
return ! empty($value) ? Chronos::parse($value) : $value;
|
||||
}
|
||||
}
|
||||
|
@ -4,14 +4,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
|
||||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
@ -26,7 +27,7 @@ use function explode;
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
|
||||
class ListShortUrlsCommand extends Command
|
||||
class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
||||
{
|
||||
use PaginatorUtilsTrait;
|
||||
|
||||
@ -43,17 +44,17 @@ class ListShortUrlsCommand extends Command
|
||||
|
||||
/** @var ShortUrlServiceInterface */
|
||||
private $shortUrlService;
|
||||
/** @var array */
|
||||
private $domainConfig;
|
||||
/** @var ShortUrlDataTransformer */
|
||||
private $transformer;
|
||||
|
||||
public function __construct(ShortUrlServiceInterface $shortUrlService, array $domainConfig)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->shortUrlService = $shortUrlService;
|
||||
$this->domainConfig = $domainConfig;
|
||||
$this->transformer = new ShortUrlDataTransformer($domainConfig);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
protected function doConfigure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
@ -68,7 +69,7 @@ class ListShortUrlsCommand extends Command
|
||||
)
|
||||
->addOption(
|
||||
'searchTerm',
|
||||
's',
|
||||
'st',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'A query used to filter results by searching for it on the longUrl and shortCode fields'
|
||||
)
|
||||
@ -87,18 +88,31 @@ class ListShortUrlsCommand extends Command
|
||||
->addOption('showTags', null, InputOption::VALUE_NONE, 'Whether to display the tags or not');
|
||||
}
|
||||
|
||||
protected function getStartDateDesc(): string
|
||||
{
|
||||
return 'Allows to filter short URLs, returning only those created after "startDate"';
|
||||
}
|
||||
|
||||
protected function getEndDateDesc(): string
|
||||
{
|
||||
return 'Allows to filter short URLs, returning only those created before "endDate"';
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$page = (int) $input->getOption('page');
|
||||
$searchTerm = $input->getOption('searchTerm');
|
||||
$tags = $input->getOption('tags');
|
||||
$tags = ! empty($tags) ? explode(',', $tags) : [];
|
||||
$showTags = (bool) $input->getOption('showTags');
|
||||
$transformer = new ShortUrlDataTransformer($this->domainConfig);
|
||||
$startDate = $this->getDateOption($input, $output, 'startDate');
|
||||
$endDate = $this->getDateOption($input, $output, 'endDate');
|
||||
$orderBy = $this->processOrderBy($input);
|
||||
|
||||
do {
|
||||
$result = $this->renderPage($input, $output, $page, $searchTerm, $tags, $showTags, $transformer);
|
||||
$result = $this->renderPage($output, $page, $searchTerm, $tags, $showTags, $startDate, $endDate, $orderBy);
|
||||
$page++;
|
||||
|
||||
$continue = $this->isLastPage($result)
|
||||
@ -108,19 +122,27 @@ class ListShortUrlsCommand extends Command
|
||||
|
||||
$io->newLine();
|
||||
$io->success('Short URLs properly listed');
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function renderPage(
|
||||
InputInterface $input,
|
||||
OutputInterface $output,
|
||||
int $page,
|
||||
?string $searchTerm,
|
||||
array $tags,
|
||||
bool $showTags,
|
||||
DataTransformerInterface $transformer
|
||||
?Chronos $startDate,
|
||||
?Chronos $endDate,
|
||||
$orderBy
|
||||
): Paginator {
|
||||
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
|
||||
$result = $this->shortUrlService->listShortUrls(
|
||||
$page,
|
||||
$searchTerm,
|
||||
$tags,
|
||||
$orderBy,
|
||||
new DateRange($startDate, $endDate)
|
||||
);
|
||||
|
||||
$headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count'];
|
||||
if ($showTags) {
|
||||
@ -129,7 +151,7 @@ class ListShortUrlsCommand extends Command
|
||||
|
||||
$rows = [];
|
||||
foreach ($result as $row) {
|
||||
$shortUrl = $transformer->transform($row);
|
||||
$shortUrl = $this->transformer->transform($row);
|
||||
if ($showTags) {
|
||||
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
|
||||
} else {
|
||||
@ -143,9 +165,13 @@ class ListShortUrlsCommand extends Command
|
||||
$result,
|
||||
'Page %s of %s'
|
||||
));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|string|null
|
||||
*/
|
||||
private function processOrderBy(InputInterface $input)
|
||||
{
|
||||
$orderBy = $input->getOption('orderBy');
|
||||
|
54
module/CLI/src/Command/Util/AbstractWithDateRangeCommand.php
Normal file
54
module/CLI/src/Command/Util/AbstractWithDateRangeCommand.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Util;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Throwable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
abstract class AbstractWithDateRangeCommand extends Command
|
||||
{
|
||||
final protected function configure(): void
|
||||
{
|
||||
$this->doConfigure();
|
||||
$this
|
||||
->addOption('startDate', 's', InputOption::VALUE_REQUIRED, $this->getStartDateDesc())
|
||||
->addOption('endDate', 'e', InputOption::VALUE_REQUIRED, $this->getEndDateDesc());
|
||||
}
|
||||
|
||||
protected function getDateOption(InputInterface $input, OutputInterface $output, string $key): ?Chronos
|
||||
{
|
||||
$value = $input->getOption($key);
|
||||
if (empty($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Chronos::parse($value);
|
||||
} catch (Throwable $e) {
|
||||
$output->writeln(sprintf(
|
||||
'<comment>> Ignored provided "%s" since its value "%s" is not a valid date. <</comment>',
|
||||
$key,
|
||||
$value
|
||||
));
|
||||
|
||||
if ($output->isVeryVerbose()) {
|
||||
$this->getApplication()->renderThrowable($e, $output);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
abstract protected function doConfigure(): void;
|
||||
|
||||
abstract protected function getStartDateDesc(): string;
|
||||
abstract protected function getEndDateDesc(): string;
|
||||
}
|
@ -22,6 +22,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\Paginator\Adapter\ArrayAdapter;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class GetVisitsCommandTest extends TestCase
|
||||
{
|
||||
/** @var CommandTester */
|
||||
@ -39,7 +41,7 @@ class GetVisitsCommandTest extends TestCase
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function noDateFlagsTriesToListWithoutDateRange()
|
||||
public function noDateFlagsTriesToListWithoutDateRange(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->visitsTracker->info($shortCode, new VisitsParams(new DateRange(null, null)))->willReturn(
|
||||
@ -50,7 +52,7 @@ class GetVisitsCommandTest extends TestCase
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function providingDateFlagsTheListGetsFiltered()
|
||||
public function providingDateFlagsTheListGetsFiltered(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$startDate = '2016-01-01';
|
||||
@ -69,6 +71,27 @@ class GetVisitsCommandTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function providingInvalidDatesPrintsWarning(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$startDate = 'foo';
|
||||
$info = $this->visitsTracker->info($shortCode, new VisitsParams(new DateRange()))
|
||||
->willReturn(new Paginator(new ArrayAdapter([])));
|
||||
|
||||
$this->commandTester->execute([
|
||||
'shortCode' => $shortCode,
|
||||
'--startDate' => $startDate,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$info->shouldHaveBeenCalledOnce();
|
||||
$this->assertStringContainsString(
|
||||
sprintf('Ignored provided "startDate" since its value "%s" is not a valid date', $startDate),
|
||||
$output
|
||||
);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function outputIsProperlyGenerated(): void
|
||||
{
|
||||
|
@ -4,10 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
@ -15,6 +17,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\Paginator\Adapter\ArrayAdapter;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
use function explode;
|
||||
|
||||
class ListShortUrlsCommandTest extends TestCase
|
||||
{
|
||||
/** @var CommandTester */
|
||||
@ -32,17 +36,7 @@ class ListShortUrlsCommandTest extends TestCase
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function noInputCallsListJustOnce()
|
||||
{
|
||||
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->setInputs(['n']);
|
||||
$this->commandTester->execute([]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function loadingMorePagesCallsListMoreTimes()
|
||||
public function loadingMorePagesCallsListMoreTimes(): void
|
||||
{
|
||||
// The paginator will return more than one page
|
||||
$data = [];
|
||||
@ -64,7 +58,7 @@ class ListShortUrlsCommandTest extends TestCase
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function havingMorePagesButAnsweringNoCallsListJustOnce()
|
||||
public function havingMorePagesButAnsweringNoCallsListJustOnce(): void
|
||||
{
|
||||
// The paginator will return more than one page
|
||||
$data = [];
|
||||
@ -72,8 +66,9 @@ class ListShortUrlsCommandTest extends TestCase
|
||||
$data[] = new ShortUrl('url_' . $i);
|
||||
}
|
||||
|
||||
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter($data)))
|
||||
->shouldBeCalledOnce();
|
||||
$this->shortUrlService->listShortUrls(1, null, [], null, new DateRange())
|
||||
->willReturn(new Paginator(new ArrayAdapter($data)))
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->setInputs(['n']);
|
||||
$this->commandTester->execute([]);
|
||||
@ -89,25 +84,105 @@ class ListShortUrlsCommandTest extends TestCase
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function passingPageWillMakeListStartOnThatPage()
|
||||
public function passingPageWillMakeListStartOnThatPage(): void
|
||||
{
|
||||
$page = 5;
|
||||
$this->shortUrlService->listShortUrls($page, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledOnce();
|
||||
$this->shortUrlService->listShortUrls($page, null, [], null, new DateRange())
|
||||
->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->setInputs(['y']);
|
||||
$this->commandTester->execute(['--page' => $page]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function ifTagsFlagIsProvidedTagsColumnIsIncluded()
|
||||
public function ifTagsFlagIsProvidedTagsColumnIsIncluded(): void
|
||||
{
|
||||
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledOnce();
|
||||
$this->shortUrlService->listShortUrls(1, null, [], null, new DateRange())
|
||||
->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->setInputs(['y']);
|
||||
$this->commandTester->execute(['--showTags' => true]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString('Tags', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideArgs
|
||||
*/
|
||||
public function serviceIsInvokedWithProvidedArgs(
|
||||
array $commandArgs,
|
||||
?int $page,
|
||||
?string $searchTerm,
|
||||
array $tags,
|
||||
?DateRange $dateRange
|
||||
): void {
|
||||
$listShortUrls = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, null, $dateRange)
|
||||
->willReturn(new Paginator(new ArrayAdapter()));
|
||||
|
||||
$this->commandTester->setInputs(['n']);
|
||||
$this->commandTester->execute($commandArgs);
|
||||
|
||||
$listShortUrls->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideArgs(): iterable
|
||||
{
|
||||
yield [[], 1, null, [], new DateRange()];
|
||||
yield [['--page' => $page = 3], $page, null, [], new DateRange()];
|
||||
yield [['--searchTerm' => $searchTerm = 'search this'], 1, $searchTerm, [], new DateRange()];
|
||||
yield [
|
||||
['--page' => $page = 3, '--searchTerm' => $searchTerm = 'search this', '--tags' => $tags = 'foo,bar'],
|
||||
$page,
|
||||
$searchTerm,
|
||||
explode(',', $tags),
|
||||
new DateRange(),
|
||||
];
|
||||
yield [
|
||||
['--startDate' => $startDate = '2019-01-01'],
|
||||
1,
|
||||
null,
|
||||
[],
|
||||
new DateRange(Chronos::parse($startDate)),
|
||||
];
|
||||
yield [
|
||||
['--endDate' => $endDate = '2020-05-23'],
|
||||
1,
|
||||
null,
|
||||
[],
|
||||
new DateRange(null, Chronos::parse($endDate)),
|
||||
];
|
||||
yield [
|
||||
['--startDate' => $startDate = '2019-01-01', '--endDate' => $endDate = '2020-05-23'],
|
||||
1,
|
||||
null,
|
||||
[],
|
||||
new DateRange(Chronos::parse($startDate), Chronos::parse($endDate)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideOrderBy
|
||||
*/
|
||||
public function orderByIsProperlyComputed(array $commandArgs, $expectedOrderBy): void
|
||||
{
|
||||
$listShortUrls = $this->shortUrlService->listShortUrls(1, null, [], $expectedOrderBy, new DateRange())
|
||||
->willReturn(new Paginator(new ArrayAdapter()));
|
||||
|
||||
$this->commandTester->setInputs(['n']);
|
||||
$this->commandTester->execute($commandArgs);
|
||||
|
||||
$listShortUrls->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideOrderBy(): iterable
|
||||
{
|
||||
yield [[], null];
|
||||
yield [['--orderBy' => 'foo'], 'foo'];
|
||||
yield [['--orderBy' => 'foo,ASC'], ['foo' => 'ASC']];
|
||||
yield [['--orderBy' => 'bar,DESC'], ['bar' => 'DESC']];
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Paginator\Adapter;
|
||||
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
||||
use Zend\Paginator\Adapter\AdapterInterface;
|
||||
|
||||
@ -22,17 +23,21 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
|
||||
private $orderBy;
|
||||
/** @var array */
|
||||
private $tags;
|
||||
/** @var DateRange|null */
|
||||
private $dateRange;
|
||||
|
||||
public function __construct(
|
||||
ShortUrlRepositoryInterface $repository,
|
||||
$searchTerm = null,
|
||||
array $tags = [],
|
||||
$orderBy = null
|
||||
$orderBy = null,
|
||||
?DateRange $dateRange = null
|
||||
) {
|
||||
$this->repository = $repository;
|
||||
$this->searchTerm = $searchTerm !== null ? trim(strip_tags($searchTerm)) : null;
|
||||
$this->orderBy = $orderBy;
|
||||
$this->tags = $tags;
|
||||
$this->dateRange = $dateRange;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -49,7 +54,8 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
|
||||
$offset,
|
||||
$this->searchTerm,
|
||||
$this->tags,
|
||||
$this->orderBy
|
||||
$this->orderBy,
|
||||
$this->dateRange
|
||||
);
|
||||
}
|
||||
|
||||
@ -64,6 +70,6 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return $this->repository->countList($this->searchTerm, $this->tags);
|
||||
return $this->repository->countList($this->searchTerm, $this->tags, $this->dateRange);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\Repository;
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
|
||||
use function array_column;
|
||||
@ -27,9 +28,10 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
|
||||
?int $offset = null,
|
||||
?string $searchTerm = null,
|
||||
array $tags = [],
|
||||
$orderBy = null
|
||||
$orderBy = null,
|
||||
?DateRange $dateRange = null
|
||||
): array {
|
||||
$qb = $this->createListQueryBuilder($searchTerm, $tags);
|
||||
$qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange);
|
||||
$qb->select('DISTINCT s');
|
||||
|
||||
// Set limit and offset
|
||||
@ -52,15 +54,9 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
|
||||
|
||||
private function processOrderByForList(QueryBuilder $qb, $orderBy): array
|
||||
{
|
||||
// Map public field names to column names
|
||||
$fieldNameMap = [
|
||||
'originalUrl' => 'longUrl',
|
||||
'longUrl' => 'longUrl',
|
||||
'shortCode' => 'shortCode',
|
||||
'dateCreated' => 'dateCreated',
|
||||
];
|
||||
$fieldName = is_array($orderBy) ? key($orderBy) : $orderBy;
|
||||
$order = is_array($orderBy) ? $orderBy[$fieldName] : 'ASC';
|
||||
$isArray = is_array($orderBy);
|
||||
$fieldName = $isArray ? key($orderBy) : $orderBy;
|
||||
$order = $isArray ? $orderBy[$fieldName] : 'ASC';
|
||||
|
||||
if (contains(['visits', 'visitsCount', 'visitCount'], $fieldName)) {
|
||||
$qb->addSelect('COUNT(DISTINCT v) AS totalVisits')
|
||||
@ -71,26 +67,45 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
|
||||
return array_column($qb->getQuery()->getResult(), 0);
|
||||
}
|
||||
|
||||
// Map public field names to column names
|
||||
$fieldNameMap = [
|
||||
'originalUrl' => 'longUrl',
|
||||
'longUrl' => 'longUrl',
|
||||
'shortCode' => 'shortCode',
|
||||
'dateCreated' => 'dateCreated',
|
||||
];
|
||||
if (array_key_exists($fieldName, $fieldNameMap)) {
|
||||
$qb->orderBy('s.' . $fieldNameMap[$fieldName], $order);
|
||||
}
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function countList(?string $searchTerm = null, array $tags = []): int
|
||||
public function countList(?string $searchTerm = null, array $tags = [], ?DateRange $dateRange = null): int
|
||||
{
|
||||
$qb = $this->createListQueryBuilder($searchTerm, $tags);
|
||||
$qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange);
|
||||
$qb->select('COUNT(DISTINCT s)');
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||
}
|
||||
|
||||
private function createListQueryBuilder(?string $searchTerm = null, array $tags = []): QueryBuilder
|
||||
{
|
||||
private function createListQueryBuilder(
|
||||
?string $searchTerm = null,
|
||||
array $tags = [],
|
||||
?DateRange $dateRange = null
|
||||
): QueryBuilder {
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->from(ShortUrl::class, 's');
|
||||
$qb->where('1=1');
|
||||
|
||||
if ($dateRange !== null && $dateRange->getStartDate() !== null) {
|
||||
$qb->andWhere($qb->expr()->gte('s.dateCreated', ':startDate'));
|
||||
$qb->setParameter('startDate', $dateRange->getStartDate());
|
||||
}
|
||||
if ($dateRange !== null && $dateRange->getEndDate() !== null) {
|
||||
$qb->andWhere($qb->expr()->lte('s.dateCreated', ':endDate'));
|
||||
$qb->setParameter('endDate', $dateRange->getEndDate());
|
||||
}
|
||||
|
||||
// Apply search term to every searchable field if not empty
|
||||
if (! empty($searchTerm)) {
|
||||
// Left join with tags only if no tags were provided. In case of tags, an inner join will be done later
|
||||
@ -98,14 +113,12 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
|
||||
$qb->leftJoin('s.tags', 't');
|
||||
}
|
||||
|
||||
$conditions = [
|
||||
// Apply search conditions
|
||||
$qb->andWhere($qb->expr()->orX(
|
||||
$qb->expr()->like('s.longUrl', ':searchPattern'),
|
||||
$qb->expr()->like('s.shortCode', ':searchPattern'),
|
||||
$qb->expr()->like('t.name', ':searchPattern'),
|
||||
];
|
||||
|
||||
// Unpack and apply search conditions
|
||||
$qb->andWhere($qb->expr()->orX(...$conditions));
|
||||
$qb->expr()->like('t.name', ':searchPattern')
|
||||
));
|
||||
$qb->setParameter('searchPattern', '%' . $searchTerm . '%');
|
||||
}
|
||||
|
||||
|
@ -4,14 +4,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Repository;
|
||||
|
||||
use Doctrine\Common\Persistence\ObjectRepository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
|
||||
interface ShortUrlRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
/**
|
||||
* Gets a list of elements using provided filtering data
|
||||
*
|
||||
* @param string|array|null $orderBy
|
||||
*/
|
||||
public function findList(
|
||||
@ -19,13 +18,11 @@ interface ShortUrlRepositoryInterface extends ObjectRepository
|
||||
?int $offset = null,
|
||||
?string $searchTerm = null,
|
||||
array $tags = [],
|
||||
$orderBy = null
|
||||
$orderBy = null,
|
||||
?DateRange $dateRange = null
|
||||
): array;
|
||||
|
||||
/**
|
||||
* Counts the number of elements in a list using provided filtering data
|
||||
*/
|
||||
public function countList(?string $searchTerm = null, array $tags = []): int;
|
||||
public function countList(?string $searchTerm = null, array $tags = [], ?DateRange $dateRange = null): int;
|
||||
|
||||
public function findOneByShortCode(string $shortCode, ?string $domain = null): ?ShortUrl;
|
||||
|
||||
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Repository;
|
||||
|
||||
use Doctrine\Common\Persistence\ObjectRepository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
interface TagRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Repository;
|
||||
|
||||
use Doctrine\Common\Persistence\ObjectRepository;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
|
||||
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Doctrine\ORM;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
@ -30,13 +31,19 @@ class ShortUrlService implements ShortUrlServiceInterface
|
||||
/**
|
||||
* @param string[] $tags
|
||||
* @param array|string|null $orderBy
|
||||
*
|
||||
* @return ShortUrl[]|Paginator
|
||||
*/
|
||||
public function listShortUrls(int $page = 1, ?string $searchQuery = null, array $tags = [], $orderBy = null)
|
||||
{
|
||||
public function listShortUrls(
|
||||
int $page = 1,
|
||||
?string $searchQuery = null,
|
||||
array $tags = [],
|
||||
$orderBy = null,
|
||||
?DateRange $dateRange = null
|
||||
) {
|
||||
/** @var ShortUrlRepository $repo */
|
||||
$repo = $this->em->getRepository(ShortUrl::class);
|
||||
$paginator = new Paginator(new ShortUrlRepositoryAdapter($repo, $searchQuery, $tags, $orderBy));
|
||||
$paginator = new Paginator(new ShortUrlRepositoryAdapter($repo, $searchQuery, $tags, $orderBy, $dateRange));
|
||||
$paginator->setItemCountPerPage(ShortUrlRepositoryAdapter::ITEMS_PER_PAGE)
|
||||
->setCurrentPageNumber($page);
|
||||
|
||||
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
@ -14,9 +15,16 @@ interface ShortUrlServiceInterface
|
||||
/**
|
||||
* @param string[] $tags
|
||||
* @param array|string|null $orderBy
|
||||
*
|
||||
* @return ShortUrl[]|Paginator
|
||||
*/
|
||||
public function listShortUrls(int $page = 1, ?string $searchQuery = null, array $tags = [], $orderBy = null);
|
||||
public function listShortUrls(
|
||||
int $page = 1,
|
||||
?string $searchQuery = null,
|
||||
array $tags = [],
|
||||
$orderBy = null,
|
||||
?DateRange $dateRange = null
|
||||
);
|
||||
|
||||
/**
|
||||
* @param string[] $tags
|
||||
|
@ -6,6 +6,8 @@ namespace ShlinkioTest\Shlink\Core\Repository;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use ReflectionObject;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
@ -108,7 +110,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function findListProperlyFiltersByTagAndSearchTerm(): void
|
||||
public function findListProperlyFiltersResult(): void
|
||||
{
|
||||
$tag = new Tag('bar');
|
||||
$this->getEntityManager()->persist($tag);
|
||||
@ -124,12 +126,17 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||
$this->getEntityManager()->persist($bar);
|
||||
|
||||
$foo2 = new ShortUrl('foo_2');
|
||||
$ref = new ReflectionObject($foo2);
|
||||
$dateProp = $ref->getProperty('dateCreated');
|
||||
$dateProp->setAccessible(true);
|
||||
$dateProp->setValue($foo2, Chronos::now()->subDays(5));
|
||||
$this->getEntityManager()->persist($foo2);
|
||||
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
$result = $this->repo->findList(null, null, 'foo', ['bar']);
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertEquals(1, $this->repo->countList('foo', ['bar']));
|
||||
$this->assertSame($foo, $result[0]);
|
||||
|
||||
$result = $this->repo->findList();
|
||||
@ -141,12 +148,22 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||
$result = $this->repo->findList(2, 1);
|
||||
$this->assertCount(2, $result);
|
||||
|
||||
$result = $this->repo->findList(2, 2);
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertCount(1, $this->repo->findList(2, 2));
|
||||
|
||||
$result = $this->repo->findList(null, null, null, [], ['visits' => 'DESC']);
|
||||
$this->assertCount(3, $result);
|
||||
$this->assertSame($bar, $result[0]);
|
||||
|
||||
$result = $this->repo->findList(null, null, null, [], null, new DateRange(null, Chronos::now()->subDays(2)));
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertEquals(1, $this->repo->countList(null, [], new DateRange(null, Chronos::now()->subDays(2))));
|
||||
$this->assertSame($foo2, $result[0]);
|
||||
|
||||
$this->assertCount(
|
||||
2,
|
||||
$this->repo->findList(null, null, null, [], null, new DateRange(Chronos::now()->subDays(2)))
|
||||
);
|
||||
$this->assertEquals(2, $this->repo->countList(null, [], new DateRange(Chronos::now()->subDays(2))));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Domain\Resolver;
|
||||
|
||||
use Doctrine\Common\Persistence\ObjectRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Domain\Resolver\PersistenceDomainResolver;
|
||||
|
@ -4,35 +4,63 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Paginator\Adapter;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
||||
|
||||
class ShortUrlRepositoryAdapterTest extends TestCase
|
||||
{
|
||||
/** @var ShortUrlRepositoryAdapter */
|
||||
private $adapter;
|
||||
/** @var ObjectProphecy */
|
||||
private $repo;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->repo = $this->prophesize(ShortUrlRepositoryInterface::class);
|
||||
$this->adapter = new ShortUrlRepositoryAdapter($this->repo->reveal(), 'search', ['foo', 'bar'], 'order');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function getItemsFallbacksToFindList(): void
|
||||
{
|
||||
$this->repo->findList(10, 5, 'search', ['foo', 'bar'], 'order')->shouldBeCalledOnce();
|
||||
$this->adapter->getItems(5, 10);
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideFilteringArgs
|
||||
*/
|
||||
public function getItemsFallsBackToFindList(
|
||||
$searchTerm = null,
|
||||
array $tags = [],
|
||||
?DateRange $dateRange = null,
|
||||
$orderBy = null
|
||||
): void {
|
||||
$adapter = new ShortUrlRepositoryAdapter($this->repo->reveal(), $searchTerm, $tags, $orderBy, $dateRange);
|
||||
|
||||
$this->repo->findList(10, 5, $searchTerm, $tags, $orderBy, $dateRange)->shouldBeCalledOnce();
|
||||
$adapter->getItems(5, 10);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function countFallbacksToCountList(): void
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideFilteringArgs
|
||||
*/
|
||||
public function countFallsBackToCountList($searchTerm = null, array $tags = [], ?DateRange $dateRange = null): void
|
||||
{
|
||||
$this->repo->countList('search', ['foo', 'bar'])->shouldBeCalledOnce();
|
||||
$this->adapter->count();
|
||||
$adapter = new ShortUrlRepositoryAdapter($this->repo->reveal(), $searchTerm, $tags, null, $dateRange);
|
||||
|
||||
$this->repo->countList($searchTerm, $tags, $dateRange)->shouldBeCalledOnce();
|
||||
$adapter->count();
|
||||
}
|
||||
|
||||
public function provideFilteringArgs(): iterable
|
||||
{
|
||||
yield [];
|
||||
yield ['search'];
|
||||
yield ['search', []];
|
||||
yield ['search', ['foo', 'bar']];
|
||||
yield ['search', ['foo', 'bar'], null, 'order'];
|
||||
yield ['search', ['foo', 'bar'], new DateRange(), 'order'];
|
||||
yield ['search', ['foo', 'bar'], new DateRange(Chronos::now()), 'order'];
|
||||
yield ['search', ['foo', 'bar'], new DateRange(null, Chronos::now()), 'order'];
|
||||
yield ['search', ['foo', 'bar'], new DateRange(Chronos::now(), Chronos::now()), 'order'];
|
||||
yield ['search', ['foo', 'bar'], new DateRange(Chronos::now())];
|
||||
yield [null, ['foo', 'bar'], new DateRange(Chronos::now(), Chronos::now())];
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Action\ShortUrl;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
@ -61,6 +63,15 @@ class ListShortUrlsAction extends AbstractRestAction
|
||||
$query['searchTerm'] ?? null,
|
||||
$query['tags'] ?? [],
|
||||
$query['orderBy'] ?? null,
|
||||
$this->determineDateRangeFromQuery($query),
|
||||
];
|
||||
}
|
||||
|
||||
private function determineDateRangeFromQuery(array $query): DateRange
|
||||
{
|
||||
return new DateRange(
|
||||
isset($query['startDate']) ? Chronos::parse($query['startDate']) : null,
|
||||
isset($query['endDate']) ? Chronos::parse($query['endDate']) : null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,107 +4,160 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioApiTest\Shlink\Rest\Action;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
|
||||
|
||||
use function count;
|
||||
|
||||
class ListShortUrlsTest extends ApiTestCase
|
||||
{
|
||||
/** @test */
|
||||
public function shortUrlsAreProperlyListed(): void
|
||||
private const SHORT_URL_SHLINK = [
|
||||
'shortCode' => 'abc123',
|
||||
'shortUrl' => 'http://doma.in/abc123',
|
||||
'longUrl' => 'https://shlink.io',
|
||||
'dateCreated' => '2018-05-01T00:00:00+00:00',
|
||||
'visitsCount' => 3,
|
||||
'tags' => ['foo'],
|
||||
'meta' => [
|
||||
'validSince' => null,
|
||||
'validUntil' => null,
|
||||
'maxVisits' => null,
|
||||
],
|
||||
'originalUrl' => 'https://shlink.io',
|
||||
];
|
||||
private const SHORT_URL_CUSTOM_SLUG_AND_DOMAIN = [
|
||||
'shortCode' => 'custom-with-domain',
|
||||
'shortUrl' => 'http://some-domain.com/custom-with-domain',
|
||||
'longUrl' => 'https://google.com',
|
||||
'dateCreated' => '2018-10-20T00:00:00+00:00',
|
||||
'visitsCount' => 0,
|
||||
'tags' => [],
|
||||
'meta' => [
|
||||
'validSince' => null,
|
||||
'validUntil' => null,
|
||||
'maxVisits' => null,
|
||||
],
|
||||
'originalUrl' => 'https://google.com',
|
||||
];
|
||||
private const SHORT_URL_META = [
|
||||
'shortCode' => 'def456',
|
||||
'shortUrl' => 'http://doma.in/def456',
|
||||
'longUrl' =>
|
||||
'https://blog.alejandrocelaya.com/2017/12/09'
|
||||
. '/acmailer-7-0-the-most-important-release-in-a-long-time/',
|
||||
'dateCreated' => '2019-01-01T00:00:00+00:00',
|
||||
'visitsCount' => 2,
|
||||
'tags' => ['bar', 'foo'],
|
||||
'meta' => [
|
||||
'validSince' => '2020-05-01T00:00:00+00:00',
|
||||
'validUntil' => null,
|
||||
'maxVisits' => null,
|
||||
],
|
||||
'originalUrl' =>
|
||||
'https://blog.alejandrocelaya.com/2017/12/09'
|
||||
. '/acmailer-7-0-the-most-important-release-in-a-long-time/',
|
||||
];
|
||||
private const SHORT_URL_CUSTOM_SLUG = [
|
||||
'shortCode' => 'custom',
|
||||
'shortUrl' => 'http://doma.in/custom',
|
||||
'longUrl' => 'https://shlink.io',
|
||||
'dateCreated' => '2019-01-01T00:00:00+00:00',
|
||||
'visitsCount' => 0,
|
||||
'tags' => [],
|
||||
'meta' => [
|
||||
'validSince' => null,
|
||||
'validUntil' => null,
|
||||
'maxVisits' => 2,
|
||||
],
|
||||
'originalUrl' => 'https://shlink.io',
|
||||
];
|
||||
private const SHORT_URL_CUSTOM_DOMAIN = [
|
||||
'shortCode' => 'ghi789',
|
||||
'shortUrl' => 'http://example.com/ghi789',
|
||||
'longUrl' =>
|
||||
'https://blog.alejandrocelaya.com/2019/04/27'
|
||||
. '/considerations-to-properly-use-open-source-software-projects/',
|
||||
'dateCreated' => '2019-01-01T00:00:00+00:00',
|
||||
'visitsCount' => 0,
|
||||
'tags' => [],
|
||||
'meta' => [
|
||||
'validSince' => null,
|
||||
'validUntil' => null,
|
||||
'maxVisits' => null,
|
||||
],
|
||||
'originalUrl' =>
|
||||
'https://blog.alejandrocelaya.com/2019/04/27'
|
||||
. '/considerations-to-properly-use-open-source-software-projects/',
|
||||
];
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideFilteredLists
|
||||
*/
|
||||
public function shortUrlsAreProperlyListed(array $query, array $expectedShortUrls): void
|
||||
{
|
||||
$resp = $this->callApiWithKey(self::METHOD_GET, '/short-urls');
|
||||
$resp = $this->callApiWithKey(self::METHOD_GET, '/short-urls', [RequestOptions::QUERY => $query]);
|
||||
$respPayload = $this->getJsonResponsePayload($resp);
|
||||
|
||||
$this->assertEquals(self::STATUS_OK, $resp->getStatusCode());
|
||||
$this->assertEquals([
|
||||
'shortUrls' => [
|
||||
'data' => [
|
||||
[
|
||||
'shortCode' => 'abc123',
|
||||
'shortUrl' => 'http://doma.in/abc123',
|
||||
'longUrl' => 'https://shlink.io',
|
||||
'dateCreated' => '2019-01-01T00:00:00+00:00',
|
||||
'visitsCount' => 3,
|
||||
'tags' => ['foo'],
|
||||
'meta' => [
|
||||
'validSince' => null,
|
||||
'validUntil' => null,
|
||||
'maxVisits' => null,
|
||||
],
|
||||
'originalUrl' => 'https://shlink.io',
|
||||
],
|
||||
[
|
||||
'shortCode' => 'def456',
|
||||
'shortUrl' => 'http://doma.in/def456',
|
||||
'longUrl' =>
|
||||
'https://blog.alejandrocelaya.com/2017/12/09'
|
||||
. '/acmailer-7-0-the-most-important-release-in-a-long-time/',
|
||||
'dateCreated' => '2019-01-01T00:00:00+00:00',
|
||||
'visitsCount' => 2,
|
||||
'tags' => ['bar', 'foo'],
|
||||
'meta' => [
|
||||
'validSince' => '2020-05-01T00:00:00+00:00',
|
||||
'validUntil' => null,
|
||||
'maxVisits' => null,
|
||||
],
|
||||
'originalUrl' =>
|
||||
'https://blog.alejandrocelaya.com/2017/12/09'
|
||||
. '/acmailer-7-0-the-most-important-release-in-a-long-time/',
|
||||
],
|
||||
[
|
||||
'shortCode' => 'custom',
|
||||
'shortUrl' => 'http://doma.in/custom',
|
||||
'longUrl' => 'https://shlink.io',
|
||||
'dateCreated' => '2019-01-01T00:00:00+00:00',
|
||||
'visitsCount' => 0,
|
||||
'tags' => [],
|
||||
'meta' => [
|
||||
'validSince' => null,
|
||||
'validUntil' => null,
|
||||
'maxVisits' => 2,
|
||||
],
|
||||
'originalUrl' => 'https://shlink.io',
|
||||
],
|
||||
[
|
||||
'shortCode' => 'ghi789',
|
||||
'shortUrl' => 'http://example.com/ghi789',
|
||||
'longUrl' =>
|
||||
'https://blog.alejandrocelaya.com/2019/04/27'
|
||||
. '/considerations-to-properly-use-open-source-software-projects/',
|
||||
'dateCreated' => '2019-01-01T00:00:00+00:00',
|
||||
'visitsCount' => 0,
|
||||
'tags' => [],
|
||||
'meta' => [
|
||||
'validSince' => null,
|
||||
'validUntil' => null,
|
||||
'maxVisits' => null,
|
||||
],
|
||||
'originalUrl' =>
|
||||
'https://blog.alejandrocelaya.com/2019/04/27'
|
||||
. '/considerations-to-properly-use-open-source-software-projects/',
|
||||
],
|
||||
[
|
||||
'shortCode' => 'custom-with-domain',
|
||||
'shortUrl' => 'http://some-domain.com/custom-with-domain',
|
||||
'longUrl' => 'https://google.com',
|
||||
'dateCreated' => '2019-01-01T00:00:00+00:00',
|
||||
'visitsCount' => 0,
|
||||
'tags' => [],
|
||||
'meta' => [
|
||||
'validSince' => null,
|
||||
'validUntil' => null,
|
||||
'maxVisits' => null,
|
||||
],
|
||||
'originalUrl' => 'https://google.com',
|
||||
],
|
||||
],
|
||||
'pagination' => [
|
||||
'currentPage' => 1,
|
||||
'pagesCount' => 1,
|
||||
'itemsPerPage' => 10,
|
||||
'itemsInCurrentPage' => 5,
|
||||
'totalItems' => 5,
|
||||
],
|
||||
'data' => $expectedShortUrls,
|
||||
'pagination' => $this->buildPagination(count($expectedShortUrls)),
|
||||
],
|
||||
], $respPayload);
|
||||
}
|
||||
|
||||
public function provideFilteredLists(): iterable
|
||||
{
|
||||
yield [[], [
|
||||
self::SHORT_URL_SHLINK,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
]];
|
||||
yield [['orderBy' => 'shortCode'], [
|
||||
self::SHORT_URL_SHLINK,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
]];
|
||||
yield [['startDate' => Chronos::parse('2018-12-01')->toAtomString()], [
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
]];
|
||||
yield [['endDate' => Chronos::parse('2018-12-01')->toAtomString()], [
|
||||
self::SHORT_URL_SHLINK,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
]];
|
||||
yield [['tags' => ['foo']], [
|
||||
self::SHORT_URL_SHLINK,
|
||||
self::SHORT_URL_META,
|
||||
]];
|
||||
yield [['tags' => ['bar']], [
|
||||
self::SHORT_URL_META,
|
||||
]];
|
||||
yield [['tags' => ['foo'], 'endDate' => Chronos::parse('2018-12-01')->toAtomString()], [
|
||||
self::SHORT_URL_SHLINK,
|
||||
]];
|
||||
yield [['searchTerm' => 'alejandro'], [
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
]];
|
||||
}
|
||||
|
||||
private function buildPagination(int $itemsCount): array
|
||||
{
|
||||
return [
|
||||
'currentPage' => 1,
|
||||
'pagesCount' => 1,
|
||||
'itemsPerPage' => 10,
|
||||
'itemsInCurrentPage' => $itemsCount,
|
||||
'totalItems' => $itemsCount,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ namespace ShlinkioApiTest\Shlink\Rest\Fixtures;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\Common\DataFixtures\FixtureInterface;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use ReflectionObject;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
|
@ -6,7 +6,7 @@ namespace ShlinkioApiTest\Shlink\Rest\Fixtures;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use ReflectionObject;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
@ -21,7 +21,8 @@ class ShortUrlsFixture extends AbstractFixture
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$abcShortUrl = $this->setShortUrlDate(
|
||||
new ShortUrl('https://shlink.io', ShortUrlMeta::createFromRawData(['customSlug' => 'abc123']))
|
||||
new ShortUrl('https://shlink.io', ShortUrlMeta::createFromRawData(['customSlug' => 'abc123'])),
|
||||
Chronos::parse('2018-05-01')
|
||||
);
|
||||
$manager->persist($abcShortUrl);
|
||||
|
||||
@ -46,7 +47,7 @@ class ShortUrlsFixture extends AbstractFixture
|
||||
$withDomainAndSlugShortUrl = $this->setShortUrlDate(new ShortUrl(
|
||||
'https://google.com',
|
||||
ShortUrlMeta::createFromRawData(['domain' => 'some-domain.com', 'customSlug' => 'custom-with-domain'])
|
||||
));
|
||||
), Chronos::parse('2018-10-20'));
|
||||
$manager->persist($withDomainAndSlugShortUrl);
|
||||
|
||||
$manager->flush();
|
||||
@ -55,12 +56,12 @@ class ShortUrlsFixture extends AbstractFixture
|
||||
$this->addReference('def456_short_url', $defShortUrl);
|
||||
}
|
||||
|
||||
private function setShortUrlDate(ShortUrl $shortUrl): ShortUrl
|
||||
private function setShortUrlDate(ShortUrl $shortUrl, ?Chronos $date = null): ShortUrl
|
||||
{
|
||||
$ref = new ReflectionObject($shortUrl);
|
||||
$dateProp = $ref->getProperty('dateCreated');
|
||||
$dateProp->setAccessible(true);
|
||||
$dateProp->setValue($shortUrl, Chronos::create(2019, 1, 1, 0, 0, 0));
|
||||
$dateProp->setValue($shortUrl, $date ?? Chronos::parse('2019-01-01'));
|
||||
|
||||
return $shortUrl;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ namespace ShlinkioApiTest\Shlink\Rest\Fixtures;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
|
||||
|
@ -6,7 +6,7 @@ namespace ShlinkioApiTest\Shlink\Rest\Fixtures;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
|
@ -4,9 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Rest\Action\ShortUrl;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||
use Shlinkio\Shlink\Rest\Action\ShortUrl\ListShortUrlsAction;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
@ -43,13 +45,15 @@ class ListShortUrlsActionTest extends TestCase
|
||||
int $expectedPage,
|
||||
?string $expectedSearchTerm,
|
||||
array $expectedTags,
|
||||
?string $expectedOrderBy
|
||||
?string $expectedOrderBy,
|
||||
DateRange $expectedDateRange
|
||||
): void {
|
||||
$listShortUrls = $this->service->listShortUrls(
|
||||
$expectedPage,
|
||||
$expectedSearchTerm,
|
||||
$expectedTags,
|
||||
$expectedOrderBy
|
||||
$expectedOrderBy,
|
||||
$expectedDateRange
|
||||
)->willReturn(new Paginator(new ArrayAdapter()));
|
||||
|
||||
/** @var JsonResponse $response */
|
||||
@ -65,17 +69,44 @@ class ListShortUrlsActionTest extends TestCase
|
||||
|
||||
public function provideFilteringData(): iterable
|
||||
{
|
||||
yield [[], 1, null, [], null];
|
||||
yield [['page' => 10], 10, null, [], null];
|
||||
yield [['page' => null], 1, null, [], null];
|
||||
yield [['page' => '8'], 8, null, [], null];
|
||||
yield [['searchTerm' => $searchTerm = 'foo'], 1, $searchTerm, [], null];
|
||||
yield [['tags' => $tags = ['foo','bar']], 1, null, $tags, null];
|
||||
yield [['orderBy' => $orderBy = 'something'], 1, null, [], $orderBy];
|
||||
yield [[], 1, null, [], null, new DateRange()];
|
||||
yield [['page' => 10], 10, null, [], null, new DateRange()];
|
||||
yield [['page' => null], 1, null, [], null, new DateRange()];
|
||||
yield [['page' => '8'], 8, null, [], null, new DateRange()];
|
||||
yield [['searchTerm' => $searchTerm = 'foo'], 1, $searchTerm, [], null, new DateRange()];
|
||||
yield [['tags' => $tags = ['foo','bar']], 1, null, $tags, null, new DateRange()];
|
||||
yield [['orderBy' => $orderBy = 'something'], 1, null, [], $orderBy, new DateRange()];
|
||||
yield [[
|
||||
'page' => '2',
|
||||
'orderBy' => $orderBy = 'something',
|
||||
'tags' => $tags = ['one', 'two'],
|
||||
], 2, null, $tags, $orderBy];
|
||||
], 2, null, $tags, $orderBy, new DateRange()];
|
||||
yield [
|
||||
['startDate' => $date = Chronos::now()->toAtomString()],
|
||||
1,
|
||||
null,
|
||||
[],
|
||||
null,
|
||||
new DateRange(Chronos::parse($date)),
|
||||
];
|
||||
yield [
|
||||
['endDate' => $date = Chronos::now()->toAtomString()],
|
||||
1,
|
||||
null,
|
||||
[],
|
||||
null,
|
||||
new DateRange(null, Chronos::parse($date)),
|
||||
];
|
||||
yield [
|
||||
[
|
||||
'startDate' => $startDate = Chronos::now()->subDays(10)->toAtomString(),
|
||||
'endDate' => $endDate = Chronos::now()->toAtomString(),
|
||||
],
|
||||
1,
|
||||
null,
|
||||
[],
|
||||
null,
|
||||
new DateRange(Chronos::parse($startDate), Chronos::parse($endDate)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user