mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-06 23:33:13 +08:00
Add new command to delete API keys
This commit is contained in:
@@ -26,6 +26,7 @@ return [
|
||||
|
||||
Command\Api\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class,
|
||||
Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class,
|
||||
Command\Api\DeleteKeyCommand::NAME => Command\Api\DeleteKeyCommand::class,
|
||||
Command\Api\ListKeysCommand::NAME => Command\Api\ListKeysCommand::class,
|
||||
Command\Api\InitialApiKeyCommand::NAME => Command\Api\InitialApiKeyCommand::class,
|
||||
Command\Api\RenameApiKeyCommand::NAME => Command\Api\RenameApiKeyCommand::class,
|
||||
|
||||
@@ -52,6 +52,7 @@ return [
|
||||
|
||||
Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\DeleteKeyCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\ListKeysCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\InitialApiKeyCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\RenameApiKeyCommand::class => ConfigAbstractFactory::class,
|
||||
@@ -108,6 +109,7 @@ return [
|
||||
|
||||
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class],
|
||||
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],
|
||||
Command\Api\DeleteKeyCommand::class => [ApiKeyService::class],
|
||||
Command\Api\ListKeysCommand::class => [ApiKeyService::class],
|
||||
Command\Api\InitialApiKeyCommand::class => [ApiKeyService::class],
|
||||
Command\Api\RenameApiKeyCommand::class => [ApiKeyService::class],
|
||||
|
||||
94
module/CLI/src/Command/Api/DeleteKeyCommand.php
Normal file
94
module/CLI/src/Command/Api/DeleteKeyCommand.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Exception\ApiKeyNotFoundException;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\map;
|
||||
use function sprintf;
|
||||
|
||||
#[AsCommand(
|
||||
name: DeleteKeyCommand::NAME,
|
||||
description: 'Deletes an API key by name',
|
||||
help: <<<HELP
|
||||
The <info>%command.name%</info> command allows you to delete an existing API key via its name.
|
||||
|
||||
If no arguments are provided, you will be prompted to select one of the existing API keys.
|
||||
|
||||
<info>%command.full_name%</info>
|
||||
|
||||
You can optionally pass the API key name to be disabled:
|
||||
|
||||
<info>%command.full_name% the_key_name</info>
|
||||
|
||||
HELP,
|
||||
)]
|
||||
class DeleteKeyCommand extends Command
|
||||
{
|
||||
public const string NAME = 'api-key:delete';
|
||||
|
||||
public function __construct(private readonly ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$apiKeyName = $input->getArgument('name');
|
||||
|
||||
if ($apiKeyName === null) {
|
||||
$apiKeys = $this->apiKeyService->listKeys();
|
||||
$name = (new SymfonyStyle($input, $output))->choice(
|
||||
'What API key do you want to delete?',
|
||||
map($apiKeys, static fn (ApiKey $apiKey) => $apiKey->name),
|
||||
);
|
||||
|
||||
$input->setArgument('name', $name);
|
||||
}
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
SymfonyStyle $io,
|
||||
InputInterface $input,
|
||||
#[Argument(description: 'The API key to delete.')]
|
||||
string|null $name = null,
|
||||
): int {
|
||||
if ($name === null) {
|
||||
$io->warning('An API key name was not provided.');
|
||||
return Command::INVALID;
|
||||
}
|
||||
|
||||
if (! $this->shouldProceed($io, $input)) {
|
||||
return Command::INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->apiKeyService->deleteByName($name);
|
||||
$io->success(sprintf('API key "%s" properly deleted', $name));
|
||||
return Command::SUCCESS;
|
||||
} catch (ApiKeyNotFoundException $e) {
|
||||
$io->error($e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldProceed(SymfonyStyle $io, InputInterface $input): bool
|
||||
{
|
||||
if (! $input->isInteractive()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$io->warning('You are about to delete an API key. This action cannot be undone.');
|
||||
return $io->confirm('Are you sure you want to delete the API key?');
|
||||
}
|
||||
}
|
||||
100
module/CLI/test/Command/Api/DeleteKeyCommandTest.php
Normal file
100
module/CLI/test/Command/Api/DeleteKeyCommandTest.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\DeleteKeyCommand;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Exception\ApiKeyNotFoundException;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class DeleteKeyCommandTest extends TestCase
|
||||
{
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ApiKeyServiceInterface $apiKeyService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->createMock(ApiKeyServiceInterface::class);
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new DeleteKeyCommand($this->apiKeyService));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function warningIsReturnedIfNoArgumentIsProvidedInNonInteractiveMode(): void
|
||||
{
|
||||
$this->apiKeyService->expects($this->never())->method('deleteByName');
|
||||
$this->apiKeyService->expects($this->never())->method('listKeys');
|
||||
|
||||
$exitCode = $this->commandTester->execute([], ['interactive' => false]);
|
||||
|
||||
self::assertEquals(Command::INVALID, $exitCode);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function confirmationIsSkippedInNonInteractiveMode(): void
|
||||
{
|
||||
$this->apiKeyService->expects($this->once())->method('deleteByName');
|
||||
$this->apiKeyService->expects($this->never())->method('listKeys');
|
||||
|
||||
$exitCode = $this->commandTester->execute(['name' => 'key to delete'], ['interactive' => false]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertEquals(Command::SUCCESS, $exitCode);
|
||||
self::assertStringNotContainsString('Are you sure you want to delete the API key?', $output);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function keyIsNotDeletedIfConfirmationIsCancelled(): void
|
||||
{
|
||||
$this->apiKeyService->expects($this->never())->method('deleteByName');
|
||||
$this->apiKeyService->expects($this->never())->method('listKeys');
|
||||
|
||||
$this->commandTester->setInputs(['no']);
|
||||
$exitCode = $this->commandTester->execute(['name' => 'key_to_delete']);
|
||||
|
||||
self::assertEquals(Command::INVALID, $exitCode);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function existingApiKeyNamesAreListedIfNoArgumentIsProvidedInInteractiveMode(): void
|
||||
{
|
||||
$name = 'the key to delete';
|
||||
$this->apiKeyService->expects($this->once())->method('deleteByName')->with($name);
|
||||
$this->apiKeyService->expects($this->once())->method('listKeys')->willReturn([
|
||||
ApiKey::fromMeta(ApiKeyMeta::fromParams(name: 'foo')),
|
||||
ApiKey::fromMeta(ApiKeyMeta::fromParams(name: $name)),
|
||||
ApiKey::fromMeta(ApiKeyMeta::fromParams(name: 'bar')),
|
||||
]);
|
||||
|
||||
$this->commandTester->setInputs([$name, 'y']);
|
||||
$exitCode = $this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('What API key do you want to delete?', $output);
|
||||
self::assertStringContainsString('API key "the key to delete" properly deleted', $output);
|
||||
self::assertEquals(Command::SUCCESS, $exitCode);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function errorIsReturnedIfDisableByKeyThrowsException(): void
|
||||
{
|
||||
$apiKey = 'key to delete';
|
||||
$e = ApiKeyNotFoundException::forName($apiKey);
|
||||
$this->apiKeyService->expects($this->once())->method('deleteByName')->with($apiKey)->willThrowException($e);
|
||||
$this->apiKeyService->expects($this->never())->method('listKeys');
|
||||
|
||||
$exitCode = $this->commandTester->execute(['name' => $apiKey]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString($e->getMessage(), $output);
|
||||
self::assertEquals(Command::FAILURE, $exitCode);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user