diff --git a/module/CLI/config/cli.config.php b/module/CLI/config/cli.config.php index c1f9043b..eb27be7f 100644 --- a/module/CLI/config/cli.config.php +++ b/module/CLI/config/cli.config.php @@ -14,6 +14,7 @@ return [ Command\Shortcode\ListShortcodesCommand::NAME => Command\Shortcode\ListShortcodesCommand::class, Command\Shortcode\GetVisitsCommand::NAME => Command\Shortcode\GetVisitsCommand::class, Command\Shortcode\GeneratePreviewCommand::NAME => Command\Shortcode\GeneratePreviewCommand::class, + Command\Shortcode\DeleteShortCodeCommand::NAME => Command\Shortcode\DeleteShortCodeCommand::class, Command\Visit\ProcessVisitsCommand::NAME => Command\Visit\ProcessVisitsCommand::class, diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 236b8d87..065c720c 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -22,12 +22,17 @@ return [ Command\Shortcode\ListShortcodesCommand::class => ConfigAbstractFactory::class, Command\Shortcode\GetVisitsCommand::class => ConfigAbstractFactory::class, Command\Shortcode\GeneratePreviewCommand::class => ConfigAbstractFactory::class, + Command\Shortcode\DeleteShortCodeCommand::class => ConfigAbstractFactory::class, + Command\Visit\ProcessVisitsCommand::class => ConfigAbstractFactory::class, + Command\Config\GenerateCharsetCommand::class => ConfigAbstractFactory::class, Command\Config\GenerateSecretCommand::class => ConfigAbstractFactory::class, + Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class, Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class, Command\Api\ListKeysCommand::class => ConfigAbstractFactory::class, + Command\Tag\ListTagsCommand::class => ConfigAbstractFactory::class, Command\Tag\CreateTagCommand::class => ConfigAbstractFactory::class, Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class, @@ -53,16 +58,24 @@ return [ PreviewGenerator::class, 'translator', ], + Command\Shortcode\DeleteShortCodeCommand::class => [ + Service\ShortUrl\DeleteShortUrlService::class, + 'translator', + ], + Command\Visit\ProcessVisitsCommand::class => [ Service\VisitService::class, IpApiLocationResolver::class, 'translator', ], + Command\Config\GenerateCharsetCommand::class => ['translator'], Command\Config\GenerateSecretCommand::class => ['translator'], + Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, 'translator'], Command\Api\DisableKeyCommand::class => [ApiKeyService::class, 'translator'], Command\Api\ListKeysCommand::class => [ApiKeyService::class, 'translator'], + Command\Tag\ListTagsCommand::class => [Service\Tag\TagService::class, Translator::class], Command\Tag\CreateTagCommand::class => [Service\Tag\TagService::class, Translator::class], Command\Tag\RenameTagCommand::class => [Service\Tag\TagService::class, Translator::class], diff --git a/module/CLI/src/Command/Shortcode/DeleteShortCodeCommand.php b/module/CLI/src/Command/Shortcode/DeleteShortCodeCommand.php new file mode 100644 index 00000000..f02750a1 --- /dev/null +++ b/module/CLI/src/Command/Shortcode/DeleteShortCodeCommand.php @@ -0,0 +1,101 @@ +deleteShortUrlService = $deleteShortUrlService; + $this->translator = $translator; + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setName(self::NAME) + ->setAliases(self::ALIASES) + ->setDescription( + $this->translator->translate('Deletes a short URL') + ) + ->addArgument( + 'shortCode', + InputArgument::REQUIRED, + $this->translator->translate('The short code to be deleted') + ) + ->addOption( + 'ignore-threshold', + 'i', + InputOption::VALUE_NONE, + $this->translator->translate( + 'Ignores the safety visits threshold check, which could make short URLs with many visits to be ' + . 'accidentally deleted' + ) + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): void + { + $io = new SymfonyStyle($input, $output); + $shortCode = $input->getArgument('shortCode'); + $ignoreThreshold = $input->getOption('ignore-threshold'); + + try { + $this->runDelete($io, $shortCode, $ignoreThreshold); + } catch (Exception\InvalidShortCodeException $e) { + $io->error( + \sprintf($this->translator->translate('Provided short code "%s" could not be found.'), $shortCode) + ); + } catch (Exception\DeleteShortUrlException $e) { + $this->retry($io, $shortCode, $e); + } + } + + private function retry(SymfonyStyle $io, string $shortCode, Exception\DeleteShortUrlException $e): void + { + $warningMsg = \sprintf($this->translator->translate( + '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('' . $warningMsg . ''); + $forceDelete = $io->confirm($this->translator->translate('Do you want to delete it anyway?'), false); + + if ($forceDelete) { + $this->runDelete($io, $shortCode, true); + } else { + $io->warning($this->translator->translate('Short URL was not deleted.')); + } + } + + private function runDelete(SymfonyStyle $io, string $shortCode, bool $ignoreThreshold): void + { + $this->deleteShortUrlService->deleteByShortCode($shortCode, $ignoreThreshold); + $io->success(\sprintf( + $this->translator->translate('Short URL with short code "%s" successfully deleted.'), + $shortCode + )); + } +} diff --git a/module/CLI/test/Command/Shortcode/DeleteShortCodeCommandTest.php b/module/CLI/test/Command/Shortcode/DeleteShortCodeCommandTest.php new file mode 100644 index 00000000..e74b16b2 --- /dev/null +++ b/module/CLI/test/Command/Shortcode/DeleteShortCodeCommandTest.php @@ -0,0 +1,120 @@ +service = $this->prophesize(DeleteShortUrlServiceInterface::class); + + $command = new DeleteShortCodeCommand($this->service->reveal(), Translator::factory([])); + $app = new Application(); + $app->add($command); + + $this->commandTester = new CommandTester($command); + } + + /** + * @test + */ + public function successMessageIsPrintedIfUrlIsProperlyDeleted() + { + $shortCode = 'abc123'; + $deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->will(function () { + }); + + $this->commandTester->execute(['shortCode' => $shortCode]); + $output = $this->commandTester->getDisplay(); + + $this->assertContains(\sprintf('Short URL with short code "%s" successfully deleted.', $shortCode), $output); + $deleteByShortCode->shouldHaveBeenCalledTimes(1); + } + + /** + * @test + */ + public function invalidShortCodePrintsMessage() + { + $shortCode = 'abc123'; + $deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow( + Exception\InvalidShortCodeException::class + ); + + $this->commandTester->execute(['shortCode' => $shortCode]); + $output = $this->commandTester->getDisplay(); + + $this->assertContains(\sprintf('Provided short code "%s" could not be found.', $shortCode), $output); + $deleteByShortCode->shouldHaveBeenCalledTimes(1); + } + + /** + * @test + */ + public function deleteIsRetriedWhenThresholdIsReachedAndQuestionIsAccepted() + { + $shortCode = 'abc123'; + $deleteByShortCode = $this->service->deleteByShortCode($shortCode, Argument::type('bool'))->will( + function (array $args) { + $ignoreThreshold = \array_pop($args); + + if (!$ignoreThreshold) { + throw new Exception\DeleteShortUrlException(10); + } + } + ); + $this->commandTester->setInputs(['yes']); + + $this->commandTester->execute(['shortCode' => $shortCode]); + $output = $this->commandTester->getDisplay(); + + $this->assertContains(\sprintf( + 'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.', + $shortCode + ), $output); + $this->assertContains(\sprintf('Short URL with short code "%s" successfully deleted.', $shortCode), $output); + $deleteByShortCode->shouldHaveBeenCalledTimes(2); + } + + /** + * @test + */ + public function deleteIsNotRetriedWhenThresholdIsReachedAndQuestionIsDeclined() + { + $shortCode = 'abc123'; + $deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow( + new Exception\DeleteShortUrlException(10) + ); + $this->commandTester->setInputs(['no']); + + $this->commandTester->execute(['shortCode' => $shortCode]); + $output = $this->commandTester->getDisplay(); + + $this->assertContains(\sprintf( + 'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.', + $shortCode + ), $output); + $this->assertContains('Short URL was not deleted.', $output); + $deleteByShortCode->shouldHaveBeenCalledTimes(1); + } +} diff --git a/module/Core/src/Service/ShortUrl/DeleteShortUrlService.php b/module/Core/src/Service/ShortUrl/DeleteShortUrlService.php index 4f7607eb..287a5a59 100644 --- a/module/Core/src/Service/ShortUrl/DeleteShortUrlService.php +++ b/module/Core/src/Service/ShortUrl/DeleteShortUrlService.php @@ -31,10 +31,10 @@ class DeleteShortUrlService implements DeleteShortUrlServiceInterface * @throws Exception\InvalidShortCodeException * @throws Exception\DeleteShortUrlException */ - public function deleteByShortCode(string $shortCode): void + public function deleteByShortCode(string $shortCode, bool $ignoreThreshold = false): void { $shortUrl = $this->findByShortCode($this->em, $shortCode); - if ($this->isThresholdReached($shortUrl)) { + if (! $ignoreThreshold && $this->isThresholdReached($shortUrl)) { throw Exception\DeleteShortUrlException::fromVisitsThreshold( $this->deleteShortUrlsOptions->getVisitsThreshold(), $shortUrl->getShortCode() diff --git a/module/Core/src/Service/ShortUrl/DeleteShortUrlServiceInterface.php b/module/Core/src/Service/ShortUrl/DeleteShortUrlServiceInterface.php index 4afbb15a..de119bbb 100644 --- a/module/Core/src/Service/ShortUrl/DeleteShortUrlServiceInterface.php +++ b/module/Core/src/Service/ShortUrl/DeleteShortUrlServiceInterface.php @@ -11,5 +11,5 @@ interface DeleteShortUrlServiceInterface * @throws Exception\InvalidShortCodeException * @throws Exception\DeleteShortUrlException */ - public function deleteByShortCode(string $shortCode): void; + public function deleteByShortCode(string $shortCode, bool $ignoreThreshold = false): void; } diff --git a/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php b/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php index 727ad187..d79ce44d 100644 --- a/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php +++ b/module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php @@ -55,6 +55,22 @@ class DeleteShortUrlServiceTest extends TestCase $service->deleteByShortCode('abc123'); } + /** + * @test + */ + public function deleteByShortCodeDeletesUrlWhenThresholdIsReachedButExplicitlyIgnored() + { + $service = $this->createService(); + + $remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null); + $flush = $this->em->flush()->willReturn(null); + + $service->deleteByShortCode('abc123', true); + + $remove->shouldHaveBeenCalledTimes(1); + $flush->shouldHaveBeenCalledTimes(1); + } + /** * @test */