Merge pull request #572 from alesub/date-filters

Date filters for short urls list endpoint
This commit is contained in:
Alejandro Celaya 2019-12-17 10:33:46 +01:00 committed by GitHub
commit 685b3f86b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 592 additions and 230 deletions

View File

@ -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.

View File

@ -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": [

View File

@ -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.'
);
}

View File

@ -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;
}
}

View File

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

View 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;
}

View File

@ -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
{

View File

@ -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']];
}
}

View File

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

View File

@ -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 . '%');
}

View File

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

View File

@ -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
{

View File

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

View File

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

View File

@ -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

View File

@ -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 */

View File

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

View File

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

View File

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

View File

@ -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,
];
}
}

View File

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

View File

@ -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;
}

View File

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

View File

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

View File

@ -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)),
];
}
}