From 56165791310121c5ffdefd5b279ad37dc79cfad8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 17 Dec 2019 09:59:54 +0100 Subject: [PATCH] Added startDate and endDate params to ListShortUrlsCommand --- .../src/Command/Api/GenerateKeyCommand.php | 2 +- .../src/Command/ShortUrl/GetVisitsCommand.php | 51 +++++----- .../Command/ShortUrl/ListShortUrlsCommand.php | 47 ++++++++-- .../Util/AbstractWithDateRangeCommand.php | 54 +++++++++++ .../Command/ShortUrl/GetVisitsCommandTest.php | 27 +++++- .../ShortUrl/ListShortUrlsCommandTest.php | 92 +++++++++++++++---- 6 files changed, 215 insertions(+), 58 deletions(-) create mode 100644 module/CLI/src/Command/Util/AbstractWithDateRangeCommand.php diff --git a/module/CLI/src/Command/Api/GenerateKeyCommand.php b/module/CLI/src/Command/Api/GenerateKeyCommand.php index f35fa012..bbe86a51 100644 --- a/module/CLI/src/Command/Api/GenerateKeyCommand.php +++ b/module/CLI/src/Command/Api/GenerateKeyCommand.php @@ -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.' ); } diff --git a/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php b/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php index 7873fba6..416c1bfb 100644 --- a/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php +++ b/module/CLI/src/Command/ShortUrl/GetVisitsCommand.php @@ -5,24 +5,25 @@ 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 Throwable; -use function array_map; +use function Functional\map; use function Functional\select_keys; +use function sprintf; -class GetVisitsCommand extends Command +class GetVisitsCommand extends AbstractWithDateRangeCommand { public const NAME = 'short-url:visits'; private const ALIASES = ['shortcode:visits', 'short-code:visits']; @@ -36,25 +37,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 +73,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; - } } diff --git a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php index 3d9b528d..5871cc76 100644 --- a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php +++ b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php @@ -4,14 +4,16 @@ 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 +28,7 @@ use function explode; use function implode; use function sprintf; -class ListShortUrlsCommand extends Command +class ListShortUrlsCommand extends AbstractWithDateRangeCommand { use PaginatorUtilsTrait; @@ -53,7 +55,7 @@ class ListShortUrlsCommand extends Command $this->domainConfig = $domainConfig; } - protected function configure(): void + protected function doConfigure(): void { $this ->setName(self::NAME) @@ -68,7 +70,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,6 +89,16 @@ 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); @@ -95,10 +107,23 @@ class ListShortUrlsCommand extends Command $tags = $input->getOption('tags'); $tags = ! empty($tags) ? explode(',', $tags) : []; $showTags = (bool) $input->getOption('showTags'); + $startDate = $this->getDateOption($input, $output, 'startDate'); + $endDate = $this->getDateOption($input, $output, 'endDate'); + $transformer = new ShortUrlDataTransformer($this->domainConfig); do { - $result = $this->renderPage($input, $output, $page, $searchTerm, $tags, $showTags, $transformer); + $result = $this->renderPage( + $input, + $output, + $page, + $searchTerm, + $tags, + $showTags, + $startDate, + $endDate, + $transformer + ); $page++; $continue = $this->isLastPage($result) @@ -108,6 +133,7 @@ class ListShortUrlsCommand extends Command $io->newLine(); $io->success('Short URLs properly listed'); + return ExitCodes::EXIT_SUCCESS; } @@ -118,9 +144,17 @@ class ListShortUrlsCommand extends Command ?string $searchTerm, array $tags, bool $showTags, + ?Chronos $startDate, + ?Chronos $endDate, DataTransformerInterface $transformer ): Paginator { - $result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input)); + $result = $this->shortUrlService->listShortUrls( + $page, + $searchTerm, + $tags, + $this->processOrderBy($input), + new DateRange($startDate, $endDate) + ); $headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count']; if ($showTags) { @@ -143,6 +177,7 @@ class ListShortUrlsCommand extends Command $result, 'Page %s of %s' )); + return $result; } diff --git a/module/CLI/src/Command/Util/AbstractWithDateRangeCommand.php b/module/CLI/src/Command/Util/AbstractWithDateRangeCommand.php new file mode 100644 index 00000000..c6b10be6 --- /dev/null +++ b/module/CLI/src/Command/Util/AbstractWithDateRangeCommand.php @@ -0,0 +1,54 @@ +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( + '> Ignored provided "%s" since its value "%s" is not a valid date. <', + $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; +} diff --git a/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php b/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php index a61dd7d4..e2ea29d1 100644 --- a/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/GetVisitsCommandTest.php @@ -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 { diff --git a/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php b/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php index 6ad96ed3..71babc47 100644 --- a/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php @@ -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,82 @@ 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)), + ]; + } }