Merge pull request #2607 from acelaya-forks/mago-poc

Mago proof of concept
This commit is contained in:
Alejandro Celaya authored and GitHub committed 2026-05-22 14:06:48 +02:00
commit 8ef6af379d
453 files changed
+6333 -4428

No files matched your search

+1 -1
View File
@@ -29,7 +29,7 @@ use function gc_collect_cycles;
// Call the garbage collector to reduce the chances of it being triggered in the middle of a page generation
gc_collect_cycles();
if (! $keepRunning) {
if (!$keepRunning) {
break;
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ use function Shlinkio\Shlink\Config\env;
} else {
$requestIdMiddleware = $container->get(RequestIdMiddleware::class);
$container->get(RoadRunnerTaskConsumerToListener::class)->listenForTasks(
fn (string $requestId) => $requestIdMiddleware->setCurrentRequestId($requestId),
$requestIdMiddleware->setCurrentRequestId(...),
);
}
})();
+7 -3
View File
@@ -59,6 +59,7 @@
"symfony/string": "^8.0"
},
"require-dev": {
"carthage-software/mago": "^1.27",
"devizzent/cebe-php-openapi": "^1.1.2",
"devster/ubench": "^2.1",
"phpstan/phpstan": "^2.1",
@@ -68,7 +69,6 @@
"phpunit/php-code-coverage": "^13.0",
"phpunit/phpcov": "^12.0",
"phpunit/phpunit": "^13.0",
"shlinkio/php-coding-standard": "~2.5.0",
"shlinkio/shlink-test-utils": "^4.5",
"symfony/var-dumper": "^8.0",
"veewee/composer-run-parallel": "^1.4"
@@ -105,8 +105,12 @@
"@parallel cs stan openapi:validate test:unit:ci test:db:sqlite:ci test:db:postgres test:db:mysql test:db:maria test:db:ms",
"@parallel test:api:ci test:cli:ci"
],
"cs": "phpcs -s",
"cs:fix": "phpcbf",
"lint": "mago lint",
"lint:fix": "mago lint --fix",
"format": "mago format",
"format:check": "mago format --check",
"cs": ["@lint", "@format:check"],
"cs:fix": ["@lint:fix", "@format"],
"stan": ["@putenv APP_ENV=test", "phpstan analyse"],
"test": [
"@parallel test:unit test:db",
+10 -8
View File
@@ -7,14 +7,16 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
return (static function (): array {
$redisServers = EnvVars::REDIS_SERVERS->loadFromEnv();
$redis = ['pub_sub_enabled' => $redisServers !== null && EnvVars::REDIS_PUB_SUB_ENABLED->loadFromEnv()];
$cacheRedisBlock = $redisServers === null ? [] : [
'redis' => [
'servers' => $redisServers,
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(),
'username' => EnvVars::REDIS_SERVERS_USER->loadFromEnv(),
'password' => EnvVars::REDIS_SERVERS_PASSWORD->loadFromEnv(),
],
];
$cacheRedisBlock = $redisServers === null
? []
: [
'redis' => [
'servers' => $redisServers,
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(),
'username' => EnvVars::REDIS_SERVERS_USER->loadFromEnv(),
'password' => EnvVars::REDIS_SERVERS_PASSWORD->loadFromEnv(),
],
];
return [
'cache' => [
+1 -3
View File
@@ -9,12 +9,10 @@ return (function () {
$isDev = EnvVars::isDevEnv();
return [
'debug' => $isDev,
// Disabling config cache for cli, ensures it's never used for RoadRunner, and also that console
// commands don't generate a cache file that's then used by php-fpm web executions
ConfigAggregator::ENABLE_CACHE => ! $isDev && PHP_SAPI !== 'cli',
ConfigAggregator::ENABLE_CACHE => !$isDev && PHP_SAPI !== 'cli',
];
})();
-2
View File
@@ -17,7 +17,6 @@ use Spiral\RoadRunner\WorkerInterface;
use Symfony\Component\Filesystem\Filesystem;
return [
'dependencies' => [
'factories' => [
PSR7Worker::class => ConfigAbstractFactory::class,
@@ -49,5 +48,4 @@ return [
UploadedFileFactoryInterface::class,
],
],
];
+12 -10
View File
@@ -33,14 +33,18 @@ return (static function (): array {
};
$driverOptions = match ($driver) {
'mssql' => ['TrustServerCertificate' => 'true'],
'maria', 'mysql' => ! $useEncryption ? [] : [
1007 => true, // PDO::MYSQL_ATTR_SSL_KEY: Require using SSL
1014 => false, // PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT: Trust any certificate
],
'postgres' => ! $useEncryption ? [] : [
'sslmode' => 'require', // Require connections to be encrypted
'sslrootcert' => '', // Allow any certificate
],
'maria', 'mysql' => !$useEncryption
? []
: [
1007 => true, // PDO::MYSQL_ATTR_SSL_KEY: Require using SSL
1014 => false, // PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT: Trust any certificate
],
'postgres' => !$useEncryption
? []
: [
'sslmode' => 'require', // Require connections to be encrypted
'sslrootcert' => '', // Allow any certificate
],
default => [],
};
$connection = match ($driver) {
@@ -62,7 +66,6 @@ return (static function (): array {
};
return [
'entity_manager' => [
'orm' => [
'proxies_dir' => 'data/proxies',
@@ -75,6 +78,5 @@ return (static function (): array {
],
'connection' => $connection,
],
];
})();
-2
View File
@@ -9,7 +9,6 @@ use Shlinkio\Shlink\Common\Logger;
use function Shlinkio\Shlink\Core\toProblemDetailsType;
return [
'problem-details' => [
'default_types_map' => [
404 => toProblemDetailsType('not-found'),
@@ -31,5 +30,4 @@ return [
],
],
],
];
-2
View File
@@ -5,11 +5,9 @@ declare(strict_types=1);
use Shlinkio\Shlink\Core\Config\EnvVars;
return [
'geolite2' => [
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
'temp_dir' => __DIR__ . '/../../data/temp-geolite',
'license_key' => EnvVars::GEOLITE_LICENSE_KEY->loadFromEnv(),
],
];
-2
View File
@@ -8,7 +8,6 @@ use Shlinkio\Shlink\Installer\Config\Option;
use Shlinkio\Shlink\Installer\Util\InstallationCommand;
return [
'installer' => [
'enabled_options' => [
Option\Server\RuntimeConfigOption::class,
@@ -102,5 +101,4 @@ return [
],
],
],
];
-2
View File
@@ -15,7 +15,6 @@ return (static function (): array {
$proxiesIsHopCount = is_numeric($trustedProxies);
return [
// Configuration for RKA\Middleware\IpAddress
'rka' => [
'ip_address' => [
@@ -43,6 +42,5 @@ return (static function (): array {
IpAddress::class => IpAddressFactory::class,
],
],
];
})();
-2
View File
@@ -12,7 +12,6 @@ use Symfony\Component\Lock;
use const Shlinkio\Shlink\LOCAL_LOCK_FACTORY;
return [
'locks' => [
'locks_dir' => __DIR__ . '/../../data/locks',
],
@@ -46,5 +45,4 @@ return [
Lock\LockFactory::class => ['lock_store'],
LOCAL_LOCK_FACTORY => ['local_lock_store'],
],
];
+10 -11
View File
@@ -39,20 +39,20 @@ return (static function (): array {
$useStreamForShlinkLogger = $isDev || env('SHLINK_RUNTIME') !== null;
return [
'logger' => [
'Shlink' => $useStreamForShlinkLogger ? [
'type' => LoggerType::STREAM->value,
'destination' => 'php://stderr',
...$buildCommonConfig(),
] : [
'type' => LoggerType::FILE->value,
...$buildCommonConfig(),
],
'Shlink' => $useStreamForShlinkLogger
? [
'type' => LoggerType::STREAM->value,
'destination' => 'php://stderr',
...$buildCommonConfig(),
] : [
'type' => LoggerType::FILE->value,
...$buildCommonConfig(),
],
'Access' => [
'type' => LoggerType::STREAM->value,
'destination' => 'php://stderr',
...$buildCommonConfig(! runningInRoadRunner()),
...$buildCommonConfig(!runningInRoadRunner()),
],
],
@@ -75,6 +75,5 @@ return (static function (): array {
ConfigAbstractFactory::class => [
RequestIdProvider::class => [RequestIdMiddleware::class],
],
];
})();
-2
View File
@@ -9,7 +9,6 @@ use Symfony\Component\Mercure\Hub;
use Symfony\Component\Mercure\HubInterface;
return [
// This config is used by shlink-common. Do not delete
'mercure' => [
'enabled' => EnvVars::MERCURE_ENABLED->loadFromEnv(),
@@ -35,5 +34,4 @@ return [
],
],
],
];
@@ -14,7 +14,6 @@ use Shlinkio\Shlink\Common\Middleware\RequestIdMiddleware;
use Shlinkio\Shlink\Core\Geolocation\Middleware\IpGeolocationMiddleware;
return [
'middleware_pipeline' => [
'error-handler' => [
'middleware' => [
@@ -82,5 +81,4 @@ return [
],
],
],
];
-2
View File
@@ -5,7 +5,6 @@ declare(strict_types=1);
use Shlinkio\Shlink\Core\Config\EnvVars;
return [
// This config is used by shlink-common. Do not delete
'rabbitmq' => [
'enabled' => (bool) EnvVars::RABBITMQ_ENABLED->loadFromEnv(),
@@ -16,5 +15,4 @@ return [
'password' => EnvVars::RABBITMQ_PASSWORD->loadFromEnv(),
'vhost' => EnvVars::RABBITMQ_VHOST->loadFromEnv(),
],
];
-2
View File
@@ -6,7 +6,6 @@ use Mezzio\Router\FastRouteRouter;
use Shlinkio\Shlink\Core\Config\EnvVars;
return [
'router' => [
'base_path' => EnvVars::BASE_PATH->loadFromEnv(),
@@ -17,5 +16,4 @@ return [
FastRouteRouter::CONFIG_CACHE_FILE => 'data/cache/fastroute_cached_routes.php',
],
],
];
-2
View File
@@ -24,7 +24,6 @@ return (static function (): array {
$shortUrlRouteSuffix = EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv() ? '[/]' : '';
return [
// The order of the routes defined here matters. Changing it might cause path conflicts
'routes' => [
// Rest
@@ -108,6 +107,5 @@ return (static function (): array {
'allowed_methods' => [RequestMethodInterface::METHOD_GET],
],
],
];
})();
+257 -16
View File
@@ -7,33 +7,274 @@ namespace Shlinkio\Shlink;
use Shlinkio\Shlink\Core\Util\RedirectStatus;
const DEFAULT_DELETE_SHORT_URL_THRESHOLD = 15;
const DEFAULT_SHORT_CODES_LENGTH = 5;
const MIN_SHORT_CODES_LENGTH = 4;
const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302;
const DEFAULT_REDIRECT_CACHE_LIFETIME = 30;
const DEFAULT_REDIRECT_CACHE_VISIBILITY = 'private';
const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory';
const IP_ADDRESS_REQUEST_ATTRIBUTE = 'remote_address';
const REDIRECT_URL_REQUEST_ATTRIBUTE = 'redirect_url';
/**
* List of ISO 3166-1 alpha-2 two-letter country codes https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
*/
const ISO_COUNTRY_CODES = [
'AF', 'AX', 'AL', 'DZ', 'AS', 'AD', 'AO', 'AI', 'AQ', 'AG', 'AR', 'AM', 'AW', 'AU', 'AT', 'AZ',
'BS', 'BH', 'BD', 'BB', 'BY', 'BE', 'BZ', 'BJ', 'BM', 'BT', 'BO', 'BQ', 'BA', 'BW', 'BV', 'BR',
'IO', 'BN', 'BG', 'BF', 'BI', 'CV', 'KH', 'CM', 'CA', 'KY', 'CF', 'TD', 'CL', 'CN', 'CX', 'CC',
'CO', 'KM', 'CG', 'CD', 'CK', 'CR', 'CI', 'HR', 'CU', 'CW', 'CY', 'CZ', 'DK', 'DJ', 'DM', 'DO',
'EC', 'EG', 'SV', 'GQ', 'ER', 'EE', 'SZ', 'ET', 'FK', 'FO', 'FJ', 'FI', 'FR', 'GF', 'PF', 'TF',
'GA', 'GM', 'GE', 'DE', 'GH', 'GI', 'GR', 'GL', 'GD', 'GP', 'GU', 'GT', 'GG', 'GN', 'GW', 'GY',
'HT', 'HM', 'VA', 'HN', 'HK', 'HU', 'IS', 'IN', 'ID', 'IR', 'IQ', 'IE', 'IM', 'IL', 'IT', 'JM',
'JP', 'JE', 'JO', 'KZ', 'KE', 'KI', 'KP', 'KR', 'KW', 'KG', 'LA', 'LV', 'LB', 'LS', 'LR', 'LY',
'LI', 'LT', 'LU', 'MO', 'MG', 'MW', 'MY', 'MV', 'ML', 'MT', 'MH', 'MQ', 'MR', 'MU', 'YT', 'MX',
'FM', 'MD', 'MC', 'MN', 'ME', 'MS', 'MA', 'MZ', 'MM', 'NA', 'NR', 'NP', 'NL', 'NC', 'NZ', 'NI',
'NE', 'NG', 'NU', 'NF', 'MK', 'MP', 'NO', 'OM', 'PK', 'PW', 'PS', 'PA', 'PG', 'PY', 'PE', 'PH',
'PN', 'PL', 'PT', 'PR', 'QA', 'RE', 'RO', 'RU', 'RW', 'BL', 'SH', 'KN', 'LC', 'MF', 'PM', 'VC',
'WS', 'SM', 'ST', 'SA', 'SN', 'RS', 'SC', 'SL', 'SG', 'SX', 'SK', 'SI', 'SB', 'SO', 'ZA', 'GS',
'SS', 'ES', 'LK', 'SD', 'SR', 'SJ', 'SE', 'CH', 'SY', 'TW', 'TJ', 'TZ', 'TH', 'TL', 'TG', 'TK',
'TO', 'TT', 'TN', 'TR', 'TM', 'TC', 'TV', 'UG', 'UA', 'AE', 'GB', 'US', 'UM', 'UY', 'UZ', 'VU',
'VE', 'VN', 'VG', 'VI', 'WF', 'EH', 'YE', 'ZM', 'ZW',
'AF',
'AX',
'AL',
'DZ',
'AS',
'AD',
'AO',
'AI',
'AQ',
'AG',
'AR',
'AM',
'AW',
'AU',
'AT',
'AZ',
'BS',
'BH',
'BD',
'BB',
'BY',
'BE',
'BZ',
'BJ',
'BM',
'BT',
'BO',
'BQ',
'BA',
'BW',
'BV',
'BR',
'IO',
'BN',
'BG',
'BF',
'BI',
'CV',
'KH',
'CM',
'CA',
'KY',
'CF',
'TD',
'CL',
'CN',
'CX',
'CC',
'CO',
'KM',
'CG',
'CD',
'CK',
'CR',
'CI',
'HR',
'CU',
'CW',
'CY',
'CZ',
'DK',
'DJ',
'DM',
'DO',
'EC',
'EG',
'SV',
'GQ',
'ER',
'EE',
'SZ',
'ET',
'FK',
'FO',
'FJ',
'FI',
'FR',
'GF',
'PF',
'TF',
'GA',
'GM',
'GE',
'DE',
'GH',
'GI',
'GR',
'GL',
'GD',
'GP',
'GU',
'GT',
'GG',
'GN',
'GW',
'GY',
'HT',
'HM',
'VA',
'HN',
'HK',
'HU',
'IS',
'IN',
'ID',
'IR',
'IQ',
'IE',
'IM',
'IL',
'IT',
'JM',
'JP',
'JE',
'JO',
'KZ',
'KE',
'KI',
'KP',
'KR',
'KW',
'KG',
'LA',
'LV',
'LB',
'LS',
'LR',
'LY',
'LI',
'LT',
'LU',
'MO',
'MG',
'MW',
'MY',
'MV',
'ML',
'MT',
'MH',
'MQ',
'MR',
'MU',
'YT',
'MX',
'FM',
'MD',
'MC',
'MN',
'ME',
'MS',
'MA',
'MZ',
'MM',
'NA',
'NR',
'NP',
'NL',
'NC',
'NZ',
'NI',
'NE',
'NG',
'NU',
'NF',
'MK',
'MP',
'NO',
'OM',
'PK',
'PW',
'PS',
'PA',
'PG',
'PY',
'PE',
'PH',
'PN',
'PL',
'PT',
'PR',
'QA',
'RE',
'RO',
'RU',
'RW',
'BL',
'SH',
'KN',
'LC',
'MF',
'PM',
'VC',
'WS',
'SM',
'ST',
'SA',
'SN',
'RS',
'SC',
'SL',
'SG',
'SX',
'SK',
'SI',
'SB',
'SO',
'ZA',
'GS',
'SS',
'ES',
'LK',
'SD',
'SR',
'SJ',
'SE',
'CH',
'SY',
'TW',
'TJ',
'TZ',
'TH',
'TL',
'TG',
'TK',
'TO',
'TT',
'TN',
'TR',
'TM',
'TC',
'TV',
'UG',
'UA',
'AE',
'GB',
'US',
'UM',
'UY',
'UZ',
'VU',
'VE',
'VN',
'VG',
'VI',
'WF',
'EH',
'YE',
'ZM',
'ZW',
];
+1 -1
View File
@@ -32,7 +32,7 @@ date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv());
// This class alias tricks the ConfigAbstractFactory to return Lock\Factory instances even with a different service name
// It needs to be placed here as individual config files will not be loaded once config is cached
if (! class_exists(LOCAL_LOCK_FACTORY)) {
if (!class_exists(LOCAL_LOCK_FACTORY)) {
class_alias(Lock\LockFactory::class, LOCAL_LOCK_FACTORY);
}
+30 -10
View File
@@ -5,27 +5,47 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink;
const API_TESTS_HOST = '127.0.0.1';
const API_TESTS_PORT = 9999;
const ANDROID_USER_AGENT = 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) '
const ANDROID_USER_AGENT =
'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) '
. 'Chrome/109.0.5414.86 Mobile Safari/537.36';
const IOS_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 '
const IOS_USER_AGENT =
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 '
. '(KHTML, like Gecko) FxiOS/109.0 Mobile/15E148 Safari/605.1.15';
const WINDOWS_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
const WINDOWS_USER_AGENT =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
. 'Chrome/138.0.0.0 Safari/537.36 Edg/138.0.3351.95';
const LINUX_USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
const LINUX_USER_AGENT =
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
. 'HeadlessChrome/81.0.4044.113 Safari/537.36';
const MACOS_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 15_5) AppleWebKit/605.1.15 (KHTML, like Gecko) '
const MACOS_USER_AGENT =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 15_5) AppleWebKit/605.1.15 (KHTML, like Gecko) '
. 'Version/18.4 Safari/605.1.15';
const CHROMEOS_USER_AGENT = 'Mozilla/5.0 (X11; CrOS x86_64 16181.61.0) AppleWebKit/537.36 (KHTML, like Gecko) '
const CHROMEOS_USER_AGENT =
'Mozilla/5.0 (X11; CrOS x86_64 16181.61.0) AppleWebKit/537.36 (KHTML, like Gecko) '
. 'Chrome/134.0.6998.198 Safari/537.36';
const BROWSER_CHROME_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
const BROWSER_CHROME_USER_AGENT =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
. 'Chrome/146.0.0.0 Safari/537.36';
const BROWSER_FIREFOX_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0';
const BROWSER_EDGE_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
const BROWSER_EDGE_USER_AGENT =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
. 'Chrome/146.0.0.0 Safari/537.36 Edg/146.0.3856.62';
const BROWSER_SAFARI_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7_4) AppleWebKit/605.1.15 '
const BROWSER_SAFARI_USER_AGENT =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7_4) AppleWebKit/605.1.15 '
. '(KHTML, like Gecko) Version/26.0 Safari/605.1.15';
const BROWSER_OPERA_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
const BROWSER_OPERA_USER_AGENT =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
. 'Chrome/146.0.0.0 Safari/537.36 OPR/128.0.0.0';
-2
View File
@@ -5,11 +5,9 @@ declare(strict_types=1);
use Shlinkio\Shlink\Core\Config\EnvVars;
return [
EnvVars::APP_ENV->value => 'test',
// URL shortener
EnvVars::DEFAULT_DOMAIN->value => 's.test',
EnvVars::IS_HTTPS_ENABLED->value => false,
];
+21 -19
View File
@@ -31,13 +31,14 @@ $isE2eTest = $isApiTest || $isCliTest;
$coverageType = env('GENERATE_COVERAGE');
$generateCoverage = $coverageType === 'yes';
$coverage = $isE2eTest && $generateCoverage ? CoverageHelper::createCoverageForDirectories(
[
__DIR__ . '/../../module/Core/src',
__DIR__ . '/../../module/' . ($isApiTest ? 'Rest' : 'CLI') . '/src',
],
__DIR__ . '/../../build/coverage-' . $testEnv,
) : null;
$coverage = $isE2eTest && $generateCoverage
? CoverageHelper::createCoverageForDirectories(
[
__DIR__ . '/../../module/Core/src',
__DIR__ . '/../../module/' . ($isApiTest ? 'Rest' : 'CLI') . '/src',
],
__DIR__ . '/../../build/coverage-' . $testEnv,
) : null;
$buildDbConnection = static function (): array {
$driver = env('DB_DRIVER', 'sqlite');
@@ -88,7 +89,6 @@ $buildTestLoggerConfig = static fn (string $filename) => [
];
return [
'debug' => true,
ConfigAggregator::ENABLE_CACHE => false,
FastRouteRouter::CONFIG_CACHE_ENABLED => false,
@@ -106,12 +106,14 @@ return [
],
],
'middleware_pipeline' => !$isApiTest ? [] : [
'capture_code_coverage' => [
'middleware' => new CoverageMiddleware($coverage),
'priority' => 9999,
'middleware_pipeline' => !$isApiTest
? []
: [
'capture_code_coverage' => [
'middleware' => new CoverageMiddleware($coverage),
'priority' => 9999,
],
],
],
'dependencies' => [
'services' => [
@@ -123,11 +125,12 @@ return [
'factories' => [
TestUtils\Helper\TestHelper::class => InvokableFactory::class,
],
'delegators' => $isCliTest ? [
Application::class => [
new CliCoverageDelegator($coverage),
],
] : [],
'delegators' => $isCliTest
? [
Application::class => [
new CliCoverageDelegator($coverage),
],
] : [],
],
'entity_manager' => [
@@ -145,5 +148,4 @@ return [
'Shlink' => $buildTestLoggerConfig('shlink.log'),
'Access' => $buildTestLoggerConfig('access.log'),
],
];
+1 -1
View File
@@ -22,6 +22,6 @@ final class <className> extends AbstractMigration
public function isTransactional(): bool
{
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
return !$this->connection->getDatabasePlatform() instanceof MySQLPlatform;
}
}
+132
View File
@@ -0,0 +1,132 @@
#:schema https://mago.carthage.software/1.27.1/schema.json
# Welcome to Mago!
# For full documentation, see https://mago.carthage.software/tools/overview
version = "1"
php-version = "8.4.0"
[source]
workspace = "."
paths = ["bin", "config", "module"]
excludes = ["vendor"]
[source.glob]
literal-separator = true
[formatter]
print-width = 120
tab-width = 4
use-tabs = false
null-type-hint = "null_pipe_last"
space-before-arrow-function-parameter-list-parenthesis = true
break-promoted-properties-list = false
parameter-attribute-on-new-line = false
preserve-breaking-member-access-chain = true
preserve-breaking-member-access-chain-first-method-on-same-line = true
preserve-breaking-argument-list = true
preserve-breaking-array-like = true
preserve-breaking-parameter-list = true
preserve-breaking-attribute-list = true
preserve-breaking-conditional-expression = true
preserve-breaking-condition-expression = true
preserve-redundant-logical-binary-expression-parentheses = true
[linter]
integrations = ["phpunit"]
[linter.rules]
no-literal-password = {
exclude = [
"config/test",
"module/CLI/test",
"module/CLI/test-cli",
"module/Core/test",
"module/Core/test-api",
"module/Core/test-db",
"module/Rest/test",
"module/Rest/test-api",
"module/Rest/test-db"
]
}
no-empty-catch-clause = {
exclude = [
"module/CLI/test",
"module/CLI/test-cli",
"module/Core/test",
"module/Core/test-api",
"module/Core/test-db",
"module/Rest/test",
"module/Rest/test-api",
"module/Rest/test-db"
]
}
no-empty-loop = {
exclude = [
"module/CLI/test",
"module/CLI/test-cli",
"module/Core/test",
"module/Core/test-api",
"module/Core/test-db",
"module/Rest/test",
"module/Rest/test-api",
"module/Rest/test-db"
]
}
prefer-static-closure = { level = "warning" }
array-style = { level = "warning" }
prefer-early-continue = { max_allowed_statements = 2 }
no-fully-qualified-global-class-like = { enabled = true, level = "warning" }
no-fully-qualified-global-constant = { enabled = true, level = "warning" }
no-fully-qualified-global-function = { enabled = true, level = "warning" }
ambiguous-constant-access = { enabled = true, level = "warning" }
ambiguous-function-call = {
enabled = true,
level = "warning",
exclude = [
"module/Core/functions",
"module/Core/config/entities-mappings"
]
}
no-multi-assignments = { enabled = false }
no-assign-in-condition = { enabled = false }
literal-named-argument = { enabled = false }
tagged-fixme = { enabled = false }
tagged-todo = { enabled = false }
no-boolean-flag-parameter = { enabled = false }
no-else-clause = { enabled = false }
no-isset = { enabled = false }
no-empty = { enabled = false }
no-shorthand-ternary = { enabled = false }
no-ini-set = { enabled = false }
too-many-enum-cases = { enabled = false }
function-name = { enabled = false }
# TODO Fine tune these
too-many-methods = { enabled = false }
too-many-properties = { enabled = false }
excessive-parameter-list = { enabled = false }
cyclomatic-complexity = { enabled = false }
kan-defect = { enabled = false }
halstead = { effort-threshold = 7000, enabled = false }
# TODO Enable this
sensitive-parameter = { enabled = false }
[analyzer]
plugins = []
find-unused-definitions = true
find-unused-expressions = true
analyze-dead-code = false
memoize-properties = true
check-throws = true
unchecked-exceptions = ["Error", "LogicException"]
unchecked-exception-classes = []
check-missing-override = true
find-unused-parameters = true
strict-list-index-checks = true
strict-array-index-existence = false
allow-array-truthy-operand = false
no-boolean-literal-comparison = false
check-missing-type-hints = false
register-super-globals = true
-2
View File
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI;
return [
'cli' => [
'commands' => [
Command\ShortUrl\CreateShortUrlCommand::NAME => Command\ShortUrl\CreateShortUrlCommand::class,
@@ -51,5 +50,4 @@ return [
Command\Config\ReadEnvVarCommand::NAME => Command\Config\ReadEnvVarCommand::class,
],
],
];
@@ -24,7 +24,6 @@ use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Process\PhpExecutableFinder;
return [
'dependencies' => [
'factories' => [
SymfonyCli\Application::class => Factory\ApplicationFactory::class,
@@ -151,5 +150,4 @@ return [
],
Command\Db\MigrateDatabaseCommand::class => [LockFactory::class, Util\PhpProcessRunner::class],
],
];
+1 -2
View File
@@ -16,8 +16,7 @@ readonly class RoleResolver implements RoleResolverInterface
public function __construct(
private DomainServiceInterface $domainService,
private UrlShortenerOptions $urlShortenerOptions,
) {
}
) {}
public function determineRoles(ApiKeyInput $input): iterable
{
@@ -21,17 +21,17 @@ use function sprintf;
name: DeleteKeyCommand::NAME,
description: 'Deletes an API key by name',
help: <<<HELP
The <info>%command.name%</info> command allows you to delete an existing API key via its name.
The <info>%command.name%</info> command allows you to delete an existing API key via its name.
If no arguments are provided, you will be prompted to select one of the existing API keys.
If no arguments are provided, you will be prompted to select one of the existing API keys.
<info>%command.full_name%</info>
<info>%command.full_name%</info>
You can optionally pass the API key name to be disabled:
You can optionally pass the API key name to be disabled:
<info>%command.full_name% the_key_name</info>
<info>%command.full_name% the_key_name</info>
HELP,
HELP,
)]
class DeleteKeyCommand extends Command
{
@@ -60,15 +60,14 @@ class DeleteKeyCommand extends Command
public function __invoke(
SymfonyStyle $io,
InputInterface $input,
#[Argument(description: 'The API key to delete.')]
string|null $name = null,
#[Argument(description: 'The API key to delete.')] string|null $name = null,
): int {
if ($name === null) {
$io->warning('An API key name was not provided.');
return Command::INVALID;
}
if (! $this->shouldProceed($io, $input)) {
if (!$this->shouldProceed($io, $input)) {
return Command::INVALID;
}
@@ -84,7 +83,7 @@ class DeleteKeyCommand extends Command
private function shouldProceed(SymfonyStyle $io, InputInterface $input): bool
{
if (! $input->isInteractive()) {
if (!$input->isInteractive()) {
return true;
}
@@ -21,17 +21,17 @@ use function sprintf;
name: DisableKeyCommand::NAME,
description: 'Disables an API key by name',
help: <<<HELP
The <info>%command.name%</info> command allows you to disable an existing API key.
The <info>%command.name%</info> command allows you to disable an existing API key.
If no arguments are provided, you will be prompted to select one of the existing non-disabled API keys.
If no arguments are provided, you will be prompted to select one of the existing non-disabled API keys.
<info>%command.full_name%</info>
<info>%command.full_name%</info>
You can optionally pass the API key name to be disabled:
You can optionally pass the API key name to be disabled:
<info>%command.full_name% the_key_name</info>
<info>%command.full_name% the_key_name</info>
HELP,
HELP,
)]
class DisableKeyCommand extends Command
{
@@ -65,10 +65,13 @@ class GenerateKeyCommand extends Command
$io->warning('Save the key in a secure location. You will not be able to get it afterwards.');
}
if (! ApiKey::isAdmin($apiKey)) {
if (!ApiKey::isAdmin($apiKey)) {
ShlinkTable::default($io)->render(
['Role name', 'Role metadata'],
$apiKey->mapRoles(fn (Role $role, array $meta) => [$role->value, arrayToString($meta, indentSize: 0)]),
$apiKey->mapRoles(static fn (Role $role, array $meta) => [
$role->value,
arrayToString($meta, indentSize: 0),
]),
headerTitle: 'Roles',
);
}
+22 -17
View File
@@ -43,26 +43,31 @@ class ListKeysCommand extends Command
)]
bool $enabledOnly = false,
): int {
$rows = array_map(function (ApiKey $apiKey) use ($enabledOnly) {
$expiration = $apiKey->expirationDate;
$messagePattern = $this->determineMessagePattern($apiKey);
$rows = array_map(
function (ApiKey $apiKey) use ($enabledOnly) {
$expiration = $apiKey->expirationDate;
$messagePattern = $this->determineMessagePattern($apiKey);
// Set columns for this row
$rowData = [sprintf($messagePattern, $apiKey->name ?? '-')];
if (! $enabledOnly) {
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey));
}
$rowData[] = $expiration?->toAtomString() ?? '-';
$rowData[] = ApiKey::isAdmin($apiKey) ? 'Admin' : implode("\n", $apiKey->mapRoles(
fn (Role $role, array $meta) => $role->toFriendlyName($meta),
));
// Set columns for this row
$rowData = [sprintf($messagePattern, $apiKey->name ?? '-')];
if (!$enabledOnly) {
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey));
}
$rowData[] = $expiration?->toAtomString() ?? '-';
$rowData[] = ApiKey::isAdmin($apiKey)
? 'Admin'
: implode("\n", $apiKey->mapRoles(
static fn (Role $role, array $meta) => $role->toFriendlyName($meta),
));
return $rowData;
}, $this->apiKeyService->listKeys($enabledOnly));
return $rowData;
},
$this->apiKeyService->listKeys($enabledOnly),
);
ShlinkTable::withRowSeparators($io)->render(array_filter([
'Name',
! $enabledOnly ? 'Is enabled' : null,
!$enabledOnly ? 'Is enabled' : null,
'Expiration date',
'Roles',
]), $rows);
@@ -72,7 +77,7 @@ class ListKeysCommand extends Command
private function determineMessagePattern(ApiKey $apiKey): string
{
if (! $apiKey->isEnabled()) {
if (!$apiKey->isEnabled()) {
return self::ERROR_STRING_PATTERN;
}
@@ -81,6 +86,6 @@ class ListKeysCommand extends Command
private function getEnabledSymbol(ApiKey $apiKey): string
{
return ! $apiKey->isEnabled() || $apiKey->isExpired() ? '---' : '+++';
return !$apiKey->isEnabled() || $apiKey->isExpired() ? '---' : '+++';
}
}
@@ -48,7 +48,7 @@ class ReadEnvVarCommand extends Command
$envVar = $io->choice('Select the env var to read', $validEnvVars);
}
if (! contains($envVar, $validEnvVars)) {
if (!contains($envVar, $validEnvVars)) {
throw new InvalidArgumentException(sprintf('%s is not a valid Shlink environment variable', $envVar));
}
@@ -46,7 +46,7 @@ class DomainRedirectsCommand extends Command
/** @var string[] $availableDomains */
$availableDomains = array_map(
static fn (DomainItem $item) => $item->toString(),
array_filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault),
array_filter($this->domainService->listDomains(), static fn (DomainItem $item) => !$item->isDefault),
);
if (empty($availableDomains)) {
$input->setArgument('domain', $askNewDomain());
@@ -45,12 +45,14 @@ class ListDomainsCommand extends Command
array_map(function (DomainItem $domain) use ($showRedirects) {
$commonValues = [$domain->toString(), $domain->isDefault ? 'Yes' : 'No'];
return $showRedirects
? [
...$commonValues,
$this->notFoundRedirectsToString($domain->notFoundRedirectConfig),
]
: $commonValues;
return (
$showRedirects
? [
...$commonValues,
$this->notFoundRedirectsToString($domain->notFoundRedirectConfig),
]
: $commonValues
);
}, $domains),
);
@@ -64,9 +66,9 @@ class ListDomainsCommand extends Command
$invalidShortUrl = $config->invalidShortUrlRedirect ?? 'N/A';
return <<<EOL
* Base URL: {$baseUrl}
* Regular 404: {$regular404}
* Invalid short URL: {$invalidShortUrl}
EOL;
* Base URL: {$baseUrl}
* Regular 404: {$regular404}
* Invalid short URL: {$invalidShortUrl}
EOL;
}
}
@@ -24,16 +24,16 @@ use function sprintf;
description: 'Send existing visits to the configured matomo instance',
help: <<<HELP
This command allows you to send existing visits from this Shlink instance to the configured Matomo server.
Its intention is to allow you to configure Matomo at some point in time, and still have your whole visits
history tracked there.
This command will unconditionally send to Matomo all visits for a specific date range, so make sure you
provide the proper limits to avoid duplicated visits.
Send all visits created so far:
<info>%command.name%</info>
Send all visits created before 2024:
<info>%command.name% --until 2023-12-31</info>
@@ -67,7 +67,7 @@ class MatomoSendVisitsCommand extends Command implements VisitSendingProgressTra
): int {
$this->io = $io;
if (! $this->matomoEnabled) {
if (!$this->matomoEnabled) {
$this->io->warning('Matomo integration is not enabled in this Shlink instance');
return self::INVALID;
}
@@ -83,9 +83,9 @@ class MatomoSendVisitsCommand extends Command implements VisitSendingProgressTra
'You are about to send visits from this Shlink instance to Matomo',
'Resolved date range -> ' . dateRangeToHumanFriendly($dateRange),
'Shlink will not check for already sent visits, which could result in some duplications. Make sure '
. 'you have verified only visits in the right date range are going to be sent.',
. 'you have verified only visits in the right date range are going to be sent.',
]);
if (! $this->io->confirm('Continue?', default: false)) {
if (!$this->io->confirm('Continue?', default: false)) {
return self::INVALID;
}
}
@@ -42,10 +42,13 @@ class CreateShortUrlCommand extends Command
$this->treeMapper->map(ShortUrlCreation::class, $inputData->toArray($this->options)),
);
$result->onEventDispatchingError(static fn () => $io->isVerbose() && $io->warning(
'Short URL properly created, but the real-time updates cannot be notified when generating the '
$result->onEventDispatchingError(
static fn () => $io->isVerbose()
&& $io->warning(
'Short URL properly created, but the real-time updates cannot be notified when generating the '
. 'short URL from the command line. Migrate to roadrunner in order to bypass this limitation.',
));
),
);
$io->writeln([
sprintf('Processed long URL: <info>%s</info>', $result->shortUrl->getLongUrl()),
@@ -37,15 +37,15 @@ class DeleteExpiredShortUrlsCommand extends Command
bool $dryRun = false,
): int {
$conditions = new ExpiredShortUrlsConditions(maxVisitsReached: $evaluateMaxVisits);
$force = $force || ! $input->isInteractive();
$force = $force || !$input->isInteractive();
if (! $force && ! $dryRun) {
if (!$force && !$dryRun) {
$io->warning([
'Careful!',
'You are about to perform a destructive operation that can result in deleted short URLs and visits.',
'This action cannot be undone. Proceed at your own risk',
]);
if (! $io->confirm('Continue?', default: false)) {
if (!$io->confirm('Continue?', default: false)) {
return self::INVALID;
}
}
@@ -32,8 +32,7 @@ class GetShortUrlVisitsCommand extends Command
#[Argument('The short code which visits we want to get'), Ask('Which short code do you want to use?')]
string $shortCode,
#[MapInput] VisitsListInput $input,
#[Option('The domain for the short code', shortcut: 'd')]
string|null $domain = null,
#[Option('The domain for the short code', shortcut: 'd')] string|null $domain = null,
): int {
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, $domain);
$dateRange = $input->dateRange();
@@ -28,9 +28,7 @@ final class ShortUrlsParamsInput
)]
public bool $all = false;
// phpcs:disable PSR2.Classes.PropertyDeclaration.Multiple
public int $itemsPerPage {
// phpcs:disable PSR2.Classes.PropertyDeclaration.ScopeMissing
get => $this->all ? Paginator::ALL_ITEMS : ShortUrlsParams::DEFAULT_ITEMS_PER_PAGE;
}
@@ -56,9 +54,7 @@ final class ShortUrlsParamsInput
#[Option('If --tag is provided, returns only short URLs including ALL of them')]
public bool $tagsAll = false;
// phpcs:disable PSR2.Classes.PropertyDeclaration.Multiple
public TagsMode $tagsMode {
// phpcs:disable PSR2.Classes.PropertyDeclaration.ScopeMissing
get => $this->tagsAll ? TagsMode::ALL : TagsMode::ANY;
}
@@ -69,9 +65,7 @@ final class ShortUrlsParamsInput
#[Option('If --exclude-tag is provided, returns only short URLs not including ANY of them')]
public bool $excludeTagsAll = false;
// phpcs:disable PSR2.Classes.PropertyDeclaration.Multiple
public TagsMode $excludeTagsMode {
// phpcs:disable PSR2.Classes.PropertyDeclaration.ScopeMissing
get => $this->excludeTagsAll ? TagsMode::ALL : TagsMode::ANY;
}
@@ -95,7 +89,9 @@ final class ShortUrlsParamsInput
#[Option(
'Whether to display the domain or not. Those belonging to default domain will have value '
. '"' . Domain::DEFAULT_AUTHORITY . '"',
. '"'
. Domain::DEFAULT_AUTHORITY
. '"',
)]
public bool $showDomain = false;
@@ -49,7 +49,8 @@ class ListShortUrlsCommand extends Command
$result = $this->renderPage($io, $columnsMap, $params, $paramsInput->all);
$paramsInput->page += 1;
$continue = $result->hasNextPage() && $io->confirm(
$continue = $result->hasNextPage()
&& $io->confirm(
sprintf('Continue with page <options=bold>%s</>?', $paramsInput->page),
default: false,
);
@@ -75,7 +76,7 @@ class ListShortUrlsCommand extends Command
$rows = map([...$shortUrls], function (ShortUrlWithDeps $shortUrl) use ($columnsMap) {
$serializedShortUrl = $this->transformer->transform($shortUrl);
return map($columnsMap, fn (callable $call) => $call($serializedShortUrl, $shortUrl->shortUrl));
return map($columnsMap, static fn (callable $call) => $call($serializedShortUrl, $shortUrl->shortUrl));
});
ShlinkTable::default($output)->render(
@@ -105,12 +106,15 @@ class ListShortUrlsCommand extends Command
$columnsMap['Tags'] = static fn (array $shortUrl): string => implode(', ', $shortUrl['tags']);
}
if ($params->showDomain) {
$columnsMap['Domain'] = static fn (array $_, ShortUrl $shortUrl): string =>
$shortUrl->getDomain()->authority ?? Domain::DEFAULT_AUTHORITY;
$columnsMap['Domain'] = static fn (array $_, ShortUrl $shortUrl): string => (
$shortUrl->getDomain()->authority ?? Domain::DEFAULT_AUTHORITY
);
}
if ($params->showApiKey) {
$columnsMap['API Key Name'] = static fn (array $_, ShortUrl $shortUrl): string|null =>
$shortUrl->authorApiKey?->name;
$columnsMap['API Key Name'] = static fn (
array $_,
ShortUrl $shortUrl,
): string|null => $shortUrl->authorApiKey?->name;
}
return $columnsMap;
@@ -32,8 +32,9 @@ class GetTagVisitsCommand extends Command
#[Argument('The tag which visits we want to get'), Ask('For what tag do you want to get visits')] string $tag,
#[MapInput] VisitsListInput $input,
#[Option(
'Return visits that belong to this domain only. Use ' . Domain::DEFAULT_AUTHORITY . ' keyword for visits '
. 'in default domain',
'Return visits that belong to this domain only. Use '
. Domain::DEFAULT_AUTHORITY
. ' keyword for visits in default domain',
shortcut: 'd',
)]
string|null $domain = null,
@@ -32,7 +32,7 @@ class RenameTagCommand extends Command
$this->tagService->renameTag(Renaming::fromNames($oldName, $newName));
$io->success('Tag properly renamed.');
return Command::SUCCESS;
} catch (TagNotFoundException | TagConflictException $e) {
} catch (TagNotFoundException|TagConflictException $e) {
$io->error($e->getMessage());
return Command::FAILURE;
}
+2 -2
View File
@@ -21,7 +21,7 @@ class CommandUtils
public static function executeWithWarning(string $warning, SymfonyStyle $io, callable $callback): int
{
$io->warning($warning);
if (! $io->confirm('<comment>Do you want to proceed?</comment>', default: false)) {
if (!$io->confirm('<comment>Do you want to proceed?</comment>', default: false)) {
$io->info('Operation aborted');
return Command::SUCCESS;
}
@@ -42,7 +42,7 @@ class CommandUtils
callable $callback,
): int {
$lock = $locker->createLock($lockConfig->lockName, $lockConfig->ttl, $lockConfig->isBlocking);
if (! $lock->acquire($lockConfig->isBlocking)) {
if (!$lock->acquire($lockConfig->isBlocking)) {
$io->writeln(
sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName),
);
+1 -2
View File
@@ -12,8 +12,7 @@ final readonly class LockConfig
public string $lockName,
public bool $isBlocking,
public float $ttl = self::DEFAULT_TTL,
) {
}
) {}
public static function blocking(string $lockName): self
{
@@ -28,8 +28,9 @@ class GetNonOrphanVisitsCommand extends Command
SymfonyStyle $io,
#[MapInput] VisitsListInput $input,
#[Option(
'Return visits that belong to this domain only. Use ' . Domain::DEFAULT_AUTHORITY . ' keyword for visits '
. 'in default domain',
'Return visits that belong to this domain only. Use '
. Domain::DEFAULT_AUTHORITY
. ' keyword for visits in default domain',
shortcut: 'd',
)]
string|null $domain = null,
@@ -29,8 +29,9 @@ class GetOrphanVisitsCommand extends Command
SymfonyStyle $io,
#[MapInput] VisitsListInput $input,
#[Option(
'Return visits that belong to this domain only. Use ' . Domain::DEFAULT_AUTHORITY . ' keyword for visits '
. 'in default domain',
'Return visits that belong to this domain only. Use '
. Domain::DEFAULT_AUTHORITY
. ' keyword for visits in default domain',
shortcut: 'd',
)]
string|null $domain = null,
@@ -68,7 +68,7 @@ class LocateVisitsCommand extends Command implements VisitGeolocationHelperInter
);
}
if ($all && $retry && ! $this->warnAndVerifyContinue()) {
if ($all && $retry && !$this->warnAndVerifyContinue()) {
throw new RuntimeException('Execution aborted');
}
@@ -85,9 +85,9 @@ class LocateVisitsCommand extends Command implements VisitGeolocationHelperInter
$this->io->warning([
'You are about to process the location of all existing visits your short URLs received.',
'Since shlink saves visitors IP addresses anonymized, you could end up losing precision on some of '
. 'your visits.',
. 'your visits.',
'Also, if you have a large amount of visits, this can be a very time consuming process. '
. 'Continue at your own risk.',
. 'Continue at your own risk.',
]);
return $this->io->confirm('Do you want to proceed?', false);
}
@@ -145,7 +145,7 @@ class LocateVisitsCommand extends Command implements VisitGeolocationHelperInter
public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void
{
if (! $visitLocation->isEmpty) {
if (!$visitLocation->isEmpty) {
$this->io->writeln(sprintf(' [<info>Address located in "%s"</info>]', $visitLocation->countryName));
} elseif ($visit->hasRemoteAddr() && $visit->remoteAddr !== IpAddress::LOCALHOST) {
$this->io->writeln(' <comment>[Could not locate address]</comment>');
@@ -95,7 +95,7 @@ class VisitsCommandUtils
'Redirect URL',
'Type',
];
$rows = array_map(function (Visit $visit) {
$rows = array_map(static function (Visit $visit) {
$visitLocation = $visit->visitLocation;
return [
@@ -6,6 +6,4 @@ namespace Shlinkio\Shlink\CLI\Exception;
use Throwable;
interface ExceptionInterface extends Throwable
{
}
interface ExceptionInterface extends Throwable {}
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Input;
enum VisitsListFormat: string
+7 -2
View File
@@ -19,8 +19,13 @@ class VisitsListInput
public string|null $endDate = null;
#[Option(
'Output format ("' . VisitsListFormat::FULL->value . '", "' . VisitsListFormat::PAGINATED->value . '" or "'
. VisitsListFormat::CSV->value . '")',
'Output format ("'
. VisitsListFormat::FULL->value
. '", "'
. VisitsListFormat::PAGINATED->value
. '" or "'
. VisitsListFormat::CSV->value
. '")',
shortcut: 'f',
)]
public VisitsListFormat $format = VisitsListFormat::FULL;
@@ -46,8 +46,8 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface
} else {
$listing = map(
$rules,
function (ShortUrlRedirectRule $rule, string|int|float $index) use ($amountOfRules): array {
$priority = ((int) $index) + 1;
static function (ShortUrlRedirectRule $rule, string|int|float $index) use ($amountOfRules): array {
$priority = (int) $index + 1;
$conditions = $rule->mapConditions(static fn (RedirectCondition $condition): string => sprintf(
'<comment>%s</comment>',
$condition->toHumanFriendly(),
@@ -209,8 +209,8 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface
return $io->ask(
'Rule priority (the lower the value, the higher the priority)',
(string) $max,
function (string $answer) use ($max): int {
if (! is_numeric($answer)) {
static function (string $answer) use ($max): int {
if (!is_numeric($answer)) {
throw new InvalidArgumentException('The priority must be a numeric positive value');
}
@@ -224,7 +224,7 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface
{
return $io->ask(
'Long URL to redirect when the rule matches',
validator: function (string|null $answer): string {
validator: static function (string|null $answer): string {
if ($answer === null) {
throw new InvalidArgumentException('The long URL is mandatory');
}
@@ -240,7 +240,7 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface
private function askMandatory(string $message, StyleInterface $io): string
{
return $io->ask($message, validator: function (string|null $answer): string {
return $io->ask($message, validator: static function (string|null $answer): string {
if ($answer === null) {
throw new InvalidArgumentException('The value is mandatory');
}
@@ -250,6 +250,6 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface
private function askOptional(string $message, StyleInterface $io): string
{
return $io->ask($message, validator: fn (string|null $answer) => $answer === null ? '' : trim($answer));
return $io->ask($message, validator: static fn (string|null $answer) => $answer === null ? '' : trim($answer));
}
}
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\RedirectRule;
enum RedirectRuleHandlerAction: string
+6 -4
View File
@@ -48,10 +48,12 @@ class ProcessRunner implements ProcessRunnerInterface
$process->mustRun($callback);
if ($output->isVeryVerbose()) {
$message = $process->isSuccessful() ? 'Command ran successfully' : sprintf(
'%s Command did not run successfully',
$process->getExitCode(),
);
$message = $process->isSuccessful()
? 'Command ran successfully'
: sprintf(
'%s Command did not run successfully',
$process->getExitCode(),
);
$output->write($formatter->stop(spl_object_hash($process), $message, $process->isSuccessful()));
}
}
+7 -9
View File
@@ -15,9 +15,7 @@ final class ShlinkTable
private const string DEFAULT_STYLE_NAME = 'default';
private const string TABLE_TITLE_STYLE = '<options=bold> %s </>';
private function __construct(private readonly Table $baseTable, private readonly bool $withRowSeparators = false)
{
}
private function __construct(private readonly Table $baseTable, private readonly bool $withRowSeparators = false) {}
public static function default(OutputInterface $output): self
{
@@ -42,16 +40,16 @@ final class ShlinkTable
): void {
$style = Table::getStyleDefinition(self::DEFAULT_STYLE_NAME);
$style->setFooterTitleFormat(self::TABLE_TITLE_STYLE)
->setHeaderTitleFormat(self::TABLE_TITLE_STYLE);
->setHeaderTitleFormat(self::TABLE_TITLE_STYLE);
$tableRows = $this->withRowSeparators ? $this->addRowSeparators($rows) : $rows;
$table = clone $this->baseTable;
$table->setStyle($style)
->setHeaders($headers)
->setRows($tableRows)
->setFooterTitle($footerTitle)
->setHeaderTitle($headerTitle)
->render();
->setHeaders($headers)
->setRows($tableRows)
->setFooterTitle($footerTitle)
->setHeaderTitle($headerTitle)
->render();
}
private function addRowSeparators(array $rows): array
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace ShlinkioCliTest\Shlink\CLI\Command;
use PHPUnit\Framework\Attributes\Test;
@@ -25,22 +25,22 @@ class ImportShortUrlsTest extends CliTestCase
protected function setUp(): void
{
$this->tempCsvFile = tempnam(sys_get_temp_dir(), 'shlink_csv');
if (! $this->tempCsvFile) {
if (!$this->tempCsvFile) {
return;
}
$handle = fopen($this->tempCsvFile, 'w+');
if (! $handle) {
if (!$handle) {
$this->fail('It was not possible to open the temporary file to write CSV on it');
}
fwrite(
$handle,
<<<CSV
longURL;tags;domain;short code;Title
https://shlink.io;foo,baz;s.test;testing-default-domain-import-1;
https://example.com;foo;s.test;testing-default-domain-import-2;
CSV,
longURL;tags;domain;short code;Title
https://shlink.io;foo,baz;s.test;testing-default-domain-import-1;
https://example.com;foo;s.test;testing-default-domain-import-2;
CSV,
);
fclose($handle);
}
@@ -55,7 +55,7 @@ class ImportShortUrlsTest extends CliTestCase
#[Test]
public function defaultDomainIsIgnoredWhenExplicitlyProvided(): void
{
if (! $this->tempCsvFile) {
if (!$this->tempCsvFile) {
$this->fail('It was not possible to create a temporary CSV file');
}
@@ -17,9 +17,9 @@ class InitialApiKeyTest extends CliTestCase
self::assertEquals(
<<<OUT
Other API keys already exist. Initial API key creation skipped.
Other API keys already exist. Initial API key creation skipped.
OUT,
OUT,
$output,
);
}
+30 -30
View File
@@ -26,40 +26,40 @@ class ListApiKeysTest extends CliTestCase
{
$expiredApiKeyDate = Chronos::now()->subDays(1)->startOfDay()->toAtomString();
$enabledOnlyOutput = <<<OUT
+--------------------+---------------------------+--------------------------+
| Name | Expiration date | Roles |
+--------------------+---------------------------+--------------------------+
| valid_api_key | - | Admin |
+--------------------+---------------------------+--------------------------+
| expired_api_key | {$expiredApiKeyDate} | Admin |
+--------------------+---------------------------+--------------------------+
| author_api_key | - | Author only |
+--------------------+---------------------------+--------------------------+
| domain_api_key | - | Domain only: example.com |
+--------------------+---------------------------+--------------------------+
| no_orphans_api_key | - | No orphan visits |
+--------------------+---------------------------+--------------------------+
+--------------------+---------------------------+--------------------------+
| Name | Expiration date | Roles |
+--------------------+---------------------------+--------------------------+
| valid_api_key | - | Admin |
+--------------------+---------------------------+--------------------------+
| expired_api_key | {$expiredApiKeyDate} | Admin |
+--------------------+---------------------------+--------------------------+
| author_api_key | - | Author only |
+--------------------+---------------------------+--------------------------+
| domain_api_key | - | Domain only: example.com |
+--------------------+---------------------------+--------------------------+
| no_orphans_api_key | - | No orphan visits |
+--------------------+---------------------------+--------------------------+
OUT;
OUT;
yield 'no flags' => [[], <<<OUT
+--------------------+------------+---------------------------+--------------------------+
| Name | Is enabled | Expiration date | Roles |
+--------------------+------------+---------------------------+--------------------------+
| valid_api_key | +++ | - | Admin |
+--------------------+------------+---------------------------+--------------------------+
| disabled_api_key | --- | - | Admin |
+--------------------+------------+---------------------------+--------------------------+
| expired_api_key | --- | {$expiredApiKeyDate} | Admin |
+--------------------+------------+---------------------------+--------------------------+
| author_api_key | +++ | - | Author only |
+--------------------+------------+---------------------------+--------------------------+
| domain_api_key | +++ | - | Domain only: example.com |
+--------------------+------------+---------------------------+--------------------------+
| no_orphans_api_key | +++ | - | No orphan visits |
+--------------------+------------+---------------------------+--------------------------+
+--------------------+------------+---------------------------+--------------------------+
| Name | Is enabled | Expiration date | Roles |
+--------------------+------------+---------------------------+--------------------------+
| valid_api_key | +++ | - | Admin |
+--------------------+------------+---------------------------+--------------------------+
| disabled_api_key | --- | - | Admin |
+--------------------+------------+---------------------------+--------------------------+
| expired_api_key | --- | {$expiredApiKeyDate} | Admin |
+--------------------+------------+---------------------------+--------------------------+
| author_api_key | +++ | - | Author only |
+--------------------+------------+---------------------------+--------------------------+
| domain_api_key | +++ | - | Domain only: example.com |
+--------------------+------------+---------------------------+--------------------------+
| no_orphans_api_key | +++ | - | No orphan visits |
+--------------------+------------+---------------------------+--------------------------+
OUT];
OUT];
yield '-e' => [['-e'], $enabledOnlyOutput];
yield '--enabled-only' => [['--enabled-only'], $enabledOnlyOutput];
}
@@ -20,82 +20,80 @@ class ListShortUrlsTest extends CliTestCase
public static function provideFlagsAndOutput(): iterable
{
// phpcs:disable Generic.Files.LineLength
yield 'no flags' => [[], <<<OUTPUT
+--------------------+---------------+-------------------------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+---------------+-------------------------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| ghi789 | | http://example.com/ghi789 | https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/ | 2019-01-01T00:00:30+00:00 | 0 |
| custom | | http://s.test/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://s.test/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
| abc123 | My cool title | http://s.test/abc123 | https://shlink.io | 2018-05-01T00:00:00+00:00 | 3 |
| ghi789 | | http://s.test/ghi789 | https://shlink.io/documentation/ | 2018-05-01T00:00:00+00:00 | 2 |
+--------------------+---------------+-------------------------------------------+---------------------------- Page 1 of 1 ------------------------------------------------------------------+---------------------------+--------------+
OUTPUT];
+--------------------+---------------+-------------------------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+---------------+-------------------------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| ghi789 | | http://example.com/ghi789 | https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/ | 2019-01-01T00:00:30+00:00 | 0 |
| custom | | http://s.test/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://s.test/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
| abc123 | My cool title | http://s.test/abc123 | https://shlink.io | 2018-05-01T00:00:00+00:00 | 3 |
| ghi789 | | http://s.test/ghi789 | https://shlink.io/documentation/ | 2018-05-01T00:00:00+00:00 | 2 |
+--------------------+---------------+-------------------------------------------+---------------------------- Page 1 of 1 ------------------------------------------------------------------+---------------------------+--------------+
OUTPUT];
yield 'start date' => [['--start-date=2019-01'], <<<OUTPUT
+------------+-------+---------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+------------+-------+---------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| ghi789 | | http://example.com/ghi789 | https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/ | 2019-01-01T00:00:30+00:00 | 0 |
| custom | | http://s.test/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://s.test/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
+------------+-------+---------------------------+-------------------------------------------- Page 1 of 1 --------------------------------------------------+---------------------------+--------------+
OUTPUT];
+------------+-------+---------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+------------+-------+---------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| ghi789 | | http://example.com/ghi789 | https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/ | 2019-01-01T00:00:30+00:00 | 0 |
| custom | | http://s.test/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://s.test/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
+------------+-------+---------------------------+-------------------------------------------- Page 1 of 1 --------------------------------------------------+---------------------------+--------------+
OUTPUT];
yield 'end date' => [['-e 2018-12-01'], <<<OUTPUT
+--------------------+---------------+-------------------------------------------+----------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+---------------+-------------------------------------------+----------------------------------+---------------------------+--------------+
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
| abc123 | My cool title | http://s.test/abc123 | https://shlink.io | 2018-05-01T00:00:00+00:00 | 3 |
| ghi789 | | http://s.test/ghi789 | https://shlink.io/documentation/ | 2018-05-01T00:00:00+00:00 | 2 |
+--------------------+---------------+----------------------------------- Page 1 of 1 ------------------------------+---------------------------+--------------+
OUTPUT];
+--------------------+---------------+-------------------------------------------+----------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+---------------+-------------------------------------------+----------------------------------+---------------------------+--------------+
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
| abc123 | My cool title | http://s.test/abc123 | https://shlink.io | 2018-05-01T00:00:00+00:00 | 3 |
| ghi789 | | http://s.test/ghi789 | https://shlink.io/documentation/ | 2018-05-01T00:00:00+00:00 | 2 |
+--------------------+---------------+----------------------------------- Page 1 of 1 ------------------------------+---------------------------+--------------+
OUTPUT];
yield 'start and end date' => [['-s 2018-06-20', '--end-date=2019-01-01T00:00:20+00:00'], <<<OUTPUT
+--------------------+-------+-------------------------------------------+-----------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+-------+-------------------------------------------+-----------------------------------------------------------------------------------------------------+---------------------------+--------------+
| custom | | http://s.test/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://s.test/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
+--------------------+-------+-------------------------------------------+----------------------------- Page 1 of 1 -----------------------------------------------------------+---------------------------+--------------+
OUTPUT];
+--------------------+-------+-------------------------------------------+-----------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+-------+-------------------------------------------+-----------------------------------------------------------------------------------------------------+---------------------------+--------------+
| custom | | http://s.test/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://s.test/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
+--------------------+-------+-------------------------------------------+----------------------------- Page 1 of 1 -----------------------------------------------------------+---------------------------+--------------+
OUTPUT];
yield 'expired excluded' => [['--exclude-max-visits-reached', '--exclude-past-valid-until'], <<<OUTPUT
+--------------------+-------+-------------------------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+-------+-------------------------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| ghi789 | | http://example.com/ghi789 | https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/ | 2019-01-01T00:00:30+00:00 | 0 |
| custom | | http://s.test/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://s.test/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
+--------------------+-------+-------------------------------------------+-------------------------------- Page 1 of 1 --------------------------------------------------------------+---------------------------+--------------+
OUTPUT];
+--------------------+-------+-------------------------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+-------+-------------------------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| ghi789 | | http://example.com/ghi789 | https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/ | 2019-01-01T00:00:30+00:00 | 0 |
| custom | | http://s.test/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://s.test/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
+--------------------+-------+-------------------------------------------+-------------------------------- Page 1 of 1 --------------------------------------------------------------+---------------------------+--------------+
OUTPUT];
yield 'non-default domain' => [['--domain=example.com'], <<<OUTPUT
+------------+-------+---------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+------------+-------+---------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| ghi789 | | http://example.com/ghi789 | https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/ | 2019-01-01T00:00:30+00:00 | 0 |
+------------+-------+---------------------------+-------------------------------------------- Page 1 of 1 --------------------------------------------------+---------------------------+--------------+
OUTPUT];
+------------+-------+---------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+------------+-------+---------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| ghi789 | | http://example.com/ghi789 | https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/ | 2019-01-01T00:00:30+00:00 | 0 |
+------------+-------+---------------------------+-------------------------------------------- Page 1 of 1 --------------------------------------------------+---------------------------+--------------+
OUTPUT];
yield 'default domain' => [['--domain=DEFAULT'], <<<OUTPUT
+------------+---------------+----------------------+-----------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+------------+---------------+----------------------+-----------------------------------------------------------------------------------------------------+---------------------------+--------------+
| custom | | http://s.test/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://s.test/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
| abc123 | My cool title | http://s.test/abc123 | https://shlink.io | 2018-05-01T00:00:00+00:00 | 3 |
| ghi789 | | http://s.test/ghi789 | https://shlink.io/documentation/ | 2018-05-01T00:00:00+00:00 | 2 |
+------------+---------------+----------------------+--------------------------------------- Page 1 of 1 -------------------------------------------------+---------------------------+--------------+
OUTPUT];
+------------+---------------+----------------------+-----------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+------------+---------------+----------------------+-----------------------------------------------------------------------------------------------------+---------------------------+--------------+
| custom | | http://s.test/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://s.test/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
| abc123 | My cool title | http://s.test/abc123 | https://shlink.io | 2018-05-01T00:00:00+00:00 | 3 |
| ghi789 | | http://s.test/ghi789 | https://shlink.io/documentation/ | 2018-05-01T00:00:00+00:00 | 2 |
+------------+---------------+----------------------+--------------------------------------- Page 1 of 1 -------------------------------------------------+---------------------------+--------------+
OUTPUT];
yield 'exclude tags' => [['--exclude-tag=foo'], <<<OUTPUT
+--------------------+-------+-------------------------------------------+----------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+-------+-------------------------------------------+----------------------------------+---------------------------+--------------+
| custom | | http://s.test/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
| ghi789 | | http://s.test/ghi789 | https://shlink.io/documentation/ | 2018-05-01T00:00:00+00:00 | 2 |
+--------------------+-------+--------------------------------------- Page 1 of 1 --------------------------+---------------------------+--------------+
OUTPUT];
// phpcs:enable
+--------------------+-------+-------------------------------------------+----------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+-------+-------------------------------------------+----------------------------------+---------------------------+--------------+
| custom | | http://s.test/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
| ghi789 | | http://s.test/ghi789 | https://shlink.io/documentation/ | 2018-05-01T00:00:00+00:00 | 2 |
+--------------------+-------+--------------------------------------- Page 1 of 1 --------------------------+---------------------------+--------------+
OUTPUT];
}
}
+11 -7
View File
@@ -19,7 +19,7 @@ use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
class RoleResolverTest extends TestCase
{
private RoleResolver $resolver;
private MockObject & DomainServiceInterface $domainService;
private MockObject&DomainServiceInterface $domainService;
protected function setUp(): void
{
@@ -33,9 +33,13 @@ class RoleResolverTest extends TestCase
array $expectedRoles,
int $expectedDomainCalls,
): void {
$this->domainService->expects($this->exactly($expectedDomainCalls))->method('getOrCreate')->with(
'example.com',
)->willReturn(self::domainWithId(Domain::withAuthority('example.com')));
$this->domainService
->expects($this->exactly($expectedDomainCalls))
->method('getOrCreate')
->with(
'example.com',
)
->willReturn(self::domainWithId(Domain::withAuthority('example.com')));
$result = [...$this->resolver->determineRoles($input)];
@@ -52,7 +56,7 @@ class RoleResolverTest extends TestCase
0,
];
yield 'domain role only' => [
(function (): ApiKeyInput {
(static function (): ApiKeyInput {
$input = new ApiKeyInput();
$input->domain = 'example.com';
@@ -62,7 +66,7 @@ class RoleResolverTest extends TestCase
1,
];
yield 'author role only' => [
(function (): ApiKeyInput {
(static function (): ApiKeyInput {
$input = new ApiKeyInput();
$input->authorOnly = true;
@@ -72,7 +76,7 @@ class RoleResolverTest extends TestCase
0,
];
yield 'all roles' => [
(function (): ApiKeyInput {
(static function (): ApiKeyInput {
$input = new ApiKeyInput();
$input->domain = 'example.com';
$input->authorOnly = true;
@@ -19,7 +19,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class DeleteKeyCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & ApiKeyServiceInterface $apiKeyService;
private MockObject&ApiKeyServiceInterface $apiKeyService;
protected function setUp(): void
{
@@ -68,11 +68,14 @@ class DeleteKeyCommandTest extends TestCase
{
$name = 'the key to delete';
$this->apiKeyService->expects($this->once())->method('deleteByName')->with($name);
$this->apiKeyService->expects($this->once())->method('listKeys')->willReturn([
ApiKey::fromMeta(ApiKeyMeta::fromParams(name: 'foo')),
ApiKey::fromMeta(ApiKeyMeta::fromParams(name: $name)),
ApiKey::fromMeta(ApiKeyMeta::fromParams(name: 'bar')),
]);
$this->apiKeyService
->expects($this->once())
->method('listKeys')
->willReturn([
ApiKey::fromMeta(ApiKeyMeta::fromParams(name: 'foo')),
ApiKey::fromMeta(ApiKeyMeta::fromParams(name: $name)),
ApiKey::fromMeta(ApiKeyMeta::fromParams(name: 'bar')),
]);
$this->commandTester->setInputs([$name, 'y']);
$exitCode = $this->commandTester->execute([]);
@@ -19,7 +19,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class DisableKeyCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & ApiKeyServiceInterface $apiKeyService;
private MockObject&ApiKeyServiceInterface $apiKeyService;
protected function setUp(): void
{
@@ -45,9 +45,13 @@ class DisableKeyCommandTest extends TestCase
{
$apiKey = 'abcd1234';
$expectedMessage = 'API key "abcd1234" does not exist.';
$this->apiKeyService->expects($this->once())->method('disableByName')->with($apiKey)->willThrowException(
new InvalidArgumentException($expectedMessage),
);
$this->apiKeyService
->expects($this->once())
->method('disableByName')
->with($apiKey)
->willThrowException(
new InvalidArgumentException($expectedMessage),
);
$exitCode = $this->commandTester->execute(['name' => $apiKey]);
$output = $this->commandTester->getDisplay();
@@ -72,11 +76,14 @@ class DisableKeyCommandTest extends TestCase
{
$name = 'the key to delete';
$this->apiKeyService->expects($this->once())->method('disableByName')->with($name);
$this->apiKeyService->expects($this->once())->method('listKeys')->willReturn([
ApiKey::fromMeta(ApiKeyMeta::fromParams(name: 'foo')),
ApiKey::fromMeta(ApiKeyMeta::fromParams(name: $name)),
ApiKey::fromMeta(ApiKeyMeta::fromParams(name: 'bar')),
]);
$this->apiKeyService
->expects($this->once())
->method('listKeys')
->willReturn([
ApiKey::fromMeta(ApiKeyMeta::fromParams(name: 'foo')),
ApiKey::fromMeta(ApiKeyMeta::fromParams(name: $name)),
ApiKey::fromMeta(ApiKeyMeta::fromParams(name: 'bar')),
]);
$this->commandTester->setInputs([$name]);
$exitCode = $this->commandTester->execute([]);
@@ -20,7 +20,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class GenerateKeyCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & ApiKeyServiceInterface $apiKeyService;
private MockObject&ApiKeyServiceInterface $apiKeyService;
protected function setUp(): void
{
@@ -35,9 +35,13 @@ class GenerateKeyCommandTest extends TestCase
#[Test]
public function noExpirationDateIsDefinedIfNotProvided(): void
{
$this->apiKeyService->expects($this->once())->method('create')->with(
$this->callback(fn (ApiKeyMeta $meta) => $meta->expirationDate === null),
)->willReturn(ApiKey::create());
$this->apiKeyService
->expects($this->once())
->method('create')
->with(
$this->callback(static fn (ApiKeyMeta $meta) => $meta->expirationDate === null),
)
->willReturn(ApiKey::create());
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
@@ -48,9 +52,13 @@ class GenerateKeyCommandTest extends TestCase
#[Test]
public function expirationDateIsDefinedIfProvided(): void
{
$this->apiKeyService->expects($this->once())->method('create')->with(
$this->callback(fn (ApiKeyMeta $meta) => $meta->expirationDate instanceof Chronos),
)->willReturn(ApiKey::create());
$this->apiKeyService
->expects($this->once())
->method('create')
->with(
$this->callback(static fn (ApiKeyMeta $meta) => $meta->expirationDate instanceof Chronos),
)
->willReturn(ApiKey::create());
$this->commandTester->execute([
'--expiration-date' => '2016-01-01',
@@ -60,9 +68,13 @@ class GenerateKeyCommandTest extends TestCase
#[Test]
public function nameIsDefinedIfProvided(): void
{
$this->apiKeyService->expects($this->once())->method('create')->with(
$this->callback(fn (ApiKeyMeta $meta) => $meta->name === 'Alice'),
)->willReturn(ApiKey::create());
$this->apiKeyService
->expects($this->once())
->method('create')
->with(
$this->callback(static fn (ApiKeyMeta $meta) => $meta->name === 'Alice'),
)
->willReturn(ApiKey::create());
$exitCode = $this->commandTester->execute([
'--name' => 'Alice',
@@ -18,7 +18,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class InitialApiKeyCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & ApiKeyServiceInterface $apiKeyService;
private MockObject&ApiKeyServiceInterface $apiKeyService;
public function setUp(): void
{
@@ -49,9 +49,8 @@ class InitialApiKeyCommandTest extends TestCase
yield 'api key created, verbose' => [ApiKey::create(), true, ''];
yield 'no api key created, no verbose' => [null, false, ''];
yield 'no api key created, verbose' => [null, true, <<<OUT
Other API keys already exist. Initial API key creation skipped.
Other API keys already exist. Initial API key creation skipped.
OUT,
];
OUT];
}
}
@@ -21,7 +21,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class ListKeysCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & ApiKeyServiceInterface $apiKeyService;
private MockObject&ApiKeyServiceInterface $apiKeyService;
protected function setUp(): void
{
@@ -52,31 +52,31 @@ class ListKeysCommandTest extends TestCase
],
false,
<<<OUTPUT
+--------------------------------------+------------+---------------------------+-------+
| Name | Is enabled | Expiration date | Roles |
+--------------------------------------+------------+---------------------------+-------+
| {$apiKey1->name} | --- | - | Admin |
+--------------------------------------+------------+---------------------------+-------+
| {$apiKey2->name} | --- | 2020-01-01T00:00:00+00:00 | Admin |
+--------------------------------------+------------+---------------------------+-------+
| {$apiKey3->name} | +++ | - | Admin |
+--------------------------------------+------------+---------------------------+-------+
+--------------------------------------+------------+---------------------------+-------+
| Name | Is enabled | Expiration date | Roles |
+--------------------------------------+------------+---------------------------+-------+
| {$apiKey1->name} | --- | - | Admin |
+--------------------------------------+------------+---------------------------+-------+
| {$apiKey2->name} | --- | 2020-01-01T00:00:00+00:00 | Admin |
+--------------------------------------+------------+---------------------------+-------+
| {$apiKey3->name} | +++ | - | Admin |
+--------------------------------------+------------+---------------------------+-------+
OUTPUT,
OUTPUT,
];
yield 'enabled keys' => [
[$apiKey1 = ApiKey::create()->disable(), $apiKey2 = ApiKey::create()],
true,
<<<OUTPUT
+--------------------------------------+-----------------+-------+
| Name | Expiration date | Roles |
+--------------------------------------+-----------------+-------+
| {$apiKey1->name} | - | Admin |
+--------------------------------------+-----------------+-------+
| {$apiKey2->name} | - | Admin |
+--------------------------------------+-----------------+-------+
+--------------------------------------+-----------------+-------+
| Name | Expiration date | Roles |
+--------------------------------------+-----------------+-------+
| {$apiKey1->name} | - | Admin |
+--------------------------------------+-----------------+-------+
| {$apiKey2->name} | - | Admin |
+--------------------------------------+-----------------+-------+
OUTPUT,
OUTPUT,
];
yield 'with roles' => [
[
@@ -94,24 +94,24 @@ class ListKeysCommandTest extends TestCase
],
true,
<<<OUTPUT
+--------------------------------------+-----------------+--------------------------+
| Name | Expiration date | Roles |
+--------------------------------------+-----------------+--------------------------+
| {$apiKey1->name} | - | Admin |
+--------------------------------------+-----------------+--------------------------+
| {$apiKey2->name} | - | Author only |
+--------------------------------------+-----------------+--------------------------+
| {$apiKey3->name} | - | Domain only: example.com |
+--------------------------------------+-----------------+--------------------------+
| {$apiKey4->name} | - | Admin |
+--------------------------------------+-----------------+--------------------------+
| {$apiKey5->name} | - | Author only |
| | | Domain only: example.com |
+--------------------------------------+-----------------+--------------------------+
| {$apiKey6->name} | - | Admin |
+--------------------------------------+-----------------+--------------------------+
+--------------------------------------+-----------------+--------------------------+
| Name | Expiration date | Roles |
+--------------------------------------+-----------------+--------------------------+
| {$apiKey1->name} | - | Admin |
+--------------------------------------+-----------------+--------------------------+
| {$apiKey2->name} | - | Author only |
+--------------------------------------+-----------------+--------------------------+
| {$apiKey3->name} | - | Domain only: example.com |
+--------------------------------------+-----------------+--------------------------+
| {$apiKey4->name} | - | Admin |
+--------------------------------------+-----------------+--------------------------+
| {$apiKey5->name} | - | Author only |
| | | Domain only: example.com |
+--------------------------------------+-----------------+--------------------------+
| {$apiKey6->name} | - | Admin |
+--------------------------------------+-----------------+--------------------------+
OUTPUT,
OUTPUT,
];
yield 'with names' => [
[
@@ -122,19 +122,19 @@ class ListKeysCommandTest extends TestCase
],
true,
<<<OUTPUT
+--------------------------------------+-----------------+-------+
| Name | Expiration date | Roles |
+--------------------------------------+-----------------+-------+
| Alice | - | Admin |
+--------------------------------------+-----------------+-------+
| Alice and Bob | - | Admin |
+--------------------------------------+-----------------+-------+
| {$apiKey3->name} | - | Admin |
+--------------------------------------+-----------------+-------+
| {$apiKey4->name} | - | Admin |
+--------------------------------------+-----------------+-------+
+--------------------------------------+-----------------+-------+
| Name | Expiration date | Roles |
+--------------------------------------+-----------------+-------+
| Alice | - | Admin |
+--------------------------------------+-----------------+-------+
| Alice and Bob | - | Admin |
+--------------------------------------+-----------------+-------+
| {$apiKey3->name} | - | Admin |
+--------------------------------------+-----------------+-------+
| {$apiKey4->name} | - | Admin |
+--------------------------------------+-----------------+-------+
OUTPUT,
OUTPUT,
];
}
@@ -16,7 +16,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class RenameApiKeyCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & ApiKeyServiceInterface $apiKeyService;
private MockObject&ApiKeyServiceInterface $apiKeyService;
protected function setUp(): void
{
@@ -30,9 +30,12 @@ class RenameApiKeyCommandTest extends TestCase
$oldName = 'old name';
$newName = 'new name';
$this->apiKeyService->expects($this->once())->method('renameApiKey')->with(
Renaming::fromNames($oldName, $newName),
);
$this->apiKeyService
->expects($this->once())
->method('renameApiKey')
->with(
Renaming::fromNames($oldName, $newName),
);
$this->commandTester->setInputs([$oldName]);
$this->commandTester->execute([
@@ -46,9 +49,12 @@ class RenameApiKeyCommandTest extends TestCase
$oldName = 'old name';
$newName = 'new name';
$this->apiKeyService->expects($this->once())->method('renameApiKey')->with(
Renaming::fromNames($oldName, $newName),
);
$this->apiKeyService
->expects($this->once())
->method('renameApiKey')
->with(
Renaming::fromNames($oldName, $newName),
);
$this->commandTester->setInputs([$newName]);
$this->commandTester->execute([
@@ -62,9 +68,12 @@ class RenameApiKeyCommandTest extends TestCase
$oldName = 'old name';
$newName = 'new name';
$this->apiKeyService->expects($this->once())->method('renameApiKey')->with(
Renaming::fromNames($oldName, $newName),
);
$this->apiKeyService
->expects($this->once())
->method('renameApiKey')
->with(
Renaming::fromNames($oldName, $newName),
);
$this->commandTester->execute([
'old-name' => $oldName,
@@ -29,12 +29,12 @@ use Symfony\Component\Lock\SharedLockInterface;
class CreateDatabaseCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & ProcessRunnerInterface $processHelper;
private MockObject & Connection $regularConn;
private Stub & ClassMetadataFactory $metadataFactory;
private MockObject&ProcessRunnerInterface $processHelper;
private MockObject&Connection $regularConn;
private Stub&ClassMetadataFactory $metadataFactory;
/** @var MockObject&AbstractSchemaManager<SQLitePlatform> */
private MockObject & AbstractSchemaManager $schemaManager;
private Stub & Driver $driver;
private MockObject&AbstractSchemaManager $schemaManager;
private Stub&Driver $driver;
protected function setUp(): void
{
@@ -107,11 +107,17 @@ class CreateDatabaseCommandTest extends TestCase
$this->metadataFactory->method('getAllMetadata')->willReturn([$metadata]);
$this->schemaManager->expects($this->never())->method('createDatabase');
$this->schemaManager->expects($this->once())->method('listTableNames')->willReturn($tables);
$this->processHelper->expects($this->once())->method('run')->with($this->isInstanceOf(OutputInterface::class), [
CreateDatabaseCommand::SCRIPT,
CreateDatabaseCommand::COMMAND,
'--no-interaction',
]);
$this->processHelper
->expects($this->once())
->method('run')
->with(
$this->isInstanceOf(OutputInterface::class),
[
CreateDatabaseCommand::SCRIPT,
CreateDatabaseCommand::COMMAND,
'--no-interaction',
],
);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
@@ -18,7 +18,7 @@ use Symfony\Component\Lock\SharedLockInterface;
class MigrateDatabaseCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & ProcessRunnerInterface $processHelper;
private MockObject&ProcessRunnerInterface $processHelper;
protected function setUp(): void
{
@@ -36,11 +36,17 @@ class MigrateDatabaseCommandTest extends TestCase
#[Test]
public function migrationsCommandIsRunWithProperVerbosity(): void
{
$this->processHelper->expects($this->once())->method('run')->with($this->isInstanceOf(OutputInterface::class), [
MigrateDatabaseCommand::SCRIPT,
MigrateDatabaseCommand::COMMAND,
'--no-interaction',
]);
$this->processHelper
->expects($this->once())
->method('run')
->with(
$this->isInstanceOf(OutputInterface::class),
[
MigrateDatabaseCommand::SCRIPT,
MigrateDatabaseCommand::COMMAND,
'--no-interaction',
],
);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
@@ -22,7 +22,7 @@ use function substr_count;
class DomainRedirectsCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & DomainServiceInterface $domainService;
private MockObject&DomainServiceInterface $domainService;
protected function setUp(): void
{
@@ -34,13 +34,21 @@ class DomainRedirectsCommandTest extends TestCase
public function onlyPlainQuestionsAreAskedForNewDomainsAndDomainsWithNoRedirects(Domain|null $domain): void
{
$domainAuthority = 'my-domain.com';
$this->domainService->expects($this->once())->method('findByAuthority')->with($domainAuthority)->willReturn(
$domain,
);
$this->domainService->expects($this->once())->method('configureNotFoundRedirects')->with(
$domainAuthority,
NotFoundRedirects::withRedirects('foo.com', null, 'baz.com'),
)->willReturn(Domain::withAuthority(''));
$this->domainService
->expects($this->once())
->method('findByAuthority')
->with($domainAuthority)
->willReturn(
$domain,
);
$this->domainService
->expects($this->once())
->method('configureNotFoundRedirects')
->with(
$domainAuthority,
NotFoundRedirects::withRedirects('foo.com', null, 'baz.com'),
)
->willReturn(Domain::withAuthority(''));
$this->domainService->expects($this->never())->method('listDomains');
$this->commandTester->setInputs(['foo.com', '', 'baz.com']);
@@ -70,13 +78,21 @@ class DomainRedirectsCommandTest extends TestCase
$domain = Domain::withAuthority($domainAuthority);
$domain->configureNotFoundRedirects(NotFoundRedirects::withRedirects('foo.com', 'bar.com', 'baz.com'));
$this->domainService->expects($this->once())->method('findByAuthority')->with($domainAuthority)->willReturn(
$domain,
);
$this->domainService->expects($this->once())->method('configureNotFoundRedirects')->with(
$domainAuthority,
NotFoundRedirects::withRedirects(null, 'edited.com', 'baz.com'),
)->willReturn($domain);
$this->domainService
->expects($this->once())
->method('findByAuthority')
->with($domainAuthority)
->willReturn(
$domain,
);
$this->domainService
->expects($this->once())
->method('configureNotFoundRedirects')
->with(
$domainAuthority,
NotFoundRedirects::withRedirects(null, 'edited.com', 'baz.com'),
)
->willReturn($domain);
$this->domainService->expects($this->never())->method('listDomains');
$this->commandTester->setInputs(['2', '1', 'edited.com', '0']);
@@ -99,13 +115,21 @@ class DomainRedirectsCommandTest extends TestCase
$domain = Domain::withAuthority($domainAuthority);
$this->domainService->expects($this->once())->method('listDomains')->with()->willReturn([]);
$this->domainService->expects($this->once())->method('findByAuthority')->with($domainAuthority)->willReturn(
$domain,
);
$this->domainService->expects($this->once())->method('configureNotFoundRedirects')->with(
$domainAuthority,
NotFoundRedirects::withoutRedirects(),
)->willReturn($domain);
$this->domainService
->expects($this->once())
->method('findByAuthority')
->with($domainAuthority)
->willReturn(
$domain,
);
$this->domainService
->expects($this->once())
->method('configureNotFoundRedirects')
->with(
$domainAuthority,
NotFoundRedirects::withoutRedirects(),
)
->willReturn($domain);
$this->commandTester->setInputs([$domainAuthority, '', '', '']);
$this->commandTester->execute([]);
@@ -120,18 +144,30 @@ class DomainRedirectsCommandTest extends TestCase
$domainAuthority = 'existing-two.com';
$domain = Domain::withAuthority($domainAuthority);
$this->domainService->expects($this->once())->method('listDomains')->with()->willReturn([
DomainItem::forDefaultDomain('default-domain.com', new NotFoundRedirectOptions()),
DomainItem::forNonDefaultDomain(Domain::withAuthority('existing-one.com')),
DomainItem::forNonDefaultDomain(Domain::withAuthority($domainAuthority)),
]);
$this->domainService->expects($this->once())->method('findByAuthority')->with($domainAuthority)->willReturn(
$domain,
);
$this->domainService->expects($this->once())->method('configureNotFoundRedirects')->with(
$domainAuthority,
NotFoundRedirects::withoutRedirects(),
)->willReturn($domain);
$this->domainService
->expects($this->once())
->method('listDomains')
->with()
->willReturn([
DomainItem::forDefaultDomain('default-domain.com', new NotFoundRedirectOptions()),
DomainItem::forNonDefaultDomain(Domain::withAuthority('existing-one.com')),
DomainItem::forNonDefaultDomain(Domain::withAuthority($domainAuthority)),
]);
$this->domainService
->expects($this->once())
->method('findByAuthority')
->with($domainAuthority)
->willReturn(
$domain,
);
$this->domainService
->expects($this->once())
->method('configureNotFoundRedirects')
->with(
$domainAuthority,
NotFoundRedirects::withoutRedirects(),
)
->willReturn($domain);
$this->commandTester->setInputs(['1', '', '', '']);
$this->commandTester->execute([]);
@@ -149,18 +185,30 @@ class DomainRedirectsCommandTest extends TestCase
$domainAuthority = 'new-domain.com';
$domain = Domain::withAuthority($domainAuthority);
$this->domainService->expects($this->once())->method('listDomains')->with()->willReturn([
DomainItem::forDefaultDomain('default-domain.com', new NotFoundRedirectOptions()),
DomainItem::forNonDefaultDomain(Domain::withAuthority('existing-one.com')),
DomainItem::forNonDefaultDomain(Domain::withAuthority('existing-two.com')),
]);
$this->domainService->expects($this->once())->method('findByAuthority')->with($domainAuthority)->willReturn(
$domain,
);
$this->domainService->expects($this->once())->method('configureNotFoundRedirects')->with(
$domainAuthority,
NotFoundRedirects::withoutRedirects(),
)->willReturn($domain);
$this->domainService
->expects($this->once())
->method('listDomains')
->with()
->willReturn([
DomainItem::forDefaultDomain('default-domain.com', new NotFoundRedirectOptions()),
DomainItem::forNonDefaultDomain(Domain::withAuthority('existing-one.com')),
DomainItem::forNonDefaultDomain(Domain::withAuthority('existing-two.com')),
]);
$this->domainService
->expects($this->once())
->method('findByAuthority')
->with($domainAuthority)
->willReturn(
$domain,
);
$this->domainService
->expects($this->once())
->method('configureNotFoundRedirects')
->with(
$domainAuthority,
NotFoundRedirects::withoutRedirects(),
)
->willReturn($domain);
$this->commandTester->setInputs(['2', $domainAuthority, '', '', '']);
$this->commandTester->execute([]);
@@ -23,7 +23,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class GetDomainVisitsCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & VisitsStatsHelperInterface $visitsHelper;
private MockObject&VisitsStatsHelperInterface $visitsHelper;
protected function setUp(): void
{
@@ -39,26 +39,28 @@ class GetDomainVisitsCommandTest extends TestCase
VisitLocation::fromLocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')),
);
$domain = 's.test';
$this->visitsHelper->expects($this->once())->method('visitsForDomain')->with(
$domain,
$this->anything(),
)->willReturn(new Paginator(new ArrayAdapter([$visit])));
$this->visitsHelper
->expects($this->once())
->method('visitsForDomain')
->with(
$domain,
$this->anything(),
)
->willReturn(new Paginator(new ArrayAdapter([$visit])));
$this->commandTester->execute(['domain' => $domain]);
$output = $this->commandTester->getDisplay();
$type = VisitType::VALID_SHORT_URL->value;
self::assertEquals(
// phpcs:disable Generic.Files.LineLength
<<<OUTPUT
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
| Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type |
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
| {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} |
+---------------------------+---------------+------------+------- Page 1 of 1 --------+--------+-------------+--------------+-----------------+
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
| Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type |
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
| {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} |
+---------------------------+---------------+------------+------- Page 1 of 1 --------+--------+-------------+--------------+-----------------+
OUTPUT,
// phpcs:enable
OUTPUT,
$output,
);
}
@@ -21,7 +21,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class ListDomainsCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & DomainServiceInterface $domainService;
private MockObject&DomainServiceInterface $domainService;
protected function setUp(): void
{
@@ -39,14 +39,18 @@ class ListDomainsCommandTest extends TestCase
'https://foo.com/baz-domain/invalid',
));
$this->domainService->expects($this->once())->method('listDomains')->with()->willReturn([
DomainItem::forDefaultDomain('foo.com', new NotFoundRedirectOptions(
invalidShortUrlRedirect: 'https://foo.com/default/invalid',
baseUrlRedirect: 'https://foo.com/default/base',
)),
DomainItem::forNonDefaultDomain(Domain::withAuthority('bar.com')),
DomainItem::forNonDefaultDomain($bazDomain),
]);
$this->domainService
->expects($this->once())
->method('listDomains')
->with()
->willReturn([
DomainItem::forDefaultDomain('foo.com', new NotFoundRedirectOptions(
invalidShortUrlRedirect: 'https://foo.com/default/invalid',
baseUrlRedirect: 'https://foo.com/default/base',
)),
DomainItem::forNonDefaultDomain(Domain::withAuthority('bar.com')),
DomainItem::forNonDefaultDomain($bazDomain),
]);
$this->commandTester->execute($input);
@@ -57,33 +61,33 @@ class ListDomainsCommandTest extends TestCase
public static function provideInputsAndOutputs(): iterable
{
$withoutRedirectsOutput = <<<OUTPUT
+---------+------------+
| Domain | Is default |
+---------+------------+
| foo.com | Yes |
| bar.com | No |
| baz.com | No |
+---------+------------+
+---------+------------+
| Domain | Is default |
+---------+------------+
| foo.com | Yes |
| bar.com | No |
| baz.com | No |
+---------+------------+
OUTPUT;
OUTPUT;
$withRedirectsOutput = <<<OUTPUT
+---------+------------+---------------------------------------------------------+
| Domain | Is default | "Not found" redirects |
+---------+------------+---------------------------------------------------------+
| foo.com | Yes | * Base URL: https://foo.com/default/base |
| | | * Regular 404: N/A |
| | | * Invalid short URL: https://foo.com/default/invalid |
+---------+------------+---------------------------------------------------------+
| bar.com | No | * Base URL: N/A |
| | | * Regular 404: N/A |
| | | * Invalid short URL: N/A |
+---------+------------+---------------------------------------------------------+
| baz.com | No | * Base URL: N/A |
| | | * Regular 404: https://foo.com/baz-domain/regular |
| | | * Invalid short URL: https://foo.com/baz-domain/invalid |
+---------+------------+---------------------------------------------------------+
+---------+------------+---------------------------------------------------------+
| Domain | Is default | "Not found" redirects |
+---------+------------+---------------------------------------------------------+
| foo.com | Yes | * Base URL: https://foo.com/default/base |
| | | * Regular 404: N/A |
| | | * Invalid short URL: https://foo.com/default/invalid |
+---------+------------+---------------------------------------------------------+
| bar.com | No | * Base URL: N/A |
| | | * Regular 404: N/A |
| | | * Invalid short URL: N/A |
+---------+------------+---------------------------------------------------------+
| baz.com | No | * Base URL: N/A |
| | | * Regular 404: https://foo.com/baz-domain/regular |
| | | * Invalid short URL: https://foo.com/baz-domain/invalid |
+---------+------------+---------------------------------------------------------+
OUTPUT;
OUTPUT;
yield 'no args' => [[], $withoutRedirectsOutput];
yield 'no show redirects' => [['--show-redirects' => false], $withoutRedirectsOutput];
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Integration;
use Exception;
@@ -18,7 +20,7 @@ use Symfony\Component\Console\Command\Command;
class MatomoSendVisitsCommandTest extends TestCase
{
private MockObject & MatomoVisitSenderInterface $visitSender;
private MockObject&MatomoVisitSenderInterface $visitSender;
protected function setUp(): void
{
@@ -57,9 +59,12 @@ class MatomoSendVisitsCommandTest extends TestCase
#[TestWith([false])]
public function canCancelExecutionInInteractiveMode(bool $interactive): void
{
$this->visitSender->expects($this->exactly($interactive ? 0 : 1))->method('sendVisitsInDateRange')->willReturn(
new SendVisitsResult(),
);
$this->visitSender
->expects($this->exactly($interactive ? 0 : 1))
->method('sendVisitsInDateRange')
->willReturn(
new SendVisitsResult(),
);
$this->executeCommand(['n'], ['interactive' => $interactive]);
}
@@ -83,19 +88,22 @@ class MatomoSendVisitsCommandTest extends TestCase
#[Test]
public function printsResultOfSendingVisits(): void
{
$this->visitSender->expects($this->once())->method('sendVisitsInDateRange')->willReturnCallback(
function (DateRange $_, MatomoSendVisitsCommand $command): SendVisitsResult {
// Call it a few times for an easier match of its result in the command putput
$command->success(0);
$command->success(1);
$command->success(2);
$command->error(3, new Exception('Error'));
$command->success(4);
$command->error(5, new Exception('Error'));
$this->visitSender
->expects($this->once())
->method('sendVisitsInDateRange')
->willReturnCallback(
static function (DateRange $_, MatomoSendVisitsCommand $command): SendVisitsResult {
// Call it a few times for an easier match of its result in the command putput
$command->success(0);
$command->success(1);
$command->success(2);
$command->error(3, new Exception('Error'));
$command->success(4);
$command->error(5, new Exception('Error'));
return new SendVisitsResult();
},
);
return new SendVisitsResult();
},
);
[$output] = $this->executeCommand(['y']);
@@ -20,9 +20,9 @@ use Symfony\Component\Console\Tester\CommandTester;
class ManageRedirectRulesCommandTest extends TestCase
{
private ShortUrlResolverInterface & MockObject $shortUrlResolver;
private ShortUrlRedirectRuleServiceInterface & MockObject $ruleService;
private RedirectRuleHandlerInterface & MockObject $ruleHandler;
private ShortUrlResolverInterface&MockObject $shortUrlResolver;
private ShortUrlRedirectRuleServiceInterface&MockObject $ruleService;
private RedirectRuleHandlerInterface&MockObject $ruleHandler;
private CommandTester $commandTester;
protected function setUp(): void
@@ -41,9 +41,13 @@ class ManageRedirectRulesCommandTest extends TestCase
#[Test]
public function errorIsReturnedIfShortUrlCannotBeFound(): void
{
$this->shortUrlResolver->expects($this->once())->method('resolveShortUrl')->with(
ShortUrlIdentifier::fromShortCodeAndDomain('foo'),
)->willThrowException(new ShortUrlNotFoundException(''));
$this->shortUrlResolver
->expects($this->once())
->method('resolveShortUrl')
->with(
ShortUrlIdentifier::fromShortCodeAndDomain('foo'),
)
->willThrowException(new ShortUrlNotFoundException(''));
$this->ruleService->expects($this->never())->method('rulesForShortUrl');
$this->ruleService->expects($this->never())->method('saveRulesForShortUrl');
$this->ruleHandler->expects($this->never())->method('manageRules');
@@ -60,9 +64,13 @@ class ManageRedirectRulesCommandTest extends TestCase
{
$shortUrl = ShortUrl::withLongUrl('https://example.com');
$this->shortUrlResolver->expects($this->once())->method('resolveShortUrl')->with(
ShortUrlIdentifier::fromShortCodeAndDomain('foo'),
)->willReturn($shortUrl);
$this->shortUrlResolver
->expects($this->once())
->method('resolveShortUrl')
->with(
ShortUrlIdentifier::fromShortCodeAndDomain('foo'),
)
->willReturn($shortUrl);
$this->ruleService->expects($this->once())->method('rulesForShortUrl')->with($shortUrl)->willReturn([]);
$this->ruleHandler->expects($this->once())->method('manageRules')->willReturn(null);
$this->ruleService->expects($this->never())->method('saveRulesForShortUrl');
@@ -79,9 +87,13 @@ class ManageRedirectRulesCommandTest extends TestCase
{
$shortUrl = ShortUrl::withLongUrl('https://example.com');
$this->shortUrlResolver->expects($this->once())->method('resolveShortUrl')->with(
ShortUrlIdentifier::fromShortCodeAndDomain('foo'),
)->willReturn($shortUrl);
$this->shortUrlResolver
->expects($this->once())
->method('resolveShortUrl')
->with(
ShortUrlIdentifier::fromShortCodeAndDomain('foo'),
)
->willReturn($shortUrl);
$this->ruleService->expects($this->once())->method('rulesForShortUrl')->with($shortUrl)->willReturn([]);
$this->ruleHandler->expects($this->once())->method('manageRules')->willReturn([]);
$this->ruleService->expects($this->once())->method('saveRulesForShortUrl')->with($shortUrl, []);
@@ -28,8 +28,8 @@ use Symfony\Component\Console\Tester\CommandTester;
class CreateShortUrlCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & UrlShortenerInterface $urlShortener;
private Stub & ShortUrlStringifierInterface $stringifier;
private MockObject&UrlShortenerInterface $urlShortener;
private Stub&ShortUrlStringifierInterface $stringifier;
protected function setUp(): void
{
@@ -49,9 +49,13 @@ class CreateShortUrlCommandTest extends TestCase
public function properShortCodeIsCreatedIfLongUrlIsCorrect(): void
{
$shortUrl = ShortUrl::createFake();
$this->urlShortener->expects($this->once())->method('shorten')->withAnyParameters()->willReturn(
UrlShorteningResult::withoutErrorOnEventDispatching($shortUrl),
);
$this->urlShortener
->expects($this->once())
->method('shorten')
->withAnyParameters()
->willReturn(
UrlShorteningResult::withoutErrorOnEventDispatching($shortUrl),
);
$this->stringifier->method('stringify')->willReturnMap([[$shortUrl, 'stringified_short_url']]);
$this->commandTester->execute([
@@ -69,9 +73,13 @@ class CreateShortUrlCommandTest extends TestCase
public function longUrlIsAskedIfNotProvided(): void
{
$shortUrl = ShortUrl::createFake();
$this->urlShortener->expects($this->once())->method('shorten')->withAnyParameters()->willReturn(
UrlShorteningResult::withoutErrorOnEventDispatching($shortUrl),
);
$this->urlShortener
->expects($this->once())
->method('shorten')
->withAnyParameters()
->willReturn(
UrlShorteningResult::withoutErrorOnEventDispatching($shortUrl),
);
$this->stringifier->method('stringify')->willReturnMap([[$shortUrl, 'stringified_short_url']]);
$this->commandTester->setInputs([$shortUrl->getLongUrl()]);
@@ -81,9 +89,13 @@ class CreateShortUrlCommandTest extends TestCase
#[Test]
public function providingNonUniqueSlugOutputsError(): void
{
$this->urlShortener->expects($this->once())->method('shorten')->withAnyParameters()->willThrowException(
NonUniqueSlugException::fromSlug('my-slug'),
);
$this->urlShortener
->expects($this->once())
->method('shorten')
->withAnyParameters()
->willThrowException(
NonUniqueSlugException::fromSlug('my-slug'),
);
$this->stringifier->method('stringify')->willReturn('');
$this->commandTester->execute(['long-url' => 'http://domain.com/invalid', '--custom-slug' => 'my-slug']);
@@ -97,12 +109,16 @@ class CreateShortUrlCommandTest extends TestCase
public function properlyProcessesProvidedTags(): void
{
$shortUrl = ShortUrl::createFake();
$this->urlShortener->expects($this->once())->method('shorten')->with(
$this->callback(function (ShortUrlCreation $creation) {
Assert::assertEquals(['foo', 'bar', 'baz', 'boo', 'zar'], $creation->tags);
return true;
}),
)->willReturn(UrlShorteningResult::withoutErrorOnEventDispatching($shortUrl));
$this->urlShortener
->expects($this->once())
->method('shorten')
->with(
$this->callback(static function (ShortUrlCreation $creation) {
Assert::assertEquals(['foo', 'bar', 'baz', 'boo', 'zar'], $creation->tags);
return true;
}),
)
->willReturn(UrlShorteningResult::withoutErrorOnEventDispatching($shortUrl));
$this->stringifier->method('stringify')->willReturnMap([[$shortUrl, 'stringified_short_url']]);
$this->commandTester->execute([
@@ -118,12 +134,16 @@ class CreateShortUrlCommandTest extends TestCase
#[Test, DataProvider('provideDomains')]
public function properlyProcessesProvidedDomain(array $input, string|null $expectedDomain): void
{
$this->urlShortener->expects($this->once())->method('shorten')->with(
$this->callback(function (ShortUrlCreation $meta) use ($expectedDomain) {
Assert::assertEquals($expectedDomain, $meta->domain);
return true;
}),
)->willReturn(UrlShorteningResult::withoutErrorOnEventDispatching(ShortUrl::createFake()));
$this->urlShortener
->expects($this->once())
->method('shorten')
->with(
$this->callback(static function (ShortUrlCreation $meta) use ($expectedDomain) {
Assert::assertEquals($expectedDomain, $meta->domain);
return true;
}),
)
->willReturn(UrlShorteningResult::withoutErrorOnEventDispatching(ShortUrl::createFake()));
$this->stringifier->method('stringify')->willReturn('');
$input['long-url'] = 'http://domain.com/foo/bar';
@@ -145,12 +165,16 @@ class CreateShortUrlCommandTest extends TestCase
bool|null $expectedCrawlable,
): void {
$shortUrl = ShortUrl::createFake();
$this->urlShortener->expects($this->once())->method('shorten')->with(
$this->callback(function (ShortUrlCreation $meta) use ($expectedCrawlable) {
Assert::assertEquals($expectedCrawlable, $meta->crawlable);
return true;
}),
)->willReturn(UrlShorteningResult::withoutErrorOnEventDispatching($shortUrl));
$this->urlShortener
->expects($this->once())
->method('shorten')
->with(
$this->callback(static function (ShortUrlCreation $meta) use ($expectedCrawlable) {
Assert::assertEquals($expectedCrawlable, $meta->crawlable);
return true;
}),
)
->willReturn(UrlShorteningResult::withoutErrorOnEventDispatching($shortUrl));
$this->stringifier->method('stringify')->willReturn('');
$options['long-url'] = 'http://domain.com/foo/bar';
@@ -170,9 +194,13 @@ class CreateShortUrlCommandTest extends TestCase
public function warningIsPrintedInVerboseModeWhenDispatchErrors(int $verbosity, callable $assert): void
{
$shortUrl = ShortUrl::createFake();
$this->urlShortener->expects($this->once())->method('shorten')->withAnyParameters()->willReturn(
UrlShorteningResult::withErrorOnEventDispatching($shortUrl, new ServiceNotFoundException()),
);
$this->urlShortener
->expects($this->once())
->method('shorten')
->withAnyParameters()
->willReturn(
UrlShorteningResult::withErrorOnEventDispatching($shortUrl, new ServiceNotFoundException()),
);
$this->stringifier->method('stringify')->willReturn('stringified_short_url');
$this->commandTester->execute(['long-url' => 'http://domain.com/foo/bar'], ['verbosity' => $verbosity]);
@@ -18,7 +18,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class DeleteExpiredShortUrlsCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & DeleteShortUrlServiceInterface $service;
private MockObject&DeleteShortUrlServiceInterface $service;
protected function setUp(): void
{
@@ -22,7 +22,7 @@ use const PHP_EOL;
class DeleteShortUrlCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & DeleteShortUrlServiceInterface $service;
private MockObject&DeleteShortUrlServiceInterface $service;
protected function setUp(): void
{
@@ -34,10 +34,13 @@ class DeleteShortUrlCommandTest extends TestCase
public function successMessageIsPrintedIfUrlIsProperlyDeleted(): void
{
$shortCode = 'abc123';
$this->service->expects($this->once())->method('deleteByShortCode')->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
$this->isFalse(),
);
$this->service
->expects($this->once())
->method('deleteByShortCode')
->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
$this->isFalse(),
);
$this->commandTester->execute(['short-code' => $shortCode]);
$output = $this->commandTester->getDisplay();
@@ -53,10 +56,14 @@ class DeleteShortUrlCommandTest extends TestCase
{
$shortCode = 'abc123';
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode);
$this->service->expects($this->once())->method('deleteByShortCode')->with(
$identifier,
$this->isFalse(),
)->willThrowException(Exception\ShortUrlNotFoundException::fromNotFound($identifier));
$this->service
->expects($this->once())
->method('deleteByShortCode')
->with(
$identifier,
$this->isFalse(),
)
->willThrowException(Exception\ShortUrlNotFoundException::fromNotFound($identifier));
$this->commandTester->execute(['short-code' => $shortCode]);
$output = $this->commandTester->getDisplay();
@@ -75,26 +82,33 @@ class DeleteShortUrlCommandTest extends TestCase
): void {
$shortCode = 'abc123';
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode);
$this->service->expects($this->exactly($expectedDeleteCalls))->method('deleteByShortCode')->with(
$identifier,
$this->isBool(),
)->willReturnCallback(function ($_, bool $ignoreThreshold) use ($shortCode): void {
if (!$ignoreThreshold) {
throw Exception\DeleteShortUrlException::fromVisitsThreshold(
10,
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
);
}
});
$this->service
->expects($this->exactly($expectedDeleteCalls))
->method('deleteByShortCode')
->with(
$identifier,
$this->isBool(),
)
->willReturnCallback(static function ($_, bool $ignoreThreshold) use ($shortCode): void {
if (!$ignoreThreshold) {
throw Exception\DeleteShortUrlException::fromVisitsThreshold(
10,
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
);
}
});
$this->commandTester->setInputs($retryAnswer);
$this->commandTester->execute(['short-code' => $shortCode]);
$output = $this->commandTester->getDisplay();
self::assertStringContainsString(sprintf(
'Impossible to delete short URL with short code "%s", since it has more than "10" visits.',
$shortCode,
), $output);
self::assertStringContainsString(
sprintf(
'Impossible to delete short URL with short code "%s", since it has more than "10" visits.',
$shortCode,
),
$output,
);
self::assertStringContainsString($expectedMessage, $output);
}
@@ -109,22 +123,29 @@ class DeleteShortUrlCommandTest extends TestCase
public function deleteIsNotRetriedWhenThresholdIsReachedAndQuestionIsDeclined(): void
{
$shortCode = 'abc123';
$this->service->expects($this->once())->method('deleteByShortCode')->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
$this->isFalse(),
)->willThrowException(Exception\DeleteShortUrlException::fromVisitsThreshold(
10,
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
));
$this->service
->expects($this->once())
->method('deleteByShortCode')
->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
$this->isFalse(),
)
->willThrowException(Exception\DeleteShortUrlException::fromVisitsThreshold(
10,
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
));
$this->commandTester->setInputs(['no']);
$this->commandTester->execute(['short-code' => $shortCode]);
$output = $this->commandTester->getDisplay();
self::assertStringContainsString(sprintf(
'Impossible to delete short URL with short code "%s", since it has more than "10" visits.',
$shortCode,
), $output);
self::assertStringContainsString(
sprintf(
'Impossible to delete short URL with short code "%s", since it has more than "10" visits.',
$shortCode,
),
$output,
);
self::assertStringContainsString('Short URL was not deleted.', $output);
}
}
@@ -19,7 +19,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class DeleteShortUrlVisitsCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & ShortUrlVisitsDeleterInterface $deleter;
private MockObject&ShortUrlVisitsDeleterInterface $deleter;
protected function setUp(): void
{
@@ -53,9 +53,12 @@ class DeleteShortUrlVisitsCommandTest extends TestCase
#[Test, DataProvider('provideErrorArgs')]
public function warningIsPrintedInCaseOfNotFoundShortUrl(array $args, string $expectedError): void
{
$this->deleter->expects($this->once())->method('deleteShortUrlVisits')->willThrowException(
new ShortUrlNotFoundException(),
);
$this->deleter
->expects($this->once())
->method('deleteShortUrlVisits')
->willThrowException(
new ShortUrlNotFoundException(),
);
$this->commandTester->setInputs(['yes']);
$exitCode = $this->commandTester->execute($args);
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
use CuyZ\Valinor\MapperBuilder;
@@ -22,8 +24,8 @@ use Symfony\Component\Console\Tester\CommandTester;
class EditShortUrlCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & ShortUrlServiceInterface $shortUrlService;
private MockObject & ShortUrlStringifierInterface $stringifier;
private MockObject&ShortUrlServiceInterface $shortUrlService;
private MockObject&ShortUrlStringifierInterface $stringifier;
protected function setUp(): void
{
@@ -38,12 +40,16 @@ class EditShortUrlCommandTest extends TestCase
public function successMessageIsPrintedIfNoErrorOccurs(): void
{
$newLongUrl = 'https://example.com';
$this->shortUrlService->expects($this->once())->method('updateShortUrl')->with(
ShortUrlIdentifier::fromShortCodeAndDomain('foobar'),
$this->callback(static fn (ShortUrlEdition $edition): bool => $edition->longUrl === $newLongUrl),
)->willReturn(
ShortUrl::createFake(),
);
$this->shortUrlService
->expects($this->once())
->method('updateShortUrl')
->with(
ShortUrlIdentifier::fromShortCodeAndDomain('foobar'),
$this->callback(static fn (ShortUrlEdition $edition): bool => $edition->longUrl === $newLongUrl),
)
->willReturn(
ShortUrl::createFake(),
);
$this->stringifier->expects($this->once())->method('stringify')->willReturn('https://s.test/foo');
$this->commandTester->execute(['short-code' => 'foobar', '--long-url' => $newLongUrl]);
@@ -30,7 +30,7 @@ use function Shlinkio\Shlink\Common\buildDateRange;
class GetShortUrlVisitsCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & VisitsStatsHelperInterface $visitsHelper;
private MockObject&VisitsStatsHelperInterface $visitsHelper;
protected function setUp(): void
{
@@ -43,10 +43,14 @@ class GetShortUrlVisitsCommandTest extends TestCase
public function noDateFlagsTriesToListWithoutDateRange(): void
{
$shortCode = 'abc123';
$this->visitsHelper->expects($this->once())->method('visitsForShortUrl')->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
new VisitsParams(DateRange::allTime()),
)->willReturn(new Paginator(new ArrayAdapter([])));
$this->visitsHelper
->expects($this->once())
->method('visitsForShortUrl')
->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
new VisitsParams(DateRange::allTime()),
)
->willReturn(new Paginator(new ArrayAdapter([])));
$this->commandTester->execute(['short-code' => $shortCode]);
}
@@ -55,10 +59,14 @@ class GetShortUrlVisitsCommandTest extends TestCase
public function shortCodeIsAskedIfNotProvided(): void
{
$shortCode = 'abc123';
$this->visitsHelper->expects($this->once())->method('visitsForShortUrl')->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
$this->anything(),
)->willReturn(new Paginator(new ArrayAdapter([])));
$this->visitsHelper
->expects($this->once())
->method('visitsForShortUrl')
->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
$this->anything(),
)
->willReturn(new Paginator(new ArrayAdapter([])));
$this->commandTester->setInputs([$shortCode]);
$this->commandTester->execute([]);
@@ -70,10 +78,14 @@ class GetShortUrlVisitsCommandTest extends TestCase
$shortCode = 'abc123';
$startDate = '2016-01-01';
$endDate = '2016-02-01';
$this->visitsHelper->expects($this->once())->method('visitsForShortUrl')->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
new VisitsParams(buildDateRange(Chronos::parse($startDate), Chronos::parse($endDate))),
)->willReturn(new Paginator(new ArrayAdapter([])));
$this->visitsHelper
->expects($this->once())
->method('visitsForShortUrl')
->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
new VisitsParams(buildDateRange(Chronos::parse($startDate), Chronos::parse($endDate))),
)
->willReturn(new Paginator(new ArrayAdapter([])));
$this->commandTester->execute([
'short-code' => $shortCode,
@@ -92,10 +104,14 @@ class GetShortUrlVisitsCommandTest extends TestCase
VisitLocation::fromLocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')),
);
$shortCode = 'abc123';
$this->visitsHelper->expects($this->once())->method('visitsForShortUrl')->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
$this->anything(),
)->willReturn(new Paginator(new ArrayAdapter([$visit])));
$this->visitsHelper
->expects($this->once())
->method('visitsForShortUrl')
->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
$this->anything(),
)
->willReturn(new Paginator(new ArrayAdapter([$visit])));
$this->commandTester->execute(['short-code' => $shortCode, '--format' => $format->value]);
$output = $this->commandTester->getDisplay();
@@ -107,7 +123,6 @@ class GetShortUrlVisitsCommandTest extends TestCase
{
yield 'regular' => [
VisitsListFormat::FULL,
// phpcs:disable Generic.Files.LineLength
static fn (Chronos $date) => <<<OUTPUT
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
| Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type |
@@ -116,7 +131,6 @@ class GetShortUrlVisitsCommandTest extends TestCase
+---------------------------+---------------+------------+------- Page 1 of 1 --------+--------+-------------+--------------+-----------------+
OUTPUT,
// phpcs:enable
];
yield 'CSV' => [
VisitsListFormat::CSV,
@@ -30,7 +30,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class ListShortUrlsCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & ShortUrlListServiceInterface $shortUrlService;
private MockObject&ShortUrlListServiceInterface $shortUrlService;
protected function setUp(): void
{
@@ -52,8 +52,11 @@ class ListShortUrlsCommandTest extends TestCase
$data[] = ShortUrlWithDeps::fromShortUrl(ShortUrl::withLongUrl('https://url_' . $i));
}
$this->shortUrlService->expects($this->exactly(3))->method('listShortUrls')->withAnyParameters()
->willReturnCallback(fn () => new Paginator(new ArrayAdapter($data)));
$this->shortUrlService
->expects($this->exactly(3))
->method('listShortUrls')
->withAnyParameters()
->willReturnCallback(static fn () => new Paginator(new ArrayAdapter($data)));
$this->commandTester->setInputs(['y', 'y', 'n']);
$this->commandTester->execute([]);
@@ -74,9 +77,13 @@ class ListShortUrlsCommandTest extends TestCase
$data[] = ShortUrlWithDeps::fromShortUrl(ShortUrl::withLongUrl('https://url_' . $i));
}
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(
new ShortUrlsParams(),
)->willReturn(new Paginator(new ArrayAdapter($data)));
$this->shortUrlService
->expects($this->once())
->method('listShortUrls')
->with(
new ShortUrlsParams(),
)
->willReturn(new Paginator(new ArrayAdapter($data)));
$this->commandTester->setInputs(['n']);
$this->commandTester->execute([]);
@@ -95,9 +102,13 @@ class ListShortUrlsCommandTest extends TestCase
public function passingPageWillMakeListStartOnThatPage(): void
{
$page = 5;
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(
new ShortUrlsParams(page: $page),
)->willReturn(new Paginator(new ArrayAdapter([])));
$this->shortUrlService
->expects($this->once())
->method('listShortUrls')
->with(
new ShortUrlsParams(page: $page),
)
->willReturn(new Paginator(new ArrayAdapter([])));
$this->commandTester->setInputs(['y']);
$this->commandTester->execute(['--page' => $page]);
@@ -109,11 +120,15 @@ class ListShortUrlsCommandTest extends TestCase
string $expectedOutput,
ShortUrl $shortUrl,
): void {
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(
new ShortUrlsParams(),
)->willReturn(new Paginator(new ArrayAdapter([
ShortUrlWithDeps::fromShortUrl($shortUrl),
])));
$this->shortUrlService
->expects($this->once())
->method('listShortUrls')
->with(
new ShortUrlsParams(),
)
->willReturn(new Paginator(new ArrayAdapter([
ShortUrlWithDeps::fromShortUrl($shortUrl),
])));
$this->commandTester->setInputs(['y']);
$this->commandTester->execute($input);
@@ -132,74 +147,72 @@ class ListShortUrlsCommandTest extends TestCase
$shortCode = $shortUrl->getShortCode();
$created = $shortUrl->dateCreated()->toAtomString();
// phpcs:disable Generic.Files.LineLength
yield 'tags only' => [
['--show-tags' => true],
<<<OUTPUT
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count | Tags |
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+
| {$shortCode} | | http:/{$shortCode} | https://foo.com | {$created} | 0 | foo, bar, baz |
+------------+-------+-------------+-------------- Page 1 of 1 ------------------+--------------+---------------+
OUTPUT,
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count | Tags |
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+
| {$shortCode} | | http:/{$shortCode} | https://foo.com | {$created} | 0 | foo, bar, baz |
+------------+-------+-------------+-------------- Page 1 of 1 ------------------+--------------+---------------+
OUTPUT,
$shortUrl,
];
yield 'domain only' => [
['--show-domain' => true],
<<<OUTPUT
+------------+-------+-------------+-----------------+---------------------------+--------------+---------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count | Domain |
+------------+-------+-------------+-----------------+---------------------------+--------------+---------+
| {$shortCode} | | http:/{$shortCode} | https://foo.com | {$created} | 0 | DEFAULT |
+------------+-------+-------------+----------- Page 1 of 1 ---------------------+--------------+---------+
OUTPUT,
+------------+-------+-------------+-----------------+---------------------------+--------------+---------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count | Domain |
+------------+-------+-------------+-----------------+---------------------------+--------------+---------+
| {$shortCode} | | http:/{$shortCode} | https://foo.com | {$created} | 0 | DEFAULT |
+------------+-------+-------------+----------- Page 1 of 1 ---------------------+--------------+---------+
OUTPUT,
$shortUrl,
];
yield 'api key only' => [
['--show-api-key' => true],
<<<OUTPUT
+------------+-------+-------------+-----------------+---------------------------+--------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count | API Key Name |
+------------+-------+-------------+-----------------+---------------------------+--------------+--------------+
| {$shortCode} | | http:/{$shortCode} | https://foo.com | {$created} | 0 | my api key |
+------------+-------+-------------+------------- Page 1 of 1 -------------------+--------------+--------------+
OUTPUT,
+------------+-------+-------------+-----------------+---------------------------+--------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count | API Key Name |
+------------+-------+-------------+-----------------+---------------------------+--------------+--------------+
| {$shortCode} | | http:/{$shortCode} | https://foo.com | {$created} | 0 | my api key |
+------------+-------+-------------+------------- Page 1 of 1 -------------------+--------------+--------------+
OUTPUT,
$shortUrl,
];
yield 'tags and api key' => [
['--show-tags' => true, '--show-api-key' => true],
<<<OUTPUT
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count | Tags | API Key Name |
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+--------------+
| {$shortCode} | | http:/{$shortCode} | https://foo.com | {$created} | 0 | foo, bar, baz | my api key |
+------------+-------+-------------+-----------------+--- Page 1 of 1 -----------+--------------+---------------+--------------+
OUTPUT,
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count | Tags | API Key Name |
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+--------------+
| {$shortCode} | | http:/{$shortCode} | https://foo.com | {$created} | 0 | foo, bar, baz | my api key |
+------------+-------+-------------+-----------------+--- Page 1 of 1 -----------+--------------+---------------+--------------+
OUTPUT,
$shortUrl,
];
yield 'tags and domain' => [
['--show-tags' => true, '--show-domain' => true],
<<<OUTPUT
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+---------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count | Tags | Domain |
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+---------+
| {$shortCode} | | http:/{$shortCode} | https://foo.com | {$created} | 0 | foo, bar, baz | DEFAULT |
+------------+-------+-------------+-----------------+- Page 1 of 1 -------------+--------------+---------------+---------+
OUTPUT,
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+---------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count | Tags | Domain |
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+---------+
| {$shortCode} | | http:/{$shortCode} | https://foo.com | {$created} | 0 | foo, bar, baz | DEFAULT |
+------------+-------+-------------+-----------------+- Page 1 of 1 -------------+--------------+---------------+---------+
OUTPUT,
$shortUrl,
];
yield 'all' => [
['--show-tags' => true, '--show-domain' => true, '--show-api-key' => true],
<<<OUTPUT
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+---------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count | Tags | Domain | API Key Name |
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+---------+--------------+
| {$shortCode} | | http:/{$shortCode} | https://foo.com | {$created} | 0 | foo, bar, baz | DEFAULT | my api key |
+------------+-------+-------------+-----------------+-------- Page 1 of 1 ------+--------------+---------------+---------+--------------+
OUTPUT,
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+---------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count | Tags | Domain | API Key Name |
+------------+-------+-------------+-----------------+---------------------------+--------------+---------------+---------+--------------+
| {$shortCode} | | http:/{$shortCode} | https://foo.com | {$created} | 0 | foo, bar, baz | DEFAULT | my api key |
+------------+-------+-------------+-----------------+-------- Page 1 of 1 ------+--------------+---------------+---------+--------------+
OUTPUT,
$shortUrl,
];
// phpcs:enable
}
/**
@@ -235,9 +248,13 @@ class ListShortUrlsCommandTest extends TestCase
$expectedData['excludeTags'] = $excludeTags;
}
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(new ShortUrlsParams(
...$expectedData,
))->willReturn(new Paginator(new ArrayAdapter([])));
$this->shortUrlService
->expects($this->once())
->method('listShortUrls')
->with(new ShortUrlsParams(
...$expectedData,
))
->willReturn(new Paginator(new ArrayAdapter([])));
$this->commandTester->setInputs(['n']);
$this->commandTester->execute($commandArgs);
@@ -310,9 +327,13 @@ class ListShortUrlsCommandTest extends TestCase
#[Test, DataProvider('provideOrderBy')]
public function orderByIsProperlyComputed(array $commandArgs, string|null $expectedOrderBy): void
{
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(new ShortUrlsParams(
orderBy: Ordering::fromOptionalString($expectedOrderBy),
))->willReturn(new Paginator(new ArrayAdapter([])));
$this->shortUrlService
->expects($this->once())
->method('listShortUrls')
->with(new ShortUrlsParams(
orderBy: Ordering::fromOptionalString($expectedOrderBy),
))
->willReturn(new Paginator(new ArrayAdapter([])));
$this->commandTester->setInputs(['n']);
$this->commandTester->execute($commandArgs);
@@ -328,10 +349,14 @@ class ListShortUrlsCommandTest extends TestCase
#[Test]
public function requestingAllElementsWillSetItemsPerPage(): void
{
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(new ShortUrlsParams(
itemsPerPage: Paginator::ALL_ITEMS,
tagsMode: TagsMode::ANY,
))->willReturn(new Paginator(new ArrayAdapter([])));
$this->shortUrlService
->expects($this->once())
->method('listShortUrls')
->with(new ShortUrlsParams(
itemsPerPage: Paginator::ALL_ITEMS,
tagsMode: TagsMode::ANY,
))
->willReturn(new Paginator(new ArrayAdapter([])));
$this->commandTester->execute(['--all' => true]);
}
@@ -22,7 +22,7 @@ use const PHP_EOL;
class ResolveUrlCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & ShortUrlResolverInterface $urlResolver;
private MockObject&ShortUrlResolverInterface $urlResolver;
protected function setUp(): void
{
@@ -36,9 +36,13 @@ class ResolveUrlCommandTest extends TestCase
$shortCode = 'abc123';
$expectedUrl = 'http://domain.com/foo/bar';
$shortUrl = ShortUrl::withLongUrl($expectedUrl);
$this->urlResolver->expects($this->once())->method('resolveShortUrl')->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
)->willReturn($shortUrl);
$this->urlResolver
->expects($this->once())
->method('resolveShortUrl')
->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
)
->willReturn($shortUrl);
$this->commandTester->execute(['short-code' => $shortCode]);
$output = $this->commandTester->getDisplay();
@@ -50,9 +54,13 @@ class ResolveUrlCommandTest extends TestCase
{
$shortCode = 'abc123';
$shortUrl = ShortUrl::createFake();
$this->urlResolver->expects($this->once())->method('resolveShortUrl')->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
)->willReturn($shortUrl);
$this->urlResolver
->expects($this->once())
->method('resolveShortUrl')
->with(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
)
->willReturn($shortUrl);
$this->commandTester->setInputs([$shortCode]);
$this->commandTester->execute([]);
@@ -64,9 +72,13 @@ class ResolveUrlCommandTest extends TestCase
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain('abc123');
$shortCode = $identifier->shortCode;
$this->urlResolver->expects($this->once())->method('resolveShortUrl')->with($identifier)->willThrowException(
ShortUrlNotFoundException::fromNotFound($identifier),
);
$this->urlResolver
->expects($this->once())
->method('resolveShortUrl')
->with($identifier)
->willThrowException(
ShortUrlNotFoundException::fromNotFound($identifier),
);
$this->commandTester->execute(['short-code' => $shortCode]);
$output = $this->commandTester->getDisplay();
@@ -15,7 +15,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class DeleteTagsCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & TagServiceInterface $tagService;
private MockObject&TagServiceInterface $tagService;
protected function setUp(): void
{
@@ -23,7 +23,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class GetTagVisitsCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & VisitsStatsHelperInterface $visitsHelper;
private MockObject&VisitsStatsHelperInterface $visitsHelper;
protected function setUp(): void
{
@@ -39,25 +39,27 @@ class GetTagVisitsCommandTest extends TestCase
VisitLocation::fromLocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')),
);
$tag = 'abc123';
$this->visitsHelper->expects($this->once())->method('visitsForTag')->with($tag, $this->anything())->willReturn(
new Paginator(new ArrayAdapter([$visit])),
);
$this->visitsHelper
->expects($this->once())
->method('visitsForTag')
->with($tag, $this->anything())
->willReturn(
new Paginator(new ArrayAdapter([$visit])),
);
$this->commandTester->execute(['tag' => $tag]);
$output = $this->commandTester->getDisplay();
$type = VisitType::VALID_SHORT_URL->value;
self::assertEquals(
// phpcs:disable Generic.Files.LineLength
<<<OUTPUT
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
| Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type |
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
| {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} |
+---------------------------+---------------+------------+------- Page 1 of 1 --------+--------+-------------+--------------+-----------------+
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
| Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type |
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
| {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} |
+---------------------------+---------------+------------+------- Page 1 of 1 --------+--------+-------------+--------------+-----------------+
OUTPUT,
// phpcs:enable
OUTPUT,
$output,
);
}
@@ -18,7 +18,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class ListTagsCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & TagServiceInterface $tagService;
private MockObject&TagServiceInterface $tagService;
protected function setUp(): void
{
@@ -29,9 +29,13 @@ class ListTagsCommandTest extends TestCase
#[Test]
public function noTagsPrintsEmptyMessage(): void
{
$this->tagService->expects($this->once())->method('tagsInfo')->withAnyParameters()->willReturn(
new Paginator(new ArrayAdapter([])),
);
$this->tagService
->expects($this->once())
->method('tagsInfo')
->withAnyParameters()
->willReturn(
new Paginator(new ArrayAdapter([])),
);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
@@ -42,26 +46,30 @@ class ListTagsCommandTest extends TestCase
#[Test]
public function listOfTagsIsPrinted(): void
{
$this->tagService->expects($this->once())->method('tagsInfo')->withAnyParameters()->willReturn(
new Paginator(new ArrayAdapter([
new TagInfo('foo', 10, 2),
new TagInfo('bar', 7, 32),
])),
);
$this->tagService
->expects($this->once())
->method('tagsInfo')
->withAnyParameters()
->willReturn(
new Paginator(new ArrayAdapter([
new TagInfo('foo', 10, 2),
new TagInfo('bar', 7, 32),
])),
);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
self::assertEquals(
<<<OUTPUT
+------+-------------+---------------+
| Name | URLs amount | Visits amount |
+------+-------------+---------------+
| foo | 10 | 2 |
| bar | 7 | 32 |
+------+-------------+---------------+
OUTPUT,
+------+-------------+---------------+
| Name | URLs amount | Visits amount |
+------+-------------+---------------+
| foo | 10 | 2 |
| bar | 7 | 32 |
+------+-------------+---------------+
OUTPUT,
$output,
);
}
@@ -18,7 +18,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class RenameTagCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & TagServiceInterface $tagService;
private MockObject&TagServiceInterface $tagService;
protected function setUp(): void
{
@@ -31,9 +31,13 @@ class RenameTagCommandTest extends TestCase
{
$oldName = 'foo';
$newName = 'bar';
$this->tagService->expects($this->once())->method('renameTag')->with(
Renaming::fromNames($oldName, $newName),
)->willThrowException(TagNotFoundException::fromTag('foo'));
$this->tagService
->expects($this->once())
->method('renameTag')
->with(
Renaming::fromNames($oldName, $newName),
)
->willThrowException(TagNotFoundException::fromTag('foo'));
$this->commandTester->execute([
'old-name' => $oldName,
@@ -49,9 +53,13 @@ class RenameTagCommandTest extends TestCase
{
$oldName = 'foo';
$newName = 'bar';
$this->tagService->expects($this->once())->method('renameTag')->with(
Renaming::fromNames($oldName, $newName),
)->willReturn(new Tag($newName));
$this->tagService
->expects($this->once())
->method('renameTag')
->with(
Renaming::fromNames($oldName, $newName),
)
->willReturn(new Tag($newName));
$this->commandTester->execute([
'old-name' => $oldName,
@@ -17,7 +17,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class DeleteOrphanVisitsCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & VisitsDeleterInterface $deleter;
private MockObject&VisitsDeleterInterface $deleter;
protected function setUp(): void
{
@@ -23,7 +23,7 @@ use function sprintf;
class DownloadGeoLiteDbCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & GeolocationDbUpdaterInterface $dbUpdater;
private MockObject&GeolocationDbUpdaterInterface $dbUpdater;
protected function setUp(): void
{
@@ -37,16 +37,20 @@ class DownloadGeoLiteDbCommandTest extends TestCase
string $expectedMessage,
int $expectedExitCode,
): void {
$this->dbUpdater->expects($this->once())->method('checkDbUpdate')->withAnyParameters()->willReturnCallback(
function (GeolocationDownloadProgressHandlerInterface $handler) use ($olderDbExists): void {
$handler->beforeDownload($olderDbExists);
$handler->handleProgress(100, 50, $olderDbExists);
$this->dbUpdater
->expects($this->once())
->method('checkDbUpdate')
->withAnyParameters()
->willReturnCallback(
static function (GeolocationDownloadProgressHandlerInterface $handler) use ($olderDbExists): void {
$handler->beforeDownload($olderDbExists);
$handler->handleProgress(100, 50, $olderDbExists);
throw $olderDbExists
? GeolocationDbUpdateFailedException::withOlderDb()
: GeolocationDbUpdateFailedException::withoutOlderDb();
},
);
throw $olderDbExists
? GeolocationDbUpdateFailedException::withOlderDb()
: GeolocationDbUpdateFailedException::withoutOlderDb();
},
);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
@@ -93,9 +97,13 @@ class DownloadGeoLiteDbCommandTest extends TestCase
#[Test, DataProvider('provideSuccessParams')]
public function printsExpectedMessageWhenNoErrorOccurs(callable $checkUpdateBehavior, string $expectedMessage): void
{
$this->dbUpdater->expects($this->once())->method('checkDbUpdate')->withAnyParameters()->willReturnCallback(
$checkUpdateBehavior,
);
$this->dbUpdater
->expects($this->once())
->method('checkDbUpdate')
->withAnyParameters()
->willReturnCallback(
$checkUpdateBehavior,
);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
@@ -107,10 +115,16 @@ class DownloadGeoLiteDbCommandTest extends TestCase
public static function provideSuccessParams(): iterable
{
yield 'up to date db' => [fn () => GeolocationResult::CHECK_SKIPPED, '[INFO] GeoLite2 db file is up to date.'];
yield 'outdated db' => [function (GeolocationDownloadProgressHandlerInterface $handler): GeolocationResult {
$handler->beforeDownload(true);
return GeolocationResult::DB_CREATED;
}, '[OK] GeoLite2 db file properly downloaded.'];
yield 'up to date db' => [
static fn () => GeolocationResult::CHECK_SKIPPED,
'[INFO] GeoLite2 db file is up to date.',
];
yield 'outdated db' => [
static function (GeolocationDownloadProgressHandlerInterface $handler): GeolocationResult {
$handler->beforeDownload(true);
return GeolocationResult::DB_CREATED;
},
'[OK] GeoLite2 db file properly downloaded.',
];
}
}
@@ -23,7 +23,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class GetNonOrphanVisitsCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & VisitsStatsHelperInterface $visitsHelper;
private MockObject&VisitsStatsHelperInterface $visitsHelper;
protected function setUp(): void
{
@@ -38,25 +38,27 @@ class GetNonOrphanVisitsCommandTest extends TestCase
$visit = Visit::forValidShortUrl($shortUrl, Visitor::fromParams('bar', 'foo', ''))->locate(
VisitLocation::fromLocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')),
);
$this->visitsHelper->expects($this->once())->method('nonOrphanVisits')->withAnyParameters()->willReturn(
new Paginator(new ArrayAdapter([$visit])),
);
$this->visitsHelper
->expects($this->once())
->method('nonOrphanVisits')
->withAnyParameters()
->willReturn(
new Paginator(new ArrayAdapter([$visit])),
);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$type = VisitType::VALID_SHORT_URL->value;
self::assertEquals(
// phpcs:disable Generic.Files.LineLength
<<<OUTPUT
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
| Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type |
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
| {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} |
+---------------------------+---------------+------------+------- Page 1 of 1 --------+--------+-------------+--------------+-----------------+
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
| Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type |
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
| {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} |
+---------------------------+---------------+------------+------- Page 1 of 1 --------+--------+-------------+--------------+-----------------+
OUTPUT,
// phpcs:enable
OUTPUT,
$output,
);
}
@@ -24,7 +24,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class GetOrphanVisitsCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & VisitsStatsHelperInterface $visitsHelper;
private MockObject&VisitsStatsHelperInterface $visitsHelper;
protected function setUp(): void
{
@@ -40,27 +40,30 @@ class GetOrphanVisitsCommandTest extends TestCase
$visit = Visit::forBasePath(Visitor::fromParams('bar', 'foo', ''))->locate(
VisitLocation::fromLocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')),
);
$this->visitsHelper->expects($this->once())->method('orphanVisits')->with($this->callback(
fn (OrphanVisitsParams $param) => (
($includesType && $param->type !== null) || (!$includesType && $param->type === null)
),
))->willReturn(new Paginator(new ArrayAdapter([$visit])));
$this->visitsHelper
->expects($this->once())
->method('orphanVisits')
->with($this->callback(
static fn (OrphanVisitsParams $param) => (
($includesType && $param->type !== null)
|| (!$includesType && $param->type === null)
),
))
->willReturn(new Paginator(new ArrayAdapter([$visit])));
$this->commandTester->execute($args);
$output = $this->commandTester->getDisplay();
$type = OrphanVisitType::BASE_URL->value;
self::assertEquals(
// phpcs:disable Generic.Files.LineLength
<<<OUTPUT
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+----------+
| Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type |
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+----------+
| {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} |
+---------------------------+---------------+------------+--- Page 1 of 1 ---+--------+--------+-------------+--------------+----------+
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+----------+
| Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type |
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+----------+
| {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} |
+---------------------------+---------------+------------+--- Page 1 of 1 ---+--------+--------+-------------+--------------+----------+
OUTPUT,
// phpcs:enable
OUTPUT,
$output,
);
}
@@ -37,10 +37,10 @@ use const PHP_EOL;
class LocateVisitsCommandTest extends TestCase
{
private CommandTester $commandTester;
private MockObject & VisitLocatorInterface $visitService;
private MockObject & VisitToLocationHelperInterface $visitToLocation;
private Stub & Lock\LockInterface $lock;
private Stub & Command $downloadDbCommand;
private MockObject&VisitLocatorInterface $visitService;
private MockObject&VisitToLocationHelperInterface $visitToLocation;
private Stub&Lock\LockInterface $lock;
private Stub&Command $downloadDbCommand;
protected function setUp(): void
{
@@ -70,21 +70,28 @@ class LocateVisitsCommandTest extends TestCase
$mockMethodBehavior = $this->invokeHelperMethods($visit, $location);
$this->lock->method('acquire')->willReturn(true);
$this->visitService->expects($this->exactly($expectedUnlocatedCalls))
->method('locateUnlocatedVisits')
->withAnyParameters()
->willReturnCallback($mockMethodBehavior);
$this->visitService->expects($this->exactly($expectedEmptyCalls))
->method('locateVisitsWithEmptyLocation')
->withAnyParameters()
->willReturnCallback($mockMethodBehavior);
$this->visitService->expects($this->exactly($expectedAllCalls))
->method('locateAllVisits')
->withAnyParameters()
->willReturnCallback($mockMethodBehavior);
$this->visitToLocation->expects(
$this->exactly($expectedUnlocatedCalls + $expectedEmptyCalls + $expectedAllCalls),
)->method('resolveVisitLocation')->withAnyParameters()->willReturn(Location::empty());
$this->visitService
->expects($this->exactly($expectedUnlocatedCalls))
->method('locateUnlocatedVisits')
->withAnyParameters()
->willReturnCallback($mockMethodBehavior);
$this->visitService
->expects($this->exactly($expectedEmptyCalls))
->method('locateVisitsWithEmptyLocation')
->withAnyParameters()
->willReturnCallback($mockMethodBehavior);
$this->visitService
->expects($this->exactly($expectedAllCalls))
->method('locateAllVisits')
->withAnyParameters()
->willReturnCallback($mockMethodBehavior);
$this->visitToLocation
->expects(
$this->exactly($expectedUnlocatedCalls + $expectedEmptyCalls + $expectedAllCalls),
)
->method('resolveVisitLocation')
->withAnyParameters()
->willReturn(Location::empty());
$this->downloadDbCommand->method('run')->willReturn(Command::SUCCESS);
$this->commandTester->setInputs(['y']);
@@ -113,10 +120,11 @@ class LocateVisitsCommandTest extends TestCase
$location = VisitLocation::fromLocation(Location::empty());
$this->lock->method('acquire')->willReturn(true);
$this->visitService->expects($this->once())
->method('locateUnlocatedVisits')
->withAnyParameters()
->willReturnCallback($this->invokeHelperMethods($visit, $location));
$this->visitService
->expects($this->once())
->method('locateUnlocatedVisits')
->withAnyParameters()
->willReturnCallback($this->invokeHelperMethods($visit, $location));
$this->visitToLocation->expects($this->once())->method('resolveVisitLocation')->willThrowException($e);
$this->downloadDbCommand->method('run')->willReturn(Command::SUCCESS);
@@ -140,13 +148,17 @@ class LocateVisitsCommandTest extends TestCase
$location = VisitLocation::fromLocation(Location::empty());
$this->lock->method('acquire')->willReturn(true);
$this->visitService->expects($this->once())
->method('locateUnlocatedVisits')
->withAnyParameters()
->willReturnCallback($this->invokeHelperMethods($visit, $location));
$this->visitToLocation->expects($this->once())->method('resolveVisitLocation')->willThrowException(
IpCannotBeLocatedException::forError(WrongIpException::fromIpAddress('1.2.3.4')),
);
$this->visitService
->expects($this->once())
->method('locateUnlocatedVisits')
->withAnyParameters()
->willReturnCallback($this->invokeHelperMethods($visit, $location));
$this->visitToLocation
->expects($this->once())
->method('resolveVisitLocation')
->willThrowException(
IpCannotBeLocatedException::forError(WrongIpException::fromIpAddress('1.2.3.4')),
);
$this->downloadDbCommand->method('run')->willReturn(Command::SUCCESS);
$this->commandTester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
@@ -18,10 +18,13 @@ class InvalidRoleConfigExceptionTest extends TestCase
{
$e = InvalidRoleConfigException::forDomainOnlyWithDefaultDomain();
self::assertEquals(sprintf(
'You cannot create an API key with the "%s" role attached to the default domain. '
. 'The role is currently limited to non-default domains.',
Role::DOMAIN_SPECIFIC->value,
), $e->getMessage());
self::assertEquals(
sprintf(
'You cannot create an API key with the "%s" role attached to the default domain. '
. 'The role is currently limited to non-default domains.',
Role::DOMAIN_SPECIFIC->value,
),
$e->getMessage(),
);
}
}
@@ -42,11 +42,13 @@ class ApplicationFactoryTest extends TestCase
private function createServiceManager(array $config = []): ServiceManager
{
return new ServiceManager(['services' => [
'config' => [
'cli' => $config,
return new ServiceManager([
'services' => [
'config' => [
'cli' => $config,
],
AppOptions::class => new AppOptions(),
],
AppOptions::class => new AppOptions(),
]]);
]);
}
}
@@ -27,7 +27,7 @@ use function sprintf;
class RedirectRuleHandlerTest extends TestCase
{
private RedirectRuleHandler $handler;
private StyleInterface & MockObject $io;
private StyleInterface&MockObject $io;
private ShortUrl $shortUrl;
private RedirectCondition $cond1;
private RedirectCondition $cond2;
@@ -57,7 +57,7 @@ class RedirectRuleHandlerTest extends TestCase
$this->handler = new RedirectRuleHandler();
}
#[Test, DataProvider('provideExitActions')]
#[Test, DataProvider('provideExitActions')]
public function commentIsDisplayedWhenRulesListIsEmpty(
RedirectRuleHandlerAction $action,
array|null $expectedResult,
@@ -77,24 +77,27 @@ class RedirectRuleHandlerTest extends TestCase
RedirectRuleHandlerAction $action,
array|null $_,
): void {
$comment = fn (string $value) => sprintf('<comment>%s</comment>', $value);
$comment = static fn (string $value) => sprintf('<comment>%s</comment>', $value);
$this->io->expects($this->once())->method('choice')->willReturn($action->value);
$this->io->expects($this->never())->method('newLine');
$this->io->expects($this->never())->method('text');
$this->io->expects($this->once())->method('table')->with($this->isArray(), [
['1', $comment($this->cond1->toHumanFriendly()), 'https://example.com/one'],
[
'2',
$comment($this->cond2->toHumanFriendly()) . ' AND ' . $comment($this->cond3->toHumanFriendly()),
'https://example.com/two',
],
[
'3',
$comment($this->cond1->toHumanFriendly()) . ' AND ' . $comment($this->cond3->toHumanFriendly()),
'https://example.com/three',
],
]);
$this->io
->expects($this->once())
->method('table')
->with($this->isArray(), [
['1', $comment($this->cond1->toHumanFriendly()), 'https://example.com/one'],
[
'2',
$comment($this->cond2->toHumanFriendly()) . ' AND ' . $comment($this->cond3->toHumanFriendly()),
'https://example.com/two',
],
[
'3',
$comment($this->cond1->toHumanFriendly()) . ' AND ' . $comment($this->cond3->toHumanFriendly()),
'https://example.com/three',
],
]);
$this->handler->manageRules($this->io, $this->shortUrl, $this->rules);
}
@@ -114,54 +117,68 @@ class RedirectRuleHandlerTest extends TestCase
array $expectedConditions,
bool $continue = false,
): void {
$this->io->method('ask')->willReturnCallback(
fn (string $message): string|int => match ($message) {
'Rule priority (the lower the value, the higher the priority)' => 2, // Add in between existing rules
'Long URL to redirect when the rule matches' => 'https://example.com/new-two',
'Language to match?' => 'en-US',
'Query param name?' => 'foo',
'Query param value?' => 'bar',
'IP address, CIDR block or wildcard-pattern (1.2.*.*)' => '1.2.3.4',
'Country code to match?' => 'FR',
'City name to match?' => 'Los angeles',
'Date to match?' => '2016-05-01T20:34:16+02:00',
default => '',
},
);
$this->io->method('choice')->willReturnCallback(
function (string $message) use (&$callIndex, $type): string {
$callIndex++;
$this->io
->method('ask')
->willReturnCallback(
static fn (string $message): string|int => match ($message) {
'Rule priority (the lower the value, the higher the priority)' => 2, // Add in between existing rules
'Long URL to redirect when the rule matches' => 'https://example.com/new-two',
'Language to match?' => 'en-US',
'Query param name?' => 'foo',
'Query param value?' => 'bar',
'IP address, CIDR block or wildcard-pattern (1.2.*.*)' => '1.2.3.4',
'Country code to match?' => 'FR',
'City name to match?' => 'Los angeles',
'Date to match?' => '2016-05-01T20:34:16+02:00',
default => '',
},
);
$this->io
->method('choice')
->willReturnCallback(
static function (string $message) use (&$callIndex, $type): string {
$callIndex++;
if ($message === 'Type of the condition?') {
return $type->value;
} elseif ($message === 'Device to match?') {
return DeviceType::ANDROID->value;
} elseif ($message === 'Browser to match?') {
return Browser::CHROME->value;
}
if ($message === 'Type of the condition?') {
return $type->value;
} elseif ($message === 'Device to match?') {
return DeviceType::ANDROID->value;
} elseif ($message === 'Browser to match?') {
return Browser::CHROME->value;
}
// First we select remove action to trigger code branch, then save to finish execution
$action = $callIndex === 1 ? RedirectRuleHandlerAction::ADD : RedirectRuleHandlerAction::SAVE;
return $action->value;
},
);
// First we select remove action to trigger code branch, then save to finish execution
$action = $callIndex === 1 ? RedirectRuleHandlerAction::ADD : RedirectRuleHandlerAction::SAVE;
return $action->value;
},
);
$continueCallCount = 0;
$this->io->method('confirm')->willReturnCallback(function () use (&$continueCallCount, $continue) {
$continueCallCount++;
return $continueCallCount < 2 && $continue;
});
$this->io
->method('confirm')
->willReturnCallback(static function () use (&$continueCallCount, $continue) {
$continueCallCount++;
return $continueCallCount < 2 && $continue;
});
$result = $this->handler->manageRules($this->io, $this->shortUrl, $this->rules);
self::assertEquals([
$this->rules[0],
new ShortUrlRedirectRule($this->shortUrl, 2, 'https://example.com/new-two', new ArrayCollection(
$expectedConditions,
)),
$this->rules[1],
$this->rules[2],
], $result);
self::assertEquals(
[
$this->rules[0],
new ShortUrlRedirectRule(
$this->shortUrl,
2,
'https://example.com/new-two',
new ArrayCollection(
$expectedConditions,
),
),
$this->rules[1],
$this->rules[2],
],
$result,
);
}
public static function provideDeviceConditions(): iterable
@@ -206,19 +223,22 @@ class RedirectRuleHandlerTest extends TestCase
public function existingRulesCanBeRemoved(): void
{
$callIndex = 0;
$this->io->expects($this->exactly(3))->method('choice')->willReturnCallback(
function (string $message) use (&$callIndex): string {
$callIndex++;
$this->io
->expects($this->exactly(3))
->method('choice')
->willReturnCallback(
static function (string $message) use (&$callIndex): string {
$callIndex++;
if ($message === 'What rule do you want to delete?') {
return '2 - https://example.com/two'; // Second rule to be removed
}
if ($message === 'What rule do you want to delete?') {
return '2 - https://example.com/two'; // Second rule to be removed
}
// First we select remove action to trigger code branch, then save to finish execution
$action = $callIndex === 1 ? RedirectRuleHandlerAction::REMOVE : RedirectRuleHandlerAction::SAVE;
return $action->value;
},
);
// First we select remove action to trigger code branch, then save to finish execution
$action = $callIndex === 1 ? RedirectRuleHandlerAction::REMOVE : RedirectRuleHandlerAction::SAVE;
return $action->value;
},
);
$this->io->expects($this->never())->method('warning');
$result = $this->handler->manageRules($this->io, $this->shortUrl, $this->rules);
@@ -230,13 +250,16 @@ class RedirectRuleHandlerTest extends TestCase
public function warningIsPrintedWhenTryingToRemoveRuleFromEmptyList(): void
{
$callIndex = 0;
$this->io->expects($this->exactly(2))->method('choice')->willReturnCallback(
function () use (&$callIndex): string {
$callIndex++;
$action = $callIndex === 1 ? RedirectRuleHandlerAction::REMOVE : RedirectRuleHandlerAction::DISCARD;
return $action->value;
},
);
$this->io
->expects($this->exactly(2))
->method('choice')
->willReturnCallback(
static function () use (&$callIndex): string {
$callIndex++;
$action = $callIndex === 1 ? RedirectRuleHandlerAction::REMOVE : RedirectRuleHandlerAction::DISCARD;
return $action->value;
},
);
$this->io->expects($this->once())->method('warning')->with('There are no rules to remove');
$this->handler->manageRules($this->io, $this->shortUrl, []);
@@ -245,25 +268,32 @@ class RedirectRuleHandlerTest extends TestCase
#[Test]
public function existingRulesCanBeReArranged(): void
{
$this->io->method('ask')->willReturnCallback(
fn (string $message): string|int => match ($message) {
'Rule priority (the lower the value, the higher the priority)' => 1,
default => '',
},
);
$this->io->expects($this->exactly(3))->method('choice')->willReturnCallback(
function (string $message) use (&$callIndex): string {
$callIndex++;
$this->io
->method('ask')
->willReturnCallback(
static fn (string $message): string|int => match ($message) {
'Rule priority (the lower the value, the higher the priority)' => 1,
default => '',
},
);
$this->io
->expects($this->exactly(3))
->method('choice')
->willReturnCallback(
static function (string $message) use (&$callIndex): string {
$callIndex++;
if ($message === 'What rule do you want to re-arrange?') {
return '2 - https://example.com/two'; // Second rule to be re-arrange
}
if ($message === 'What rule do you want to re-arrange?') {
return '2 - https://example.com/two'; // Second rule to be re-arrange
}
// First we select remove action to trigger code branch, then save to finish execution
$action = $callIndex === 1 ? RedirectRuleHandlerAction::RE_ARRANGE : RedirectRuleHandlerAction::SAVE;
return $action->value;
},
);
// First we select remove action to trigger code branch, then save to finish execution
$action = $callIndex === 1
? RedirectRuleHandlerAction::RE_ARRANGE
: RedirectRuleHandlerAction::SAVE;
return $action->value;
},
);
$this->io->expects($this->never())->method('warning');
$result = $this->handler->manageRules($this->io, $this->shortUrl, $this->rules);
@@ -275,13 +305,18 @@ class RedirectRuleHandlerTest extends TestCase
public function warningIsPrintedWhenTryingToReArrangeRuleFromEmptyList(): void
{
$callIndex = 0;
$this->io->expects($this->exactly(2))->method('choice')->willReturnCallback(
function () use (&$callIndex): string {
$callIndex++;
$action = $callIndex === 1 ? RedirectRuleHandlerAction::RE_ARRANGE : RedirectRuleHandlerAction::DISCARD;
return $action->value;
},
);
$this->io
->expects($this->exactly(2))
->method('choice')
->willReturnCallback(
static function () use (&$callIndex): string {
$callIndex++;
$action = $callIndex === 1
? RedirectRuleHandlerAction::RE_ARRANGE
: RedirectRuleHandlerAction::DISCARD;
return $action->value;
},
);
$this->io->expects($this->once())->method('warning')->with('There are no rules to re-arrange');
$this->handler->manageRules($this->io, $this->shortUrl, []);
+1 -1
View File
@@ -13,7 +13,7 @@ use Symfony\Component\Console\Tester\CommandTester;
class CliTestUtils
{
public static function createCommandStub(string $name): Stub & Command
public static function createCommandStub(string $name): Stub&Command
{
static $generator = null;
@@ -15,8 +15,8 @@ use Symfony\Component\Process\PhpExecutableFinder;
class PhpProcessRunnerTest extends TestCase
{
private MockObject & ProcessRunnerInterface $wrapped;
private MockObject & PhpExecutableFinder $executableFinder;
private MockObject&ProcessRunnerInterface $wrapped;
private MockObject&PhpExecutableFinder $executableFinder;
protected function setUp(): void
{
+11 -8
View File
@@ -17,10 +17,10 @@ use Symfony\Component\Process\Process;
class ProcessRunnerTest extends TestCase
{
private ProcessRunner $runner;
private MockObject & ProcessHelper $helper;
private MockObject & DebugFormatterHelper $formatter;
private MockObject & Process $process;
private MockObject & OutputInterface $output;
private MockObject&ProcessHelper $helper;
private MockObject&DebugFormatterHelper $formatter;
private MockObject&Process $process;
private MockObject&OutputInterface $output;
protected function setUp(): void
{
@@ -76,10 +76,13 @@ class ProcessRunnerTest extends TestCase
$this->process->expects($this->once())->method('mustRun')->withAnyParameters()->willReturnSelf();
$this->process->expects($this->never())->method('isSuccessful');
$this->process->expects($this->never())->method('getCommandLine');
$this->helper->expects($this->once())->method('wrapCallback')->withAnyParameters()->willReturn(
function (): void {
},
);
$this->helper
->expects($this->once())
->method('wrapCallback')
->withAnyParameters()
->willReturn(
static function (): void {},
);
$this->formatter->expects($this->never())->method('start');
$this->formatter->expects($this->never())->method('stop');
+6 -3
View File
@@ -23,9 +23,12 @@ class ShlinkTableTest extends TestCase
$footerTitle = 'Footer';
$baseTable = $this->createMock(Table::class);
$baseTable->expects($this->once())->method('setStyle')->with(
$this->isInstanceOf(TableStyle::class),
)->willReturnSelf();
$baseTable->expects($this->once())
->method('setStyle')
->with(
$this->isInstanceOf(TableStyle::class),
)
->willReturnSelf();
$baseTable->expects($this->once())->method('setHeaders')->with($headers)->willReturnSelf();
$baseTable->expects($this->once())->method('setRows')->with($rows)->willReturnSelf();
$baseTable->expects($this->once())->method('setFooterTitle')->with($footerTitle)->willReturnSelf();
@@ -20,9 +20,7 @@ use Symfony\Component\Lock;
use const Shlinkio\Shlink\LOCAL_LOCK_FACTORY;
return [
'dependencies' => [
'delegators' => [
MapperBuilder::class => [
ObjectMapper\MapperBuilderWithCacheDelegatorFactory::class,
@@ -270,5 +268,4 @@ return [
Crawling\CrawlingHelper::class => [ShortUrl\Repository\CrawlableShortCodesQuery::class],
],
];
Loaded 100 of 453 files, more files were not shown because too many files have changed in this diff. Show more