mirror of
https://github.com/shlinkio/shlink.git
synced 2025-03-14 04:00:57 +03:00
Merge pull request #1779 from acelaya-forks/feature/clear-short-url-visits
Feature/clear short url visits
This commit is contained in:
commit
b51c149c30
58 changed files with 757 additions and 167 deletions
|
@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
* [#1148](https://github.com/shlinkio/shlink/issues/1148) Add support to delete short URL visits.
|
||||
|
||||
This can be done via `DELETE /short-urls/{shortCode}/visits` REST endpoint or via `short-url:delete-visits` console command.
|
||||
|
||||
The CLI command includes a warning and requires the user to confirm before proceeding.
|
||||
|
||||
* [#1656](https://github.com/shlinkio/shlink/issues/1656) Add support for openswoole 22
|
||||
|
||||
### Changed
|
||||
|
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
use GuzzleHttp\Client;
|
||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Mezzio\Application;
|
||||
use Mezzio\Container;
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
use Psr\Http\Message\ServerRequestFactoryInterface;
|
||||
|
@ -20,7 +21,7 @@ return [
|
|||
],
|
||||
|
||||
'delegators' => [
|
||||
Mezzio\Application::class => [
|
||||
Application::class => [
|
||||
Container\ApplicationConfigInjectionDelegator::class,
|
||||
],
|
||||
],
|
||||
|
|
|
@ -53,6 +53,7 @@ return (static function (): array {
|
|||
]),
|
||||
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\DeleteShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\DeleteShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ResolveShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ListShortUrlsAction::getRouteDef(),
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
version: '2.7'
|
||||
version: '3.0'
|
||||
|
||||
rpc:
|
||||
listen: tcp://127.0.0.1:6001
|
||||
|
@ -14,10 +14,12 @@ http:
|
|||
forbid: ['.php', '.htaccess']
|
||||
pool:
|
||||
num_workers: 1
|
||||
debug: true
|
||||
|
||||
jobs:
|
||||
pool:
|
||||
num_workers: 1
|
||||
debug: true
|
||||
timeout: 300
|
||||
consume: ['shlink']
|
||||
pipelines:
|
||||
|
@ -36,14 +38,3 @@ logs:
|
|||
level: debug
|
||||
metrics:
|
||||
level: debug
|
||||
|
||||
reload:
|
||||
interval: 1s
|
||||
patterns: ['.php']
|
||||
services:
|
||||
http:
|
||||
dirs: ['../../bin', '../../config', '../../data/migrations', '../../module', '../../vendor']
|
||||
recursive: true
|
||||
jobs:
|
||||
dirs: ['../../bin', '../../config', '../../data/migrations', '../../module', '../../vendor']
|
||||
recursive: true
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
version: '2.7'
|
||||
version: '3.0'
|
||||
|
||||
rpc:
|
||||
listen: tcp://127.0.0.1:6001
|
||||
|
|
9
docs/swagger/parameters/shortCode.json
Normal file
9
docs/swagger/parameters/shortCode.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code for the short URL.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
|
@ -11,13 +11,7 @@
|
|||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
"$ref": "../parameters/shortCode.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json"
|
||||
|
@ -127,13 +121,7 @@
|
|||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to edit.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
"$ref": "../parameters/shortCode.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json"
|
||||
|
@ -295,13 +283,7 @@
|
|||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to edit.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
"$ref": "../parameters/shortCode.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json"
|
||||
|
|
|
@ -11,13 +11,7 @@
|
|||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code for the short URL from which we want to get the visits.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
"$ref": "../parameters/shortCode.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json"
|
||||
|
@ -172,5 +166,79 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"delete": {
|
||||
"operationId": "deleteShortUrlVisits",
|
||||
"tags": [
|
||||
"Visits"
|
||||
],
|
||||
"summary": "Delete visits for short URL",
|
||||
"description": "Delete all existing visits on the short URL behind provided short code.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/shortCode.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json"
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Deleted visits",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deletedVisits": {
|
||||
"description": "Amount of affected visits",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"deletedVisits": 536
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The short code does not belong to any short URL.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"examples": {
|
||||
"Short URL not found with API v3 and newer": {
|
||||
"$ref": "../examples/short-url-not-found-v3.json"
|
||||
},
|
||||
"Short URL not found previous to API v3": {
|
||||
"$ref": "../examples/short-url-not-found-v2.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,13 +8,7 @@
|
|||
"description": "Represents a short URL. Tracks the visit and redirects tio the corresponding long URL",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
"$ref": "../parameters/shortCode.json"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
|
|
@ -8,13 +8,7 @@
|
|||
"description": "Generates a QR code image pointing to a short URL.<br />Since this is not an API endpoint but an image one, when an invalid value is provided for any of the query params, they will fall to their default values instead of throwing an error.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
"$ref": "../parameters/shortCode.json"
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
|
|
|
@ -8,13 +8,7 @@
|
|||
"description": "Generates a 1px transparent image which can be used to track emails with a short URL",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
"$ref": "../parameters/shortCode.json"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
|
|
@ -13,6 +13,7 @@ return [
|
|||
Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class,
|
||||
Command\ShortUrl\GetShortUrlVisitsCommand::NAME => Command\ShortUrl\GetShortUrlVisitsCommand::class,
|
||||
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
|
||||
Command\ShortUrl\DeleteShortUrlVisitsCommand::NAME => Command\ShortUrl\DeleteShortUrlVisitsCommand::class,
|
||||
|
||||
Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::class,
|
||||
Command\Visit\DownloadGeoLiteDbCommand::NAME => Command\Visit\DownloadGeoLiteDbCommand::class,
|
||||
|
|
|
@ -42,6 +42,7 @@ return [
|
|||
Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\GetShortUrlVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\DeleteShortUrlVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Visit\DownloadGeoLiteDbCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
|
@ -88,6 +89,7 @@ return [
|
|||
],
|
||||
Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => [ShortUrl\DeleteShortUrlService::class],
|
||||
Command\ShortUrl\DeleteShortUrlVisitsCommand::class => [ShortUrl\ShortUrlVisitsDeleter::class],
|
||||
|
||||
Command\Visit\DownloadGeoLiteDbCommand::class => [GeoLite\GeolocationDbUpdater::class],
|
||||
Command\Visit\LocateVisitsCommand::class => [
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
|
@ -39,10 +39,10 @@ class DisableKeyCommand extends Command
|
|||
try {
|
||||
$this->apiKeyService->disable($apiKey);
|
||||
$io->success(sprintf('API key "%s" properly disabled', $apiKey));
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$io->error($e->getMessage());
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
return ExitCode::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Api;
|
|||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
@ -109,6 +109,6 @@ class GenerateKeyCommand extends Command
|
|||
);
|
||||
}
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
@ -77,7 +77,7 @@ class ListKeysCommand extends Command
|
|||
'Roles',
|
||||
]), $rows);
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function determineMessagePattern(ApiKey $apiKey): string
|
||||
|
|
|
@ -8,7 +8,7 @@ use Doctrine\DBAL\Connection;
|
|||
use Doctrine\DBAL\Platforms\SqlitePlatform;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
@ -57,7 +57,7 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
|
|||
|
||||
if ($this->schemaExists()) {
|
||||
$io->success('Database already exists. Run "db:migrate" command to make sure it is up to date.');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
// Create database
|
||||
|
@ -65,7 +65,7 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
|
|||
$this->runPhpCommand($output, [self::DOCTRINE_SCRIPT, self::DOCTRINE_CREATE_SCHEMA_COMMAND]);
|
||||
$io->success('Database properly created!');
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function checkDbExists(): void
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Db;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
@ -31,6 +31,6 @@ class MigrateDatabaseCommand extends AbstractDatabaseCommand
|
|||
$this->runPhpCommand($output, [self::DOCTRINE_MIGRATIONS_SCRIPT, self::DOCTRINE_MIGRATE_COMMAND]);
|
||||
$io->success('Database properly migrated!');
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Domain;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
|
||||
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
|
||||
|
@ -109,6 +109,6 @@ class DomainRedirectsCommand extends Command
|
|||
|
||||
$io->success(sprintf('"Not found" redirects properly set for "%s"', $domainAuthority));
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Domain;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Core\Config\NotFoundRedirectConfigInterface;
|
||||
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||
|
@ -59,7 +59,7 @@ class ListDomainsCommand extends Command
|
|||
}),
|
||||
);
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function notFoundRedirectsToString(NotFoundRedirectConfigInterface $config): string
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
|
@ -141,7 +141,7 @@ class CreateShortUrlCommand extends Command
|
|||
$longUrl = $input->getArgument('longUrl');
|
||||
if (empty($longUrl)) {
|
||||
$io->error('A URL was not provided!');
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
return ExitCode::EXIT_FAILURE;
|
||||
}
|
||||
|
||||
$explodeWithComma = curry(explode(...))(',');
|
||||
|
@ -176,10 +176,10 @@ class CreateShortUrlCommand extends Command
|
|||
sprintf('Processed long URL: <info>%s</info>', $longUrl),
|
||||
sprintf('Generated short URL: <info>%s</info>', $this->stringifier->stringify($result->shortUrl)),
|
||||
]);
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
} catch (InvalidUrlException | NonUniqueSlugException $e) {
|
||||
$io->error($e->getMessage());
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
return ExitCode::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
|
@ -55,10 +55,10 @@ class DeleteShortUrlCommand extends Command
|
|||
|
||||
try {
|
||||
$this->runDelete($io, $identifier, $ignoreThreshold);
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
} catch (Exception\ShortUrlNotFoundException $e) {
|
||||
$io->error($e->getMessage());
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
return ExitCode::EXIT_FAILURE;
|
||||
} catch (Exception\DeleteShortUrlException $e) {
|
||||
return $this->retry($io, $identifier, $e->getMessage());
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ class DeleteShortUrlCommand extends Command
|
|||
$io->warning('Short URL was not deleted.');
|
||||
}
|
||||
|
||||
return $forceDelete ? ExitCodes::EXIT_SUCCESS : ExitCodes::EXIT_WARNING;
|
||||
return $forceDelete ? ExitCode::EXIT_SUCCESS : ExitCode::EXIT_WARNING;
|
||||
}
|
||||
|
||||
private function runDelete(SymfonyStyle $io, ShortUrlIdentifier $identifier, bool $ignoreThreshold): void
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlVisitsDeleterInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DeleteShortUrlVisitsCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:delete-visits';
|
||||
|
||||
public function __construct(private readonly ShortUrlVisitsDeleterInterface $deleter)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Deletes visits from a short URL')
|
||||
->addArgument(
|
||||
'shortCode',
|
||||
InputArgument::REQUIRED,
|
||||
'The short code for the short URL which visits will be deleted',
|
||||
)
|
||||
->addOption(
|
||||
'domain',
|
||||
'd',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The domain if the short code does not belong to the default one',
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$identifier = ShortUrlIdentifier::fromCli($input);
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
if (! $this->confirm($io)) {
|
||||
$io->info('Operation aborted');
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->deleter->deleteShortUrlVisits($identifier);
|
||||
$io->success(sprintf('Successfully deleted %s visits', $result->affectedItems));
|
||||
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
} catch (ShortUrlNotFoundException) {
|
||||
$io->warning(sprintf('Short URL not found for "%s"', $identifier->__toString()));
|
||||
return ExitCode::EXIT_WARNING;
|
||||
}
|
||||
}
|
||||
|
||||
private function confirm(SymfonyStyle $io): bool
|
||||
{
|
||||
$io->warning('You are about to delete all visits for a short URL. This operation cannot be undone.');
|
||||
return $io->confirm('<comment>Continue deleting visits?</comment>', false);
|
||||
}
|
||||
}
|
|
@ -4,9 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Option\EndDateOption;
|
||||
use Shlinkio\Shlink\CLI\Option\StartDateOption;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Input\EndDateOption;
|
||||
use Shlinkio\Shlink\CLI\Input\StartDateOption;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
|
||||
|
@ -173,7 +173,7 @@ class ListShortUrlsCommand extends Command
|
|||
$io->newLine();
|
||||
$io->success('Short URLs properly listed');
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function renderPage(
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
|
||||
|
@ -56,10 +56,10 @@ class ResolveUrlCommand extends Command
|
|||
try {
|
||||
$url = $this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromCli($input));
|
||||
$output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
} catch (ShortUrlNotFoundException $e) {
|
||||
$io->error($e->getMessage());
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
return ExitCode::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
@ -41,11 +41,11 @@ class DeleteTagsCommand extends Command
|
|||
|
||||
if (empty($tagNames)) {
|
||||
$io->warning('You have to provide at least one tag name');
|
||||
return ExitCodes::EXIT_WARNING;
|
||||
return ExitCode::EXIT_WARNING;
|
||||
}
|
||||
|
||||
$this->tagService->deleteTags($tagNames);
|
||||
$io->success('Tags properly deleted');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\TagsParams;
|
||||
|
@ -34,7 +34,7 @@ class ListTagsCommand extends Command
|
|||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
ShlinkTable::default($output)->render(['Name', 'URLs amount', 'Visits amount'], $this->getTagsRows());
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function getTagsRows(): array
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Exception\TagConflictException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\TagRenaming;
|
||||
|
@ -42,10 +42,10 @@ class RenameTagCommand extends Command
|
|||
try {
|
||||
$this->tagService->renameTag(TagRenaming::fromNames($oldName, $newName));
|
||||
$io->success('Tag properly renamed.');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
} catch (TagNotFoundException | TagConflictException $e) {
|
||||
$io->error($e->getMessage());
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
return ExitCode::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Util;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
@ -28,7 +28,7 @@ abstract class AbstractLockedCommand extends Command
|
|||
$output->writeln(
|
||||
sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName),
|
||||
);
|
||||
return ExitCodes::EXIT_WARNING;
|
||||
return ExitCode::EXIT_WARNING;
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -4,9 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Option\EndDateOption;
|
||||
use Shlinkio\Shlink\CLI\Option\StartDateOption;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Input\EndDateOption;
|
||||
use Shlinkio\Shlink\CLI\Input\StartDateOption;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
|
@ -43,7 +43,7 @@ abstract class AbstractVisitsListCommand extends Command
|
|||
|
||||
ShlinkTable::default($output)->render($headers, $rows);
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function resolveRowsAndHeaders(Paginator $paginator): array
|
||||
|
|
|
@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
|
|||
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
@ -56,7 +56,7 @@ class DownloadGeoLiteDbCommand extends Command
|
|||
$io->success('GeoLite2 db file properly downloaded.');
|
||||
}
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
} catch (GeolocationDbUpdateFailedException $e) {
|
||||
$olderDbExists = $e->olderDbExists();
|
||||
|
||||
|
@ -72,7 +72,7 @@ class DownloadGeoLiteDbCommand extends Command
|
|||
$this->getApplication()?->renderThrowable($e, $io);
|
||||
}
|
||||
|
||||
return $olderDbExists ? ExitCodes::EXIT_WARNING : ExitCodes::EXIT_FAILURE;
|
||||
return $olderDbExists ? ExitCode::EXIT_WARNING : ExitCode::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
|
|||
|
||||
use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
|
||||
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
|
@ -116,14 +116,14 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||
}
|
||||
|
||||
$this->io->success('Finished locating visits');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
} catch (Throwable $e) {
|
||||
$this->io->error($e->getMessage());
|
||||
if ($this->io->isVerbose()) {
|
||||
$this->getApplication()?->renderThrowable($e, $this->io);
|
||||
}
|
||||
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
return ExitCode::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,7 +171,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||
$downloadDbCommand = $cliApp->find(DownloadGeoLiteDbCommand::NAME);
|
||||
$exitCode = $downloadDbCommand->run(new ArrayInput([]), $this->io);
|
||||
|
||||
if ($exitCode === ExitCodes::EXIT_FAILURE) {
|
||||
if ($exitCode === ExitCode::EXIT_FAILURE) {
|
||||
throw new RuntimeException('It is not possible to locate visits without a GeoLite2 db file.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Option;
|
||||
namespace Shlinkio\Shlink\CLI\Input;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Symfony\Component\Console\Command\Command;
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Option;
|
||||
namespace Shlinkio\Shlink\CLI\Input;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Symfony\Component\Console\Command\Command;
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Option;
|
||||
namespace Shlinkio\Shlink\CLI\Input;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Symfony\Component\Console\Command\Command;
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Util;
|
||||
|
||||
final class ExitCodes
|
||||
final class ExitCode
|
||||
{
|
||||
public const EXIT_SUCCESS = 0;
|
||||
public const EXIT_FAILURE = -1;
|
|
@ -7,7 +7,7 @@ namespace ShlinkioCliTest\Shlink\CLI\Command;
|
|||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
|
||||
|
||||
class CreateShortUrlTest extends CliTestCase
|
||||
|
@ -22,7 +22,7 @@ class CreateShortUrlTest extends CliTestCase
|
|||
[CreateShortUrlCommand::NAME, 'https://example.com', '--domain', $defaultDomain, '--custom-slug', $slug],
|
||||
);
|
||||
|
||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
|
||||
self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
|
||||
self::assertStringContainsString('Generated short URL: http://' . $defaultDomain . '/' . $slug, $output);
|
||||
|
||||
[$listOutput] = $this->exec([ListShortUrlsCommand::NAME, '--show-domain', '--search-term', $slug]);
|
||||
|
|
|
@ -6,7 +6,7 @@ namespace ShlinkioCliTest\Shlink\CLI\Command;
|
|||
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
|
||||
|
||||
class GenerateApiKeyTest extends CliTestCase
|
||||
|
@ -17,6 +17,6 @@ class GenerateApiKeyTest extends CliTestCase
|
|||
[$output, $exitCode] = $this->exec([GenerateKeyCommand::NAME]);
|
||||
|
||||
self::assertStringContainsString('[OK] Generated API key', $output);
|
||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
|
||||
self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use Cake\Chronos\Chronos;
|
|||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
|
||||
|
||||
class ListApiKeysTest extends CliTestCase
|
||||
|
@ -19,7 +19,7 @@ class ListApiKeysTest extends CliTestCase
|
|||
[$output, $exitCode] = $this->exec([ListKeysCommand::NAME, ...$flags]);
|
||||
|
||||
self::assertEquals($expectedOutput, $output);
|
||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
|
||||
self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
|
||||
}
|
||||
|
||||
public static function provideFlags(): iterable
|
||||
|
|
|
@ -9,7 +9,7 @@ use PHPUnit\Framework\Attributes\Test;
|
|||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Domain\ListDomainsCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
|
||||
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||
|
@ -53,7 +53,7 @@ class ListDomainsCommandTest extends TestCase
|
|||
$this->commandTester->execute($input);
|
||||
|
||||
self::assertEquals($expectedOutput, $this->commandTester->getDisplay());
|
||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||
self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public static function provideInputsAndOutputs(): iterable
|
||||
|
|
|
@ -11,7 +11,7 @@ use PHPUnit\Framework\Attributes\Test;
|
|||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
|
@ -65,7 +65,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||
self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||
self::assertStringContainsString('stringified_short_url', $output);
|
||||
self::assertStringNotContainsString('but the real-time updates cannot', $output);
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||
$this->commandTester->execute(['longUrl' => $url]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
||||
self::assertEquals(ExitCode::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
||||
self::assertStringContainsString('Provided URL http://domain.com/invalid is invalid.', $output);
|
||||
}
|
||||
|
||||
|
@ -97,7 +97,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--custom-slug' => 'my-slug']);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
||||
self::assertEquals(ExitCode::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
||||
self::assertStringContainsString('Provided slug "my-slug" is already in use', $output);
|
||||
}
|
||||
|
||||
|
@ -121,7 +121,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||
self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||
self::assertStringContainsString('stringified_short_url', $output);
|
||||
}
|
||||
|
||||
|
@ -139,7 +139,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||
$input['longUrl'] = 'http://domain.com/foo/bar';
|
||||
$this->commandTester->execute($input);
|
||||
|
||||
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||
self::assertEquals(ExitCode::EXIT_SUCCESS, $this->commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public static function provideDomains(): iterable
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlVisitsCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\BulkDeleteResult;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlVisitsDeleterInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class DeleteShortUrlVisitsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ShortUrlVisitsDeleterInterface $deleter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->deleter = $this->createMock(ShortUrlVisitsDeleterInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DeleteShortUrlVisitsCommand($this->deleter));
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideCancellingInputs')]
|
||||
public function executionIsAbortedIfManuallyCancelled(array $input): void
|
||||
{
|
||||
$this->deleter->expects($this->never())->method('deleteShortUrlVisits');
|
||||
$this->commandTester->setInputs($input);
|
||||
|
||||
$exitCode = $this->commandTester->execute(['shortCode' => 'foo']);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
|
||||
self::assertStringContainsString('Operation aborted', $output);
|
||||
}
|
||||
|
||||
public static function provideCancellingInputs(): iterable
|
||||
{
|
||||
yield 'default input' => [[]];
|
||||
yield 'no' => [['no']];
|
||||
yield 'n' => [['n']];
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideErrorArgs')]
|
||||
public function warningIsPrintedInCaseOfNotFoundShortUrl(array $args, string $expectedError): void
|
||||
{
|
||||
$this->deleter->expects($this->once())->method('deleteShortUrlVisits')->willThrowException(
|
||||
new ShortUrlNotFoundException(),
|
||||
);
|
||||
$this->commandTester->setInputs(['yes']);
|
||||
|
||||
$exitCode = $this->commandTester->execute($args);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertEquals(ExitCode::EXIT_WARNING, $exitCode);
|
||||
self::assertStringContainsString($expectedError, $output);
|
||||
}
|
||||
|
||||
public static function provideErrorArgs(): iterable
|
||||
{
|
||||
yield 'domain' => [['shortCode' => 'foo'], 'Short URL not found for "foo"'];
|
||||
yield 'no domain' => [['shortCode' => 'foo', '--domain' => 's.test'], 'Short URL not found for "s.test/foo"'];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function successMessageIsPrintedForValidShortUrls(): void
|
||||
{
|
||||
$this->deleter->expects($this->once())->method('deleteShortUrlVisits')->willReturn(new BulkDeleteResult(5));
|
||||
$this->commandTester->setInputs(['yes']);
|
||||
|
||||
$exitCode = $this->commandTester->execute(['shortCode' => 'foo']);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
|
||||
self::assertStringContainsString('Successfully deleted 5 visits', $output);
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
|
|||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
|
||||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
|
@ -65,12 +65,12 @@ class DownloadGeoLiteDbCommandTest extends TestCase
|
|||
yield 'existing db' => [
|
||||
true,
|
||||
'[WARNING] GeoLite2 db file update failed. Visits will continue to be located',
|
||||
ExitCodes::EXIT_WARNING,
|
||||
ExitCode::EXIT_WARNING,
|
||||
];
|
||||
yield 'not existing db' => [
|
||||
false,
|
||||
'[ERROR] GeoLite2 db file download failed. It will not be possible to locate',
|
||||
ExitCodes::EXIT_FAILURE,
|
||||
ExitCode::EXIT_FAILURE,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -86,7 +86,7 @@ class DownloadGeoLiteDbCommandTest extends TestCase
|
|||
$exitCode = $this->commandTester->getStatusCode();
|
||||
|
||||
self::assertStringContainsString($expectedMessage, $output);
|
||||
self::assertSame(ExitCodes::EXIT_SUCCESS, $exitCode);
|
||||
self::assertSame(ExitCode::EXIT_SUCCESS, $exitCode);
|
||||
}
|
||||
|
||||
public static function provideSuccessParams(): iterable
|
||||
|
|
|
@ -10,7 +10,7 @@ use PHPUnit\Framework\MockObject\MockObject;
|
|||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Visit\DownloadGeoLiteDbCommand;
|
||||
use Shlinkio\Shlink\CLI\Command\Visit\LocateVisitsCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
|
@ -85,7 +85,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||
$this->visitToLocation->expects(
|
||||
$this->exactly($expectedUnlocatedCalls + $expectedEmptyCalls + $expectedAllCalls),
|
||||
)->method('resolveVisitLocation')->withAnyParameters()->willReturn(Location::emptyInstance());
|
||||
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
|
||||
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
|
||||
|
||||
$this->commandTester->setInputs(['y']);
|
||||
$this->commandTester->execute($args);
|
||||
|
@ -118,7 +118,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||
->withAnyParameters()
|
||||
->willReturnCallback($this->invokeHelperMethods($visit, $location));
|
||||
$this->visitToLocation->expects($this->once())->method('resolveVisitLocation')->willThrowException($e);
|
||||
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
|
||||
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
|
||||
|
||||
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
|
||||
|
@ -147,7 +147,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||
$this->visitToLocation->expects($this->once())->method('resolveVisitLocation')->willThrowException(
|
||||
IpCannotBeLocatedException::forError(WrongIpException::fromIpAddress('1.2.3.4')),
|
||||
);
|
||||
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
|
||||
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
|
||||
|
||||
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
|
||||
|
@ -171,7 +171,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||
|
||||
$this->visitService->expects($this->never())->method('locateUnlocatedVisits');
|
||||
$this->visitToLocation->expects($this->never())->method('resolveVisitLocation');
|
||||
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
|
||||
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
|
||||
|
||||
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
@ -186,7 +186,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||
public function showsProperMessageWhenGeoLiteUpdateFails(): void
|
||||
{
|
||||
$this->lock->method('acquire')->with($this->isFalse())->willReturn(true);
|
||||
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_FAILURE);
|
||||
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_FAILURE);
|
||||
$this->visitService->expects($this->never())->method('locateUnlocatedVisits');
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
|
@ -199,7 +199,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||
public function providingAllFlagOnItsOwnDisplaysNotice(): void
|
||||
{
|
||||
$this->lock->method('acquire')->with($this->isFalse())->willReturn(true);
|
||||
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
|
||||
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
|
||||
|
||||
$this->commandTester->execute(['--all' => true]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
@ -210,7 +210,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||
#[Test, DataProvider('provideAbortInputs')]
|
||||
public function processingAllCancelsCommandIfUserDoesNotActivelyAgreeToConfirmation(array $inputs): void
|
||||
{
|
||||
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCodes::EXIT_SUCCESS);
|
||||
$this->downloadDbCommand->method('run')->withAnyParameters()->willReturn(ExitCode::EXIT_SUCCESS);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Execution aborted');
|
||||
|
|
|
@ -38,6 +38,7 @@ return [
|
|||
ShortUrl\ShortUrlListService::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\DeleteShortUrlService::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\ShortUrlResolver::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\ShortUrlVisitsDeleter::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\Helper\ShortCodeUniquenessHelper::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\Helper\ShortUrlStringifier::class => ConfigAbstractFactory::class,
|
||||
|
@ -69,6 +70,10 @@ return [
|
|||
EntityRepositoryFactory::class,
|
||||
Visit\Entity\Visit::class,
|
||||
],
|
||||
Visit\Repository\VisitDeleterRepository::class => [
|
||||
EntityRepositoryFactory::class,
|
||||
Visit\Entity\Visit::class,
|
||||
],
|
||||
|
||||
Util\UrlValidator::class => ConfigAbstractFactory::class,
|
||||
Util\DoctrineBatchHelper::class => ConfigAbstractFactory::class,
|
||||
|
@ -137,6 +142,10 @@ return [
|
|||
ShortUrl\ShortUrlResolver::class,
|
||||
],
|
||||
ShortUrl\ShortUrlResolver::class => ['em', Options\UrlShortenerOptions::class],
|
||||
ShortUrl\ShortUrlVisitsDeleter::class => [
|
||||
Visit\Repository\VisitDeleterRepository::class,
|
||||
ShortUrl\ShortUrlResolver::class,
|
||||
],
|
||||
ShortUrl\Helper\ShortCodeUniquenessHelper::class => ['em', Options\UrlShortenerOptions::class],
|
||||
Domain\DomainService::class => ['em', 'config.url_shortener.domain.hostname'],
|
||||
|
||||
|
|
17
module/Core/src/Model/BulkDeleteResult.php
Normal file
17
module/Core/src/Model/BulkDeleteResult.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Model;
|
||||
|
||||
final class BulkDeleteResult
|
||||
{
|
||||
public function __construct(public readonly int $affectedItems)
|
||||
{
|
||||
}
|
||||
|
||||
public function toArray(string $fieldName): array
|
||||
{
|
||||
return [$fieldName => $this->affectedItems];
|
||||
}
|
||||
}
|
|
@ -8,6 +8,8 @@ use Psr\Http\Message\ServerRequestInterface;
|
|||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class ShortUrlIdentifier
|
||||
{
|
||||
private function __construct(public readonly string $shortCode, public readonly ?string $domain = null)
|
||||
|
@ -54,4 +56,13 @@ final class ShortUrlIdentifier
|
|||
{
|
||||
return new self($shortCode, $domain);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
if ($this->domain === null) {
|
||||
return $this->shortCode;
|
||||
}
|
||||
|
||||
return sprintf('%s/%s', $this->domain, $this->shortCode);
|
||||
}
|
||||
}
|
||||
|
|
29
module/Core/src/ShortUrl/ShortUrlVisitsDeleter.php
Normal file
29
module/Core/src/ShortUrl/ShortUrlVisitsDeleter.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\BulkDeleteResult;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\Visit\Repository\VisitDeleterRepositoryInterface;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
class ShortUrlVisitsDeleter implements ShortUrlVisitsDeleterInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly VisitDeleterRepositoryInterface $repository,
|
||||
private readonly ShortUrlResolverInterface $resolver,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ShortUrlNotFoundException
|
||||
*/
|
||||
public function deleteShortUrlVisits(ShortUrlIdentifier $identifier, ?ApiKey $apiKey = null): BulkDeleteResult
|
||||
{
|
||||
$shortUrl = $this->resolver->resolveShortUrl($identifier, $apiKey);
|
||||
return new BulkDeleteResult($this->repository->deleteShortUrlVisits($shortUrl));
|
||||
}
|
||||
}
|
18
module/Core/src/ShortUrl/ShortUrlVisitsDeleterInterface.php
Normal file
18
module/Core/src/ShortUrl/ShortUrlVisitsDeleterInterface.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\BulkDeleteResult;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
interface ShortUrlVisitsDeleterInterface
|
||||
{
|
||||
/**
|
||||
* @throws ShortUrlNotFoundException
|
||||
*/
|
||||
public function deleteShortUrlVisits(ShortUrlIdentifier $identifier, ?ApiKey $apiKey = null): BulkDeleteResult;
|
||||
}
|
22
module/Core/src/Visit/Repository/VisitDeleterRepository.php
Normal file
22
module/Core/src/Visit/Repository/VisitDeleterRepository.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Visit\Repository;
|
||||
|
||||
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
|
||||
class VisitDeleterRepository extends EntitySpecificationRepository implements VisitDeleterRepositoryInterface
|
||||
{
|
||||
public function deleteShortUrlVisits(ShortUrl $shortUrl): int
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->delete(Visit::class, 'v')
|
||||
->where($qb->expr()->eq('v.shortUrl', ':shortUrl'))
|
||||
->setParameter('shortUrl', $shortUrl);
|
||||
|
||||
return $qb->getQuery()->execute();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Visit\Repository;
|
||||
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
|
||||
interface VisitDeleterRepositoryInterface
|
||||
{
|
||||
public function deleteShortUrlVisits(ShortUrl $shortUrl): int;
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioDbTest\Shlink\Core\Visit\Repository;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver;
|
||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Visit\Repository\VisitDeleterRepository;
|
||||
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
|
||||
|
||||
class VisitDeleterRepositoryTest extends DatabaseTestCase
|
||||
{
|
||||
private VisitDeleterRepository $repo;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$this->repo = new VisitDeleterRepository($em, $em->getClassMetadata(Visit::class));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function deletesExpectedVisits(): void
|
||||
{
|
||||
$shortUrl1 = ShortUrl::withLongUrl('https://foo.com');
|
||||
$this->getEntityManager()->persist($shortUrl1);
|
||||
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl1, Visitor::emptyInstance()));
|
||||
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl1, Visitor::emptyInstance()));
|
||||
|
||||
$shortUrl2 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||
ShortUrlInputFilter::LONG_URL => 'https://foo.com',
|
||||
ShortUrlInputFilter::DOMAIN => 's.test',
|
||||
ShortUrlInputFilter::CUSTOM_SLUG => 'foo',
|
||||
]), new PersistenceShortUrlRelationResolver($this->getEntityManager()));
|
||||
$this->getEntityManager()->persist($shortUrl2);
|
||||
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::emptyInstance()));
|
||||
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::emptyInstance()));
|
||||
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::emptyInstance()));
|
||||
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::emptyInstance()));
|
||||
|
||||
$shortUrl3 = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||
ShortUrlInputFilter::LONG_URL => 'https://foo.com',
|
||||
ShortUrlInputFilter::CUSTOM_SLUG => 'foo',
|
||||
]), new PersistenceShortUrlRelationResolver($this->getEntityManager()));
|
||||
$this->getEntityManager()->persist($shortUrl3);
|
||||
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl3, Visitor::emptyInstance()));
|
||||
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
self::assertEquals(2, $this->repo->deleteShortUrlVisits($shortUrl1));
|
||||
self::assertEquals(0, $this->repo->deleteShortUrlVisits($shortUrl1));
|
||||
self::assertEquals(4, $this->repo->deleteShortUrlVisits($shortUrl2));
|
||||
self::assertEquals(0, $this->repo->deleteShortUrlVisits($shortUrl2));
|
||||
self::assertEquals(1, $this->repo->deleteShortUrlVisits($shortUrl3));
|
||||
self::assertEquals(0, $this->repo->deleteShortUrlVisits($shortUrl3));
|
||||
}
|
||||
}
|
55
module/Core/test/ShortUrl/ShortUrlVisitsDeleterTest.php
Normal file
55
module/Core/test/ShortUrl/ShortUrlVisitsDeleterTest.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\ShortUrl;
|
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlVisitsDeleter;
|
||||
use Shlinkio\Shlink\Core\Visit\Repository\VisitDeleterRepositoryInterface;
|
||||
|
||||
class ShortUrlVisitsDeleterTest extends TestCase
|
||||
{
|
||||
private ShortUrlVisitsDeleter $deleter;
|
||||
private MockObject & VisitDeleterRepositoryInterface $repository;
|
||||
private MockObject & ShortUrlResolverInterface $resolver;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = $this->createMock(VisitDeleterRepositoryInterface::class);
|
||||
$this->resolver = $this->createMock(ShortUrlResolverInterface::class);
|
||||
|
||||
$this->deleter = new ShortUrlVisitsDeleter($this->repository, $this->resolver);
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideVisitsCounts')]
|
||||
public function returnsDeletedVisitsFromRepo(int $visitsCount): void
|
||||
{
|
||||
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain('');
|
||||
$shortUrl = ShortUrl::withLongUrl('https://example.com');
|
||||
|
||||
$this->resolver->expects($this->once())->method('resolveShortUrl')->with($identifier, null)->willReturn(
|
||||
$shortUrl,
|
||||
);
|
||||
$this->repository->expects($this->once())->method('deleteShortUrlVisits')->with($shortUrl)->willReturn(
|
||||
$visitsCount,
|
||||
);
|
||||
|
||||
$result = $this->deleter->deleteShortUrlVisits($identifier, null);
|
||||
|
||||
self::assertEquals($visitsCount, $result->affectedItems);
|
||||
}
|
||||
|
||||
public static function provideVisitsCounts(): iterable
|
||||
{
|
||||
yield '45' => [45];
|
||||
yield '5000' => [5000];
|
||||
yield '0' => [0];
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ return [
|
|||
Action\ShortUrl\DeleteShortUrlAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortUrl\ResolveShortUrlAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortUrl\ListShortUrlsAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortUrl\DeleteShortUrlVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\ShortUrlVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\TagVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\DomainVisitsAction::class => ConfigAbstractFactory::class,
|
||||
|
@ -94,6 +95,7 @@ return [
|
|||
ShortUrl\ShortUrlListService::class,
|
||||
ShortUrlDataTransformer::class,
|
||||
],
|
||||
Action\ShortUrl\DeleteShortUrlVisitsAction::class => [ShortUrl\ShortUrlVisitsDeleter::class],
|
||||
Action\Tag\ListTagsAction::class => [TagService::class],
|
||||
Action\Tag\TagsStatsAction::class => [TagService::class],
|
||||
Action\Tag\DeleteTagsAction::class => [TagService::class],
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Action\ShortUrl;
|
||||
|
||||
use Laminas\Diactoros\Response\JsonResponse;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlVisitsDeleterInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
|
||||
|
||||
class DeleteShortUrlVisitsAction extends AbstractRestAction
|
||||
{
|
||||
protected const ROUTE_PATH = '/short-urls/{shortCode}/visits';
|
||||
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_DELETE];
|
||||
|
||||
public function __construct(private readonly ShortUrlVisitsDeleterInterface $deleter)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$identifier = ShortUrlIdentifier::fromApiRequest($request);
|
||||
$apiKey = AuthenticationMiddleware::apiKeyFromRequest($request);
|
||||
|
||||
$result = $this->deleter->deleteShortUrlVisits($identifier, $apiKey);
|
||||
|
||||
return new JsonResponse($result->toArray('deletedVisits'));
|
||||
}
|
||||
}
|
|
@ -12,7 +12,6 @@ use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
|
|||
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
use function Shlinkio\Shlink\Config\env;
|
||||
use function sprintf;
|
||||
|
||||
class CreateShortUrlTest extends ApiTestCase
|
||||
|
@ -320,27 +319,6 @@ class CreateShortUrlTest extends ApiTestCase
|
|||
yield 'example domain' => ['example.com'];
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideTwitterUrls')]
|
||||
public function urlsWithBotProtectionCanBeShortenedWithUrlValidationEnabled(string $longUrl): void
|
||||
{
|
||||
// Requests to Twitter are randomly failing from GitHub actions. Let's skip this test there.
|
||||
// This is a deprecated and low-used feature anyway.
|
||||
if (env('CI', false)) {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
[$statusCode] = $this->createShortUrl(['longUrl' => $longUrl, 'validateUrl' => true]);
|
||||
self::assertEquals(self::STATUS_OK, $statusCode);
|
||||
}
|
||||
|
||||
public static function provideTwitterUrls(): iterable
|
||||
{
|
||||
yield ['https://twitter.com/shlinkio'];
|
||||
yield ['https://mobile.twitter.com/shlinkio'];
|
||||
yield ['https://twitter.com/shlinkio/status/1360637738421268481'];
|
||||
yield ['https://mobile.twitter.com/shlinkio/status/1360637738421268481'];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function canCreateShortUrlsWithEmojis(): void
|
||||
{
|
||||
|
|
86
module/Rest/test-api/Action/DeleteShortUrlVisitsTest.php
Normal file
86
module/Rest/test-api/Action/DeleteShortUrlVisitsTest.php
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioApiTest\Shlink\Rest\Action;
|
||||
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
|
||||
|
||||
class DeleteShortUrlVisitsTest extends ApiTestCase
|
||||
{
|
||||
#[Test]
|
||||
public function deletesVisitsForShortUrlWithoutAffectingTheRest(): void
|
||||
{
|
||||
self::assertEquals(7, $this->getTotalVisits());
|
||||
self::assertEquals(3, $this->getOrphanVisits());
|
||||
|
||||
$resp = $this->callApiWithKey(self::METHOD_DELETE, '/short-urls/abc123/visits');
|
||||
$payload = $this->getJsonResponsePayload($resp);
|
||||
|
||||
self::assertEquals(200, $resp->getStatusCode());
|
||||
self::assertEquals(3, $payload['deletedVisits']);
|
||||
self::assertEquals(4, $this->getTotalVisits());
|
||||
self::assertEquals(3, $this->getOrphanVisits());
|
||||
}
|
||||
|
||||
private function getTotalVisits(): int
|
||||
{
|
||||
$resp = $this->callApiWithKey(self::METHOD_GET, '/visits/non-orphan');
|
||||
$payload = $this->getJsonResponsePayload($resp);
|
||||
|
||||
return $payload['visits']['pagination']['totalItems'];
|
||||
}
|
||||
|
||||
private function getOrphanVisits(): int
|
||||
{
|
||||
$resp = $this->callApiWithKey(self::METHOD_GET, '/visits/orphan');
|
||||
$payload = $this->getJsonResponsePayload($resp);
|
||||
|
||||
return $payload['visits']['pagination']['totalItems'];
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideInvalidShortUrls')]
|
||||
public function returnsErrorForInvalidShortUrls(string $uri, array $options, string $expectedError): void
|
||||
{
|
||||
$resp = $this->callApiWithKey(self::METHOD_DELETE, '/rest/v3' . $uri, $options);
|
||||
$payload = $this->getJsonResponsePayload($resp);
|
||||
|
||||
self::assertEquals(404, $resp->getStatusCode());
|
||||
self::assertEquals($expectedError, $payload['detail']);
|
||||
self::assertEquals('https://shlink.io/api/error/short-url-not-found', $payload['type']);
|
||||
}
|
||||
|
||||
public static function provideInvalidShortUrls(): iterable
|
||||
{
|
||||
yield 'not exists' => [
|
||||
'/short-urls/does-not-exist/visits',
|
||||
[],
|
||||
'No URL found with short code "does-not-exist"',
|
||||
];
|
||||
yield 'needs domain' => [
|
||||
'/short-urls/custom-with-domain/visits',
|
||||
[],
|
||||
'No URL found with short code "custom-with-domain"',
|
||||
];
|
||||
yield 'invalid domain' => [
|
||||
'/short-urls/abc123/visits',
|
||||
[RequestOptions::QUERY => ['domain' => 'ff.test']],
|
||||
'No URL found with short code "abc123" for domain "ff.test"',
|
||||
];
|
||||
yield 'wrong domain' => [
|
||||
'/short-urls/custom-with-domain/visits',
|
||||
[RequestOptions::QUERY => ['domain' => 'ff.test']],
|
||||
'No URL found with short code "custom-with-domain" for domain "ff.test"',
|
||||
];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function cannotDeleteVisitsForShortUrlWithWrongApiKeyPermissions(): void
|
||||
{
|
||||
$resp = $this->callApiWithKey(self::METHOD_DELETE, '/short-urls/abc123/visits', [], 'domain_api_key');
|
||||
self::assertEquals(404, $resp->getStatusCode());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Rest\Action\ShortUrl;
|
||||
|
||||
use Laminas\Diactoros\Response\JsonResponse;
|
||||
use Laminas\Diactoros\ServerRequestFactory;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Model\BulkDeleteResult;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlVisitsDeleterInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\ShortUrl\DeleteShortUrlVisitsAction;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
class DeleteShortUrlVisitsActionTest extends TestCase
|
||||
{
|
||||
private DeleteShortUrlVisitsAction $action;
|
||||
private MockObject & ShortUrlVisitsDeleterInterface $deleter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->deleter = $this->createMock(ShortUrlVisitsDeleterInterface::class);
|
||||
$this->action = new DeleteShortUrlVisitsAction($this->deleter);
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideVisitsCounts')]
|
||||
public function visitsAreDeletedForShortUrl(int $visitsCount): void
|
||||
{
|
||||
$apiKey = ApiKey::create();
|
||||
$request = ServerRequestFactory::fromGlobals()->withAttribute(ApiKey::class, $apiKey)
|
||||
->withAttribute('shortCode', 'foo');
|
||||
|
||||
$this->deleter->expects($this->once())->method('deleteShortUrlVisits')->with(
|
||||
ShortUrlIdentifier::fromShortCodeAndDomain('foo'),
|
||||
$apiKey,
|
||||
)->willReturn(new BulkDeleteResult($visitsCount));
|
||||
|
||||
/** @var JsonResponse $resp */
|
||||
$resp = $this->action->handle($request);
|
||||
$payload = $resp->getPayload();
|
||||
|
||||
self::assertEquals(['deletedVisits' => $visitsCount], $payload);
|
||||
}
|
||||
|
||||
public static function provideVisitsCounts(): iterable
|
||||
{
|
||||
yield '1' => [1];
|
||||
yield '0' => [0];
|
||||
yield '300' => [300];
|
||||
yield '1234' => [1234];
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue