mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-17 07:49:54 +03:00
Merge pull request #199 from acelaya/feature/delete-short-codes
Delete short URLs
This commit is contained in:
commit
ff8441fa95
46 changed files with 1171 additions and 409 deletions
|
@ -9,6 +9,7 @@ return [
|
|||
'name' => 'Shlink',
|
||||
'version' => '%SHLINK_VERSION%',
|
||||
'secret_key' => env('SECRET_KEY'),
|
||||
'disable_track_param' => null,
|
||||
],
|
||||
|
||||
];
|
||||
|
|
13
config/autoload/delete_short_urls.global.php
Normal file
13
config/autoload/delete_short_urls.global.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
return [
|
||||
|
||||
'delete_short_urls' => [
|
||||
'visits_threshold' => 15,
|
||||
'check_visits_threshold' => true,
|
||||
],
|
||||
|
||||
];
|
50
data/migrations/Version20180915110857.php
Normal file
50
data/migrations/Version20180915110857.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20180915110857 extends AbstractMigration
|
||||
{
|
||||
private const ON_DELETE_MAP = [
|
||||
'visit_locations' => 'SET NULL',
|
||||
'short_urls' => 'CASCADE',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visits = $schema->getTable('visits');
|
||||
$foreignKeys = $visits->getForeignKeys();
|
||||
|
||||
// Remove all existing foreign keys and add them again with CASCADE delete
|
||||
foreach ($foreignKeys as $foreignKey) {
|
||||
$visits->removeForeignKey($foreignKey->getName());
|
||||
$foreignTable = $foreignKey->getForeignTableName();
|
||||
|
||||
$visits->addForeignKeyConstraint(
|
||||
$foreignTable,
|
||||
$foreignKey->getLocalColumns(),
|
||||
$foreignKey->getForeignColumns(),
|
||||
[
|
||||
'onDelete' => self::ON_DELETE_MAP[$foreignTable],
|
||||
'onUpdate' => 'RESTRICT',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// Nothing to run
|
||||
}
|
||||
}
|
|
@ -159,5 +159,70 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"delete": {
|
||||
"tags": [
|
||||
"ShortCodes"
|
||||
],
|
||||
"summary": "Delete short code",
|
||||
"description": "Deletes the short URL for provided short code.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to edit.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The short code has been properly deleted."
|
||||
},
|
||||
"400": {
|
||||
"description": "The visits threshold in shlink does not allow this short URL to be deleted.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"error": "INVALID_SHORTCODE_DELETION",
|
||||
"message": "It is not possible to delete URL with short code \"abc123\" because it has reached more than \"15\" visits."
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No short URL was found for provided short code.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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],
|
||||
|
|
Binary file not shown.
|
@ -1,8 +1,8 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Shlink 1.0\n"
|
||||
"POT-Creation-Date: 2018-08-04 16:35+0200\n"
|
||||
"PO-Revision-Date: 2018-08-04 16:37+0200\n"
|
||||
"POT-Creation-Date: 2018-09-15 17:57+0200\n"
|
||||
"PO-Revision-Date: 2018-09-15 18:02+0200\n"
|
||||
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: es_ES\n"
|
||||
|
@ -80,6 +80,41 @@ msgstr ""
|
|||
msgid "Secret key: \"%s\""
|
||||
msgstr "Clave secreta: \"%s\""
|
||||
|
||||
msgid "Deletes a short URL"
|
||||
msgstr "Elimina una URL"
|
||||
|
||||
msgid "The short code to be deleted"
|
||||
msgstr "El código corto a eliminar"
|
||||
|
||||
msgid ""
|
||||
"Ignores the safety visits threshold check, which could make short URLs with "
|
||||
"many visits to be accidentally deleted"
|
||||
msgstr ""
|
||||
"Ignora el límite de seguridad de visitas, pudiendo resultar en el borrado "
|
||||
"accidental de URLs con muchas visitas"
|
||||
|
||||
#, php-format
|
||||
msgid "Provided short code \"%s\" could not be found."
|
||||
msgstr "El código corto proporcionado \"%s\" no ha podido ser encontrado."
|
||||
|
||||
#, php-format
|
||||
msgid ""
|
||||
"It was not possible to delete the short URL with short code \"%s\" because "
|
||||
"it has more than %s visits."
|
||||
msgstr ""
|
||||
"No se pudo eliminar la URL acortada con código corto \"%s\" porque tiene más "
|
||||
"de %s visitas."
|
||||
|
||||
msgid "Do you want to delete it anyway?"
|
||||
msgstr "¿Aún así quieres eliminarla?"
|
||||
|
||||
msgid "Short URL was not deleted."
|
||||
msgstr "La URL corta no ha sido eliminada."
|
||||
|
||||
#, php-format
|
||||
msgid "Short URL with short code \"%s\" successfully deleted."
|
||||
msgstr "La URL acortada con el código corto \"%s\" eliminada correctamente."
|
||||
|
||||
msgid ""
|
||||
"Processes and generates the previews for every URL, improving performance "
|
||||
"for later web requests."
|
||||
|
@ -183,12 +218,12 @@ msgstr "Origen"
|
|||
msgid "Date"
|
||||
msgstr "Fecha"
|
||||
|
||||
msgid "Remote Address"
|
||||
msgstr "Dirección remota"
|
||||
|
||||
msgid "User agent"
|
||||
msgstr "Agente de usuario"
|
||||
|
||||
msgid "Country"
|
||||
msgstr "País"
|
||||
|
||||
msgid "List all short URLs"
|
||||
msgstr "Listar todas las URLs cortas"
|
||||
|
||||
|
@ -218,8 +253,11 @@ msgstr "Si se desea mostrar las etiquetas o no"
|
|||
msgid "Short code"
|
||||
msgstr "Código corto"
|
||||
|
||||
msgid "Original URL"
|
||||
msgstr "URL original"
|
||||
msgid "Short URL"
|
||||
msgstr "URL corta"
|
||||
|
||||
msgid "Long URL"
|
||||
msgstr "URL larga"
|
||||
|
||||
msgid "Date created"
|
||||
msgstr "Fecha de creación"
|
||||
|
@ -253,10 +291,6 @@ msgstr "URL larga:"
|
|||
msgid "Provided short code \"%s\" has an invalid format."
|
||||
msgstr "El código corto proporcionado \"%s\" tiene un formato inválido."
|
||||
|
||||
#, php-format
|
||||
msgid "Provided short code \"%s\" could not be found."
|
||||
msgstr "El código corto proporcionado \"%s\" no ha podido ser encontrado."
|
||||
|
||||
msgid "Creates one or more tags."
|
||||
msgstr "Crea una o más etiquetas."
|
||||
|
||||
|
@ -327,6 +361,12 @@ msgstr "Limite del localizador de IPs alcanzado. Esperando %s segundos..."
|
|||
msgid "Finished processing all IPs"
|
||||
msgstr "Finalizado el procesado de todas las IPs"
|
||||
|
||||
#~ msgid "Remote Address"
|
||||
#~ msgstr "Dirección remota"
|
||||
|
||||
#~ msgid "Original URL"
|
||||
#~ msgstr "URL original"
|
||||
|
||||
#~ msgid "You have reached last page"
|
||||
#~ msgstr "Has alcanzado la última página"
|
||||
|
||||
|
|
101
module/CLI/src/Command/Shortcode/DeleteShortCodeCommand.php
Normal file
101
module/CLI/src/Command/Shortcode/DeleteShortCodeCommand.php
Normal file
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
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\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class DeleteShortCodeCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-code:delete';
|
||||
private const ALIASES = [];
|
||||
|
||||
/**
|
||||
* @var DeleteShortUrlServiceInterface
|
||||
*/
|
||||
private $deleteShortUrlService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(DeleteShortUrlServiceInterface $deleteShortUrlService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->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('<bg=yellow>' . $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
|
||||
));
|
||||
}
|
||||
}
|
|
@ -14,7 +14,8 @@ use Zend\I18n\Translator\TranslatorInterface;
|
|||
|
||||
class GeneratePreviewCommand extends Command
|
||||
{
|
||||
const NAME = 'shortcode:process-previews';
|
||||
public const NAME = 'short-code:process-previews';
|
||||
private const ALIASES = ['shortcode:process-previews'];
|
||||
|
||||
/**
|
||||
* @var PreviewGeneratorInterface
|
||||
|
@ -40,17 +41,19 @@ class GeneratePreviewCommand extends Command
|
|||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription(
|
||||
$this->translator->translate(
|
||||
'Processes and generates the previews for every URL, improving performance for later web requests.'
|
||||
)
|
||||
);
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription(
|
||||
$this->translator->translate(
|
||||
'Processes and generates the previews for every URL, improving performance for later web requests.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$page = 1;
|
||||
do {
|
||||
|
@ -65,7 +68,7 @@ class GeneratePreviewCommand extends Command
|
|||
(new SymfonyStyle($input, $output))->success($this->translator->translate('Finished processing all URLs'));
|
||||
}
|
||||
|
||||
protected function processUrl($url, OutputInterface $output)
|
||||
private function processUrl($url, OutputInterface $output): void
|
||||
{
|
||||
try {
|
||||
$output->write(\sprintf($this->translator->translate('Processing URL %s...'), $url));
|
||||
|
|
|
@ -20,7 +20,8 @@ class GenerateShortcodeCommand extends Command
|
|||
{
|
||||
use ShortUrlBuilderTrait;
|
||||
|
||||
public const NAME = 'shortcode:generate';
|
||||
public const NAME = 'short-code:generate';
|
||||
private const ALIASES = ['shortcode:generate'];
|
||||
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
|
@ -46,36 +47,38 @@ class GenerateShortcodeCommand extends Command
|
|||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription(
|
||||
$this->translator->translate('Generates a short code for provided URL and returns the short URL')
|
||||
)
|
||||
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'))
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
|
||||
$this->translator->translate('Tags to apply to the new short URL')
|
||||
)
|
||||
->addOption('validSince', 's', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
'The date from which this short URL will be valid. '
|
||||
. 'If someone tries to access it before this date, it will not be found.'
|
||||
))
|
||||
->addOption('validUntil', 'u', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
'The date until which this short URL will be valid. '
|
||||
. 'If someone tries to access it after this date, it will not be found.'
|
||||
))
|
||||
->addOption('customSlug', 'c', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
'If provided, this slug will be used instead of generating a short code'
|
||||
))
|
||||
->addOption('maxVisits', 'm', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
'This will limit the number of visits for this short URL.'
|
||||
));
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription(
|
||||
$this->translator->translate('Generates a short code for provided URL and returns the short URL')
|
||||
)
|
||||
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'))
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
|
||||
$this->translator->translate('Tags to apply to the new short URL')
|
||||
)
|
||||
->addOption('validSince', 's', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
'The date from which this short URL will be valid. '
|
||||
. 'If someone tries to access it before this date, it will not be found.'
|
||||
))
|
||||
->addOption('validUntil', 'u', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
'The date until which this short URL will be valid. '
|
||||
. 'If someone tries to access it after this date, it will not be found.'
|
||||
))
|
||||
->addOption('customSlug', 'c', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
'If provided, this slug will be used instead of generating a short code'
|
||||
))
|
||||
->addOption('maxVisits', 'm', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
'This will limit the number of visits for this short URL.'
|
||||
));
|
||||
}
|
||||
|
||||
public function interact(InputInterface $input, OutputInterface $output)
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$longUrl = $input->getArgument('longUrl');
|
||||
|
@ -91,7 +94,7 @@ class GenerateShortcodeCommand extends Command
|
|||
}
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$longUrl = $input->getArgument('longUrl');
|
||||
|
@ -140,7 +143,7 @@ class GenerateShortcodeCommand extends Command
|
|||
}
|
||||
}
|
||||
|
||||
private function getOptionalDate(InputInterface $input, string $fieldName)
|
||||
private function getOptionalDate(InputInterface $input, string $fieldName): ?\DateTime
|
||||
{
|
||||
$since = $input->getOption($fieldName);
|
||||
return $since !== null ? new \DateTime($since) : null;
|
||||
|
|
|
@ -15,7 +15,8 @@ use Zend\I18n\Translator\TranslatorInterface;
|
|||
|
||||
class GetVisitsCommand extends Command
|
||||
{
|
||||
const NAME = 'shortcode:visits';
|
||||
public const NAME = 'short-code:visits';
|
||||
private const ALIASES = ['shortcode:visits'];
|
||||
|
||||
/**
|
||||
* @var VisitsTrackerInterface
|
||||
|
@ -33,9 +34,11 @@ class GetVisitsCommand extends Command
|
|||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription(
|
||||
$this->translator->translate('Returns the detailed visits information for provided short code')
|
||||
)
|
||||
|
@ -58,7 +61,7 @@ class GetVisitsCommand extends Command
|
|||
);
|
||||
}
|
||||
|
||||
public function interact(InputInterface $input, OutputInterface $output)
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
if (! empty($shortCode)) {
|
||||
|
@ -74,7 +77,7 @@ class GetVisitsCommand extends Command
|
|||
}
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
|
@ -101,7 +104,7 @@ class GetVisitsCommand extends Command
|
|||
], $rows);
|
||||
}
|
||||
|
||||
protected function getDateOption(InputInterface $input, $key)
|
||||
private function getDateOption(InputInterface $input, $key)
|
||||
{
|
||||
$value = $input->getOption($key);
|
||||
if (! empty($value)) {
|
||||
|
|
|
@ -18,7 +18,8 @@ class ListShortcodesCommand extends Command
|
|||
{
|
||||
use PaginatorUtilsTrait;
|
||||
|
||||
const NAME = 'shortcode:list';
|
||||
public const NAME = 'short-code:list';
|
||||
private const ALIASES = ['shortcode:list'];
|
||||
|
||||
/**
|
||||
* @var ShortUrlServiceInterface
|
||||
|
@ -46,49 +47,51 @@ class ListShortcodesCommand extends Command
|
|||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('List all short URLs'))
|
||||
->addOption(
|
||||
'page',
|
||||
'p',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
sprintf(
|
||||
$this->translator->translate('The first page to list (%s items per page)'),
|
||||
PaginableRepositoryAdapter::ITEMS_PER_PAGE
|
||||
),
|
||||
1
|
||||
)
|
||||
->addOption(
|
||||
'searchTerm',
|
||||
's',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate(
|
||||
'A query used to filter results by searching for it on the longUrl and shortCode fields'
|
||||
)
|
||||
)
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('A comma-separated list of tags to filter results')
|
||||
)
|
||||
->addOption(
|
||||
'orderBy',
|
||||
'o',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate(
|
||||
'The field from which we want to order by. Pass ASC or DESC separated by a comma'
|
||||
)
|
||||
)
|
||||
->addOption(
|
||||
'showTags',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
$this->translator->translate('Whether to display the tags or not')
|
||||
);
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription($this->translator->translate('List all short URLs'))
|
||||
->addOption(
|
||||
'page',
|
||||
'p',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
sprintf(
|
||||
$this->translator->translate('The first page to list (%s items per page)'),
|
||||
PaginableRepositoryAdapter::ITEMS_PER_PAGE
|
||||
),
|
||||
1
|
||||
)
|
||||
->addOption(
|
||||
'searchTerm',
|
||||
's',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate(
|
||||
'A query used to filter results by searching for it on the longUrl and shortCode fields'
|
||||
)
|
||||
)
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('A comma-separated list of tags to filter results')
|
||||
)
|
||||
->addOption(
|
||||
'orderBy',
|
||||
'o',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate(
|
||||
'The field from which we want to order by. Pass ASC or DESC separated by a comma'
|
||||
)
|
||||
)
|
||||
->addOption(
|
||||
'showTags',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
$this->translator->translate('Whether to display the tags or not')
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$page = (int) $input->getOption('page');
|
||||
|
|
|
@ -15,7 +15,8 @@ use Zend\I18n\Translator\TranslatorInterface;
|
|||
|
||||
class ResolveUrlCommand extends Command
|
||||
{
|
||||
const NAME = 'shortcode:parse';
|
||||
public const NAME = 'short-code:parse';
|
||||
private const ALIASES = ['shortcode:parse'];
|
||||
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
|
@ -33,18 +34,20 @@ class ResolveUrlCommand extends Command
|
|||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Returns the long URL behind a short code'))
|
||||
->addArgument(
|
||||
'shortCode',
|
||||
InputArgument::REQUIRED,
|
||||
$this->translator->translate('The short code to parse')
|
||||
);
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription($this->translator->translate('Returns the long URL behind a short code'))
|
||||
->addArgument(
|
||||
'shortCode',
|
||||
InputArgument::REQUIRED,
|
||||
$this->translator->translate('The short code to parse')
|
||||
);
|
||||
}
|
||||
|
||||
public function interact(InputInterface $input, OutputInterface $output)
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
if (! empty($shortCode)) {
|
||||
|
@ -60,7 +63,7 @@ class ResolveUrlCommand extends Command
|
|||
}
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
|
|
120
module/CLI/test/Command/Shortcode/DeleteShortCodeCommandTest.php
Normal file
120
module/CLI/test/Command/Shortcode/DeleteShortCodeCommandTest.php
Normal file
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Shortcode\DeleteShortCodeCommand;
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class DeleteShortCodeCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $service;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
|
@ -8,26 +8,19 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
abstract class AbstractEntity
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
* @var string
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
* @ORM\Column(name="id", type="bigint", options={"unsigned"=true})
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return $this
|
||||
*/
|
||||
public function setId($id)
|
||||
public function setId(string $id): self
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
|
|
|
@ -17,6 +17,7 @@ return [
|
|||
'dependencies' => [
|
||||
'factories' => [
|
||||
Options\AppOptions::class => Options\AppOptionsFactory::class,
|
||||
Options\DeleteShortUrlsOptions::class => Options\DeleteShortUrlsOptionsFactory::class,
|
||||
NotFoundHandler::class => ConfigAbstractFactory::class,
|
||||
|
||||
// Services
|
||||
|
@ -25,6 +26,7 @@ return [
|
|||
Service\ShortUrlService::class => ConfigAbstractFactory::class,
|
||||
Service\VisitService::class => ConfigAbstractFactory::class,
|
||||
Service\Tag\TagService::class => ConfigAbstractFactory::class,
|
||||
Service\ShortUrl\DeleteShortUrlService::class => ConfigAbstractFactory::class,
|
||||
|
||||
// Middleware
|
||||
Action\RedirectAction::class => ConfigAbstractFactory::class,
|
||||
|
@ -49,6 +51,7 @@ return [
|
|||
Service\ShortUrlService::class => ['em'],
|
||||
Service\VisitService::class => ['em'],
|
||||
Service\Tag\TagService::class => ['em'],
|
||||
Service\ShortUrl\DeleteShortUrlService::class => ['em', Options\DeleteShortUrlsOptions::class],
|
||||
|
||||
// Middleware
|
||||
Action\RedirectAction::class => [
|
||||
|
|
|
@ -22,6 +22,10 @@ class QrCodeAction implements MiddlewareInterface
|
|||
{
|
||||
use ErrorResponseBuilderTrait;
|
||||
|
||||
private const DEFAULT_SIZE = 300;
|
||||
private const MIN_SIZE = 50;
|
||||
private const MAX_SIZE = 1000;
|
||||
|
||||
/**
|
||||
* @var RouterInterface
|
||||
*/
|
||||
|
@ -82,11 +86,11 @@ class QrCodeAction implements MiddlewareInterface
|
|||
*/
|
||||
private function getSizeParam(Request $request): int
|
||||
{
|
||||
$size = (int) $request->getAttribute('size', 300);
|
||||
if ($size < 50) {
|
||||
return 50;
|
||||
$size = (int) $request->getAttribute('size', self::DEFAULT_SIZE);
|
||||
if ($size < self::MIN_SIZE) {
|
||||
return self::MIN_SIZE;
|
||||
}
|
||||
|
||||
return $size > 1000 ? 1000 : $size;
|
||||
return $size > self::MAX_SIZE ? self::MAX_SIZE : $size;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,13 +7,14 @@ use Doctrine\Common\Collections\ArrayCollection;
|
|||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
|
||||
/**
|
||||
* Class ShortUrl
|
||||
* @author
|
||||
* @link
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="Shlinkio\Shlink\Core\Repository\ShortUrlRepository")
|
||||
* @ORM\Entity(repositoryClass=ShortUrlRepository::class)
|
||||
* @ORM\Table(name="short_urls")
|
||||
*/
|
||||
class ShortUrl extends AbstractEntity
|
||||
|
@ -22,7 +23,7 @@ class ShortUrl extends AbstractEntity
|
|||
* @var string
|
||||
* @ORM\Column(name="original_url", type="string", nullable=false, length=1024)
|
||||
*/
|
||||
protected $originalUrl;
|
||||
private $originalUrl;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(
|
||||
|
@ -33,17 +34,17 @@ class ShortUrl extends AbstractEntity
|
|||
* unique=true
|
||||
* )
|
||||
*/
|
||||
protected $shortCode;
|
||||
private $shortCode;
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @ORM\Column(name="date_created", type="datetime")
|
||||
*/
|
||||
protected $dateCreated;
|
||||
private $dateCreated;
|
||||
/**
|
||||
* @var Collection|Visit[]
|
||||
* @ORM\OneToMany(targetEntity=Visit::class, mappedBy="shortUrl", fetch="EXTRA_LAZY")
|
||||
*/
|
||||
protected $visits;
|
||||
private $visits;
|
||||
/**
|
||||
* @var Collection|Tag[]
|
||||
* @ORM\ManyToMany(targetEntity=Tag::class, cascade={"persist"})
|
||||
|
@ -53,46 +54,36 @@ class ShortUrl extends AbstractEntity
|
|||
* @ORM\JoinColumn(name="tag_id", referencedColumnName="id")
|
||||
* })
|
||||
*/
|
||||
protected $tags;
|
||||
private $tags;
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @ORM\Column(name="valid_since", type="datetime", nullable=true)
|
||||
*/
|
||||
protected $validSince;
|
||||
private $validSince;
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @ORM\Column(name="valid_until", type="datetime", nullable=true)
|
||||
*/
|
||||
protected $validUntil;
|
||||
private $validUntil;
|
||||
/**
|
||||
* @var integer
|
||||
* @ORM\Column(name="max_visits", type="integer", nullable=true)
|
||||
*/
|
||||
protected $maxVisits;
|
||||
private $maxVisits;
|
||||
|
||||
/**
|
||||
* ShortUrl constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->shortCode = '';
|
||||
$this->dateCreated = new \DateTime();
|
||||
$this->visits = new ArrayCollection();
|
||||
$this->shortCode = '';
|
||||
$this->tags = new ArrayCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLongUrl(): string
|
||||
{
|
||||
return $this->originalUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $longUrl
|
||||
* @return $this
|
||||
*/
|
||||
public function setLongUrl(string $longUrl): self
|
||||
{
|
||||
$this->originalUrl = $longUrl;
|
||||
|
@ -100,7 +91,6 @@ class ShortUrl extends AbstractEntity
|
|||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @deprecated Use getLongUrl() instead
|
||||
*/
|
||||
public function getOriginalUrl(): string
|
||||
|
@ -109,8 +99,6 @@ class ShortUrl extends AbstractEntity
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string $originalUrl
|
||||
* @return $this
|
||||
* @deprecated Use setLongUrl() instead
|
||||
*/
|
||||
public function setOriginalUrl(string $originalUrl): self
|
||||
|
@ -118,37 +106,23 @@ class ShortUrl extends AbstractEntity
|
|||
return $this->setLongUrl($originalUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getShortCode(): string
|
||||
{
|
||||
return $this->shortCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $shortCode
|
||||
* @return $this
|
||||
*/
|
||||
public function setShortCode(string $shortCode)
|
||||
public function setShortCode(string $shortCode): self
|
||||
{
|
||||
$this->shortCode = $shortCode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getDateCreated(): \DateTime
|
||||
{
|
||||
return $this->dateCreated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $dateCreated
|
||||
* @return $this
|
||||
*/
|
||||
public function setDateCreated(\DateTime $dateCreated)
|
||||
public function setDateCreated(\DateTime $dateCreated): self
|
||||
{
|
||||
$this->dateCreated = $dateCreated;
|
||||
return $this;
|
||||
|
@ -164,55 +138,36 @@ class ShortUrl extends AbstractEntity
|
|||
|
||||
/**
|
||||
* @param Collection|Tag[] $tags
|
||||
* @return $this
|
||||
*/
|
||||
public function setTags(Collection $tags)
|
||||
public function setTags(Collection $tags): self
|
||||
{
|
||||
$this->tags = $tags;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tag $tag
|
||||
* @return $this
|
||||
*/
|
||||
public function addTag(Tag $tag)
|
||||
public function addTag(Tag $tag): self
|
||||
{
|
||||
$this->tags->add($tag);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime|null
|
||||
*/
|
||||
public function getValidSince()
|
||||
public function getValidSince(): ?\DateTime
|
||||
{
|
||||
return $this->validSince;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime|null $validSince
|
||||
* @return $this|self
|
||||
*/
|
||||
public function setValidSince($validSince): self
|
||||
public function setValidSince(?\DateTime $validSince): self
|
||||
{
|
||||
$this->validSince = $validSince;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime|null
|
||||
*/
|
||||
public function getValidUntil()
|
||||
public function getValidUntil(): ?\DateTime
|
||||
{
|
||||
return $this->validUntil;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime|null $validUntil
|
||||
* @return $this|self
|
||||
*/
|
||||
public function setValidUntil($validUntil): self
|
||||
public function setValidUntil(?\DateTime $validUntil): self
|
||||
{
|
||||
$this->validUntil = $validUntil;
|
||||
return $this;
|
||||
|
@ -220,11 +175,11 @@ class ShortUrl extends AbstractEntity
|
|||
|
||||
public function getVisitsCount(): int
|
||||
{
|
||||
return count($this->visits);
|
||||
return \count($this->visits);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $visits
|
||||
* @param Collection|Visit[] $visits
|
||||
* @return ShortUrl
|
||||
* @internal
|
||||
*/
|
||||
|
@ -234,19 +189,12 @@ class ShortUrl extends AbstractEntity
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getMaxVisits()
|
||||
public function getMaxVisits(): ?int
|
||||
{
|
||||
return $this->maxVisits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $maxVisits
|
||||
* @return $this|self
|
||||
*/
|
||||
public function setMaxVisits($maxVisits): self
|
||||
public function setMaxVisits(?int $maxVisits): self
|
||||
{
|
||||
$this->maxVisits = $maxVisits;
|
||||
return $this;
|
||||
|
|
|
@ -21,39 +21,25 @@ class Tag extends AbstractEntity implements \JsonSerializable
|
|||
* @var string
|
||||
* @ORM\Column(unique=true)
|
||||
*/
|
||||
protected $name;
|
||||
private $name;
|
||||
|
||||
public function __construct($name = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return $this
|
||||
*/
|
||||
public function setName($name)
|
||||
public function setName(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify data which should be serialized to JSON
|
||||
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
|
||||
* @return mixed data which can be serialized by <b>json_encode</b>,
|
||||
* which is a value of any type other than a resource.
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
public function jsonSerialize(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
|
|
@ -7,13 +7,14 @@ use Doctrine\ORM\Mapping as ORM;
|
|||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
use Shlinkio\Shlink\Core\Repository\VisitRepository;
|
||||
|
||||
/**
|
||||
* Class Visit
|
||||
* @author
|
||||
* @link
|
||||
*
|
||||
* @ORM\Entity(repositoryClass="Shlinkio\Shlink\Core\Repository\VisitRepository")
|
||||
* @ORM\Entity(repositoryClass=VisitRepository::class)
|
||||
* @ORM\Table(name="visits")
|
||||
*/
|
||||
class Visit extends AbstractEntity implements \JsonSerializable
|
||||
|
|
|
@ -21,159 +21,110 @@ class VisitLocation extends AbstractEntity implements ArraySerializableInterface
|
|||
* @var string
|
||||
* @ORM\Column(nullable=true)
|
||||
*/
|
||||
protected $countryCode;
|
||||
private $countryCode;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(nullable=true)
|
||||
*/
|
||||
protected $countryName;
|
||||
private $countryName;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(nullable=true)
|
||||
*/
|
||||
protected $regionName;
|
||||
private $regionName;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(nullable=true)
|
||||
*/
|
||||
protected $cityName;
|
||||
private $cityName;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(nullable=true)
|
||||
*/
|
||||
protected $latitude;
|
||||
private $latitude;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(nullable=true)
|
||||
*/
|
||||
protected $longitude;
|
||||
private $longitude;
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\Column(nullable=true)
|
||||
*/
|
||||
protected $timezone;
|
||||
private $timezone;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCountryCode()
|
||||
public function getCountryCode(): string
|
||||
{
|
||||
return $this->countryCode;
|
||||
return $this->countryCode ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $countryCode
|
||||
* @return $this
|
||||
*/
|
||||
public function setCountryCode($countryCode)
|
||||
public function setCountryCode(string $countryCode)
|
||||
{
|
||||
$this->countryCode = $countryCode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCountryName()
|
||||
public function getCountryName(): string
|
||||
{
|
||||
return $this->countryName;
|
||||
return $this->countryName ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $countryName
|
||||
* @return $this
|
||||
*/
|
||||
public function setCountryName($countryName)
|
||||
public function setCountryName(string $countryName): self
|
||||
{
|
||||
$this->countryName = $countryName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRegionName()
|
||||
public function getRegionName(): string
|
||||
{
|
||||
return $this->regionName;
|
||||
return $this->regionName ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $regionName
|
||||
* @return $this
|
||||
*/
|
||||
public function setRegionName($regionName)
|
||||
public function setRegionName(string $regionName): self
|
||||
{
|
||||
$this->regionName = $regionName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCityName()
|
||||
public function getCityName(): string
|
||||
{
|
||||
return $this->cityName;
|
||||
return $this->cityName ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $cityName
|
||||
* @return $this
|
||||
*/
|
||||
public function setCityName($cityName)
|
||||
public function setCityName(string $cityName): self
|
||||
{
|
||||
$this->cityName = $cityName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLatitude()
|
||||
public function getLatitude(): string
|
||||
{
|
||||
return $this->latitude;
|
||||
return $this->latitude ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $latitude
|
||||
* @return $this
|
||||
*/
|
||||
public function setLatitude($latitude)
|
||||
public function setLatitude(string $latitude): self
|
||||
{
|
||||
$this->latitude = $latitude;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLongitude()
|
||||
public function getLongitude(): string
|
||||
{
|
||||
return $this->longitude;
|
||||
return $this->longitude ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $longitude
|
||||
* @return $this
|
||||
*/
|
||||
public function setLongitude($longitude)
|
||||
public function setLongitude(string $longitude): self
|
||||
{
|
||||
$this->longitude = $longitude;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTimezone()
|
||||
public function getTimezone(): string
|
||||
{
|
||||
return $this->timezone;
|
||||
return $this->timezone ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $timezone
|
||||
* @return $this
|
||||
*/
|
||||
public function setTimezone($timezone)
|
||||
public function setTimezone(string $timezone): self
|
||||
{
|
||||
$this->timezone = $timezone;
|
||||
return $this;
|
||||
|
@ -181,41 +132,36 @@ class VisitLocation extends AbstractEntity implements ArraySerializableInterface
|
|||
|
||||
/**
|
||||
* Exchange internal values from provided array
|
||||
*
|
||||
* @param array $array
|
||||
* @return void
|
||||
*/
|
||||
public function exchangeArray(array $array)
|
||||
public function exchangeArray(array $array): void
|
||||
{
|
||||
if (array_key_exists('country_code', $array)) {
|
||||
if (\array_key_exists('country_code', $array)) {
|
||||
$this->setCountryCode($array['country_code']);
|
||||
}
|
||||
if (array_key_exists('country_name', $array)) {
|
||||
if (\array_key_exists('country_name', $array)) {
|
||||
$this->setCountryName($array['country_name']);
|
||||
}
|
||||
if (array_key_exists('region_name', $array)) {
|
||||
if (\array_key_exists('region_name', $array)) {
|
||||
$this->setRegionName($array['region_name']);
|
||||
}
|
||||
if (array_key_exists('city', $array)) {
|
||||
if (\array_key_exists('city', $array)) {
|
||||
$this->setCityName($array['city']);
|
||||
}
|
||||
if (array_key_exists('latitude', $array)) {
|
||||
if (\array_key_exists('latitude', $array)) {
|
||||
$this->setLatitude($array['latitude']);
|
||||
}
|
||||
if (array_key_exists('longitude', $array)) {
|
||||
if (\array_key_exists('longitude', $array)) {
|
||||
$this->setLongitude($array['longitude']);
|
||||
}
|
||||
if (array_key_exists('time_zone', $array)) {
|
||||
if (\array_key_exists('time_zone', $array)) {
|
||||
$this->setTimezone($array['time_zone']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array representation of the object
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getArrayCopy()
|
||||
public function getArrayCopy(): array
|
||||
{
|
||||
return [
|
||||
'countryCode' => $this->countryCode,
|
||||
|
@ -228,14 +174,7 @@ class VisitLocation extends AbstractEntity implements ArraySerializableInterface
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify data which should be serialized to JSON
|
||||
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
|
||||
* @return mixed data which can be serialized by <b>json_encode</b>,
|
||||
* which is a value of any type other than a resource.
|
||||
* @since 5.4.0
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->getArrayCopy();
|
||||
}
|
||||
|
|
34
module/Core/src/Exception/DeleteShortUrlException.php
Normal file
34
module/Core/src/Exception/DeleteShortUrlException.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
class DeleteShortUrlException extends RuntimeException
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $visitsThreshold;
|
||||
|
||||
public function __construct(int $visitsThreshold, string $message = '', int $code = 0, Throwable $previous = null)
|
||||
{
|
||||
$this->visitsThreshold = $visitsThreshold;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public static function fromVisitsThreshold(int $threshold, string $shortCode): self
|
||||
{
|
||||
return new self($threshold, \sprintf(
|
||||
'Impossible to delete short URL with short code "%s" since it has more than "%s" visits.',
|
||||
$shortCode,
|
||||
$threshold
|
||||
));
|
||||
}
|
||||
|
||||
public function getVisitsThreshold(): int
|
||||
{
|
||||
return $this->visitsThreshold;
|
||||
}
|
||||
}
|
|
@ -7,9 +7,9 @@ class InvalidShortCodeException extends RuntimeException
|
|||
{
|
||||
public static function fromCharset($shortCode, $charSet, \Exception $previous = null)
|
||||
{
|
||||
$code = isset($previous) ? $previous->getCode() : -1;
|
||||
$code = $previous !== null ? $previous->getCode() : -1;
|
||||
return new static(
|
||||
sprintf('Provided short code "%s" does not match the char set "%s"', $shortCode, $charSet),
|
||||
\sprintf('Provided short code "%s" does not match the char set "%s"', $shortCode, $charSet),
|
||||
$code,
|
||||
$previous
|
||||
);
|
||||
|
@ -17,6 +17,6 @@ class InvalidShortCodeException extends RuntimeException
|
|||
|
||||
public static function fromNotFoundShortCode($shortCode)
|
||||
{
|
||||
return new static(sprintf('Provided short code "%s" does not belong to a short URL', $shortCode));
|
||||
return new static(\sprintf('Provided short code "%s" does not belong to a short URL', $shortCode));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,6 @@ class AppOptionsFactory implements FactoryInterface
|
|||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
$config = $container->has('config') ? $container->get('config') : [];
|
||||
return new AppOptions(isset($config['app_options']) ? $config['app_options'] : []);
|
||||
return new AppOptions($config['app_options'] ?? []);
|
||||
}
|
||||
}
|
||||
|
|
34
module/Core/src/Options/DeleteShortUrlsOptions.php
Normal file
34
module/Core/src/Options/DeleteShortUrlsOptions.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Options;
|
||||
|
||||
use Zend\Stdlib\AbstractOptions;
|
||||
|
||||
class DeleteShortUrlsOptions extends AbstractOptions
|
||||
{
|
||||
private $visitsThreshold = 15;
|
||||
private $checkVisitsThreshold = true;
|
||||
|
||||
public function getVisitsThreshold(): int
|
||||
{
|
||||
return $this->visitsThreshold;
|
||||
}
|
||||
|
||||
protected function setVisitsThreshold(int $visitsThreshold): self
|
||||
{
|
||||
$this->visitsThreshold = $visitsThreshold;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function doCheckVisitsThreshold(): bool
|
||||
{
|
||||
return $this->checkVisitsThreshold;
|
||||
}
|
||||
|
||||
protected function setCheckVisitsThreshold(bool $checkVisitsThreshold): self
|
||||
{
|
||||
$this->checkVisitsThreshold = $checkVisitsThreshold;
|
||||
return $this;
|
||||
}
|
||||
}
|
31
module/Core/src/Options/DeleteShortUrlsOptionsFactory.php
Normal file
31
module/Core/src/Options/DeleteShortUrlsOptionsFactory.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Options;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class DeleteShortUrlsOptionsFactory implements FactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create an object
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @param string $requestedName
|
||||
* @param null|array $options
|
||||
* @return object
|
||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||
* @throws ServiceNotCreatedException if an exception is raised when
|
||||
* creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
$config = $container->has('config') ? $container->get('config') : [];
|
||||
return new DeleteShortUrlsOptions($config['delete_short_urls'] ?? []);
|
||||
}
|
||||
}
|
56
module/Core/src/Service/ShortUrl/DeleteShortUrlService.php
Normal file
56
module/Core/src/Service/ShortUrl/DeleteShortUrlService.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Service\ShortUrl;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Options\DeleteShortUrlsOptions;
|
||||
|
||||
class DeleteShortUrlService implements DeleteShortUrlServiceInterface
|
||||
{
|
||||
use FindShortCodeTrait;
|
||||
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
private $em;
|
||||
/**
|
||||
* @var DeleteShortUrlsOptions
|
||||
*/
|
||||
private $deleteShortUrlsOptions;
|
||||
|
||||
public function __construct(EntityManagerInterface $em, DeleteShortUrlsOptions $deleteShortUrlsOptions)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->deleteShortUrlsOptions = $deleteShortUrlsOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception\InvalidShortCodeException
|
||||
* @throws Exception\DeleteShortUrlException
|
||||
*/
|
||||
public function deleteByShortCode(string $shortCode, bool $ignoreThreshold = false): void
|
||||
{
|
||||
$shortUrl = $this->findByShortCode($this->em, $shortCode);
|
||||
if (! $ignoreThreshold && $this->isThresholdReached($shortUrl)) {
|
||||
throw Exception\DeleteShortUrlException::fromVisitsThreshold(
|
||||
$this->deleteShortUrlsOptions->getVisitsThreshold(),
|
||||
$shortUrl->getShortCode()
|
||||
);
|
||||
}
|
||||
|
||||
$this->em->remove($shortUrl);
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
private function isThresholdReached(ShortUrl $shortUrl): bool
|
||||
{
|
||||
if (! $this->deleteShortUrlsOptions->doCheckVisitsThreshold()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $shortUrl->getVisitsCount() >= $this->deleteShortUrlsOptions->getVisitsThreshold();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Service\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
interface DeleteShortUrlServiceInterface
|
||||
{
|
||||
/**
|
||||
* @throws Exception\InvalidShortCodeException
|
||||
* @throws Exception\DeleteShortUrlException
|
||||
*/
|
||||
public function deleteByShortCode(string $shortCode, bool $ignoreThreshold = false): void;
|
||||
}
|
29
module/Core/src/Service/ShortUrl/FindShortCodeTrait.php
Normal file
29
module/Core/src/Service/ShortUrl/FindShortCodeTrait.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Service\ShortUrl;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
|
||||
trait FindShortCodeTrait
|
||||
{
|
||||
/**
|
||||
* @param string $shortCode
|
||||
* @return ShortUrl
|
||||
* @throws InvalidShortCodeException
|
||||
*/
|
||||
private function findByShortCode(EntityManagerInterface $em, string $shortCode): ShortUrl
|
||||
{
|
||||
/** @var ShortUrl|null $shortUrl */
|
||||
$shortUrl = $em->getRepository(ShortUrl::class)->findOneBy([
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
if ($shortUrl === null) {
|
||||
throw InvalidShortCodeException::fromNotFoundShortCode($shortCode);
|
||||
}
|
||||
|
||||
return $shortUrl;
|
||||
}
|
||||
}
|
|
@ -9,11 +9,13 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
|||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\FindShortCodeTrait;
|
||||
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
class ShortUrlService implements ShortUrlServiceInterface
|
||||
{
|
||||
use FindShortCodeTrait;
|
||||
use TagManagerTrait;
|
||||
|
||||
/**
|
||||
|
@ -27,13 +29,11 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
* @param string $searchQuery
|
||||
* @param array $tags
|
||||
* @param null $orderBy
|
||||
* @param string[] $tags
|
||||
* @param array|string|null $orderBy
|
||||
* @return ShortUrl[]|Paginator
|
||||
*/
|
||||
public function listShortUrls($page = 1, $searchQuery = null, array $tags = [], $orderBy = null)
|
||||
public function listShortUrls(int $page = 1, string $searchQuery = null, array $tags = [], $orderBy = null)
|
||||
{
|
||||
/** @var ShortUrlRepository $repo */
|
||||
$repo = $this->em->getRepository(ShortUrl::class);
|
||||
|
@ -45,14 +45,12 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string $shortCode
|
||||
* @param string[] $tags
|
||||
* @return ShortUrl
|
||||
* @throws InvalidShortCodeException
|
||||
*/
|
||||
public function setTagsByShortCode(string $shortCode, array $tags = []): ShortUrl
|
||||
{
|
||||
$shortUrl = $this->findByShortCode($shortCode);
|
||||
$shortUrl = $this->findByShortCode($this->em, $shortCode);
|
||||
$shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
|
||||
$this->em->flush();
|
||||
|
||||
|
@ -60,14 +58,11 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string $shortCode
|
||||
* @param ShortUrlMeta $shortCodeMeta
|
||||
* @return ShortUrl
|
||||
* @throws InvalidShortCodeException
|
||||
*/
|
||||
public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortCodeMeta): ShortUrl
|
||||
{
|
||||
$shortUrl = $this->findByShortCode($shortCode);
|
||||
$shortUrl = $this->findByShortCode($this->em, $shortCode);
|
||||
if ($shortCodeMeta->hasValidSince()) {
|
||||
$shortUrl->setValidSince($shortCodeMeta->getValidSince());
|
||||
}
|
||||
|
@ -81,23 +76,6 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||
/** @var ORM\EntityManager $em */
|
||||
$em = $this->em;
|
||||
$em->flush($shortUrl);
|
||||
return $shortUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $shortCode
|
||||
* @return ShortUrl
|
||||
* @throws InvalidShortCodeException
|
||||
*/
|
||||
private function findByShortCode(string $shortCode): ShortUrl
|
||||
{
|
||||
/** @var ShortUrl|null $shortUrl */
|
||||
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
if ($shortUrl === null) {
|
||||
throw InvalidShortCodeException::fromNotFoundShortCode($shortCode);
|
||||
}
|
||||
|
||||
return $shortUrl;
|
||||
}
|
||||
|
|
|
@ -11,26 +11,19 @@ use Zend\Paginator\Paginator;
|
|||
interface ShortUrlServiceInterface
|
||||
{
|
||||
/**
|
||||
* @param int $page
|
||||
* @param string $searchQuery
|
||||
* @param array $tags
|
||||
* @param null $orderBy
|
||||
* @param string[] $tags
|
||||
* @param array|string|null $orderBy
|
||||
* @return ShortUrl[]|Paginator
|
||||
*/
|
||||
public function listShortUrls($page = 1, $searchQuery = null, array $tags = [], $orderBy = null);
|
||||
public function listShortUrls(int $page = 1, string $searchQuery = null, array $tags = [], $orderBy = null);
|
||||
|
||||
/**
|
||||
* @param string $shortCode
|
||||
* @param string[] $tags
|
||||
* @return ShortUrl
|
||||
* @throws InvalidShortCodeException
|
||||
*/
|
||||
public function setTagsByShortCode(string $shortCode, array $tags = []): ShortUrl;
|
||||
|
||||
/**
|
||||
* @param string $shortCode
|
||||
* @param ShortUrlMeta $shortCodeMeta
|
||||
* @return ShortUrl
|
||||
* @throws InvalidShortCodeException
|
||||
*/
|
||||
public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortCodeMeta): ShortUrl;
|
||||
|
|
61
module/Core/test/Exception/DeleteShortUrlExceptionTest.php
Normal file
61
module/Core/test/Exception/DeleteShortUrlExceptionTest.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Exception;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Exception\DeleteShortUrlException;
|
||||
|
||||
class DeleteShortUrlExceptionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideMessages
|
||||
*/
|
||||
public function fromVisitsThresholdGeneratesMessageProperly(
|
||||
int $threshold,
|
||||
string $shortCode,
|
||||
string $expectedMessage
|
||||
) {
|
||||
$e = DeleteShortUrlException::fromVisitsThreshold($threshold, $shortCode);
|
||||
$this->assertEquals($expectedMessage, $e->getMessage());
|
||||
}
|
||||
|
||||
public function provideMessages(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
50,
|
||||
'abc123',
|
||||
'Impossible to delete short URL with short code "abc123" since it has more than "50" visits.',
|
||||
],
|
||||
[
|
||||
33,
|
||||
'def456',
|
||||
'Impossible to delete short URL with short code "def456" since it has more than "33" visits.',
|
||||
],
|
||||
[
|
||||
5713,
|
||||
'foobar',
|
||||
'Impossible to delete short URL with short code "foobar" since it has more than "5713" visits.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideThresholds
|
||||
*/
|
||||
public function visitsThresholdIsProperlyReturned(int $threshold)
|
||||
{
|
||||
$e = new DeleteShortUrlException($threshold);
|
||||
$this->assertEquals($threshold, $e->getVisitsThreshold());
|
||||
}
|
||||
|
||||
public function provideThresholds(): array
|
||||
{
|
||||
return \array_map(function (int $number) {
|
||||
return [$number];
|
||||
}, \range(5, 50, 5));
|
||||
}
|
||||
}
|
113
module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php
Normal file
113
module/Core/test/Service/ShortUrl/DeleteShortUrlServiceTest.php
Normal file
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Service\ShortUrl;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Exception\DeleteShortUrlException;
|
||||
use Shlinkio\Shlink\Core\Options\DeleteShortUrlsOptions;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlService;
|
||||
|
||||
class DeleteShortUrlServiceTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var DeleteShortUrlService
|
||||
*/
|
||||
private $service;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $em;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$shortUrl = (new ShortUrl())->setShortCode('abc123')
|
||||
->setVisits(new ArrayCollection(\array_map(function () {
|
||||
return new Visit();
|
||||
}, \range(0, 10))));
|
||||
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
|
||||
$repo = $this->prophesize(ShortUrlRepositoryInterface::class);
|
||||
$repo->findOneBy(Argument::type('array'))->willReturn($shortUrl);
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function deleteByShortCodeThrowsExceptionWhenThresholdIsReached()
|
||||
{
|
||||
$service = $this->createService();
|
||||
|
||||
$this->expectException(DeleteShortUrlException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Impossible to delete short URL with short code "abc123" since it has more than "5" visits.'
|
||||
);
|
||||
|
||||
$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
|
||||
*/
|
||||
public function deleteByShortCodeDeletesUrlWhenThresholdIsReachedButCheckIsDisabled()
|
||||
{
|
||||
$service = $this->createService(false);
|
||||
|
||||
$remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null);
|
||||
$flush = $this->em->flush()->willReturn(null);
|
||||
|
||||
$service->deleteByShortCode('abc123');
|
||||
|
||||
$remove->shouldHaveBeenCalledTimes(1);
|
||||
$flush->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function deleteByShortCodeDeletesUrlWhenThresholdIsNotReached()
|
||||
{
|
||||
$service = $this->createService(true, 100);
|
||||
|
||||
$remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null);
|
||||
$flush = $this->em->flush()->willReturn(null);
|
||||
|
||||
$service->deleteByShortCode('abc123');
|
||||
|
||||
$remove->shouldHaveBeenCalledTimes(1);
|
||||
$flush->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
private function createService(bool $checkVisitsThreshold = true, int $visitsThreshold = 5): DeleteShortUrlService
|
||||
{
|
||||
return new DeleteShortUrlService($this->em->reveal(), new DeleteShortUrlsOptions([
|
||||
'visitsThreshold' => $visitsThreshold,
|
||||
'checkVisitsThreshold' => $checkVisitsThreshold,
|
||||
]));
|
||||
}
|
||||
}
|
|
@ -54,7 +54,7 @@ class UrlShortenerTest extends TestCase
|
|||
$this->em->persist(Argument::any())->will(function ($arguments) {
|
||||
/** @var ShortUrl $shortUrl */
|
||||
$shortUrl = $arguments[0];
|
||||
$shortUrl->setId(10);
|
||||
$shortUrl->setId('10');
|
||||
});
|
||||
$repo = $this->prophesize(ObjectRepository::class);
|
||||
$repo->findOneBy(Argument::any())->willReturn(null);
|
||||
|
|
|
@ -23,6 +23,7 @@ return [
|
|||
Action\ShortCode\CreateShortCodeAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortCode\SingleStepCreateShortCodeAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortCode\EditShortCodeAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortCode\DeleteShortCodeAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortCode\ResolveUrlAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\GetVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortCode\ListShortCodesAction::class => ConfigAbstractFactory::class,
|
||||
|
@ -58,7 +59,12 @@ return [
|
|||
'config.url_shortener.domain',
|
||||
'Logger_Shlink',
|
||||
],
|
||||
Action\ShortCode\EditShortCodeAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink',],
|
||||
Action\ShortCode\EditShortCodeAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink'],
|
||||
Action\ShortCode\DeleteShortCodeAction::class => [
|
||||
Service\ShortUrl\DeleteShortUrlService::class,
|
||||
'translator',
|
||||
'Logger_Shlink',
|
||||
],
|
||||
Action\ShortCode\ResolveUrlAction::class => [
|
||||
Service\UrlShortener::class,
|
||||
'translator',
|
||||
|
|
|
@ -18,6 +18,7 @@ return [
|
|||
Middleware\ShortCode\CreateShortCodeContentNegotiationMiddleware::class,
|
||||
]),
|
||||
Action\ShortCode\EditShortCodeAction::getRouteDef(),
|
||||
Action\ShortCode\DeleteShortCodeAction::getRouteDef(),
|
||||
Action\ShortCode\ResolveUrlAction::getRouteDef(),
|
||||
Action\ShortCode\ListShortCodesAction::getRouteDef(),
|
||||
Action\ShortCode\EditShortCodeTagsAction::getRouteDef(),
|
||||
|
|
Binary file not shown.
|
@ -1,8 +1,8 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Shlink 1.0\n"
|
||||
"POT-Creation-Date: 2018-05-06 12:34+0200\n"
|
||||
"PO-Revision-Date: 2018-05-06 12:35+0200\n"
|
||||
"POT-Creation-Date: 2018-09-15 18:02+0200\n"
|
||||
"PO-Revision-Date: 2018-09-15 18:03+0200\n"
|
||||
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: es_ES\n"
|
||||
|
@ -43,6 +43,14 @@ msgstr "No se ha proporcionado una URL"
|
|||
msgid "No URL found for short code \"%s\""
|
||||
msgstr "No se ha encontrado ninguna URL para el código corto \"%s\""
|
||||
|
||||
#, php-format
|
||||
msgid ""
|
||||
"It is not possible to delete URL with short code \"%s\" because it has "
|
||||
"reached more than \"%s\" visits."
|
||||
msgstr ""
|
||||
"No es posible eliminar la URL con el código corto \"%s\" porque ha alcanzado "
|
||||
"más de \"%s\" visitas."
|
||||
|
||||
msgid "Provided data is invalid."
|
||||
msgstr "Los datos proporcionados son inválidos."
|
||||
|
||||
|
|
71
module/Rest/src/Action/ShortCode/DeleteShortCodeAction.php
Normal file
71
module/Rest/src/Action/ShortCode/DeleteShortCodeAction.php
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Action\ShortCode;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class DeleteShortCodeAction extends AbstractRestAction
|
||||
{
|
||||
protected const ROUTE_PATH = '/short-codes/{shortCode}';
|
||||
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_DELETE];
|
||||
|
||||
/**
|
||||
* @var DeleteShortUrlServiceInterface
|
||||
*/
|
||||
private $deleteShortUrlService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(
|
||||
DeleteShortUrlServiceInterface $deleteShortUrlService,
|
||||
TranslatorInterface $translator,
|
||||
LoggerInterface $logger = null
|
||||
) {
|
||||
parent::__construct($logger);
|
||||
$this->deleteShortUrlService = $deleteShortUrlService;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the request and return a response.
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$shortCode = $request->getAttribute('shortCode', '');
|
||||
|
||||
try {
|
||||
$this->deleteShortUrlService->deleteByShortCode($shortCode);
|
||||
return new EmptyResponse();
|
||||
} catch (Exception\InvalidShortCodeException $e) {
|
||||
$this->logger->warning(
|
||||
\sprintf('Provided short code %s does not belong to any URL.', $shortCode) . PHP_EOL . $e
|
||||
);
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||
'message' => \sprintf($this->translator->translate('No URL found for short code "%s"'), $shortCode),
|
||||
], self::STATUS_NOT_FOUND);
|
||||
} catch (Exception\DeleteShortUrlException $e) {
|
||||
$this->logger->warning('Provided data is invalid.' . PHP_EOL . $e);
|
||||
$messagePlaceholder = $this->translator->translate(
|
||||
'It is not possible to delete URL with short code "%s" because it has reached more than "%s" visits.'
|
||||
);
|
||||
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||
'message' => \sprintf($messagePlaceholder, $shortCode, $e->getVisitsThreshold()),
|
||||
], self::STATUS_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -75,7 +75,7 @@ class ListShortCodesAction extends AbstractRestAction
|
|||
private function queryToListParams(array $query): array
|
||||
{
|
||||
return [
|
||||
$query['page'] ?? 1,
|
||||
(int) ($query['page'] ?? 1),
|
||||
$query['searchTerm'] ?? null,
|
||||
$query['tags'] ?? [],
|
||||
$query['orderBy'] ?? null,
|
||||
|
|
|
@ -23,17 +23,17 @@ class ApiKey extends AbstractEntity
|
|||
* @var string
|
||||
* @ORM\Column(name="`key`", nullable=false, unique=true)
|
||||
*/
|
||||
protected $key;
|
||||
private $key;
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
* @ORM\Column(name="expiration_date", nullable=true, type="datetime")
|
||||
*/
|
||||
protected $expirationDate;
|
||||
private $expirationDate;
|
||||
/**
|
||||
* @var bool
|
||||
* @ORM\Column(type="boolean")
|
||||
*/
|
||||
protected $enabled;
|
||||
private $enabled;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
@ -41,45 +41,28 @@ class ApiKey extends AbstractEntity
|
|||
$this->key = $this->generateV4Uuid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getKey(): string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return $this
|
||||
*/
|
||||
public function setKey($key): self
|
||||
public function setKey(string $key): self
|
||||
{
|
||||
$this->key = $key;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime|null
|
||||
*/
|
||||
public function getExpirationDate(): ?\DateTime
|
||||
{
|
||||
return $this->expirationDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $expirationDate
|
||||
* @return $this
|
||||
*/
|
||||
public function setExpirationDate($expirationDate): self
|
||||
public function setExpirationDate(\DateTime $expirationDate): self
|
||||
{
|
||||
$this->expirationDate = $expirationDate;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isExpired(): bool
|
||||
{
|
||||
if ($this->expirationDate === null) {
|
||||
|
@ -89,29 +72,17 @@ class ApiKey extends AbstractEntity
|
|||
return $this->expirationDate < new \DateTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return boolean
|
||||
*/
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param boolean $enabled
|
||||
* @return $this
|
||||
*/
|
||||
public function setEnabled($enabled): self
|
||||
public function setEnabled(bool $enabled): self
|
||||
{
|
||||
$this->enabled = $enabled;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables this API key
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function disable(): self
|
||||
{
|
||||
return $this->setEnabled(false);
|
||||
|
@ -119,19 +90,12 @@ class ApiKey extends AbstractEntity
|
|||
|
||||
/**
|
||||
* Tells if this api key is enabled and not expired
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->isEnabled() && ! $this->isExpired();
|
||||
}
|
||||
|
||||
/**
|
||||
* The string representation of an API key is the key itself
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getKey();
|
||||
|
|
|
@ -10,6 +10,7 @@ use Shlinkio\Shlink\Rest\Exception as Rest;
|
|||
class RestUtils
|
||||
{
|
||||
public const INVALID_SHORTCODE_ERROR = 'INVALID_SHORTCODE';
|
||||
public const INVALID_SHORTCODE_DELETION_ERROR = 'INVALID_SHORTCODE_DELETION';
|
||||
public const INVALID_URL_ERROR = 'INVALID_URL';
|
||||
public const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT';
|
||||
public const INVALID_SLUG_ERROR = 'INVALID_SLUG';
|
||||
|
@ -35,6 +36,8 @@ class RestUtils
|
|||
return self::INVALID_ARGUMENT_ERROR;
|
||||
case $e instanceof Rest\AuthenticationException:
|
||||
return self::INVALID_CREDENTIALS_ERROR;
|
||||
case $e instanceof Core\DeleteShortUrlException:
|
||||
return self::INVALID_SHORTCODE_DELETION_ERROR;
|
||||
default:
|
||||
return self::UNKNOWN_ERROR;
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ class AuthenticateActionTest extends TestCase
|
|||
*/
|
||||
public function properApiKeyReturnsTokenInResponse()
|
||||
{
|
||||
$this->apiKeyService->getByKey('foo')->willReturn((new ApiKey())->setId(5))
|
||||
$this->apiKeyService->getByKey('foo')->willReturn((new ApiKey())->setId('5'))
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\ShortCode\DeleteShortCodeAction;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\Diactoros\ServerRequestFactory;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class DeleteShortCodeActionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var DeleteShortCodeAction
|
||||
*/
|
||||
private $action;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $service;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->service = $this->prophesize(DeleteShortUrlServiceInterface::class);
|
||||
$this->action = new DeleteShortCodeAction($this->service->reveal(), Translator::factory([]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function emptyResponseIsReturnedIfProperlyDeleted()
|
||||
{
|
||||
$deleteByShortCode = $this->service->deleteByShortCode(Argument::any())->will(function () {
|
||||
});
|
||||
|
||||
$resp = $this->action->handle(ServerRequestFactory::fromGlobals());
|
||||
|
||||
$this->assertEquals(204, $resp->getStatusCode());
|
||||
$deleteByShortCode->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideExceptions
|
||||
*/
|
||||
public function returnsErrorResponseInCaseOfException(\Throwable $e, string $error, int $statusCode)
|
||||
{
|
||||
$deleteByShortCode = $this->service->deleteByShortCode(Argument::any())->willThrow($e);
|
||||
|
||||
/** @var JsonResponse $resp */
|
||||
$resp = $this->action->handle(ServerRequestFactory::fromGlobals());
|
||||
$payload = $resp->getPayload();
|
||||
|
||||
$this->assertEquals($statusCode, $resp->getStatusCode());
|
||||
$this->assertEquals($error, $payload['error']);
|
||||
$deleteByShortCode->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
public function provideExceptions(): array
|
||||
{
|
||||
return [
|
||||
[new Exception\InvalidShortCodeException(), RestUtils::INVALID_SHORTCODE_ERROR, 404],
|
||||
[new Exception\DeleteShortUrlException(5), RestUtils::INVALID_SHORTCODE_DELETION_ERROR, 400],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ class JWTServiceTest extends TestCase
|
|||
*/
|
||||
public function tokenIsProperlyCreated()
|
||||
{
|
||||
$id = 34;
|
||||
$id = '34';
|
||||
$token = $this->service->create((new ApiKey())->setId($id));
|
||||
$payload = (array) JWT::decode($token, 'foo', [JWTService::DEFAULT_ENCRYPTION_ALG]);
|
||||
$this->assertGreaterThanOrEqual($payload['iat'], time());
|
||||
|
|
Loading…
Add table
Reference in a new issue