Create DeleteShortUrlVisitsCommand

This commit is contained in:
Alejandro Celaya 2023-05-15 09:43:05 +02:00
parent 6bb8c1b2f5
commit 02a8ef7dd9
34 changed files with 163 additions and 99 deletions

View File

@ -13,6 +13,7 @@ return [
Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class, Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class,
Command\ShortUrl\GetShortUrlVisitsCommand::NAME => Command\ShortUrl\GetShortUrlVisitsCommand::class, Command\ShortUrl\GetShortUrlVisitsCommand::NAME => Command\ShortUrl\GetShortUrlVisitsCommand::class,
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class, Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
Command\ShortUrl\DeleteShortUrlVisitsCommand::NAME => Command\ShortUrl\DeleteShortUrlVisitsCommand::class,
Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::class, Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::class,
Command\Visit\DownloadGeoLiteDbCommand::NAME => Command\Visit\DownloadGeoLiteDbCommand::class, Command\Visit\DownloadGeoLiteDbCommand::NAME => Command\Visit\DownloadGeoLiteDbCommand::class,

View File

@ -42,6 +42,7 @@ return [
Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class, Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\GetShortUrlVisitsCommand::class => ConfigAbstractFactory::class, Command\ShortUrl\GetShortUrlVisitsCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class, Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\DeleteShortUrlVisitsCommand::class => ConfigAbstractFactory::class,
Command\Visit\DownloadGeoLiteDbCommand::class => ConfigAbstractFactory::class, Command\Visit\DownloadGeoLiteDbCommand::class => ConfigAbstractFactory::class,
Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class, Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class,
@ -88,6 +89,7 @@ return [
], ],
Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class], Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
Command\ShortUrl\DeleteShortUrlCommand::class => [ShortUrl\DeleteShortUrlService::class], Command\ShortUrl\DeleteShortUrlCommand::class => [ShortUrl\DeleteShortUrlService::class],
Command\ShortUrl\DeleteShortUrlVisitsCommand::class => [ShortUrl\ShortUrlVisitsDeleter::class],
Command\Visit\DownloadGeoLiteDbCommand::class => [GeoLite\GeolocationDbUpdater::class], Command\Visit\DownloadGeoLiteDbCommand::class => [GeoLite\GeolocationDbUpdater::class],
Command\Visit\LocateVisitsCommand::class => [ Command\Visit\LocateVisitsCommand::class => [

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api; namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@ -39,10 +39,10 @@ class DisableKeyCommand extends Command
try { try {
$this->apiKeyService->disable($apiKey); $this->apiKeyService->disable($apiKey);
$io->success(sprintf('API key "%s" properly disabled', $apiKey)); $io->success(sprintf('API key "%s" properly disabled', $apiKey));
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
$io->error($e->getMessage()); $io->error($e->getMessage());
return ExitCodes::EXIT_FAILURE; return ExitCode::EXIT_FAILURE;
} }
} }
} }

View File

@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Api;
use Cake\Chronos\Chronos; use Cake\Chronos\Chronos;
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface; use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\CLI\Util\ShlinkTable; use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Rest\ApiKey\Role; use Shlinkio\Shlink\Rest\ApiKey\Role;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;
@ -109,6 +109,6 @@ class GenerateKeyCommand extends Command
); );
} }
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} }
} }

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api; namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\CLI\Util\ShlinkTable; use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Rest\ApiKey\Role; use Shlinkio\Shlink\Rest\ApiKey\Role;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;
@ -77,7 +77,7 @@ class ListKeysCommand extends Command
'Roles', 'Roles',
]), $rows); ]), $rows);
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} }
private function determineMessagePattern(ApiKey $apiKey): string private function determineMessagePattern(ApiKey $apiKey): string

View File

@ -8,7 +8,7 @@ use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\SqlitePlatform; use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\ClassMetadata;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface; use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -57,7 +57,7 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
if ($this->schemaExists()) { if ($this->schemaExists()) {
$io->success('Database already exists. Run "db:migrate" command to make sure it is up to date.'); $io->success('Database already exists. Run "db:migrate" command to make sure it is up to date.');
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} }
// Create database // Create database
@ -65,7 +65,7 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
$this->runPhpCommand($output, [self::DOCTRINE_SCRIPT, self::DOCTRINE_CREATE_SCHEMA_COMMAND]); $this->runPhpCommand($output, [self::DOCTRINE_SCRIPT, self::DOCTRINE_CREATE_SCHEMA_COMMAND]);
$io->success('Database properly created!'); $io->success('Database properly created!');
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} }
private function checkDbExists(): void private function checkDbExists(): void

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Db; namespace Shlinkio\Shlink\CLI\Command\Db;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
@ -31,6 +31,6 @@ class MigrateDatabaseCommand extends AbstractDatabaseCommand
$this->runPhpCommand($output, [self::DOCTRINE_MIGRATIONS_SCRIPT, self::DOCTRINE_MIGRATE_COMMAND]); $this->runPhpCommand($output, [self::DOCTRINE_MIGRATIONS_SCRIPT, self::DOCTRINE_MIGRATE_COMMAND]);
$io->success('Database properly migrated!'); $io->success('Database properly migrated!');
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} }
} }

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Domain; namespace Shlinkio\Shlink\CLI\Command\Domain;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Config\NotFoundRedirects; use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface; use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
use Shlinkio\Shlink\Core\Domain\Model\DomainItem; use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
@ -109,6 +109,6 @@ class DomainRedirectsCommand extends Command
$io->success(sprintf('"Not found" redirects properly set for "%s"', $domainAuthority)); $io->success(sprintf('"Not found" redirects properly set for "%s"', $domainAuthority));
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} }
} }

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Domain; namespace Shlinkio\Shlink\CLI\Command\Domain;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\CLI\Util\ShlinkTable; use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Core\Config\NotFoundRedirectConfigInterface; use Shlinkio\Shlink\Core\Config\NotFoundRedirectConfigInterface;
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface; use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
@ -59,7 +59,7 @@ class ListDomainsCommand extends Command
}), }),
); );
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} }
private function notFoundRedirectsToString(NotFoundRedirectConfigInterface $config): string private function notFoundRedirectsToString(NotFoundRedirectConfigInterface $config): string

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl; namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions; use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
@ -141,7 +141,7 @@ class CreateShortUrlCommand extends Command
$longUrl = $input->getArgument('longUrl'); $longUrl = $input->getArgument('longUrl');
if (empty($longUrl)) { if (empty($longUrl)) {
$io->error('A URL was not provided!'); $io->error('A URL was not provided!');
return ExitCodes::EXIT_FAILURE; return ExitCode::EXIT_FAILURE;
} }
$explodeWithComma = curry(explode(...))(','); $explodeWithComma = curry(explode(...))(',');
@ -176,10 +176,10 @@ class CreateShortUrlCommand extends Command
sprintf('Processed long URL: <info>%s</info>', $longUrl), sprintf('Processed long URL: <info>%s</info>', $longUrl),
sprintf('Generated short URL: <info>%s</info>', $this->stringifier->stringify($result->shortUrl)), sprintf('Generated short URL: <info>%s</info>', $this->stringifier->stringify($result->shortUrl)),
]); ]);
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} catch (InvalidUrlException | NonUniqueSlugException $e) { } catch (InvalidUrlException | NonUniqueSlugException $e) {
$io->error($e->getMessage()); $io->error($e->getMessage());
return ExitCodes::EXIT_FAILURE; return ExitCode::EXIT_FAILURE;
} }
} }

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl; namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception; use Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface; use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
@ -55,10 +55,10 @@ class DeleteShortUrlCommand extends Command
try { try {
$this->runDelete($io, $identifier, $ignoreThreshold); $this->runDelete($io, $identifier, $ignoreThreshold);
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} catch (Exception\ShortUrlNotFoundException $e) { } catch (Exception\ShortUrlNotFoundException $e) {
$io->error($e->getMessage()); $io->error($e->getMessage());
return ExitCodes::EXIT_FAILURE; return ExitCode::EXIT_FAILURE;
} catch (Exception\DeleteShortUrlException $e) { } catch (Exception\DeleteShortUrlException $e) {
return $this->retry($io, $identifier, $e->getMessage()); return $this->retry($io, $identifier, $e->getMessage());
} }
@ -75,7 +75,7 @@ class DeleteShortUrlCommand extends Command
$io->warning('Short URL was not deleted.'); $io->warning('Short URL was not deleted.');
} }
return $forceDelete ? ExitCodes::EXIT_SUCCESS : ExitCodes::EXIT_WARNING; return $forceDelete ? ExitCode::EXIT_SUCCESS : ExitCode::EXIT_WARNING;
} }
private function runDelete(SymfonyStyle $io, ShortUrlIdentifier $identifier, bool $ignoreThreshold): void private function runDelete(SymfonyStyle $io, ShortUrlIdentifier $identifier, bool $ignoreThreshold): void

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlVisitsDeleterInterface;
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 function sprintf;
class DeleteShortUrlVisitsCommand extends Command
{
public const NAME = 'short-url:delete-visits';
public function __construct(private readonly ShortUrlVisitsDeleterInterface $deleter)
{
parent::__construct();
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription('Deletes visits from a short URL')
->addArgument(
'shortCode',
InputArgument::REQUIRED,
'The short code for the short URL which visits will be deleted',
)
->addOption(
'domain',
'd',
InputOption::VALUE_REQUIRED,
'The domain if the short code does not belong to the default one',
);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$identifier = ShortUrlIdentifier::fromCli($input);
$io = new SymfonyStyle($input, $output);
if (! $this->confirm($io)) {
$io->info('Operation aborted');
return ExitCode::EXIT_SUCCESS;
}
try {
$result = $this->deleter->deleteShortUrlVisits($identifier);
$io->success(sprintf('Successfully deleted %s visits', $result->affectedItems));
return ExitCode::EXIT_SUCCESS;
} catch (ShortUrlNotFoundException) {
$io->warning(sprintf('Short URL not found for "%s"', $identifier->__toString()));
return ExitCode::EXIT_WARNING;
}
}
private function confirm(SymfonyStyle $io): bool
{
$io->warning('You are about to delete all visits for a short URL. This operation cannot be undone.');
return $io->confirm('<comment>Continue deleting visits?</comment>', false);
}
}

View File

@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Input\EndDateOption; use Shlinkio\Shlink\CLI\Input\EndDateOption;
use Shlinkio\Shlink\CLI\Input\StartDateOption; use Shlinkio\Shlink\CLI\Input\StartDateOption;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\CLI\Util\ShlinkTable; use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait; use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
@ -173,7 +173,7 @@ class ListShortUrlsCommand extends Command
$io->newLine(); $io->newLine();
$io->success('Short URLs properly listed'); $io->success('Short URLs properly listed');
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} }
private function renderPage( private function renderPage(

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl; namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
@ -56,10 +56,10 @@ class ResolveUrlCommand extends Command
try { try {
$url = $this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromCli($input)); $url = $this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromCli($input));
$output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl())); $output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} catch (ShortUrlNotFoundException $e) { } catch (ShortUrlNotFoundException $e) {
$io->error($e->getMessage()); $io->error($e->getMessage());
return ExitCodes::EXIT_FAILURE; return ExitCode::EXIT_FAILURE;
} }
} }
} }

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag; namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Tag\TagServiceInterface; use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -41,11 +41,11 @@ class DeleteTagsCommand extends Command
if (empty($tagNames)) { if (empty($tagNames)) {
$io->warning('You have to provide at least one tag name'); $io->warning('You have to provide at least one tag name');
return ExitCodes::EXIT_WARNING; return ExitCode::EXIT_WARNING;
} }
$this->tagService->deleteTags($tagNames); $this->tagService->deleteTags($tagNames);
$io->success('Tags properly deleted'); $io->success('Tags properly deleted');
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} }
} }

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag; namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\CLI\Util\ShlinkTable; use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Core\Tag\Model\TagInfo; use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
use Shlinkio\Shlink\Core\Tag\Model\TagsParams; use Shlinkio\Shlink\Core\Tag\Model\TagsParams;
@ -34,7 +34,7 @@ class ListTagsCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): ?int protected function execute(InputInterface $input, OutputInterface $output): ?int
{ {
ShlinkTable::default($output)->render(['Name', 'URLs amount', 'Visits amount'], $this->getTagsRows()); ShlinkTable::default($output)->render(['Name', 'URLs amount', 'Visits amount'], $this->getTagsRows());
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} }
private function getTagsRows(): array private function getTagsRows(): array

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag; namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception\TagConflictException; use Shlinkio\Shlink\Core\Exception\TagConflictException;
use Shlinkio\Shlink\Core\Exception\TagNotFoundException; use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
use Shlinkio\Shlink\Core\Tag\Model\TagRenaming; use Shlinkio\Shlink\Core\Tag\Model\TagRenaming;
@ -42,10 +42,10 @@ class RenameTagCommand extends Command
try { try {
$this->tagService->renameTag(TagRenaming::fromNames($oldName, $newName)); $this->tagService->renameTag(TagRenaming::fromNames($oldName, $newName));
$io->success('Tag properly renamed.'); $io->success('Tag properly renamed.');
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} catch (TagNotFoundException | TagConflictException $e) { } catch (TagNotFoundException | TagConflictException $e) {
$io->error($e->getMessage()); $io->error($e->getMessage());
return ExitCodes::EXIT_FAILURE; return ExitCode::EXIT_FAILURE;
} }
} }
} }

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Util; namespace Shlinkio\Shlink\CLI\Command\Util;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -28,7 +28,7 @@ abstract class AbstractLockedCommand extends Command
$output->writeln( $output->writeln(
sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName), sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName),
); );
return ExitCodes::EXIT_WARNING; return ExitCode::EXIT_WARNING;
} }
try { try {

View File

@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\CLI\Input\EndDateOption; use Shlinkio\Shlink\CLI\Input\EndDateOption;
use Shlinkio\Shlink\CLI\Input\StartDateOption; use Shlinkio\Shlink\CLI\Input\StartDateOption;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\CLI\Util\ShlinkTable; use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Paginator; use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
@ -43,7 +43,7 @@ abstract class AbstractVisitsListCommand extends Command
ShlinkTable::default($output)->render($headers, $rows); ShlinkTable::default($output)->render($headers, $rows);
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} }
private function resolveRowsAndHeaders(Paginator $paginator): array private function resolveRowsAndHeaders(Paginator $paginator): array

View File

@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface; use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -56,7 +56,7 @@ class DownloadGeoLiteDbCommand extends Command
$io->success('GeoLite2 db file properly downloaded.'); $io->success('GeoLite2 db file properly downloaded.');
} }
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} catch (GeolocationDbUpdateFailedException $e) { } catch (GeolocationDbUpdateFailedException $e) {
$olderDbExists = $e->olderDbExists(); $olderDbExists = $e->olderDbExists();
@ -72,7 +72,7 @@ class DownloadGeoLiteDbCommand extends Command
$this->getApplication()?->renderThrowable($e, $io); $this->getApplication()?->renderThrowable($e, $io);
} }
return $olderDbExists ? ExitCodes::EXIT_WARNING : ExitCodes::EXIT_FAILURE; return $olderDbExists ? ExitCode::EXIT_WARNING : ExitCode::EXIT_FAILURE;
} }
} }
} }

View File

@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand; use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig; use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Common\Util\IpAddress; use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException; use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Entity\Visit;
@ -116,14 +116,14 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
} }
$this->io->success('Finished locating visits'); $this->io->success('Finished locating visits');
return ExitCodes::EXIT_SUCCESS; return ExitCode::EXIT_SUCCESS;
} catch (Throwable $e) { } catch (Throwable $e) {
$this->io->error($e->getMessage()); $this->io->error($e->getMessage());
if ($this->io->isVerbose()) { if ($this->io->isVerbose()) {
$this->getApplication()?->renderThrowable($e, $this->io); $this->getApplication()?->renderThrowable($e, $this->io);
} }
return ExitCodes::EXIT_FAILURE; return ExitCode::EXIT_FAILURE;
} }
} }
@ -171,7 +171,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
$downloadDbCommand = $cliApp->find(DownloadGeoLiteDbCommand::NAME); $downloadDbCommand = $cliApp->find(DownloadGeoLiteDbCommand::NAME);
$exitCode = $downloadDbCommand->run(new ArrayInput([]), $this->io); $exitCode = $downloadDbCommand->run(new ArrayInput([]), $this->io);
if ($exitCode === ExitCodes::EXIT_FAILURE) { if ($exitCode === ExitCode::EXIT_FAILURE) {
throw new RuntimeException('It is not possible to locate visits without a GeoLite2 db file.'); throw new RuntimeException('It is not possible to locate visits without a GeoLite2 db file.');
} }
} }

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Util; namespace Shlinkio\Shlink\CLI\Util;
final class ExitCodes final class ExitCode
{ {
public const EXIT_SUCCESS = 0; public const EXIT_SUCCESS = 0;
public const EXIT_FAILURE = -1; public const EXIT_FAILURE = -1;

View File

@ -7,7 +7,7 @@ namespace ShlinkioCliTest\Shlink\CLI\Command;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand; use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand;
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand; use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase; use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
class CreateShortUrlTest extends CliTestCase class CreateShortUrlTest extends CliTestCase
@ -22,7 +22,7 @@ class CreateShortUrlTest extends CliTestCase
[CreateShortUrlCommand::NAME, 'https://example.com', '--domain', $defaultDomain, '--custom-slug', $slug], [CreateShortUrlCommand::NAME, 'https://example.com', '--domain', $defaultDomain, '--custom-slug', $slug],
); );
self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode); self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
self::assertStringContainsString('Generated short URL: http://' . $defaultDomain . '/' . $slug, $output); self::assertStringContainsString('Generated short URL: http://' . $defaultDomain . '/' . $slug, $output);
[$listOutput] = $this->exec([ListShortUrlsCommand::NAME, '--show-domain', '--search-term', $slug]); [$listOutput] = $this->exec([ListShortUrlsCommand::NAME, '--show-domain', '--search-term', $slug]);

View File

@ -6,7 +6,7 @@ namespace ShlinkioCliTest\Shlink\CLI\Command;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand; use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase; use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
class GenerateApiKeyTest extends CliTestCase class GenerateApiKeyTest extends CliTestCase
@ -17,6 +17,6 @@ class GenerateApiKeyTest extends CliTestCase
[$output, $exitCode] = $this->exec([GenerateKeyCommand::NAME]); [$output, $exitCode] = $this->exec([GenerateKeyCommand::NAME]);
self::assertStringContainsString('[OK] Generated API key', $output); self::assertStringContainsString('[OK] Generated API key', $output);
self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode); self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
} }
} }

View File

@ -8,7 +8,7 @@ use Cake\Chronos\Chronos;
use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand; use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase; use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
class ListApiKeysTest extends CliTestCase class ListApiKeysTest extends CliTestCase
@ -19,7 +19,7 @@ class ListApiKeysTest extends CliTestCase
[$output, $exitCode] = $this->exec([ListKeysCommand::NAME, ...$flags]); [$output, $exitCode] = $this->exec([ListKeysCommand::NAME, ...$flags]);
self::assertEquals($expectedOutput, $output); self::assertEquals($expectedOutput, $output);
self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode); self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
} }
public static function provideFlags(): iterable public static function provideFlags(): iterable

View File

@ -9,7 +9,7 @@ use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Domain\ListDomainsCommand; use Shlinkio\Shlink\CLI\Command\Domain\ListDomainsCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Config\NotFoundRedirects; use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface; use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
use Shlinkio\Shlink\Core\Domain\Entity\Domain; use Shlinkio\Shlink\Core\Domain\Entity\Domain;
@ -53,7 +53,7 @@ class ListDomainsCommandTest extends TestCase
$this->commandTester->execute($input); $this->commandTester->execute($input);
self::assertEquals($expectedOutput, $this->commandTester->getDisplay()); self::assertEquals($expectedOutput, $this->commandTester->getDisplay());
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode()); self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
} }
public static function provideInputsAndOutputs(): iterable public static function provideInputsAndOutputs(): iterable

View File

@ -11,7 +11,7 @@ use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand; use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions; use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
@ -65,7 +65,7 @@ class CreateShortUrlCommandTest extends TestCase
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]); ], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
$output = $this->commandTester->getDisplay(); $output = $this->commandTester->getDisplay();
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode()); self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
self::assertStringContainsString('stringified_short_url', $output); self::assertStringContainsString('stringified_short_url', $output);
self::assertStringNotContainsString('but the real-time updates cannot', $output); self::assertStringNotContainsString('but the real-time updates cannot', $output);
} }
@ -82,7 +82,7 @@ class CreateShortUrlCommandTest extends TestCase
$this->commandTester->execute(['longUrl' => $url]); $this->commandTester->execute(['longUrl' => $url]);
$output = $this->commandTester->getDisplay(); $output = $this->commandTester->getDisplay();
self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode()); self::assertEquals(ExitCode::EXIT_FAILURE, $this->commandTester->getStatusCode());
self::assertStringContainsString('Provided URL http://domain.com/invalid is invalid.', $output); self::assertStringContainsString('Provided URL http://domain.com/invalid is invalid.', $output);
} }
@ -97,7 +97,7 @@ class CreateShortUrlCommandTest extends TestCase
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--custom-slug' => 'my-slug']); $this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--custom-slug' => 'my-slug']);
$output = $this->commandTester->getDisplay(); $output = $this->commandTester->getDisplay();
self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode()); self::assertEquals(ExitCode::EXIT_FAILURE, $this->commandTester->getStatusCode());
self::assertStringContainsString('Provided slug "my-slug" is already in use', $output); self::assertStringContainsString('Provided slug "my-slug" is already in use', $output);
} }
@ -121,7 +121,7 @@ class CreateShortUrlCommandTest extends TestCase
]); ]);
$output = $this->commandTester->getDisplay(); $output = $this->commandTester->getDisplay();
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode()); self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
self::assertStringContainsString('stringified_short_url', $output); self::assertStringContainsString('stringified_short_url', $output);
} }
@ -139,7 +139,7 @@ class CreateShortUrlCommandTest extends TestCase
$input['longUrl'] = 'http://domain.com/foo/bar'; $input['longUrl'] = 'http://domain.com/foo/bar';
$this->commandTester->execute($input); $this->commandTester->execute($input);
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode()); self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
} }
public static function provideDomains(): iterable public static function provideDomains(): iterable

View File

@ -12,7 +12,7 @@ use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException; use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface; use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult; use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait; use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tester\CommandTester;
@ -65,12 +65,12 @@ class DownloadGeoLiteDbCommandTest extends TestCase
yield 'existing db' => [ yield 'existing db' => [
true, true,
'[WARNING] GeoLite2 db file update failed. Visits will continue to be located', '[WARNING] GeoLite2 db file update failed. Visits will continue to be located',
ExitCodes::EXIT_WARNING, ExitCode::EXIT_WARNING,
]; ];
yield 'not existing db' => [ yield 'not existing db' => [
false, false,
'[ERROR] GeoLite2 db file download failed. It will not be possible to locate', '[ERROR] GeoLite2 db file download failed. It will not be possible to locate',
ExitCodes::EXIT_FAILURE, ExitCode::EXIT_FAILURE,
]; ];
} }
@ -86,7 +86,7 @@ class DownloadGeoLiteDbCommandTest extends TestCase
$exitCode = $this->commandTester->getStatusCode(); $exitCode = $this->commandTester->getStatusCode();
self::assertStringContainsString($expectedMessage, $output); self::assertStringContainsString($expectedMessage, $output);
self::assertSame(ExitCodes::EXIT_SUCCESS, $exitCode); self::assertSame(ExitCode::EXIT_SUCCESS, $exitCode);
} }
public static function provideSuccessParams(): iterable public static function provideSuccessParams(): iterable

View File

@ -10,7 +10,7 @@ use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand; use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand; use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException; use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Entity\Visit;
@ -85,7 +85,7 @@ class LocateVisitsCommandTest extends TestCase
$this->visitToLocation->expects( $this->visitToLocation->expects(
$this->exactly($expectedUnlocatedCalls + $expectedEmptyCalls + $expectedAllCalls), $this->exactly($expectedUnlocatedCalls + $expectedEmptyCalls + $expectedAllCalls),
)->method('resolveVisitLocation')->withAnyParameters()->willReturn(Location::emptyInstance()); )->method('resolveVisitLocation')->withAnyParameters()->willReturn(Location::emptyInstance());
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS); $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
$this->commandTester->setInputs(['y']); $this->commandTester->setInputs(['y']);
$this->commandTester->execute($args); $this->commandTester->execute($args);
@ -118,7 +118,7 @@ class LocateVisitsCommandTest extends TestCase
->withAnyParameters() ->withAnyParameters()
->willReturnCallback($this->invokeHelperMethods($visit, $location)); ->willReturnCallback($this->invokeHelperMethods($visit, $location));
$this->visitToLocation->expects($this->once())->method('resolveVisitLocation')->willThrowException($e); $this->visitToLocation->expects($this->once())->method('resolveVisitLocation')->willThrowException($e);
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS); $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]); $this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
@ -147,7 +147,7 @@ class LocateVisitsCommandTest extends TestCase
$this->visitToLocation->expects($this->once())->method('resolveVisitLocation')->willThrowException( $this->visitToLocation->expects($this->once())->method('resolveVisitLocation')->willThrowException(
IpCannotBeLocatedException::forError(WrongIpException::fromIpAddress('1.2.3.4')), IpCannotBeLocatedException::forError(WrongIpException::fromIpAddress('1.2.3.4')),
); );
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS); $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]); $this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
@ -171,7 +171,7 @@ class LocateVisitsCommandTest extends TestCase
$this->visitService->expects($this->never())->method('locateUnlocatedVisits'); $this->visitService->expects($this->never())->method('locateUnlocatedVisits');
$this->visitToLocation->expects($this->never())->method('resolveVisitLocation'); $this->visitToLocation->expects($this->never())->method('resolveVisitLocation');
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS); $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]); $this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
$output = $this->commandTester->getDisplay(); $output = $this->commandTester->getDisplay();
@ -186,7 +186,7 @@ class LocateVisitsCommandTest extends TestCase
public function showsProperMessageWhenGeoLiteUpdateFails(): void public function showsProperMessageWhenGeoLiteUpdateFails(): void
{ {
$this->lock->method('acquire')->with($this->isFalse())->willReturn(true); $this->lock->method('acquire')->with($this->isFalse())->willReturn(true);
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_FAILURE); $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_FAILURE);
$this->visitService->expects($this->never())->method('locateUnlocatedVisits'); $this->visitService->expects($this->never())->method('locateUnlocatedVisits');
$this->commandTester->execute([]); $this->commandTester->execute([]);
@ -199,7 +199,7 @@ class LocateVisitsCommandTest extends TestCase
public function providingAllFlagOnItsOwnDisplaysNotice(): void public function providingAllFlagOnItsOwnDisplaysNotice(): void
{ {
$this->lock->method('acquire')->with($this->isFalse())->willReturn(true); $this->lock->method('acquire')->with($this->isFalse())->willReturn(true);
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS); $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
$this->commandTester->execute(['--all' => true]); $this->commandTester->execute(['--all' => true]);
$output = $this->commandTester->getDisplay(); $output = $this->commandTester->getDisplay();
@ -210,7 +210,7 @@ class LocateVisitsCommandTest extends TestCase
#[Test, DataProvider('provideAbortInputs')] #[Test, DataProvider('provideAbortInputs')]
public function processingAllCancelsCommandIfUserDoesNotActivelyAgreeToConfirmation(array $inputs): void public function processingAllCancelsCommandIfUserDoesNotActivelyAgreeToConfirmation(array $inputs): void
{ {
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS); $this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
$this->expectException(RuntimeException::class); $this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Execution aborted'); $this->expectExceptionMessage('Execution aborted');

View File

@ -8,6 +8,8 @@ use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use function sprintf;
final class ShortUrlIdentifier final class ShortUrlIdentifier
{ {
private function __construct(public readonly string $shortCode, public readonly ?string $domain = null) private function __construct(public readonly string $shortCode, public readonly ?string $domain = null)
@ -54,4 +56,13 @@ final class ShortUrlIdentifier
{ {
return new self($shortCode, $domain); return new self($shortCode, $domain);
} }
public function __toString(): string
{
if ($this->domain === null) {
return $this->shortCode;
}
return sprintf('%s/%s', $this->domain, $this->shortCode);
}
} }

View File

@ -21,7 +21,7 @@ class ShortUrlVisitsDeleter implements ShortUrlVisitsDeleterInterface
/** /**
* @throws ShortUrlNotFoundException * @throws ShortUrlNotFoundException
*/ */
public function deleteShortUrlVisits(ShortUrlIdentifier $identifier, ?ApiKey $apiKey): BulkDeleteResult public function deleteShortUrlVisits(ShortUrlIdentifier $identifier, ?ApiKey $apiKey = null): BulkDeleteResult
{ {
$shortUrl = $this->resolver->resolveShortUrl($identifier, $apiKey); $shortUrl = $this->resolver->resolveShortUrl($identifier, $apiKey);
return new BulkDeleteResult($this->repository->deleteShortUrlVisits($shortUrl)); return new BulkDeleteResult($this->repository->deleteShortUrlVisits($shortUrl));

View File

@ -14,5 +14,5 @@ interface ShortUrlVisitsDeleterInterface
/** /**
* @throws ShortUrlNotFoundException * @throws ShortUrlNotFoundException
*/ */
public function deleteShortUrlVisits(ShortUrlIdentifier $identifier, ?ApiKey $apiKey): BulkDeleteResult; public function deleteShortUrlVisits(ShortUrlIdentifier $identifier, ?ApiKey $apiKey = null): BulkDeleteResult;
} }

View File

@ -52,7 +52,6 @@ class VisitDeleterRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->flush(); $this->getEntityManager()->flush();
self::assertEquals(0, $this->repo->deleteShortUrlVisits(ShortUrl::withLongUrl('https://invalid')->setId('99')));
self::assertEquals(2, $this->repo->deleteShortUrlVisits($shortUrl1)); self::assertEquals(2, $this->repo->deleteShortUrlVisits($shortUrl1));
self::assertEquals(0, $this->repo->deleteShortUrlVisits($shortUrl1)); self::assertEquals(0, $this->repo->deleteShortUrlVisits($shortUrl1));
self::assertEquals(4, $this->repo->deleteShortUrlVisits($shortUrl2)); self::assertEquals(4, $this->repo->deleteShortUrlVisits($shortUrl2));

View File

@ -320,27 +320,6 @@ class CreateShortUrlTest extends ApiTestCase
yield 'example domain' => ['example.com']; yield 'example domain' => ['example.com'];
} }
#[Test, DataProvider('provideTwitterUrls')]
public function urlsWithBotProtectionCanBeShortenedWithUrlValidationEnabled(string $longUrl): void
{
// Requests to Twitter are randomly failing from GitHub actions. Let's skip this test there.
// This is a deprecated and low-used feature anyway.
if (env('CI', false)) {
$this->markTestSkipped();
}
[$statusCode] = $this->createShortUrl(['longUrl' => $longUrl, 'validateUrl' => true]);
self::assertEquals(self::STATUS_OK, $statusCode);
}
public static function provideTwitterUrls(): iterable
{
yield ['https://twitter.com/shlinkio'];
yield ['https://mobile.twitter.com/shlinkio'];
yield ['https://twitter.com/shlinkio/status/1360637738421268481'];
yield ['https://mobile.twitter.com/shlinkio/status/1360637738421268481'];
}
#[Test] #[Test]
public function canCreateShortUrlsWithEmojis(): void public function canCreateShortUrlsWithEmojis(): void
{ {