Updated CLI commands to just print exception messages when possible

This commit is contained in:
Alejandro Celaya 2019-11-28 18:42:27 +01:00
parent d83d2f82bd
commit 5055ddf995
14 changed files with 40 additions and 97 deletions

View File

@ -4,8 +4,8 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api; namespace Shlinkio\Shlink\CLI\Command\Api;
use InvalidArgumentException;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCodes;
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;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -45,7 +45,7 @@ class DisableKeyCommand extends Command
$io->success(sprintf('API key "%s" properly disabled', $apiKey)); $io->success(sprintf('API key "%s" properly disabled', $apiKey));
return ExitCodes::EXIT_SUCCESS; return ExitCodes::EXIT_SUCCESS;
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
$io->error(sprintf('API key "%s" does not exist.', $apiKey)); $io->error($e->getMessage());
return ExitCodes::EXIT_FAILURE; return ExitCodes::EXIT_FAILURE;
} }
} }

View File

@ -56,21 +56,16 @@ class DeleteShortUrlCommand extends Command
$this->runDelete($io, $shortCode, $ignoreThreshold); $this->runDelete($io, $shortCode, $ignoreThreshold);
return ExitCodes::EXIT_SUCCESS; return ExitCodes::EXIT_SUCCESS;
} catch (Exception\ShortUrlNotFoundException $e) { } catch (Exception\ShortUrlNotFoundException $e) {
$io->error(sprintf('Provided short code "%s" could not be found.', $shortCode)); $io->error($e->getMessage());
return ExitCodes::EXIT_FAILURE; return ExitCodes::EXIT_FAILURE;
} catch (Exception\DeleteShortUrlException $e) { } catch (Exception\DeleteShortUrlException $e) {
return $this->retry($io, $shortCode, $e); return $this->retry($io, $shortCode, $e->getMessage());
} }
} }
private function retry(SymfonyStyle $io, string $shortCode, Exception\DeleteShortUrlException $e): int private function retry(SymfonyStyle $io, string $shortCode, string $warningMsg): int
{ {
$warningMsg = sprintf( $io->writeln(sprintf('<bg=yellow>%s</>', $warningMsg));
'It was not possible to delete the short URL with short code "%s" because it has more than %s visits.',
$shortCode,
$e->getVisitsThreshold()
);
$io->writeln('<bg=yellow>' . $warningMsg . '</>');
$forceDelete = $io->confirm('Do you want to delete it anyway?', false); $forceDelete = $io->confirm('Do you want to delete it anyway?', false);
if ($forceDelete) { if ($forceDelete) {

View File

@ -141,13 +141,8 @@ class GenerateShortUrlCommand extends Command
sprintf('Generated short URL: <info>%s</info>', $shortUrl->toString($this->domainConfig)), sprintf('Generated short URL: <info>%s</info>', $shortUrl->toString($this->domainConfig)),
]); ]);
return ExitCodes::EXIT_SUCCESS; return ExitCodes::EXIT_SUCCESS;
} catch (InvalidUrlException $e) { } catch (InvalidUrlException | NonUniqueSlugException $e) {
$io->error(sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl)); $io->error($e->getMessage());
return ExitCodes::EXIT_FAILURE;
} catch (NonUniqueSlugException $e) {
$io->error(
sprintf('Provided slug "%s" is already in use by another URL. Try with a different one.', $customSlug)
);
return ExitCodes::EXIT_FAILURE; return ExitCodes::EXIT_FAILURE;
} }
} }

View File

@ -65,7 +65,7 @@ class ResolveUrlCommand extends Command
$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 ExitCodes::EXIT_SUCCESS;
} catch (ShortUrlNotFoundException $e) { } catch (ShortUrlNotFoundException $e) {
$io->error(sprintf('Provided short code "%s" could not be found.', $shortCode)); $io->error($e->getMessage());
return ExitCodes::EXIT_FAILURE; return ExitCodes::EXIT_FAILURE;
} }
} }

View File

@ -13,8 +13,6 @@ 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;
use function sprintf;
class RenameTagCommand extends Command class RenameTagCommand extends Command
{ {
public const NAME = 'tag:rename'; public const NAME = 'tag:rename';
@ -48,7 +46,7 @@ class RenameTagCommand extends Command
$io->success('Tag properly renamed.'); $io->success('Tag properly renamed.');
return ExitCodes::EXIT_SUCCESS; return ExitCodes::EXIT_SUCCESS;
} catch (TagNotFoundException $e) { } catch (TagNotFoundException $e) {
$io->error(sprintf('A tag with name "%s" was not found', $oldName)); $io->error($e->getMessage());
return ExitCodes::EXIT_FAILURE; return ExitCodes::EXIT_FAILURE;
} }
} }

View File

@ -12,20 +12,16 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc
/** @var bool */ /** @var bool */
private $olderDbExists; private $olderDbExists;
public function __construct(bool $olderDbExists, string $message = '', int $code = 0, ?Throwable $previous = null)
{
$this->olderDbExists = $olderDbExists;
parent::__construct($message, $code, $previous);
}
public static function create(bool $olderDbExists, ?Throwable $prev = null): self public static function create(bool $olderDbExists, ?Throwable $prev = null): self
{ {
return new self( $e = new self(
$olderDbExists,
'An error occurred while updating geolocation database, and an older version could not be found', 'An error occurred while updating geolocation database, and an older version could not be found',
0, 0,
$prev $prev
); );
$e->olderDbExists = $olderDbExists;
return $e;
} }
public function olderDbExists(): bool public function olderDbExists(): bool

View File

@ -10,7 +10,6 @@ use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException; use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Lock\Factory as Locker; use Symfony\Component\Lock\Factory as Locker;
use Throwable;
class GeolocationDbUpdater implements GeolocationDbUpdaterInterface class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
{ {
@ -40,8 +39,6 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
try { try {
$this->downloadIfNeeded($mustBeUpdated, $handleProgress); $this->downloadIfNeeded($mustBeUpdated, $handleProgress);
} catch (Throwable $e) {
throw $e;
} finally { } finally {
$lock->release(); $lock->release();
} }

View File

@ -29,7 +29,7 @@ class DisableKeyCommandTest extends TestCase
} }
/** @test */ /** @test */
public function providedApiKeyIsDisabled() public function providedApiKeyIsDisabled(): void
{ {
$apiKey = 'abcd1234'; $apiKey = 'abcd1234';
$this->apiKeyService->disable($apiKey)->shouldBeCalledOnce(); $this->apiKeyService->disable($apiKey)->shouldBeCalledOnce();
@ -43,17 +43,18 @@ class DisableKeyCommandTest extends TestCase
} }
/** @test */ /** @test */
public function errorIsReturnedIfServiceThrowsException() public function errorIsReturnedIfServiceThrowsException(): void
{ {
$apiKey = 'abcd1234'; $apiKey = 'abcd1234';
$disable = $this->apiKeyService->disable($apiKey)->willThrow(InvalidArgumentException::class); $expectedMessage = 'API key "abcd1234" does not exist.';
$disable = $this->apiKeyService->disable($apiKey)->willThrow(new InvalidArgumentException($expectedMessage));
$this->commandTester->execute([ $this->commandTester->execute([
'apiKey' => $apiKey, 'apiKey' => $apiKey,
]); ]);
$output = $this->commandTester->getDisplay(); $output = $this->commandTester->getDisplay();
$this->assertStringContainsString('API key "abcd1234" does not exist.', $output); $this->assertStringContainsString($expectedMessage, $output);
$disable->shouldHaveBeenCalledOnce(); $disable->shouldHaveBeenCalledOnce();
} }
} }

View File

@ -58,13 +58,13 @@ class DeleteShortUrlCommandTest extends TestCase
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow( $deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow(
Exception\ShortUrlNotFoundException::class Exception\ShortUrlNotFoundException::fromNotFoundShortCode($shortCode)
); );
$this->commandTester->execute(['shortCode' => $shortCode]); $this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay(); $output = $this->commandTester->getDisplay();
$this->assertStringContainsString(sprintf('Provided short code "%s" could not be found.', $shortCode), $output); $this->assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
$deleteByShortCode->shouldHaveBeenCalledOnce(); $deleteByShortCode->shouldHaveBeenCalledOnce();
} }
@ -79,11 +79,11 @@ class DeleteShortUrlCommandTest extends TestCase
): void { ): void {
$shortCode = 'abc123'; $shortCode = 'abc123';
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, Argument::type('bool'))->will( $deleteByShortCode = $this->service->deleteByShortCode($shortCode, Argument::type('bool'))->will(
function (array $args) { function (array $args) use ($shortCode) {
$ignoreThreshold = array_pop($args); $ignoreThreshold = array_pop($args);
if (!$ignoreThreshold) { if (!$ignoreThreshold) {
throw Exception\DeleteShortUrlException::fromVisitsThreshold(10, ''); throw Exception\DeleteShortUrlException::fromVisitsThreshold(10, $shortCode);
} }
} }
); );
@ -93,7 +93,7 @@ class DeleteShortUrlCommandTest extends TestCase
$output = $this->commandTester->getDisplay(); $output = $this->commandTester->getDisplay();
$this->assertStringContainsString(sprintf( $this->assertStringContainsString(sprintf(
'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.', 'Impossible to delete short URL with short code "%s" since it has more than "10" visits.',
$shortCode $shortCode
), $output); ), $output);
$this->assertStringContainsString($expectedMessage, $output); $this->assertStringContainsString($expectedMessage, $output);
@ -112,7 +112,7 @@ class DeleteShortUrlCommandTest extends TestCase
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow( $deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow(
Exception\DeleteShortUrlException::fromVisitsThreshold(10, '') Exception\DeleteShortUrlException::fromVisitsThreshold(10, $shortCode)
); );
$this->commandTester->setInputs(['no']); $this->commandTester->setInputs(['no']);
@ -120,7 +120,7 @@ class DeleteShortUrlCommandTest extends TestCase
$output = $this->commandTester->getDisplay(); $output = $this->commandTester->getDisplay();
$this->assertStringContainsString(sprintf( $this->assertStringContainsString(sprintf(
'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.', 'Impossible to delete short URL with short code "%s" since it has more than "10" visits.',
$shortCode $shortCode
), $output); ), $output);
$this->assertStringContainsString('Short URL was not deleted.', $output); $this->assertStringContainsString('Short URL was not deleted.', $output);

View File

@ -59,21 +59,22 @@ class GenerateShortUrlCommandTest extends TestCase
/** @test */ /** @test */
public function exceptionWhileParsingLongUrlOutputsError(): void public function exceptionWhileParsingLongUrlOutputsError(): void
{ {
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(new InvalidUrlException()) $url = 'http://domain.com/invalid';
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(InvalidUrlException::fromUrl($url))
->shouldBeCalledOnce(); ->shouldBeCalledOnce();
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid']); $this->commandTester->execute(['longUrl' => $url]);
$output = $this->commandTester->getDisplay(); $output = $this->commandTester->getDisplay();
$this->assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode()); $this->assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
$this->assertStringContainsString('Provided URL "http://domain.com/invalid" is invalid.', $output); $this->assertStringContainsString('Provided URL http://domain.com/invalid is invalid.', $output);
} }
/** @test */ /** @test */
public function providingNonUniqueSlugOutputsError(): void public function providingNonUniqueSlugOutputsError(): void
{ {
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willThrow( $urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(
NonUniqueSlugException::class NonUniqueSlugException::fromSlug('my-slug')
); );
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--customSlug' => 'my-slug']); $this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--customSlug' => 'my-slug']);

View File

@ -52,11 +52,12 @@ class ResolveUrlCommandTest extends TestCase
public function incorrectShortCodeOutputsErrorMessage(): void public function incorrectShortCodeOutputsErrorMessage(): void
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode, null)->willThrow(ShortUrlNotFoundException::class) $this->urlShortener->shortCodeToUrl($shortCode, null)
->shouldBeCalledOnce(); ->willThrow(ShortUrlNotFoundException::fromNotFoundShortCode($shortCode))
->shouldBeCalledOnce();
$this->commandTester->execute(['shortCode' => $shortCode]); $this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay(); $output = $this->commandTester->getDisplay();
$this->assertStringContainsString(sprintf('Provided short code "%s" could not be found', $shortCode), $output); $this->assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
} }
} }

View File

@ -34,11 +34,11 @@ class RenameTagCommandTest extends TestCase
} }
/** @test */ /** @test */
public function errorIsPrintedIfExceptionIsThrown() public function errorIsPrintedIfExceptionIsThrown(): void
{ {
$oldName = 'foo'; $oldName = 'foo';
$newName = 'bar'; $newName = 'bar';
$renameTag = $this->tagService->renameTag($oldName, $newName)->willThrow(TagNotFoundException::class); $renameTag = $this->tagService->renameTag($oldName, $newName)->willThrow(TagNotFoundException::fromTag('foo'));
$this->commandTester->execute([ $this->commandTester->execute([
'oldName' => $oldName, 'oldName' => $oldName,
@ -46,12 +46,12 @@ class RenameTagCommandTest extends TestCase
]); ]);
$output = $this->commandTester->getDisplay(); $output = $this->commandTester->getDisplay();
$this->assertStringContainsString('A tag with name "foo" was not found', $output); $this->assertStringContainsString('Tag with name "foo" could not be found', $output);
$renameTag->shouldHaveBeenCalled(); $renameTag->shouldHaveBeenCalled();
} }
/** @test */ /** @test */
public function successIsPrintedIfNoErrorOccurs() public function successIsPrintedIfNoErrorOccurs(): void
{ {
$oldName = 'foo'; $oldName = 'foo';
$newName = 'bar'; $newName = 'bar';

View File

@ -12,47 +12,6 @@ use Throwable;
class GeolocationDbUpdateFailedExceptionTest extends TestCase class GeolocationDbUpdateFailedExceptionTest extends TestCase
{ {
/**
* @test
* @dataProvider provideOlderDbExists
*/
public function constructCreatesExceptionWithDefaultArgs(bool $olderDbExists): void
{
$e = new GeolocationDbUpdateFailedException($olderDbExists);
$this->assertEquals($olderDbExists, $e->olderDbExists());
$this->assertEquals('', $e->getMessage());
$this->assertEquals(0, $e->getCode());
$this->assertNull($e->getPrevious());
}
public function provideOlderDbExists(): iterable
{
yield 'with older DB' => [true];
yield 'without older DB' => [false];
}
/**
* @test
* @dataProvider provideConstructorArgs
*/
public function constructCreatesException(bool $olderDbExists, string $message, int $code, ?Throwable $prev): void
{
$e = new GeolocationDbUpdateFailedException($olderDbExists, $message, $code, $prev);
$this->assertEquals($olderDbExists, $e->olderDbExists());
$this->assertEquals($message, $e->getMessage());
$this->assertEquals($code, $e->getCode());
$this->assertEquals($prev, $e->getPrevious());
}
public function provideConstructorArgs(): iterable
{
yield [true, 'This is a nice error message', 99, new Exception('prev')];
yield [false, 'Another message', 0, new RuntimeException('prev')];
yield [true, 'An yet another message', -50, null];
}
/** /**
* @test * @test
* @dataProvider provideCreateArgs * @dataProvider provideCreateArgs

View File

@ -17,7 +17,7 @@ class NonUniqueSlugException extends InvalidArgumentException implements Problem
private const TITLE = 'Invalid custom slug'; private const TITLE = 'Invalid custom slug';
private const TYPE = 'INVALID_SLUG'; private const TYPE = 'INVALID_SLUG';
public static function fromSlug(string $slug, ?string $domain): self public static function fromSlug(string $slug, ?string $domain = null): self
{ {
$suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain); $suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain);
$e = new self(sprintf('Provided slug "%s" is already in use%s.', $slug, $suffix)); $e = new self(sprintf('Provided slug "%s" is already in use%s.', $slug, $suffix));