mirror of
https://github.com/shlinkio/shlink.git
synced 2024-12-27 01:11:39 -06:00
Merge pull request #660 from acelaya-forks/feature/short-codes-length
Feature/short codes length
This commit is contained in:
commit
f53fa5c90f
@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||
#### Added
|
||||
|
||||
* [#626](https://github.com/shlinkio/shlink/issues/626) Added support for Microsoft SQL Server.
|
||||
* [#556](https://github.com/shlinkio/shlink/issues/556) Short code lengths can now be customized, both globally and on a per-short URL basis.
|
||||
|
||||
#### Changed
|
||||
|
||||
|
@ -49,7 +49,7 @@
|
||||
"pugx/shortid-php": "^0.5",
|
||||
"shlinkio/shlink-common": "^2.7.0",
|
||||
"shlinkio/shlink-event-dispatcher": "^1.3",
|
||||
"shlinkio/shlink-installer": "^4.1.0",
|
||||
"shlinkio/shlink-installer": "^4.2.0",
|
||||
"shlinkio/shlink-ip-geolocation": "^1.3.1",
|
||||
"symfony/console": "^5.0",
|
||||
"symfony/filesystem": "^5.0",
|
||||
|
@ -30,6 +30,7 @@ return [
|
||||
Option\TaskWorkerNumConfigOption::class,
|
||||
Option\WebWorkerNumConfigOption::class,
|
||||
Option\RedisServersConfigOption::class,
|
||||
Option\ShortCodeLengthOption::class,
|
||||
],
|
||||
|
||||
'installation_commands' => [
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
|
||||
|
||||
return [
|
||||
|
||||
'url_shortener' => [
|
||||
@ -11,6 +13,7 @@ return [
|
||||
],
|
||||
'validate_url' => false,
|
||||
'visits_webhooks' => [],
|
||||
'default_short_codes_length' => DEFAULT_SHORT_CODES_LENGTH,
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -113,6 +113,7 @@ This is the complete list of supported env vars:
|
||||
* `WEB_WORKER_NUM`: The amount of concurrent http requests this shlink instance will be able to server. Defaults to 16.
|
||||
* `TASK_WORKER_NUM`: The amount of concurrent background tasks this shlink instance will be able to execute. Defaults to 16.
|
||||
* `VISITS_WEBHOOKS`: A comma-separated list of URLs that will receive a `POST` request when a short URL receives a visit.
|
||||
* `DEFAULT_SHORT_CODES_LENGTH`: The length you want generated short codes to have. It defaults to 5 and has to be at least 4, so any value smaller than that will fall back to 4.
|
||||
* `REDIS_SERVERS`: A comma-separated list of redis servers where Shlink locks are stored (locks are used to prevent some operations to be run more than once in parallel).
|
||||
|
||||
This is important when running more than one Shlink instance ([Multi instance considerations](#multi-instance-considerations)). If not provided, Shlink stores locks on every instance separately.
|
||||
@ -146,6 +147,7 @@ docker run \
|
||||
-e WEB_WORKER_NUM=64 \
|
||||
-e TASK_WORKER_NUM=32 \
|
||||
-e "VISITS_WEBHOOKS=http://my-api.com/api/v2.3/notify,https://third-party.io/foo" \
|
||||
-e DEFAULT_SHORT_CODES_LENGTH=6 \
|
||||
shlinkio/shlink:stable
|
||||
```
|
||||
|
||||
@ -170,6 +172,7 @@ The whole configuration should have this format, but it can be split into multip
|
||||
"base_path": "/my-campaign",
|
||||
"web_worker_num": 64,
|
||||
"task_worker_num": 32,
|
||||
"default_short_codes_length": 6,
|
||||
"redis_servers": [
|
||||
"tcp://172.20.0.1:6379",
|
||||
"tcp://172.20.0.2:6379"
|
||||
|
@ -11,6 +11,9 @@ use function explode;
|
||||
use function Functional\contains;
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
|
||||
use const Shlinkio\Shlink\Core\MIN_SHORT_CODES_LENGTH;
|
||||
|
||||
$helper = new class {
|
||||
private const DB_DRIVERS_MAP = [
|
||||
'mysql' => 'pdo_mysql',
|
||||
@ -70,6 +73,12 @@ $helper = new class {
|
||||
$redisServers = env('REDIS_SERVERS');
|
||||
return $redisServers === null ? null : ['servers' => $redisServers];
|
||||
}
|
||||
|
||||
public function getDefaultShortCodesLength(): int
|
||||
{
|
||||
$value = (int) env('DEFAULT_SHORT_CODES_LENGTH', DEFAULT_SHORT_CODES_LENGTH);
|
||||
return $value < MIN_SHORT_CODES_LENGTH ? MIN_SHORT_CODES_LENGTH : $value;
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
@ -96,6 +105,7 @@ return [
|
||||
],
|
||||
'validate_url' => (bool) env('VALIDATE_URLS', false),
|
||||
'visits_webhooks' => $helper->getVisitsWebhooks(),
|
||||
'default_short_codes_length' => $helper->getDefaultShortCodesLength(),
|
||||
],
|
||||
|
||||
'not_found_redirects' => $helper->getNotFoundRedirectsConfig(),
|
||||
|
@ -243,6 +243,10 @@
|
||||
"domain": {
|
||||
"description": "The domain to which the short URL will be attached",
|
||||
"type": "string"
|
||||
},
|
||||
"shortCodeLength": {
|
||||
"description": "The length for generated short code. It has to be at least 4 and defaults to 5. It will be ignored when customSlug is provided",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,11 @@ return [
|
||||
ConfigAbstractFactory::class => [
|
||||
GeolocationDbUpdater::class => [DbUpdater::class, Reader::class, 'Shlinkio\Shlink\LocalLockFactory'],
|
||||
|
||||
Command\ShortUrl\GenerateShortUrlCommand::class => [Service\UrlShortener::class, 'config.url_shortener.domain'],
|
||||
Command\ShortUrl\GenerateShortUrlCommand::class => [
|
||||
Service\UrlShortener::class,
|
||||
'config.url_shortener.domain',
|
||||
'config.url_shortener.default_short_codes_length',
|
||||
],
|
||||
Command\ShortUrl\ResolveUrlCommand::class => [Service\ShortUrl\ShortUrlResolver::class],
|
||||
Command\ShortUrl\ListShortUrlsCommand::class => [Service\ShortUrlService::class, 'config.url_shortener.domain'],
|
||||
Command\ShortUrl\GetVisitsCommand::class => [Service\VisitsTracker::class],
|
||||
|
@ -30,12 +30,14 @@ class GenerateShortUrlCommand extends Command
|
||||
|
||||
private UrlShortenerInterface $urlShortener;
|
||||
private array $domainConfig;
|
||||
private int $defaultShortCodeLength;
|
||||
|
||||
public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig)
|
||||
public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig, int $defaultShortCodeLength)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->domainConfig = $domainConfig;
|
||||
$this->defaultShortCodeLength = $defaultShortCodeLength;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@ -87,6 +89,12 @@ class GenerateShortUrlCommand extends Command
|
||||
'd',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The domain to which this short URL will be attached.',
|
||||
)
|
||||
->addOption(
|
||||
'shortCodeLength',
|
||||
'l',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The length for generated short code (it will be ignored if --customSlug was provided).',
|
||||
);
|
||||
}
|
||||
|
||||
@ -117,6 +125,7 @@ class GenerateShortUrlCommand extends Command
|
||||
$tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
||||
$customSlug = $input->getOption('customSlug');
|
||||
$maxVisits = $input->getOption('maxVisits');
|
||||
$shortCodeLength = $input->getOption('shortCodeLength') ?? $this->defaultShortCodeLength;
|
||||
|
||||
try {
|
||||
$shortUrl = $this->urlShortener->urlToShortCode(
|
||||
@ -129,6 +138,7 @@ class GenerateShortUrlCommand extends Command
|
||||
ShortUrlMetaInputFilter::MAX_VISITS => $maxVisits !== null ? (int) $maxVisits : null,
|
||||
ShortUrlMetaInputFilter::FIND_IF_EXISTS => $input->getOption('findIfExists'),
|
||||
ShortUrlMetaInputFilter::DOMAIN => $input->getOption('domain'),
|
||||
ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
|
||||
]),
|
||||
);
|
||||
|
||||
|
@ -31,7 +31,7 @@ class GenerateShortUrlCommandTest extends TestCase
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
||||
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), self::DOMAIN_CONFIG);
|
||||
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), self::DOMAIN_CONFIG, 5);
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
|
@ -10,7 +10,10 @@ use PUGX\Shortid\Factory as ShortIdFactory;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
function generateRandomShortCode(int $length = 5): string
|
||||
const DEFAULT_SHORT_CODES_LENGTH = 5;
|
||||
const MIN_SHORT_CODES_LENGTH = 4;
|
||||
|
||||
function generateRandomShortCode(int $length): string
|
||||
{
|
||||
static $shortIdFactory;
|
||||
if ($shortIdFactory === null) {
|
||||
|
@ -32,6 +32,7 @@ class SimplifiedConfigParser
|
||||
'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'],
|
||||
];
|
||||
private const SIMPLIFIED_CONFIG_SIDE_EFFECTS = [
|
||||
'delete_short_url_threshold' => [
|
||||
|
@ -34,6 +34,7 @@ class ShortUrl extends AbstractEntity
|
||||
private ?int $maxVisits = null;
|
||||
private ?Domain $domain;
|
||||
private bool $customSlugWasProvided;
|
||||
private int $shortCodeLength;
|
||||
|
||||
public function __construct(
|
||||
string $longUrl,
|
||||
@ -50,7 +51,8 @@ class ShortUrl extends AbstractEntity
|
||||
$this->validUntil = $meta->getValidUntil();
|
||||
$this->maxVisits = $meta->getMaxVisits();
|
||||
$this->customSlugWasProvided = $meta->hasCustomSlug();
|
||||
$this->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode();
|
||||
$this->shortCodeLength = $meta->getShortCodeLength();
|
||||
$this->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode($this->shortCodeLength);
|
||||
$this->domain = ($domainResolver ?? new SimpleDomainResolver())->resolveDomain($meta->getDomain());
|
||||
}
|
||||
|
||||
@ -119,7 +121,7 @@ class ShortUrl extends AbstractEntity
|
||||
throw ShortCodeCannotBeRegeneratedException::forShortUrlAlreadyPersisted();
|
||||
}
|
||||
|
||||
$this->shortCode = generateRandomShortCode();
|
||||
$this->shortCode = generateRandomShortCode($this->shortCodeLength);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,8 @@ use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
|
||||
use function array_key_exists;
|
||||
use function Shlinkio\Shlink\Core\parseDateField;
|
||||
|
||||
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
|
||||
|
||||
final class ShortUrlMeta
|
||||
{
|
||||
private bool $validSincePropWasProvided = false;
|
||||
@ -22,6 +24,7 @@ final class ShortUrlMeta
|
||||
private ?int $maxVisits = null;
|
||||
private ?bool $findIfExists = null;
|
||||
private ?string $domain = null;
|
||||
private int $shortCodeLength = 5;
|
||||
|
||||
// Force named constructors
|
||||
private function __construct()
|
||||
@ -58,11 +61,20 @@ final class ShortUrlMeta
|
||||
$this->validUntil = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL));
|
||||
$this->validUntilPropWasProvided = array_key_exists(ShortUrlMetaInputFilter::VALID_UNTIL, $data);
|
||||
$this->customSlug = $inputFilter->getValue(ShortUrlMetaInputFilter::CUSTOM_SLUG);
|
||||
$maxVisits = $inputFilter->getValue(ShortUrlMetaInputFilter::MAX_VISITS);
|
||||
$this->maxVisits = $maxVisits !== null ? (int) $maxVisits : null;
|
||||
$this->maxVisits = $this->getOptionalIntFromInputFilter($inputFilter, ShortUrlMetaInputFilter::MAX_VISITS);
|
||||
$this->maxVisitsPropWasProvided = array_key_exists(ShortUrlMetaInputFilter::MAX_VISITS, $data);
|
||||
$this->findIfExists = $inputFilter->getValue(ShortUrlMetaInputFilter::FIND_IF_EXISTS);
|
||||
$this->domain = $inputFilter->getValue(ShortUrlMetaInputFilter::DOMAIN);
|
||||
$this->shortCodeLength = $this->getOptionalIntFromInputFilter(
|
||||
$inputFilter,
|
||||
ShortUrlMetaInputFilter::SHORT_CODE_LENGTH,
|
||||
) ?? DEFAULT_SHORT_CODES_LENGTH;
|
||||
}
|
||||
|
||||
private function getOptionalIntFromInputFilter(ShortUrlMetaInputFilter $inputFilter, string $fieldName): ?int
|
||||
{
|
||||
$value = $inputFilter->getValue($fieldName);
|
||||
return $value !== null ? (int) $value : null;
|
||||
}
|
||||
|
||||
public function getValidSince(): ?Chronos
|
||||
@ -119,4 +131,9 @@ final class ShortUrlMeta
|
||||
{
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
public function getShortCodeLength(): int
|
||||
{
|
||||
return $this->shortCodeLength;
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,13 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Validation;
|
||||
|
||||
use DateTime;
|
||||
use Laminas\InputFilter\Input;
|
||||
use Laminas\InputFilter\InputFilter;
|
||||
use Laminas\Validator;
|
||||
use Shlinkio\Shlink\Common\Validation;
|
||||
|
||||
use const Shlinkio\Shlink\Core\MIN_SHORT_CODES_LENGTH;
|
||||
|
||||
class ShortUrlMetaInputFilter extends InputFilter
|
||||
{
|
||||
use Validation\InputFactoryTrait;
|
||||
@ -19,6 +22,7 @@ class ShortUrlMetaInputFilter extends InputFilter
|
||||
public const MAX_VISITS = 'maxVisits';
|
||||
public const FIND_IF_EXISTS = 'findIfExists';
|
||||
public const DOMAIN = 'domain';
|
||||
public const SHORT_CODE_LENGTH = 'shortCodeLength';
|
||||
|
||||
public function __construct(array $data)
|
||||
{
|
||||
@ -40,10 +44,8 @@ class ShortUrlMetaInputFilter extends InputFilter
|
||||
$customSlug->getFilterChain()->attach(new Validation\SluggerFilter());
|
||||
$this->add($customSlug);
|
||||
|
||||
$maxVisits = $this->createInput(self::MAX_VISITS, false);
|
||||
$maxVisits->getValidatorChain()->attach(new Validator\Digits())
|
||||
->attach(new Validator\GreaterThan(['min' => 1, 'inclusive' => true]));
|
||||
$this->add($maxVisits);
|
||||
$this->add($this->createPositiveNumberInput(self::MAX_VISITS));
|
||||
$this->add($this->createPositiveNumberInput(self::SHORT_CODE_LENGTH, MIN_SHORT_CODES_LENGTH));
|
||||
|
||||
$this->add($this->createBooleanInput(self::FIND_IF_EXISTS, false));
|
||||
|
||||
@ -51,4 +53,13 @@ class ShortUrlMetaInputFilter extends InputFilter
|
||||
$domain->getValidatorChain()->attach(new Validation\HostAndPortValidator());
|
||||
$this->add($domain);
|
||||
}
|
||||
|
||||
private function createPositiveNumberInput(string $name, int $min = 1): Input
|
||||
{
|
||||
$input = $this->createInput($name, false);
|
||||
$input->getValidatorChain()->attach(new Validator\Digits())
|
||||
->attach(new Validator\GreaterThan(['min' => $min, 'inclusive' => true]));
|
||||
|
||||
return $input;
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ class SimplifiedConfigParserTest extends TestCase
|
||||
'http://my-api.com/api/v2.3/notify',
|
||||
'https://third-party.io/foo',
|
||||
],
|
||||
'default_short_codes_length' => 8,
|
||||
];
|
||||
$expected = [
|
||||
'app_options' => [
|
||||
@ -84,6 +85,7 @@ class SimplifiedConfigParserTest extends TestCase
|
||||
'http://my-api.com/api/v2.3/notify',
|
||||
'https://third-party.io/foo',
|
||||
],
|
||||
'default_short_codes_length' => 8,
|
||||
],
|
||||
|
||||
'delete_short_urls' => [
|
||||
|
@ -8,6 +8,13 @@ use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortCodeCannotBeRegeneratedException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
|
||||
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
use function strlen;
|
||||
|
||||
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
|
||||
|
||||
class ShortUrlTest extends TestCase
|
||||
{
|
||||
@ -48,4 +55,23 @@ class ShortUrlTest extends TestCase
|
||||
|
||||
$this->assertNotEquals($firstShortCode, $secondShortCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideLengths
|
||||
*/
|
||||
public function shortCodesHaveExpectedLength(?int $length, int $expectedLength): void
|
||||
{
|
||||
$shortUrl = new ShortUrl('', ShortUrlMeta::fromRawData(
|
||||
[ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $length],
|
||||
));
|
||||
|
||||
$this->assertEquals($expectedLength, strlen($shortUrl->getShortCode()));
|
||||
}
|
||||
|
||||
public function provideLengths(): iterable
|
||||
{
|
||||
yield [null, DEFAULT_SHORT_CODES_LENGTH];
|
||||
yield from map(range(4, 10), fn (int $value) => [$value, $value]);
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,9 @@ class ShortUrlMetaTest extends TestCase
|
||||
ShortUrlMetaInputFilter::VALID_UNTIL => 500,
|
||||
ShortUrlMetaInputFilter::DOMAIN => 4,
|
||||
]];
|
||||
yield [[
|
||||
ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => 3,
|
||||
]];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
@ -38,6 +38,7 @@ return [
|
||||
Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
|
||||
Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class => InvokableFactory::class,
|
||||
Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class => ConfigAbstractFactory::class,
|
||||
Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
@ -75,6 +76,9 @@ return [
|
||||
Action\Tag\UpdateTagAction::class => [Service\Tag\TagService::class, LoggerInterface::class],
|
||||
|
||||
Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class => ['config.url_shortener.domain.hostname'],
|
||||
Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class => [
|
||||
'config.url_shortener.default_short_codes_length',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -13,7 +13,11 @@ return [
|
||||
Action\HealthAction::getRouteDef(),
|
||||
|
||||
// Short codes
|
||||
Action\ShortUrl\CreateShortUrlAction::getRouteDef([$contentNegotiationMiddleware, $dropDomainMiddleware]),
|
||||
Action\ShortUrl\CreateShortUrlAction::getRouteDef([
|
||||
$contentNegotiationMiddleware,
|
||||
$dropDomainMiddleware,
|
||||
Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class,
|
||||
]),
|
||||
Action\ShortUrl\SingleStepCreateShortUrlAction::getRouteDef([$contentNegotiationMiddleware]),
|
||||
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\DeleteShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
|
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Middleware\ShortUrl;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
|
||||
|
||||
class DefaultShortCodesLengthMiddleware implements MiddlewareInterface
|
||||
{
|
||||
private int $defaultShortCodesLength;
|
||||
|
||||
public function __construct(int $defaultShortCodesLength)
|
||||
{
|
||||
$this->defaultShortCodesLength = $defaultShortCodesLength;
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$body = $request->getParsedBody();
|
||||
if (! isset($body[ShortUrlMetaInputFilter::SHORT_CODE_LENGTH])) {
|
||||
$body[ShortUrlMetaInputFilter::SHORT_CODE_LENGTH] = $this->defaultShortCodesLength;
|
||||
}
|
||||
|
||||
return $handler->handle($request->withParsedBody($body));
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioApiTest\Shlink\Rest\Middleware\ShortUrl;
|
||||
|
||||
use Laminas\Diactoros\Response;
|
||||
use Laminas\Diactoros\ServerRequestFactory;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
|
||||
use Shlinkio\Shlink\Rest\Middleware\ShortUrl\DefaultShortCodesLengthMiddleware;
|
||||
|
||||
class DefaultShortCodesLengthMiddlewareTest extends TestCase
|
||||
{
|
||||
private DefaultShortCodesLengthMiddleware $middleware;
|
||||
private ObjectProphecy $handler;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->handler = $this->prophesize(RequestHandlerInterface::class);
|
||||
$this->middleware = new DefaultShortCodesLengthMiddleware(8);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideBodies
|
||||
*/
|
||||
public function defaultValueIsInjectedInBodyWhenNotProvided(array $body, int $expectedLength): void
|
||||
{
|
||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody($body);
|
||||
$handle = $this->handler->handle(Argument::that(function (ServerRequestInterface $req) use ($expectedLength) {
|
||||
$parsedBody = $req->getParsedBody();
|
||||
Assert::assertArrayHasKey(ShortUrlMetaInputFilter::SHORT_CODE_LENGTH, $parsedBody);
|
||||
Assert::assertEquals($expectedLength, $parsedBody[ShortUrlMetaInputFilter::SHORT_CODE_LENGTH]);
|
||||
|
||||
return $req;
|
||||
}))->willReturn(new Response());
|
||||
|
||||
$this->middleware->process($request, $this->handler->reveal());
|
||||
|
||||
$handle->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideBodies(): iterable
|
||||
{
|
||||
yield 'value provided' => [[ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => 6], 6];
|
||||
yield 'value not provided' => [[], 8];
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user