Removed everything that was deprecated

This commit is contained in:
Alejandro Celaya 2021-12-14 22:21:53 +01:00
parent 351e36b273
commit 1ff241411b
54 changed files with 108 additions and 1507 deletions

View file

@ -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

View file

@ -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();

View file

@ -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

View file

@ -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'),
],
];

View file

@ -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' => [

View file

@ -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),

View file

@ -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();

View file

@ -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",

View file

@ -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"
}
}
}
}
}
}
}

View file

@ -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": [

View file

@ -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"
}
}
}
}
}
}
}

View file

@ -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"
}
}
}

View file

@ -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,

View file

@ -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],

View file

@ -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'),

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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));

View file

@ -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')) {

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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];
}
}

View file

@ -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 */

View file

@ -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();
}
}

View file

@ -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],
],
],
];

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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
{

View file

@ -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

View file

@ -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);

View file

@ -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
}
}

View file

@ -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();

View file

@ -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

View file

@ -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

View file

@ -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);
}
}

View file

@ -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],

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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());
}

View file

@ -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

View file

@ -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],

View file

@ -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

View file

@ -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()]);
}
}

View file

@ -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(),
],
]);
}
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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']);
}

View file

@ -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']);
}
}

View file

@ -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,

View file

@ -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());
}
}

View file

@ -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' => [[]];
}
}

View file

@ -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']];

View file

@ -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);