mirror of
https://github.com/shlinkio/shlink.git
synced 2025-03-14 04:00:57 +03:00
Merge pull request #1279 from acelaya-forks/feature/remove-deprecated-stuff
Feature/remove deprecated stuff
This commit is contained in:
commit
04cf1aed9c
57 changed files with 131 additions and 1511 deletions
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* [#1275](https://github.com/shlinkio/shlink/issues/1275) Removed everything that was deprecated.
|
||||
|
||||
See [UPGRADE](UPGRADE.md#from-v2x-to-v3x) doc in order to get details on how to migrate to this version.
|
||||
|
||||
### Fixed
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## [2.10.0] - 2021-12-12
|
||||
### Added
|
||||
* [#1163](https://github.com/shlinkio/shlink/issues/1163) Allowed setting not-found redirects for default domain in the same way it's done for any other domain.
|
||||
|
@ -852,7 +871,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
|||
* Preview generation feature completely removed.
|
||||
* Authentication against REST API using JWT is no longer supported.
|
||||
|
||||
See [UPGRADE](UPGRADE.md) doc in order to get details on how to migrate to this version.
|
||||
See [UPGRADE](UPGRADE.md#from-v1x-to-v2x) doc in order to get details on how to migrate to this version.
|
||||
|
||||
### Fixed
|
||||
* [#600](https://github.com/shlinkio/shlink/issues/600) Fixed health action so that it works with and without version in the path.
|
||||
|
|
39
UPGRADE.md
39
UPGRADE.md
|
@ -1,5 +1,44 @@
|
|||
# Upgrading
|
||||
|
||||
## From v2.x to v3.x
|
||||
|
||||
### Changes in REST API
|
||||
|
||||
* The `type` property returned when trying to delete a URL that reached the visits threshold, when using the `DELETE /short-urls/{shortCode}` endpoint, is now `INVALID_SHORT_URL_DELETION` instead pf `INVALID_SHORTCODE_DELETION`.
|
||||
* The `INVALID_AUTHORIZATION` error no longer includes the `expectedTypes` property. Use `expectedHeaders` one instead.
|
||||
* The `GET /rest/v2/short-urls` endpoint no longer allows ordering by `visitsCount`, `visitCount` or `originalUrl`. Use `visits` to replace the first two, and `longUrl` to replace the last one.
|
||||
* The `GET /rest/v2/short-urls` endpoint no longer allows providing the ordering params with array notation, as in `/shortUrls?orderBy[longUrl]=DESC`. Instead, use the following notation `/shortUrls?orderBy?longUrl-DESC`.
|
||||
* Requests expecting a body no longer support url-encoded payloads. Instead, always use JSON bodies.
|
||||
* The next endpoints have been removed:
|
||||
* `PUT /rest/v2/short-urls/{shortCode}/tags`: Use the `PATCH /rest/v2/short-urls/{shortCode}` endpoint to set the short URL tags.
|
||||
* `POST /rest/v2/tags`: Use `POST /rest/v2/short-urls` or `PATCH /rest/v2/short-urls/{shortCodes}` to create new tags already attached to a short URL. Creating orphan tags makes no sense.
|
||||
|
||||
### Changes in CLI
|
||||
|
||||
* The next commands have been removed:
|
||||
* `short-url:generate`: Use `short-url:create` instead.
|
||||
* `tag:create`: Creating orphan tags makes no sense.
|
||||
* Params in camelCase format are no longer supported. They all have an equivalent kebab-case replacement. (for example, from `--startDate` to `--start-date`).
|
||||
* The `short-url:create` command no longer accepts the `--no-validate-url` flag. Now URLs are never validated, unless `--validate-url` is passed.
|
||||
|
||||
### Changes in config
|
||||
|
||||
* The next env vars have been removed:
|
||||
* `INVALID_SHORT_URL_REDIRECT_TO`: Replaced by `DEFAULT_INVALID_SHORT_URL_REDIRECT`.
|
||||
* `REGULAR_404_REDIRECT_TO`: Replaced by `DEFAULT_REGULAR_404_REDIRECT`.
|
||||
* `BASE_URL_REDIRECT_TO`: Replaced by `DEFAULT_BASE_URL_REDIRECT`.
|
||||
* `SHORT_DOMAIN_HOST`: Replaced by `DEFAULT_DOMAIN`.
|
||||
* `SHORT_DOMAIN_SCHEMA`: Replaced by `IS_HTTPS_ENABLED`.
|
||||
* `USE_HTTPS`: Replaced by `IS_HTTPS_ENABLED`.
|
||||
* `VALIDATE_URLS`: There's no replacement. URLs are not validated, unless explicitly requested during creation or edition.
|
||||
|
||||
### Other changes
|
||||
|
||||
* A default GeoLite2 license key is no longer provided. If you don't provide your own as explained in [the docs](https://shlink.io/documentation/geolite-license-key/), Shlink will not try to update the file anymore.
|
||||
* The docker image no longer accepts providing configuration via json files mounted in the `config/params` folder. Only env vars are supported now.
|
||||
* If you were manually serving Shlink with swoole, the entry script has to be changed from `/path/to/shlink/vendor/bin/mezzio-swoole start` to `/path/to/shlink/vendor/bin/laminas mezzio:swoole:start`
|
||||
* The `GET /{shortCode}/qr-code/{size}` url has been removed. Use `GET /{shortCode}/qr-code?size={size}` instead.
|
||||
|
||||
## From v1.x to v2.x
|
||||
|
||||
### PHP 7.4 required
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @deprecated To be removed with Shlink 3.0.0
|
||||
* This script is provided to keep backwards compatibility on how to run shlink with swoole while being still able to
|
||||
* update to mezzio/mezzio-swoole 3.x
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mezzio\Swoole\Command;
|
||||
|
||||
use Laminas\ServiceManager\ServiceManager;
|
||||
use PackageVersions\Versions;
|
||||
use Symfony\Component\Console\Application as CommandLine;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
|
||||
|
||||
use function explode;
|
||||
use function Functional\filter;
|
||||
use function str_starts_with;
|
||||
use function strstr;
|
||||
|
||||
/** @var ServiceManager $container */
|
||||
$container = require __DIR__ . '/../../config/container.php';
|
||||
$version = strstr(Versions::getVersion('mezzio/mezzio-swoole'), '@', true);
|
||||
$commandsPrefix = 'mezzio:swoole:';
|
||||
$commands = filter(
|
||||
$container->get('config')['laminas-cli']['commands'] ?? [],
|
||||
fn ($c, string $command) => str_starts_with($command, $commandsPrefix),
|
||||
);
|
||||
$registeredCommands = [];
|
||||
|
||||
foreach ($commands as $newName => $commandServiceName) {
|
||||
[, $oldName] = explode($commandsPrefix, $newName);
|
||||
$registeredCommands[$oldName] = $commandServiceName;
|
||||
|
||||
$container->addDelegator($commandServiceName, static function ($c, $n, callable $factory) use ($oldName) {
|
||||
/** @var Command $command */
|
||||
$command = $factory();
|
||||
$command->setAliases([$oldName]);
|
||||
|
||||
return $command;
|
||||
});
|
||||
}
|
||||
|
||||
$commandLine = new CommandLine('Mezzio web server', $version);
|
||||
$commandLine->setAutoExit(true);
|
||||
$commandLine->setCommandLoader(new ContainerCommandLoader($container, $registeredCommands));
|
||||
$commandLine->run();
|
3
build.sh
3
build.sh
|
@ -36,9 +36,6 @@ ${composerBin} install --no-dev --prefer-dist $composerFlags
|
|||
if [[ $noSwoole ]]; then
|
||||
# If generating a dist not for swoole, uninstall mezzio-swoole
|
||||
${composerBin} remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev $composerFlags
|
||||
else
|
||||
# Copy mezzio helper script to vendor (deprecated - Remove with Shlink 3.0.0)
|
||||
cp "${projectdir}/bin/helper/mezzio-swoole" "./vendor/bin"
|
||||
fi
|
||||
|
||||
# Delete development files
|
||||
|
|
|
@ -9,7 +9,7 @@ return [
|
|||
'geolite2' => [
|
||||
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
|
||||
'temp_dir' => __DIR__ . '/../../data',
|
||||
'license_key' => env('GEOLITE_LICENSE_KEY', 'G4Lm0C60yJsnkdPi'), // Deprecated. Remove hardcoded license on v3
|
||||
'license_key' => env('GEOLITE_LICENSE_KEY'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -10,10 +10,9 @@ use const Shlinkio\Shlink\DEFAULT_REDIRECT_STATUS_CODE;
|
|||
return [
|
||||
|
||||
'not_found_redirects' => [
|
||||
// Deprecated env vars
|
||||
'invalid_short_url' => env('DEFAULT_INVALID_SHORT_URL_REDIRECT', env('INVALID_SHORT_URL_REDIRECT_TO')),
|
||||
'regular_404' => env('DEFAULT_REGULAR_404_REDIRECT', env('REGULAR_404_REDIRECT_TO')),
|
||||
'base_url' => env('DEFAULT_BASE_URL_REDIRECT', env('BASE_URL_REDIRECT_TO')),
|
||||
'invalid_short_url' => env('DEFAULT_INVALID_SHORT_URL_REDIRECT'),
|
||||
'regular_404' => env('DEFAULT_REGULAR_404_REDIRECT'),
|
||||
'base_url' => env('DEFAULT_BASE_URL_REDIRECT'),
|
||||
],
|
||||
|
||||
'url_shortener' => [
|
||||
|
|
|
@ -12,27 +12,14 @@ return (static function (): array {
|
|||
(int) env('DEFAULT_SHORT_CODES_LENGTH', DEFAULT_SHORT_CODES_LENGTH),
|
||||
MIN_SHORT_CODES_LENGTH,
|
||||
);
|
||||
$resolveSchema = static function (): string {
|
||||
// Deprecated. For v3, IS_HTTPS_ENABLED should be true by default, instead of null
|
||||
// return ((bool) env('IS_HTTPS_ENABLED', true)) ? 'https' : 'http';
|
||||
$isHttpsEnabled = env('IS_HTTPS_ENABLED', env('USE_HTTPS'));
|
||||
if ($isHttpsEnabled !== null) {
|
||||
$boolIsHttpsEnabled = (bool) $isHttpsEnabled;
|
||||
return $boolIsHttpsEnabled ? 'https' : 'http';
|
||||
}
|
||||
|
||||
return env('SHORT_DOMAIN_SCHEMA', 'http');
|
||||
};
|
||||
|
||||
return [
|
||||
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
// Deprecated SHORT_DOMAIN_* env vars
|
||||
'schema' => $resolveSchema(),
|
||||
'hostname' => env('DEFAULT_DOMAIN', env('SHORT_DOMAIN_HOST', '')),
|
||||
'schema' => ((bool) env('IS_HTTPS_ENABLED', true)) ? 'https' : 'http',
|
||||
'hostname' => env('DEFAULT_DOMAIN', ''),
|
||||
],
|
||||
'validate_url' => (bool) env('VALIDATE_URLS', false), // Deprecated
|
||||
'default_short_codes_length' => $shortCodesLength,
|
||||
'auto_resolve_titles' => (bool) env('AUTO_RESOLVE_TITLES', false),
|
||||
'append_extra_path' => (bool) env('REDIRECT_APPEND_EXTRA_PATH', false),
|
||||
|
|
|
@ -37,10 +37,7 @@ return (new ConfigAggregator\ConfigAggregator([
|
|||
new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
|
||||
env('APP_ENV') === 'test'
|
||||
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
|
||||
// Deprecated. When the SimplifiedConfigParser is removed, load only generated_config.php here
|
||||
: new ConfigAggregator\LaminasConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'),
|
||||
: new ConfigAggregator\LaminasConfigProvider('config/params/generated_config.php'),
|
||||
], 'data/cache/app_config.php', [
|
||||
Core\Config\SimplifiedConfigParser::class,
|
||||
Core\Config\BasePathPrefixer::class,
|
||||
Core\Config\DeprecatedConfigParser::class,
|
||||
]))->getMergedConfig();
|
||||
|
|
|
@ -320,7 +320,7 @@
|
|||
},
|
||||
"example": {
|
||||
"title": "Cannot delete short URL",
|
||||
"type": "INVALID_SHORTCODE_DELETION",
|
||||
"type": "INVALID_SHORT_URL_DELETION",
|
||||
"detail": "Impossible to delete short URL with short code \"abc123\", since it has more than \"15\" visits.",
|
||||
"status": 422,
|
||||
"shortCode": "abc123",
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
{
|
||||
"put": {
|
||||
"deprecated": true,
|
||||
"operationId": "editShortUrlTags",
|
||||
"tags": [
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Edit tags on short URL",
|
||||
"description": "Edit the tags on URL identified by provided short code.<br />This endpoint is deprecated. Use the [Edit short URL](#/Short%20URLs/editShortUrl) endpoint to edit tags.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code for the short URL in which we want to edit tags.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"tags"
|
||||
],
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The list of tags to set to the short URL."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of tags.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "The request body does not contain a \"tags\" param with array type.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No short URL was found for provided short code.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -110,84 +110,6 @@
|
|||
}
|
||||
},
|
||||
|
||||
"post": {
|
||||
"deprecated": true,
|
||||
"operationId": "createTags",
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
"summary": "Create tags",
|
||||
"description": "Provided a list of tags, creates all that do not yet exist<br />This endpoint is deprecated, as tags are automatically created while creating a short URL",
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"tags"
|
||||
],
|
||||
"properties": {
|
||||
"tags": {
|
||||
"description": "The list of tag names to create",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of tags",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"put": {
|
||||
"operationId": "renameTag",
|
||||
"tags": [
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
{
|
||||
"get": {
|
||||
"operationId": "shortUrlQrCodeSize",
|
||||
"deprecated": true,
|
||||
"tags": [
|
||||
"URL Shortener"
|
||||
],
|
||||
"summary": "Short URL QR code",
|
||||
"description": "Generates a QR code image pointing to a short URL",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"in": "path",
|
||||
"description": "The size of the image to be returned.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 50,
|
||||
"maximum": 1000,
|
||||
"default": 300
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "format",
|
||||
"in": "query",
|
||||
"description": "The format for the QR code image, being valid values png and svg. Not providing the param or providing any other value will fall back to png.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"png",
|
||||
"svg"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "QR code in PNG format",
|
||||
"content": {
|
||||
"image/png": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
},
|
||||
"image/svg+xml": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -78,9 +78,6 @@
|
|||
"/rest/v{version}/short-urls/{shortCode}": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}.json"
|
||||
},
|
||||
"/rest/v{version}/short-urls/{shortCode}/tags": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}_tags.json"
|
||||
},
|
||||
|
||||
"/rest/v{version}/tags": {
|
||||
"$ref": "paths/v1_tags.json"
|
||||
|
@ -122,9 +119,6 @@
|
|||
},
|
||||
"/{shortCode}/qr-code": {
|
||||
"$ref": "paths/{shortCode}_qr-code.json"
|
||||
},
|
||||
"/{shortCode}/qr-code/{size}": {
|
||||
"$ref": "paths/{shortCode}_qr-code_{size}.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ return [
|
|||
Command\Api\ListKeysCommand::NAME => Command\Api\ListKeysCommand::class,
|
||||
|
||||
Command\Tag\ListTagsCommand::NAME => Command\Tag\ListTagsCommand::class,
|
||||
Command\Tag\CreateTagCommand::NAME => Command\Tag\CreateTagCommand::class,
|
||||
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
|
||||
Command\Tag\DeleteTagsCommand::NAME => Command\Tag\DeleteTagsCommand::class,
|
||||
|
||||
|
|
|
@ -53,7 +53,6 @@ return [
|
|||
Command\Api\ListKeysCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Tag\ListTagsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Tag\CreateTagCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Tag\DeleteTagsCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
|
@ -101,7 +100,6 @@ return [
|
|||
Command\Api\ListKeysCommand::class => [ApiKeyService::class],
|
||||
|
||||
Command\Tag\ListTagsCommand::class => [TagService::class],
|
||||
Command\Tag\CreateTagCommand::class => [TagService::class],
|
||||
Command\Tag\RenameTagCommand::class => [TagService::class],
|
||||
Command\Tag\DeleteTagsCommand::class => [TagService::class],
|
||||
|
||||
|
|
|
@ -6,11 +6,11 @@ namespace Shlinkio\Shlink\CLI\Command\Api;
|
|||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
|
||||
use Shlinkio\Shlink\CLI\Command\BaseCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
@ -19,7 +19,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
|||
use function Shlinkio\Shlink\Core\arrayToString;
|
||||
use function sprintf;
|
||||
|
||||
class GenerateKeyCommand extends BaseCommand
|
||||
class GenerateKeyCommand extends Command
|
||||
{
|
||||
public const NAME = 'api-key:generate';
|
||||
|
||||
|
@ -63,7 +63,7 @@ class GenerateKeyCommand extends BaseCommand
|
|||
InputOption::VALUE_REQUIRED,
|
||||
'The name by which this API key will be known.',
|
||||
)
|
||||
->addOptionWithDeprecatedFallback(
|
||||
->addOption(
|
||||
'expiration-date',
|
||||
'e',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
|
@ -86,7 +86,7 @@ class GenerateKeyCommand extends BaseCommand
|
|||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$expirationDate = $this->getOptionWithDeprecatedFallback($input, 'expiration-date');
|
||||
$expirationDate = $input->getOption('expiration-date');
|
||||
$apiKey = $this->apiKeyService->create(
|
||||
isset($expirationDate) ? Chronos::parse($expirationDate) : null,
|
||||
$input->getOption('name'),
|
||||
|
|
|
@ -4,12 +4,12 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Command\BaseCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
@ -19,7 +19,7 @@ use function Functional\map;
|
|||
use function implode;
|
||||
use function sprintf;
|
||||
|
||||
class ListKeysCommand extends BaseCommand
|
||||
class ListKeysCommand extends Command
|
||||
{
|
||||
private const ERROR_STRING_PATTERN = '<fg=red>%s</>';
|
||||
private const SUCCESS_STRING_PATTERN = '<info>%s</info>';
|
||||
|
@ -37,7 +37,7 @@ class ListKeysCommand extends BaseCommand
|
|||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Lists all the available API keys.')
|
||||
->addOptionWithDeprecatedFallback(
|
||||
->addOption(
|
||||
'enabled-only',
|
||||
'e',
|
||||
InputOption::VALUE_NONE,
|
||||
|
@ -47,7 +47,7 @@ class ListKeysCommand extends BaseCommand
|
|||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$enabledOnly = $this->getOptionWithDeprecatedFallback($input, 'enabled-only');
|
||||
$enabledOnly = $input->getOption('enabled-only');
|
||||
|
||||
$rows = map($this->apiKeyService->listKeys($enabledOnly), function (ApiKey $apiKey) use ($enabledOnly) {
|
||||
$expiration = $apiKey->getExpirationDate();
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
use function method_exists;
|
||||
use function Shlinkio\Shlink\Core\kebabCaseToCamelCase;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
|
||||
/** @deprecated */
|
||||
abstract class BaseCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @param string|string[]|bool|null $default
|
||||
*/
|
||||
protected function addOptionWithDeprecatedFallback(
|
||||
string $name,
|
||||
?string $shortcut = null,
|
||||
?int $mode = null,
|
||||
string $description = '',
|
||||
bool|string|array|null $default = null,
|
||||
): self {
|
||||
$this->addOption($name, $shortcut, $mode, $description, $default);
|
||||
|
||||
if (str_contains($name, '-')) {
|
||||
$camelCaseName = kebabCaseToCamelCase($name);
|
||||
$this->addOption($camelCaseName, null, $mode, sprintf('[DEPRECATED] Alias for "%s".', $name), $default);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
protected function getOptionWithDeprecatedFallback(InputInterface $input, string $name) // phpcs:ignore
|
||||
{
|
||||
$rawInput = method_exists($input, '__toString') ? $input->__toString() : '';
|
||||
$camelCaseName = kebabCaseToCamelCase($name);
|
||||
$resolvedOptionName = str_contains($rawInput, $camelCaseName) ? $camelCaseName : $name;
|
||||
|
||||
return $input->getOption($resolvedOptionName);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Command\BaseCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
|
@ -12,6 +11,7 @@ use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
|||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
@ -22,11 +22,9 @@ use function array_map;
|
|||
use function Functional\curry;
|
||||
use function Functional\flatten;
|
||||
use function Functional\unique;
|
||||
use function method_exists;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
|
||||
class CreateShortUrlCommand extends BaseCommand
|
||||
class CreateShortUrlCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:create';
|
||||
|
||||
|
@ -45,7 +43,6 @@ class CreateShortUrlCommand extends BaseCommand
|
|||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(['short-url:generate']) // Deprecated
|
||||
->setDescription('Generates a short URL for provided long URL and returns it')
|
||||
->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse')
|
||||
->addOption(
|
||||
|
@ -54,33 +51,33 @@ class CreateShortUrlCommand extends BaseCommand
|
|||
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
|
||||
'Tags to apply to the new short URL',
|
||||
)
|
||||
->addOptionWithDeprecatedFallback(
|
||||
->addOption(
|
||||
'valid-since',
|
||||
's',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The date from which this short URL will be valid. '
|
||||
. 'If someone tries to access it before this date, it will not be found.',
|
||||
)
|
||||
->addOptionWithDeprecatedFallback(
|
||||
->addOption(
|
||||
'valid-until',
|
||||
'u',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The date until which this short URL will be valid. '
|
||||
. 'If someone tries to access it after this date, it will not be found.',
|
||||
)
|
||||
->addOptionWithDeprecatedFallback(
|
||||
->addOption(
|
||||
'custom-slug',
|
||||
'c',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'If provided, this slug will be used instead of generating a short code',
|
||||
)
|
||||
->addOptionWithDeprecatedFallback(
|
||||
->addOption(
|
||||
'max-visits',
|
||||
'm',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'This will limit the number of visits for this short URL.',
|
||||
)
|
||||
->addOptionWithDeprecatedFallback(
|
||||
->addOption(
|
||||
'find-if-exists',
|
||||
'f',
|
||||
InputOption::VALUE_NONE,
|
||||
|
@ -92,7 +89,7 @@ class CreateShortUrlCommand extends BaseCommand
|
|||
InputOption::VALUE_REQUIRED,
|
||||
'The domain to which this short URL will be attached.',
|
||||
)
|
||||
->addOptionWithDeprecatedFallback(
|
||||
->addOption(
|
||||
'short-code-length',
|
||||
'l',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
|
@ -104,12 +101,6 @@ class CreateShortUrlCommand extends BaseCommand
|
|||
InputOption::VALUE_NONE,
|
||||
'Forces the long URL to be validated, regardless what is globally configured.',
|
||||
)
|
||||
->addOption(
|
||||
'no-validate-url',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'[DEPRECATED] Forces the long URL to not be validated, regardless what is globally configured.',
|
||||
)
|
||||
->addOption(
|
||||
'crawlable',
|
||||
'r',
|
||||
|
@ -161,25 +152,19 @@ class CreateShortUrlCommand extends BaseCommand
|
|||
|
||||
$explodeWithComma = curry('explode')(',');
|
||||
$tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
||||
$customSlug = $this->getOptionWithDeprecatedFallback($input, 'custom-slug');
|
||||
$maxVisits = $this->getOptionWithDeprecatedFallback($input, 'max-visits');
|
||||
$shortCodeLength = $this->getOptionWithDeprecatedFallback(
|
||||
$input,
|
||||
'short-code-length',
|
||||
) ?? $this->defaultShortCodeLength;
|
||||
$doValidateUrl = $this->doValidateUrl($input);
|
||||
$customSlug = $input->getOption('custom-slug');
|
||||
$maxVisits = $input->getOption('max-visits');
|
||||
$shortCodeLength = $input->getOption('short-code-length') ?? $this->defaultShortCodeLength;
|
||||
$doValidateUrl = $input->getOption('validate-url');
|
||||
|
||||
try {
|
||||
$shortUrl = $this->urlShortener->shorten(ShortUrlMeta::fromRawData([
|
||||
ShortUrlInputFilter::LONG_URL => $longUrl,
|
||||
ShortUrlInputFilter::VALID_SINCE => $this->getOptionWithDeprecatedFallback($input, 'valid-since'),
|
||||
ShortUrlInputFilter::VALID_UNTIL => $this->getOptionWithDeprecatedFallback($input, 'valid-until'),
|
||||
ShortUrlInputFilter::VALID_SINCE => $input->getOption('valid-since'),
|
||||
ShortUrlInputFilter::VALID_UNTIL => $input->getOption('valid-until'),
|
||||
ShortUrlInputFilter::CUSTOM_SLUG => $customSlug,
|
||||
ShortUrlInputFilter::MAX_VISITS => $maxVisits !== null ? (int) $maxVisits : null,
|
||||
ShortUrlInputFilter::FIND_IF_EXISTS => $this->getOptionWithDeprecatedFallback(
|
||||
$input,
|
||||
'find-if-exists',
|
||||
),
|
||||
ShortUrlInputFilter::FIND_IF_EXISTS => $input->getOption('find-if-exists'),
|
||||
ShortUrlInputFilter::DOMAIN => $input->getOption('domain'),
|
||||
ShortUrlInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
|
||||
ShortUrlInputFilter::VALIDATE_URL => $doValidateUrl,
|
||||
|
@ -199,20 +184,6 @@ class CreateShortUrlCommand extends BaseCommand
|
|||
}
|
||||
}
|
||||
|
||||
private function doValidateUrl(InputInterface $input): ?bool
|
||||
{
|
||||
$rawInput = method_exists($input, '__toString') ? $input->__toString() : '';
|
||||
|
||||
if (str_contains($rawInput, '--no-validate-url')) {
|
||||
return false;
|
||||
}
|
||||
if (str_contains($rawInput, '--validate-url')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getIO(InputInterface $input, OutputInterface $output): SymfonyStyle
|
||||
{
|
||||
return $this->io ?? ($this->io = new SymfonyStyle($input, $output));
|
||||
|
|
|
@ -52,7 +52,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||
'The first page to list (10 items per page unless "--all" is provided).',
|
||||
'1',
|
||||
)
|
||||
->addOptionWithDeprecatedFallback(
|
||||
->addOption(
|
||||
'search-term',
|
||||
'st',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
|
@ -64,14 +64,14 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||
InputOption::VALUE_REQUIRED,
|
||||
'A comma-separated list of tags to filter results.',
|
||||
)
|
||||
->addOptionWithDeprecatedFallback(
|
||||
->addOption(
|
||||
'order-by',
|
||||
'o',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The field from which you want to order by. '
|
||||
. 'Define ordering dir by passing ASC or DESC after "," or "-".',
|
||||
. 'Define ordering dir by passing ASC or DESC after "-" or ",".',
|
||||
)
|
||||
->addOptionWithDeprecatedFallback(
|
||||
->addOption(
|
||||
'show-tags',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
|
@ -113,7 +113,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$page = (int) $input->getOption('page');
|
||||
$searchTerm = $this->getOptionWithDeprecatedFallback($input, 'search-term');
|
||||
$searchTerm = $input->getOption('search-term');
|
||||
$tags = $input->getOption('tags');
|
||||
$tags = ! empty($tags) ? explode(',', $tags) : [];
|
||||
$all = $input->getOption('all');
|
||||
|
@ -175,7 +175,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||
|
||||
private function processOrderBy(InputInterface $input): ?string
|
||||
{
|
||||
$orderBy = $this->getOptionWithDeprecatedFallback($input, 'order-by');
|
||||
$orderBy = $input->getOption('order-by');
|
||||
if (empty($orderBy)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -195,7 +195,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||
'Date created' => $pickProp('dateCreated'),
|
||||
'Visits count' => $pickProp('visitsCount'),
|
||||
];
|
||||
if ($this->getOptionWithDeprecatedFallback($input, 'show-tags')) {
|
||||
if ($input->getOption('show-tags')) {
|
||||
$columnsMap['Tags'] = static fn (array $shortUrl): string => implode(', ', $shortUrl['tags']);
|
||||
}
|
||||
if ($input->getOption('show-api-key')) {
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/** @deprecated */
|
||||
class CreateTagCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:create';
|
||||
|
||||
public function __construct(private TagServiceInterface $tagService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('[Deprecated] Creates one or more tags.')
|
||||
->addOption(
|
||||
'name',
|
||||
't',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'The name of the tags to create',
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$tagNames = $input->getOption('name');
|
||||
|
||||
if (empty($tagNames)) {
|
||||
$io->warning('You have to provide at least one tag name');
|
||||
return ExitCodes::EXIT_WARNING;
|
||||
}
|
||||
|
||||
$this->tagService->createTags($tagNames);
|
||||
$io->success('Tags properly created');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink\CLI\Command\Util;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\CLI\Command\BaseCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
@ -14,7 +14,7 @@ use Throwable;
|
|||
use function is_string;
|
||||
use function sprintf;
|
||||
|
||||
abstract class AbstractWithDateRangeCommand extends BaseCommand
|
||||
abstract class AbstractWithDateRangeCommand extends Command
|
||||
{
|
||||
private const START_DATE = 'start-date';
|
||||
private const END_DATE = 'end-date';
|
||||
|
@ -23,18 +23,8 @@ abstract class AbstractWithDateRangeCommand extends BaseCommand
|
|||
{
|
||||
$this->doConfigure();
|
||||
$this
|
||||
->addOptionWithDeprecatedFallback(
|
||||
self::START_DATE,
|
||||
's',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
$this->getStartDateDesc(self::START_DATE),
|
||||
)
|
||||
->addOptionWithDeprecatedFallback(
|
||||
self::END_DATE,
|
||||
'e',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
$this->getEndDateDesc(self::END_DATE),
|
||||
);
|
||||
->addOption(self::START_DATE, 's', InputOption::VALUE_REQUIRED, $this->getStartDateDesc(self::START_DATE))
|
||||
->addOption(self::END_DATE, 'e', InputOption::VALUE_REQUIRED, $this->getEndDateDesc(self::END_DATE));
|
||||
}
|
||||
|
||||
protected function getStartDateOption(InputInterface $input, OutputInterface $output): ?Chronos
|
||||
|
@ -49,7 +39,7 @@ abstract class AbstractWithDateRangeCommand extends BaseCommand
|
|||
|
||||
private function getDateOption(InputInterface $input, OutputInterface $output, string $key): ?Chronos
|
||||
{
|
||||
$value = $this->getOptionWithDeprecatedFallback($input, $key);
|
||||
$value = $input->getOption($key);
|
||||
if (empty($value) || ! is_string($value)) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -149,7 +149,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||
* @test
|
||||
* @dataProvider provideFlags
|
||||
*/
|
||||
public function urlValidationHasExpectedValueBasedOnProvidedTags(array $options, ?bool $expectedValidateUrl): void
|
||||
public function urlValidationHasExpectedValueBasedOnProvidedFlags(array $options, ?bool $expectedValidateUrl): void
|
||||
{
|
||||
$shortUrl = ShortUrl::createEmpty();
|
||||
$urlToShortCode = $this->urlShortener->shorten(
|
||||
|
@ -168,8 +168,6 @@ class CreateShortUrlCommandTest extends TestCase
|
|||
public function provideFlags(): iterable
|
||||
{
|
||||
yield 'no flags' => [[], null];
|
||||
yield 'no-validate-url only' => [['--no-validate-url' => true], false];
|
||||
yield 'validate-url' => [['--validate-url' => true], true];
|
||||
yield 'both flags' => [['--validate-url' => true, '--no-validate-url' => true], false];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -241,7 +241,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||
* @test
|
||||
* @dataProvider provideOrderBy
|
||||
*/
|
||||
public function orderByIsProperlyComputed(array $commandArgs, string|array|null $expectedOrderBy): void
|
||||
public function orderByIsProperlyComputed(array $commandArgs, ?string $expectedOrderBy): void
|
||||
{
|
||||
$listShortUrls = $this->shortUrlService->listShortUrls(ShortUrlsParams::fromRawData([
|
||||
'orderBy' => $expectedOrderBy,
|
||||
|
@ -257,8 +257,9 @@ class ListShortUrlsCommandTest extends TestCase
|
|||
{
|
||||
yield [[], null];
|
||||
yield [['--order-by' => 'foo'], 'foo'];
|
||||
yield [['--order-by' => 'foo,ASC'], ['foo' => 'ASC']];
|
||||
yield [['--order-by' => 'bar,DESC'], ['bar' => 'DESC']];
|
||||
yield [['--order-by' => 'foo,ASC'], 'foo-ASC'];
|
||||
yield [['--order-by' => 'bar,DESC'], 'bar-DESC'];
|
||||
yield [['--order-by' => 'baz-DESC'], 'baz-DESC'];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\CreateTagCommand;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class CreateTagCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private ObjectProphecy $tagService;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new CreateTagCommand($this->tagService->reveal()));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function errorIsReturnedWhenNoTagsAreProvided(): void
|
||||
{
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
self::assertStringContainsString('You have to provide at least one tag name', $output);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function serviceIsInvokedOnSuccess(): void
|
||||
{
|
||||
$tagNames = ['foo', 'bar'];
|
||||
$createTags = $this->tagService->createTags($tagNames)->willReturn(new ArrayCollection());
|
||||
|
||||
$this->commandTester->execute([
|
||||
'--name' => $tagNames,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Tags properly created', $output);
|
||||
$createTags->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
|
@ -43,16 +43,6 @@ return [
|
|||
],
|
||||
'allowed_methods' => [RequestMethod::METHOD_GET],
|
||||
],
|
||||
|
||||
// Deprecated
|
||||
[
|
||||
'name' => 'old_' . Action\QrCodeAction::class,
|
||||
'path' => '/{shortCode}/qr-code/{size:[0-9]+}',
|
||||
'middleware' => [
|
||||
Action\QrCodeAction::class,
|
||||
],
|
||||
'allowed_methods' => [RequestMethod::METHOD_GET],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -16,7 +16,6 @@ use Endroid\QrCode\Writer\PngWriter;
|
|||
use Endroid\QrCode\Writer\SvgWriter;
|
||||
use Endroid\QrCode\Writer\WriterInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Shlinkio\Shlink\Core\Options\QrCodeOptions;
|
||||
|
||||
use function Functional\contains;
|
||||
|
@ -43,7 +42,7 @@ final class QrCodeParams
|
|||
$query = $request->getQueryParams();
|
||||
|
||||
return new self(
|
||||
self::resolveSize($request, $query, $defaults),
|
||||
self::resolveSize($query, $defaults),
|
||||
self::resolveMargin($query, $defaults),
|
||||
self::resolveWriter($query, $defaults),
|
||||
self::resolveErrorCorrection($query, $defaults),
|
||||
|
@ -51,10 +50,9 @@ final class QrCodeParams
|
|||
);
|
||||
}
|
||||
|
||||
private static function resolveSize(Request $request, array $query, QrCodeOptions $defaults): int
|
||||
private static function resolveSize(array $query, QrCodeOptions $defaults): int
|
||||
{
|
||||
// FIXME Size attribute is deprecated. After v3.0.0, always use the query param instead
|
||||
$size = (int) $request->getAttribute('size', $query['size'] ?? $defaults->size());
|
||||
$size = (int) ($query['size'] ?? $defaults->size());
|
||||
if ($size < self::MIN_SIZE) {
|
||||
return self::MIN_SIZE;
|
||||
}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Config;
|
||||
|
||||
use function Functional\compose;
|
||||
|
||||
/** @deprecated */
|
||||
class DeprecatedConfigParser
|
||||
{
|
||||
public function __invoke(array $config): array
|
||||
{
|
||||
return compose([$this, 'parseNotFoundRedirect'], [$this, 'removeSecretKey'])($config);
|
||||
}
|
||||
|
||||
public function parseNotFoundRedirect(array $config): array
|
||||
{
|
||||
// If the new config value is already set, keep it
|
||||
if (isset($config['not_found_redirects']['invalid_short_url'])) {
|
||||
return $config;
|
||||
}
|
||||
|
||||
$oldRedirectEnabled = $config['url_shortener']['not_found_short_url']['enable_redirection'] ?? false;
|
||||
if (! $oldRedirectEnabled) {
|
||||
return $config;
|
||||
}
|
||||
|
||||
$oldRedirectValue = $config['url_shortener']['not_found_short_url']['redirect_to'] ?? null;
|
||||
$config['not_found_redirects']['invalid_short_url'] = $oldRedirectValue;
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function removeSecretKey(array $config): array
|
||||
{
|
||||
// Removing secret_key from any generated config will prevent the AppOptions object from crashing
|
||||
unset($config['app_options']['secret_key']);
|
||||
return $config;
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Config;
|
||||
|
||||
use Laminas\Stdlib\ArrayUtils;
|
||||
use Shlinkio\Shlink\Config\Collection\PathCollection;
|
||||
|
||||
use function array_flip;
|
||||
use function array_intersect_key;
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function Functional\contains;
|
||||
use function Functional\reduce_left;
|
||||
use function uksort;
|
||||
|
||||
/** @deprecated */
|
||||
class SimplifiedConfigParser
|
||||
{
|
||||
private const SIMPLIFIED_CONFIG_MAPPING = [
|
||||
'disable_track_param' => ['tracking', 'disable_track_param'],
|
||||
'short_domain_schema' => ['url_shortener', 'domain', 'schema'],
|
||||
'short_domain_host' => ['url_shortener', 'domain', 'hostname'],
|
||||
'validate_url' => ['url_shortener', 'validate_url'],
|
||||
'invalid_short_url_redirect_to' => ['not_found_redirects', 'invalid_short_url'],
|
||||
'regular_404_redirect_to' => ['not_found_redirects', 'regular_404'],
|
||||
'base_url_redirect_to' => ['not_found_redirects', 'base_url'],
|
||||
'db_config' => ['entity_manager', 'connection'],
|
||||
'delete_short_url_threshold' => ['delete_short_urls', 'visits_threshold'],
|
||||
'redis_servers' => ['cache', 'redis', 'servers'],
|
||||
'base_path' => ['router', 'base_path'],
|
||||
'web_worker_num' => ['mezzio-swoole', 'swoole-http-server', 'options', 'worker_num'],
|
||||
'task_worker_num' => ['mezzio-swoole', 'swoole-http-server', 'options', 'task_worker_num'],
|
||||
'visits_webhooks' => ['url_shortener', 'visits_webhooks'],
|
||||
'default_short_codes_length' => ['url_shortener', 'default_short_codes_length'],
|
||||
'geolite_license_key' => ['geolite2', 'license_key'],
|
||||
'mercure_public_hub_url' => ['mercure', 'public_hub_url'],
|
||||
'mercure_internal_hub_url' => ['mercure', 'internal_hub_url'],
|
||||
'mercure_jwt_secret' => ['mercure', 'jwt_secret'],
|
||||
'anonymize_remote_addr' => ['tracking', 'anonymize_remote_addr'],
|
||||
'redirect_status_code' => ['url_shortener', 'redirect_status_code'],
|
||||
'redirect_cache_lifetime' => ['url_shortener', 'redirect_cache_lifetime'],
|
||||
'port' => ['mezzio-swoole', 'swoole-http-server', 'port'],
|
||||
];
|
||||
private const SIMPLIFIED_CONFIG_SIDE_EFFECTS = [
|
||||
'delete_short_url_threshold' => [
|
||||
'path' => ['delete_short_urls', 'check_visits_threshold'],
|
||||
'value' => true,
|
||||
],
|
||||
'redis_servers' => [
|
||||
'path' => ['dependencies', 'aliases', 'lock_store'],
|
||||
'value' => 'redis_lock_store',
|
||||
],
|
||||
];
|
||||
private const SIMPLIFIED_MERGEABLE_CONFIG = ['db_config'];
|
||||
|
||||
public function __invoke(array $config): array
|
||||
{
|
||||
$configForExistingKeys = $this->getConfigForKeysInMappingOrderedByMapping($config);
|
||||
|
||||
return reduce_left($configForExistingKeys, function ($value, string $key, $c, PathCollection $collection) {
|
||||
$path = self::SIMPLIFIED_CONFIG_MAPPING[$key];
|
||||
if (contains(self::SIMPLIFIED_MERGEABLE_CONFIG, $key)) {
|
||||
$value = ArrayUtils::merge($collection->getValueInPath($path), $value);
|
||||
}
|
||||
|
||||
$collection->setValueInPath($value, $path);
|
||||
if (array_key_exists($key, self::SIMPLIFIED_CONFIG_SIDE_EFFECTS)) {
|
||||
['path' => $sideEffectPath, 'value' => $sideEffectValue] = self::SIMPLIFIED_CONFIG_SIDE_EFFECTS[$key];
|
||||
$collection->setValueInPath($sideEffectValue, $sideEffectPath);
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}, new PathCollection($config))->toArray();
|
||||
}
|
||||
|
||||
private function getConfigForKeysInMappingOrderedByMapping(array $config): array
|
||||
{
|
||||
// Ignore any config which is not defined in the mapping
|
||||
$configForExistingKeys = array_intersect_key($config, self::SIMPLIFIED_CONFIG_MAPPING);
|
||||
|
||||
// Order the config by their key, based on the order it was defined in the mapping.
|
||||
// This mainly allows deprecating keys and defining new ones that will replace the older and always take
|
||||
// preference, while the old one keeps working for backwards compatibility if the new one is not provided.
|
||||
$simplifiedConfigOrder = array_flip(array_keys(self::SIMPLIFIED_CONFIG_MAPPING));
|
||||
uksort(
|
||||
$configForExistingKeys,
|
||||
fn (string $a, string $b): int => $simplifiedConfigOrder[$a] - $simplifiedConfigOrder[$b],
|
||||
);
|
||||
|
||||
return $configForExistingKeys;
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ class DeleteShortUrlException extends DomainException implements ProblemDetailsE
|
|||
use CommonProblemDetailsExceptionTrait;
|
||||
|
||||
private const TITLE = 'Cannot delete short URL';
|
||||
private const TYPE = 'INVALID_SHORTCODE_DELETION'; // FIXME Deprecated: Should be INVALID_SHORT_URL_DELETION
|
||||
private const TYPE = 'INVALID_SHORT_URL_DELETION';
|
||||
|
||||
public static function fromVisitsThreshold(int $threshold, ShortUrlIdentifier $identifier): self
|
||||
{
|
||||
|
|
|
@ -8,9 +8,6 @@ use Shlinkio\Shlink\Core\Exception\ValidationException;
|
|||
|
||||
use function array_pad;
|
||||
use function explode;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use function key;
|
||||
|
||||
final class ShortUrlsOrdering
|
||||
{
|
||||
|
@ -41,22 +38,9 @@ final class ShortUrlsOrdering
|
|||
return;
|
||||
}
|
||||
|
||||
// FIXME Providing the ordering as array is considered deprecated. To be removed in v3.0.0
|
||||
$isArray = is_array($orderBy);
|
||||
if (! $isArray && ! is_string($orderBy)) {
|
||||
throw ValidationException::fromArray([
|
||||
'orderBy' => '"Order by" must be an array, string or null',
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $isArray) {
|
||||
[$field, $dir] = array_pad(explode('-', $orderBy), 2, null);
|
||||
$this->orderField = $field;
|
||||
$this->orderDirection = $dir ?? self::DEFAULT_ORDER_DIRECTION;
|
||||
} else {
|
||||
$this->orderField = key($orderBy);
|
||||
$this->orderDirection = $orderBy[$this->orderField];
|
||||
}
|
||||
[$field, $dir] = array_pad(explode('-', $orderBy), 2, null);
|
||||
$this->orderField = $field;
|
||||
$this->orderDirection = $dir ?? self::DEFAULT_ORDER_DIRECTION;
|
||||
}
|
||||
|
||||
public function orderField(): ?string
|
||||
|
|
|
@ -10,8 +10,8 @@ use function sprintf;
|
|||
|
||||
class AppOptions extends AbstractOptions
|
||||
{
|
||||
private string $name = '';
|
||||
private string $version = '1.0';
|
||||
private string $name = 'Shlink';
|
||||
private string $version = '3.0.0';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
|
@ -35,13 +35,6 @@ class AppOptions extends AbstractOptions
|
|||
return $this;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
protected function setDisableTrackParam(?string $disableTrackParam): self
|
||||
{
|
||||
// Keep just for backwards compatibility during hydration
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf('%s:v%s', $this->name, $this->version);
|
||||
|
|
|
@ -77,16 +77,4 @@ class UrlShortenerOptions extends AbstractOptions
|
|||
{
|
||||
$this->appendExtraPath = $appendExtraPath;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
protected function setAnonymizeRemoteAddr(bool $anonymizeRemoteAddr): void
|
||||
{
|
||||
// Keep just for backwards compatibility during hydration
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
protected function setTrackOrphanVisits(bool $trackOrphanVisits): void
|
||||
{
|
||||
// Keep just for backwards compatibility during hydration
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,8 +56,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
|||
$fieldName = $orderBy->orderField();
|
||||
$order = $orderBy->orderDirection();
|
||||
|
||||
// visitsCount and visitCount are deprecated. Only visits should work
|
||||
if (contains(['visits', 'visitsCount', 'visitCount'], $fieldName)) {
|
||||
if ($fieldName === 'visits') {
|
||||
// FIXME This query is inefficient. Debug it.
|
||||
$qb->addSelect('COUNT(DISTINCT v) AS totalVisits')
|
||||
->leftJoin('s.visits', 'v')
|
||||
|
@ -67,17 +66,9 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
|||
return array_column($qb->getQuery()->getResult(), 0);
|
||||
}
|
||||
|
||||
// Map public field names to column names
|
||||
$fieldNameMap = [
|
||||
'originalUrl' => 'longUrl', // Deprecated
|
||||
'longUrl' => 'longUrl',
|
||||
'shortCode' => 'shortCode',
|
||||
'dateCreated' => 'dateCreated',
|
||||
'title' => 'title',
|
||||
];
|
||||
$resolvedFieldName = $fieldNameMap[$fieldName] ?? null;
|
||||
if ($resolvedFieldName !== null) {
|
||||
$qb->orderBy('s.' . $resolvedFieldName, $order);
|
||||
$orderableFields = ['longUrl', 'shortCode', 'dateCreated', 'title'];
|
||||
if (contains($orderableFields, $fieldName)) {
|
||||
$qb->orderBy('s.' . $fieldName, $order);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
|
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Core\Tag;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM;
|
||||
use Happyr\DoctrineSpecification\Spec;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
|
@ -15,14 +14,11 @@ use Shlinkio\Shlink\Core\Repository\TagRepository;
|
|||
use Shlinkio\Shlink\Core\Repository\TagRepositoryInterface;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\TagRenaming;
|
||||
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Spec\WithApiKeySpecsEnsuringJoin;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
class TagService implements TagServiceInterface
|
||||
{
|
||||
use TagManagerTrait;
|
||||
|
||||
public function __construct(private ORM\EntityManagerInterface $em)
|
||||
{
|
||||
}
|
||||
|
@ -34,12 +30,10 @@ class TagService implements TagServiceInterface
|
|||
{
|
||||
/** @var TagRepository $repo */
|
||||
$repo = $this->em->getRepository(Tag::class);
|
||||
/** @var Tag[] $tags */
|
||||
$tags = $repo->match(Spec::andX(
|
||||
return $repo->match(Spec::andX(
|
||||
Spec::orderBy('name'),
|
||||
new WithApiKeySpecsEnsuringJoin($apiKey),
|
||||
));
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -67,21 +61,6 @@ class TagService implements TagServiceInterface
|
|||
$repo->deleteByName($tagNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provided a list of tag names, creates all that do not exist yet
|
||||
*
|
||||
* @deprecated
|
||||
* @param string[] $tagNames
|
||||
* @return Collection|Tag[]
|
||||
*/
|
||||
public function createTags(array $tagNames): Collection
|
||||
{
|
||||
$tags = $this->tagNamesToEntities($this->em, $tagNames);
|
||||
$this->em->flush();
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TagNotFoundException
|
||||
* @throws TagConflictException
|
||||
|
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Core\Tag;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Exception\ForbiddenTagOperationException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagConflictException;
|
||||
|
@ -31,13 +30,6 @@ interface TagServiceInterface
|
|||
*/
|
||||
public function deleteTags(array $tagNames, ?ApiKey $apiKey = null): void;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @param string[] $tagNames
|
||||
* @return Collection|Tag[]
|
||||
*/
|
||||
public function createTags(array $tagNames): Collection;
|
||||
|
||||
/**
|
||||
* @throws TagNotFoundException
|
||||
* @throws TagConflictException
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Util;
|
||||
|
||||
use Doctrine\Common\Collections;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
|
||||
|
||||
use function Functional\map;
|
||||
|
||||
/** @deprecated */
|
||||
trait TagManagerTrait
|
||||
{
|
||||
/**
|
||||
* @param string[] $tags
|
||||
* @deprecated
|
||||
* @return Collections\Collection|Tag[]
|
||||
*/
|
||||
private function tagNamesToEntities(EntityManagerInterface $em, array $tags): Collections\Collection
|
||||
{
|
||||
$normalizedTags = ShortUrlInputFilter::withNonRequiredLongUrl([
|
||||
ShortUrlInputFilter::TAGS => $tags,
|
||||
])->getValue(ShortUrlInputFilter::TAGS);
|
||||
|
||||
$entities = map($normalizedTags, function (string $tagName) use ($em) {
|
||||
$tag = $em->getRepository(Tag::class)->findOneBy(['name' => $tagName]) ?? new Tag($tagName);
|
||||
$em->persist($tag);
|
||||
|
||||
return $tag;
|
||||
});
|
||||
|
||||
return new Collections\ArrayCollection($entities);
|
||||
}
|
||||
}
|
|
@ -128,7 +128,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||
self::assertCount(1, $this->repo->findList(2, 2));
|
||||
|
||||
$result = $this->repo->findList(null, null, null, [], ShortUrlsOrdering::fromRawData([
|
||||
'orderBy' => ['visits' => 'DESC'],
|
||||
'orderBy' => 'visits-DESC',
|
||||
]));
|
||||
self::assertCount(3, $result);
|
||||
self::assertSame($bar, $result[0]);
|
||||
|
@ -156,7 +156,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
|||
$this->getEntityManager()->flush();
|
||||
|
||||
$result = $this->repo->findList(null, null, null, [], ShortUrlsOrdering::fromRawData([
|
||||
'orderBy' => ['longUrl' => 'ASC'],
|
||||
'orderBy' => 'longUrl-ASC',
|
||||
]));
|
||||
|
||||
self::assertCount(count($urls), $result);
|
||||
|
|
|
@ -154,18 +154,12 @@ class QrCodeActionTest extends TestCase
|
|||
];
|
||||
yield 'no size' => [[], ServerRequestFactory::fromGlobals(), 300];
|
||||
yield 'no size, different default' => [['size' => 500], ServerRequestFactory::fromGlobals(), 500];
|
||||
yield 'size in attr' => [[], ServerRequestFactory::fromGlobals()->withAttribute('size', '400'), 400];
|
||||
yield 'size in query' => [[], ServerRequestFactory::fromGlobals()->withQueryParams(['size' => '123']), 123];
|
||||
yield 'size in query, default margin' => [
|
||||
['margin' => 25],
|
||||
ServerRequestFactory::fromGlobals()->withQueryParams(['size' => '123']),
|
||||
173,
|
||||
];
|
||||
yield 'size in query and attr' => [
|
||||
[],
|
||||
ServerRequestFactory::fromGlobals()->withAttribute('size', '350')->withQueryParams(['size' => '123']),
|
||||
350,
|
||||
];
|
||||
yield 'margin' => [[], ServerRequestFactory::fromGlobals()->withQueryParams(['margin' => '35']), 370];
|
||||
yield 'margin and different default' => [
|
||||
['size' => 400],
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Config;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Config\DeprecatedConfigParser;
|
||||
|
||||
use function array_merge;
|
||||
|
||||
class DeprecatedConfigParserTest extends TestCase
|
||||
{
|
||||
private DeprecatedConfigParser $postProcessor;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->postProcessor = new DeprecatedConfigParser();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function returnsConfigAsIsIfNewValueIsDefined(): void
|
||||
{
|
||||
$config = [
|
||||
'not_found_redirects' => [
|
||||
'invalid_short_url' => 'somewhere',
|
||||
],
|
||||
];
|
||||
|
||||
$result = ($this->postProcessor)($config);
|
||||
|
||||
self::assertEquals($config, $result);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function doesNotProvideNewConfigIfOldOneIsDefinedButDisabled(): void
|
||||
{
|
||||
$config = [
|
||||
'url_shortener' => [
|
||||
'not_found_short_url' => [
|
||||
'enable_redirection' => false,
|
||||
'redirect_to' => 'somewhere',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$result = ($this->postProcessor)($config);
|
||||
|
||||
self::assertEquals($config, $result);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function mapsOldConfigToNewOneWhenOldOneIsEnabled(): void
|
||||
{
|
||||
$config = [
|
||||
'url_shortener' => [
|
||||
'not_found_short_url' => [
|
||||
'enable_redirection' => true,
|
||||
'redirect_to' => 'somewhere',
|
||||
],
|
||||
],
|
||||
];
|
||||
$expected = array_merge($config, [
|
||||
'not_found_redirects' => [
|
||||
'invalid_short_url' => 'somewhere',
|
||||
],
|
||||
]);
|
||||
|
||||
$result = ($this->postProcessor)($config);
|
||||
|
||||
self::assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function definesNewConfigAsNullIfOldOneIsEnabledWithNoRedirectValue(): void
|
||||
{
|
||||
$config = [
|
||||
'url_shortener' => [
|
||||
'not_found_short_url' => [
|
||||
'enable_redirection' => true,
|
||||
],
|
||||
],
|
||||
];
|
||||
$expected = array_merge($config, [
|
||||
'not_found_redirects' => [
|
||||
'invalid_short_url' => null,
|
||||
],
|
||||
]);
|
||||
|
||||
$result = ($this->postProcessor)($config);
|
||||
|
||||
self::assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function removesTheOldSecretKey(): void
|
||||
{
|
||||
$config = [
|
||||
'app_options' => [
|
||||
'secret_key' => 'foobar',
|
||||
],
|
||||
];
|
||||
$expected = [
|
||||
'app_options' => [],
|
||||
];
|
||||
|
||||
$result = ($this->postProcessor)($config);
|
||||
|
||||
self::assertEquals($expected, $result);
|
||||
}
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Config;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Config\SimplifiedConfigParser;
|
||||
|
||||
use function array_merge;
|
||||
|
||||
class SimplifiedConfigParserTest extends TestCase
|
||||
{
|
||||
private SimplifiedConfigParser $postProcessor;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->postProcessor = new SimplifiedConfigParser();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function properlyMapsSimplifiedConfig(): void
|
||||
{
|
||||
$config = [
|
||||
'tracking' => [
|
||||
'disable_track_param' => 'foo',
|
||||
],
|
||||
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => 'shlink_db_mysql',
|
||||
'port' => '3306',
|
||||
],
|
||||
],
|
||||
];
|
||||
$simplified = [
|
||||
'disable_track_param' => 'bar',
|
||||
'short_domain_schema' => 'https',
|
||||
'short_domain_host' => 'doma.in',
|
||||
'validate_url' => true,
|
||||
'delete_short_url_threshold' => 50,
|
||||
'invalid_short_url_redirect_to' => 'foobar.com',
|
||||
'regular_404_redirect_to' => 'bar.com',
|
||||
'base_url_redirect_to' => 'foo.com',
|
||||
'redis_servers' => [
|
||||
'tcp://1.1.1.1:1111',
|
||||
'tcp://1.2.2.2:2222',
|
||||
],
|
||||
'db_config' => [
|
||||
'dbname' => 'shlink',
|
||||
'user' => 'foo',
|
||||
'password' => 'bar',
|
||||
'port' => '1234',
|
||||
],
|
||||
'base_path' => '/foo/bar',
|
||||
'task_worker_num' => 50,
|
||||
'visits_webhooks' => [
|
||||
'http://my-api.com/api/v2.3/notify',
|
||||
'https://third-party.io/foo',
|
||||
],
|
||||
'default_short_codes_length' => 8,
|
||||
'geolite_license_key' => 'kjh23ljkbndskj345',
|
||||
'mercure_public_hub_url' => 'public_url',
|
||||
'mercure_internal_hub_url' => 'internal_url',
|
||||
'mercure_jwt_secret' => 'super_secret_value',
|
||||
'anonymize_remote_addr' => false,
|
||||
'redirect_status_code' => 301,
|
||||
'redirect_cache_lifetime' => 90,
|
||||
'port' => 8888,
|
||||
];
|
||||
$expected = [
|
||||
'tracking' => [
|
||||
'disable_track_param' => 'bar',
|
||||
'anonymize_remote_addr' => false,
|
||||
],
|
||||
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => 'shlink_db_mysql',
|
||||
'dbname' => 'shlink',
|
||||
'user' => 'foo',
|
||||
'password' => 'bar',
|
||||
'port' => '1234',
|
||||
],
|
||||
],
|
||||
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => 'https',
|
||||
'hostname' => 'doma.in',
|
||||
],
|
||||
'validate_url' => true,
|
||||
'visits_webhooks' => [
|
||||
'http://my-api.com/api/v2.3/notify',
|
||||
'https://third-party.io/foo',
|
||||
],
|
||||
'default_short_codes_length' => 8,
|
||||
'redirect_status_code' => 301,
|
||||
'redirect_cache_lifetime' => 90,
|
||||
],
|
||||
|
||||
'delete_short_urls' => [
|
||||
'visits_threshold' => 50,
|
||||
'check_visits_threshold' => true,
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'aliases' => [
|
||||
'lock_store' => 'redis_lock_store',
|
||||
],
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'redis' => [
|
||||
'servers' => [
|
||||
'tcp://1.1.1.1:1111',
|
||||
'tcp://1.2.2.2:2222',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'router' => [
|
||||
'base_path' => '/foo/bar',
|
||||
],
|
||||
|
||||
'not_found_redirects' => [
|
||||
'invalid_short_url' => 'foobar.com',
|
||||
'regular_404' => 'bar.com',
|
||||
'base_url' => 'foo.com',
|
||||
],
|
||||
|
||||
'mezzio-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'port' => 8888,
|
||||
'options' => [
|
||||
'task_worker_num' => 50,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'geolite2' => [
|
||||
'license_key' => 'kjh23ljkbndskj345',
|
||||
],
|
||||
|
||||
'mercure' => [
|
||||
'public_hub_url' => 'public_url',
|
||||
'internal_hub_url' => 'internal_url',
|
||||
'jwt_secret' => 'super_secret_value',
|
||||
],
|
||||
];
|
||||
|
||||
$result = ($this->postProcessor)(array_merge($config, $simplified));
|
||||
|
||||
self::assertEquals(array_merge($expected, $simplified), $result);
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ class DeleteShortUrlExceptionTest extends TestCase
|
|||
'threshold' => $threshold,
|
||||
], $e->getAdditionalData());
|
||||
self::assertEquals('Cannot delete short URL', $e->getTitle());
|
||||
self::assertEquals('INVALID_SHORTCODE_DELETION', $e->getType());
|
||||
self::assertEquals('INVALID_SHORT_URL_DELETION', $e->getType());
|
||||
self::assertEquals(422, $e->getStatus());
|
||||
}
|
||||
|
||||
|
|
|
@ -97,21 +97,6 @@ class TagServiceTest extends TestCase
|
|||
);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function createTagsPersistsEntities(): void
|
||||
{
|
||||
$find = $this->repo->findOneBy(Argument::cetera())->willReturn(new Tag('foo'));
|
||||
$persist = $this->em->persist(Argument::type(Tag::class))->willReturn(null);
|
||||
$flush = $this->em->flush()->willReturn(null);
|
||||
|
||||
$result = $this->service->createTags(['foo', 'bar']);
|
||||
|
||||
self::assertCount(2, $result);
|
||||
$find->shouldHaveBeenCalled();
|
||||
$persist->shouldHaveBeenCalledTimes(2);
|
||||
$flush->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideAdminApiKeys
|
||||
|
|
|
@ -30,14 +30,12 @@ return [
|
|||
Action\ShortUrl\DeleteShortUrlAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortUrl\ResolveShortUrlAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortUrl\ListShortUrlsAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortUrl\EditShortUrlTagsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\ShortUrlVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\TagVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\GlobalVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\OrphanVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Tag\ListTagsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Tag\DeleteTagsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Tag\CreateTagsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Tag\UpdateTagAction::class => ConfigAbstractFactory::class,
|
||||
Action\Domain\ListDomainsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Domain\DomainRedirectsAction::class => ConfigAbstractFactory::class,
|
||||
|
@ -76,10 +74,8 @@ return [
|
|||
Visit\Transformer\OrphanVisitDataTransformer::class,
|
||||
],
|
||||
Action\ShortUrl\ListShortUrlsAction::class => [Service\ShortUrlService::class, ShortUrlDataTransformer::class],
|
||||
Action\ShortUrl\EditShortUrlTagsAction::class => [Service\ShortUrlService::class],
|
||||
Action\Tag\ListTagsAction::class => [TagService::class],
|
||||
Action\Tag\DeleteTagsAction::class => [TagService::class],
|
||||
Action\Tag\CreateTagsAction::class => [TagService::class],
|
||||
Action\Tag\UpdateTagAction::class => [TagService::class],
|
||||
Action\Domain\ListDomainsAction::class => [DomainService::class, Options\NotFoundRedirectOptions::class],
|
||||
Action\Domain\DomainRedirectsAction::class => [DomainService::class],
|
||||
|
|
|
@ -28,7 +28,6 @@ return [
|
|||
Action\ShortUrl\DeleteShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ResolveShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ListShortUrlsAction::getRouteDef(),
|
||||
Action\ShortUrl\EditShortUrlTagsAction::getRouteDef([$dropDomainMiddleware]),
|
||||
|
||||
// Visits
|
||||
Action\Visit\ShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||
|
@ -39,7 +38,6 @@ return [
|
|||
// Tags
|
||||
Action\Tag\ListTagsAction::getRouteDef(),
|
||||
Action\Tag\DeleteTagsAction::getRouteDef(),
|
||||
Action\Tag\CreateTagsAction::getRouteDef(),
|
||||
Action\Tag\UpdateTagAction::getRouteDef(),
|
||||
|
||||
// Domains
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Action\ShortUrl;
|
||||
|
||||
use Laminas\Diactoros\Response\JsonResponse;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlEdit;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
|
||||
|
||||
/** @deprecated */
|
||||
class EditShortUrlTagsAction extends AbstractRestAction
|
||||
{
|
||||
protected const ROUTE_PATH = '/short-urls/{shortCode}/tags';
|
||||
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_PUT];
|
||||
|
||||
public function __construct(private ShortUrlServiceInterface $shortUrlService)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
/** @var array $bodyParams */
|
||||
$bodyParams = $request->getParsedBody();
|
||||
|
||||
if (! isset($bodyParams['tags'])) {
|
||||
throw ValidationException::fromArray([
|
||||
'tags' => 'List of tags has to be provided',
|
||||
]);
|
||||
}
|
||||
['tags' => $tags] = $bodyParams;
|
||||
$identifier = ShortUrlIdentifier::fromApiRequest($request);
|
||||
$apiKey = AuthenticationMiddleware::apiKeyFromRequest($request);
|
||||
|
||||
$shortUrl = $this->shortUrlService->updateShortUrl($identifier, ShortUrlEdit::fromRawData([
|
||||
ShortUrlInputFilter::TAGS => $tags,
|
||||
]), $apiKey);
|
||||
return new JsonResponse(['tags' => $shortUrl->getTags()->toArray()]);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Action\Tag;
|
||||
|
||||
use Laminas\Diactoros\Response\JsonResponse;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
|
||||
/** @deprecated */
|
||||
class CreateTagsAction extends AbstractRestAction
|
||||
{
|
||||
protected const ROUTE_PATH = '/tags';
|
||||
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_POST];
|
||||
|
||||
public function __construct(private TagServiceInterface $tagService)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
/** @var array $body */
|
||||
$body = $request->getParsedBody();
|
||||
$tags = $body['tags'] ?? [];
|
||||
|
||||
return new JsonResponse([
|
||||
'tags' => [
|
||||
'data' => $this->tagService->createTags($tags)->toArray(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -24,10 +24,7 @@ class MissingAuthenticationException extends RuntimeException implements Problem
|
|||
'Expected one of the following authentication headers, ["%s"], but none were provided',
|
||||
implode('", "', $expectedHeaders),
|
||||
));
|
||||
$e->additional = [
|
||||
'expectedTypes' => $expectedHeaders, // Deprecated
|
||||
'expectedHeaders' => $expectedHeaders,
|
||||
];
|
||||
$e->additional = ['expectedHeaders' => $expectedHeaders];
|
||||
|
||||
return $e;
|
||||
}
|
||||
|
|
|
@ -10,12 +10,8 @@ use Psr\Http\Message\ServerRequestInterface as Request;
|
|||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
use function array_shift;
|
||||
use function explode;
|
||||
use function Functional\contains;
|
||||
use function parse_str;
|
||||
use function Shlinkio\Shlink\Common\json_decode;
|
||||
use function trim;
|
||||
|
||||
class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterface
|
||||
{
|
||||
|
@ -36,20 +32,7 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac
|
|||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
// If the accepted content is JSON, try to parse the body from JSON
|
||||
$contentType = $this->getRequestContentType($request);
|
||||
if (contains(['application/json', 'text/json', 'application/x-json'], $contentType)) {
|
||||
return $handler->handle($this->parseFromJson($request));
|
||||
}
|
||||
|
||||
return $handler->handle($this->parseFromUrlEncoded($request));
|
||||
}
|
||||
|
||||
private function getRequestContentType(Request $request): string
|
||||
{
|
||||
$contentType = $request->getHeaderLine('Content-type');
|
||||
$contentTypes = explode(';', $contentType);
|
||||
return trim(array_shift($contentTypes));
|
||||
return $handler->handle($this->parseFromJson($request));
|
||||
}
|
||||
|
||||
private function parseFromJson(Request $request): Request
|
||||
|
@ -62,20 +45,4 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac
|
|||
$parsedJson = json_decode($rawBody);
|
||||
return $request->withParsedBody($parsedJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated To be removed on Shlink v3.0.0, supporting only JSON requests.
|
||||
*/
|
||||
private function parseFromUrlEncoded(Request $request): Request
|
||||
{
|
||||
$rawBody = $request->getBody()->__toString();
|
||||
if (empty($rawBody)) {
|
||||
return $request;
|
||||
}
|
||||
|
||||
$parsedBody = [];
|
||||
parse_str($rawBody, $parsedBody);
|
||||
|
||||
return $request->withParsedBody($parsedBody);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ class DeleteShortUrlTest extends ApiTestCase
|
|||
|
||||
self::assertEquals(self::STATUS_UNPROCESSABLE_ENTITY, $resp->getStatusCode());
|
||||
self::assertEquals(self::STATUS_UNPROCESSABLE_ENTITY, $payload['status']);
|
||||
self::assertEquals('INVALID_SHORTCODE_DELETION', $payload['type']);
|
||||
self::assertEquals('INVALID_SHORT_URL_DELETION', $payload['type']);
|
||||
self::assertEquals($expectedDetail, $payload['detail']);
|
||||
self::assertEquals('Cannot delete short URL', $payload['title']);
|
||||
}
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioApiTest\Shlink\Rest\Action;
|
||||
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
|
||||
use ShlinkioApiTest\Shlink\Rest\Utils\NotFoundUrlHelpersTrait;
|
||||
|
||||
class EditShortUrlTagsTest extends ApiTestCase
|
||||
{
|
||||
use NotFoundUrlHelpersTrait;
|
||||
|
||||
/** @test */
|
||||
public function notProvidingTagsReturnsBadRequest(): void
|
||||
{
|
||||
$expectedDetail = 'Provided data is not valid';
|
||||
|
||||
$resp = $this->callApiWithKey(self::METHOD_PUT, '/short-urls/abc123/tags', [RequestOptions::JSON => []]);
|
||||
$payload = $this->getJsonResponsePayload($resp);
|
||||
|
||||
self::assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode());
|
||||
self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
|
||||
self::assertEquals('INVALID_ARGUMENT', $payload['type']);
|
||||
self::assertEquals($expectedDetail, $payload['detail']);
|
||||
self::assertEquals('Invalid data', $payload['title']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideInvalidUrls
|
||||
*/
|
||||
public function providingInvalidShortCodeReturnsBadRequest(
|
||||
string $shortCode,
|
||||
?string $domain,
|
||||
string $expectedDetail,
|
||||
string $apiKey,
|
||||
): void {
|
||||
$url = $this->buildShortUrlPath($shortCode, $domain, '/tags');
|
||||
$resp = $this->callApiWithKey(self::METHOD_PUT, $url, [RequestOptions::JSON => [
|
||||
'tags' => ['foo', 'bar'],
|
||||
]], $apiKey);
|
||||
$payload = $this->getJsonResponsePayload($resp);
|
||||
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
|
||||
self::assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
|
||||
self::assertEquals('INVALID_SHORTCODE', $payload['type']);
|
||||
self::assertEquals($expectedDetail, $payload['detail']);
|
||||
self::assertEquals('Short URL not found', $payload['title']);
|
||||
self::assertEquals($shortCode, $payload['shortCode']);
|
||||
self::assertEquals($domain, $payload['domain'] ?? null);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function allowsEditingTagsWithTwoEndpoints(): void
|
||||
{
|
||||
$getUrlTagsFromApi = fn () => $this->getJsonResponsePayload(
|
||||
$this->callApiWithKey(self::METHOD_GET, '/short-urls/abc123'),
|
||||
)['tags'] ?? null;
|
||||
self::assertEquals(['foo'], $getUrlTagsFromApi());
|
||||
|
||||
$this->callApiWithKey(self::METHOD_PUT, '/short-urls/abc123/tags', [RequestOptions::JSON => [
|
||||
'tags' => ['a', 'e'],
|
||||
]]);
|
||||
self::assertEquals(['a', 'e'], $getUrlTagsFromApi());
|
||||
|
||||
$this->callApiWithKey(self::METHOD_PATCH, '/short-urls/abc123', [RequestOptions::JSON => [
|
||||
'tags' => ['i', 'o', 'u'],
|
||||
]]);
|
||||
self::assertEquals(['i', 'o', 'u'], $getUrlTagsFromApi());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function tagsAreSetOnProperShortUrlBasedOnProvidedDomain(): void
|
||||
{
|
||||
$urlWithoutDomain = '/short-urls/ghi789/tags';
|
||||
$urlWithDomain = $urlWithoutDomain . '?domain=example.com';
|
||||
|
||||
$setTagsWithDomain = $this->callApiWithKey(self::METHOD_PUT, $urlWithDomain, [RequestOptions::JSON => [
|
||||
'tags' => ['foo', 'bar'],
|
||||
]]);
|
||||
$fetchWithoutDomain = $this->getJsonResponsePayload(
|
||||
$this->callApiWithKey(self::METHOD_GET, '/short-urls/ghi789'),
|
||||
);
|
||||
$fetchWithDomain = $this->getJsonResponsePayload(
|
||||
$this->callApiWithKey(self::METHOD_GET, '/short-urls/ghi789?domain=example.com'),
|
||||
);
|
||||
|
||||
self::assertEquals(self::STATUS_OK, $setTagsWithDomain->getStatusCode());
|
||||
self::assertEquals([], $fetchWithoutDomain['tags']);
|
||||
self::assertEquals(['bar', 'foo'], $fetchWithDomain['tags']);
|
||||
}
|
||||
}
|
|
@ -155,14 +155,6 @@ class ListShortUrlsTest extends ApiTestCase
|
|||
self::SHORT_URL_DOCS,
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
], 'valid_api_key'];
|
||||
yield [['orderBy' => ['shortCode' => 'DESC']], [ // Deprecated
|
||||
self::SHORT_URL_DOCS,
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'valid_api_key'];
|
||||
yield [['orderBy' => 'shortCode-DESC'], [
|
||||
self::SHORT_URL_DOCS,
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
|
|
|
@ -73,7 +73,7 @@ class CorsTest extends ApiTestCase
|
|||
{
|
||||
yield 'invalid route' => ['/foo/bar', 'GET,POST,PUT,PATCH,DELETE'];
|
||||
yield 'short URLs route' => ['/short-urls', 'GET,POST'];
|
||||
yield 'tags route' => ['/tags', 'GET,POST,PUT,DELETE'];
|
||||
yield 'tags route' => ['/tags', 'GET,PUT,DELETE'];
|
||||
yield 'health route' => ['/health', 'GET'];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Rest\Action\ShortUrl;
|
||||
|
||||
use Laminas\Diactoros\ServerRequestFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlEdit;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||
use Shlinkio\Shlink\Rest\Action\ShortUrl\EditShortUrlTagsAction;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
class EditShortUrlTagsActionTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private EditShortUrlTagsAction $action;
|
||||
private ObjectProphecy $shortUrlService;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->shortUrlService = $this->prophesize(ShortUrlService::class);
|
||||
$this->action = new EditShortUrlTagsAction($this->shortUrlService->reveal());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function notProvidingTagsReturnsError(): void
|
||||
{
|
||||
$this->expectException(ValidationException::class);
|
||||
$this->action->handle($this->createRequestWithAPiKey()->withAttribute('shortCode', 'abc123'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function tagsListIsReturnedIfCorrectShortCodeIsProvided(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->shortUrlService->updateShortUrl(
|
||||
new ShortUrlIdentifier($shortCode),
|
||||
Argument::type(ShortUrlEdit::class),
|
||||
Argument::type(ApiKey::class),
|
||||
)->willReturn(ShortUrl::createEmpty())
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$response = $this->action->handle(
|
||||
$this->createRequestWithAPiKey()->withAttribute('shortCode', 'abc123')
|
||||
->withParsedBody(['tags' => []]),
|
||||
);
|
||||
self::assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
private function createRequestWithAPiKey(): ServerRequestInterface
|
||||
{
|
||||
return ServerRequestFactory::fromGlobals()->withAttribute(ApiKey::class, ApiKey::create());
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Rest\Action\Tag;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Laminas\Diactoros\ServerRequest;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\Tag\CreateTagsAction;
|
||||
|
||||
class CreateTagsActionTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private CreateTagsAction $action;
|
||||
private ObjectProphecy $tagService;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
$this->action = new CreateTagsAction($this->tagService->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideTags
|
||||
*/
|
||||
public function processDelegatesIntoService(?array $tags): void
|
||||
{
|
||||
$request = (new ServerRequest())->withParsedBody(['tags' => $tags]);
|
||||
$deleteTags = $this->tagService->createTags($tags ?: [])->willReturn(new ArrayCollection());
|
||||
|
||||
$response = $this->action->handle($request);
|
||||
|
||||
self::assertEquals(200, $response->getStatusCode());
|
||||
$deleteTags->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function provideTags(): iterable
|
||||
{
|
||||
yield 'three tags' => [['foo', 'bar', 'baz']];
|
||||
yield 'two tags' => [['some', 'thing']];
|
||||
yield 'null tags' => [null];
|
||||
yield 'empty tags' => [[]];
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ class MissingAuthenticationExceptionTest extends TestCase
|
|||
{
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideExpectedTypes
|
||||
* @dataProvider provideExpectedHeaders
|
||||
*/
|
||||
public function exceptionIsProperlyCreatedFromExpectedHeaders(array $expectedHeaders): void
|
||||
{
|
||||
|
@ -28,13 +28,10 @@ class MissingAuthenticationExceptionTest extends TestCase
|
|||
$this->assertCommonExceptionShape($e);
|
||||
self::assertEquals($expectedMessage, $e->getMessage());
|
||||
self::assertEquals($expectedMessage, $e->getDetail());
|
||||
self::assertEquals([
|
||||
'expectedTypes' => $expectedHeaders,
|
||||
'expectedHeaders' => $expectedHeaders,
|
||||
], $e->getAdditionalData());
|
||||
self::assertEquals(['expectedHeaders' => $expectedHeaders], $e->getAdditionalData());
|
||||
}
|
||||
|
||||
public function provideExpectedTypes(): iterable
|
||||
public function provideExpectedHeaders(): iterable
|
||||
{
|
||||
yield [['foo', 'bar']];
|
||||
yield [['something']];
|
||||
|
|
|
@ -78,35 +78,6 @@ class BodyParserMiddlewareTest extends TestCase
|
|||
$test = $this;
|
||||
$body = new Stream('php://temp', 'wr');
|
||||
$body->write('{"foo": "bar", "bar": ["one", 5]}');
|
||||
$request = (new ServerRequest())->withMethod('PUT')
|
||||
->withBody($body)
|
||||
->withHeader('content-type', 'application/json');
|
||||
$delegate = $this->prophesize(RequestHandlerInterface::class);
|
||||
$process = $delegate->handle(Argument::type(ServerRequestInterface::class))->will(
|
||||
function (array $args) use ($test) {
|
||||
/** @var ServerRequestInterface $req */
|
||||
$req = array_shift($args);
|
||||
|
||||
$test->assertEquals([
|
||||
'foo' => 'bar',
|
||||
'bar' => ['one', 5],
|
||||
], $req->getParsedBody());
|
||||
|
||||
return new Response();
|
||||
},
|
||||
);
|
||||
|
||||
$this->middleware->process($request, $delegate->reveal());
|
||||
|
||||
$process->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function regularRequestsAreUrlDecoded(): void
|
||||
{
|
||||
$test = $this;
|
||||
$body = new Stream('php://temp', 'wr');
|
||||
$body->write('foo=bar&bar[]=one&bar[]=5');
|
||||
$request = (new ServerRequest())->withMethod('PUT')
|
||||
->withBody($body);
|
||||
$delegate = $this->prophesize(RequestHandlerInterface::class);
|
||||
|
|
Loading…
Add table
Reference in a new issue