Removed several deprecated components

This commit is contained in:
Alejandro Celaya 2019-12-31 15:38:37 +01:00
parent 78b484e657
commit 434b56fa8c
41 changed files with 16 additions and 952 deletions

View file

@ -2,14 +2,11 @@
declare(strict_types=1);
use function Shlinkio\Shlink\Common\env;
return [
'app_options' => [
'name' => 'Shlink',
'version' => '%SHLINK_VERSION%',
'secret_key' => env('SECRET_KEY', ''),
'disable_track_param' => null,
],

View file

@ -15,10 +15,6 @@ return [
],
],
'backwards_compatible_problem_details' => [
'json_flags' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION,
],
'error_handler' => [
'listeners' => [Logger\ErrorLogger::class],
],

View file

@ -21,7 +21,6 @@ return [
'path' => '/rest',
'middleware' => [
Rest\Middleware\CrossDomainMiddleware::class,
Rest\Middleware\BackwardsCompatibleProblemDetailsMiddleware::class,
ProblemDetails\ProblemDetailsMiddleware::class,
],
],
@ -35,7 +34,6 @@ return [
'path' => '/rest',
'middleware' => [
Rest\Middleware\PathVersionMiddleware::class,
Rest\Middleware\ShortUrl\ShortCodePathMiddleware::class,
],
],

View file

@ -119,7 +119,6 @@ This is the complete list of supported env vars:
In the future, these redis servers could be used for other caching operations performed by shlink.
* `NOT_FOUND_REDIRECT_TO`: **Deprecated since v1.20 in favor of `INVALID_SHORT_URL_REDIRECT_TO`** If a URL is provided here, when a user tries to access an invalid short URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
* `SHORTCODE_CHARS`: **Ignored when using Shlink 1.20 or newer**. A charset to use when building short codes. Only needed when using more than one shlink instance ([Multi instance considerations](#multi-instance-considerations)).
An example using all env vars could look like this:
@ -186,15 +185,12 @@ The whole configuration should have this format, but it can be split into multip
"password": "123abc",
"host": "something.rds.amazonaws.com",
"port": "3306"
},
"not_found_redirect_to": "https://my-landing-page.com"
}
}
```
> This is internally parsed to how shlink expects the config. If you are using a version previous to 1.17.0, this parser is not present and you need to provide a config structure like the one [documented previously](https://github.com/shlinkio/shlink-docker-image/tree/v1.16.3#provide-config-via-volumes).
> The `not_found_redirect_to` option has been deprecated in v1.20. Use `invalid_short_url_redirect_to` instead (however, it will still work for backwards compatibility).
Once created just run shlink with the volume:
```bash

View file

@ -8,19 +8,10 @@ use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use function explode;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function Functional\contains;
use function implode;
use function Shlinkio\Shlink\Common\env;
use function sprintf;
use function str_shuffle;
use function substr;
use function sys_get_temp_dir;
$helper = new class {
private const BASE62 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
private const DB_DRIVERS_MAP = [
'mysql' => 'pdo_mysql',
'maria' => 'pdo_mysql',
@ -32,40 +23,6 @@ $helper = new class {
'postgres' => '5432',
];
/** @var string */
private $secretKey;
public function __construct()
{
[, $this->secretKey] = $this->initShlinkSecretKey();
}
private function initShlinkSecretKey(): array
{
$keysFile = sprintf('%s/shlink.keys', sys_get_temp_dir());
if (file_exists($keysFile)) {
return explode(',', file_get_contents($keysFile));
}
$keys = [
'', // This was the SHORTCODE_CHARS. Kept as empty string for BC
env('SECRET_KEY', $this->generateSecretKey()), // Deprecated
];
file_put_contents($keysFile, implode(',', $keys));
return $keys;
}
private function generateSecretKey(): string
{
return substr(str_shuffle(self::BASE62), 0, 32);
}
public function getSecretKey(): string
{
return $this->secretKey;
}
public function getDbConfig(): array
{
$driver = env('DB_DRIVER');
@ -94,7 +51,7 @@ $helper = new class {
public function getNotFoundRedirectsConfig(): array
{
return [
'invalid_short_url' => env('INVALID_SHORT_URL_REDIRECT_TO', env('NOT_FOUND_REDIRECT_TO')),
'invalid_short_url' => env('INVALID_SHORT_URL_REDIRECT_TO'),
'regular_404' => env('REGULAR_404_REDIRECT_TO'),
'base_url' => env('BASE_URL_REDIRECT_TO'),
];
@ -112,7 +69,6 @@ return [
'config_cache_enabled' => false,
'app_options' => [
'secret_key' => $helper->getSecretKey(),
'disable_track_param' => env('DISABLE_TRACK_PARAM'),
],

View file

@ -15,10 +15,6 @@ return [
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::class,
Command\Visit\UpdateDbCommand::NAME => Command\Visit\UpdateDbCommand::class,
Command\Config\GenerateCharsetCommand::NAME => Command\Config\GenerateCharsetCommand::class,
Command\Config\GenerateSecretCommand::NAME => Command\Config\GenerateSecretCommand::class,
Command\Api\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class,
Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class,

View file

@ -36,10 +36,6 @@ return [
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class,
Command\Visit\UpdateDbCommand::class => ConfigAbstractFactory::class,
Command\Config\GenerateCharsetCommand::class => InvokableFactory::class,
Command\Config\GenerateSecretCommand::class => InvokableFactory::class,
Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class,
Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class,
@ -70,7 +66,6 @@ return [
LockFactory::class,
GeolocationDbUpdater::class,
],
Command\Visit\UpdateDbCommand::class => [DbUpdater::class],
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class],
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],

View file

@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Config;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
use function str_shuffle;
/** @deprecated */
class GenerateCharsetCommand extends Command
{
public const NAME = 'config:generate-charset';
private const DEFAULT_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription(sprintf(
'[DEPRECATED] Generates a character set sample just by shuffling the default one, "%s". '
. 'Then it can be set in the SHORTCODE_CHARS environment variable',
self::DEFAULT_CHARS
))
->setHelp('<fg=red;options=bold>This command is deprecated. Better leave shlink generate the charset.</>');
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$charSet = str_shuffle(self::DEFAULT_CHARS);
(new SymfonyStyle($input, $output))->success(sprintf('Character set: "%s"', $charSet));
return ExitCodes::EXIT_SUCCESS;
}
}

View file

@ -1,39 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Config;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
/** @deprecated */
class GenerateSecretCommand extends Command
{
use StringUtilsTrait;
public const NAME = 'config:generate-secret';
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription('[DEPRECATED] Generates a random secret string that can be used for JWT token encryption')
->setHelp(
'<fg=red;options=bold>This command is deprecated. Better leave shlink generate the secret key.</>'
);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$secret = $this->generateRandomString(32);
(new SymfonyStyle($input, $output))->success(sprintf('Secret key: "%s"', $secret));
return ExitCodes::EXIT_SUCCESS;
}
}

View file

@ -1,91 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
/** @deprecated */
class UpdateDbCommand extends Command
{
public const NAME = 'visit:update-db';
/** @var DbUpdaterInterface */
private $geoLiteDbUpdater;
public function __construct(DbUpdaterInterface $geoLiteDbUpdater)
{
parent::__construct();
$this->geoLiteDbUpdater = $geoLiteDbUpdater;
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription('[DEPRECATED] Updates the GeoLite2 database file used to geolocate IP addresses')
->setHelp(
'The GeoLite2 database is updated first Tuesday every month, so this command should be ideally run '
. 'every first Wednesday'
)
->addOption(
'ignoreErrors',
'i',
InputOption::VALUE_NONE,
'Makes the command success even iof the update fails.'
);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
$progressBar = new ProgressBar($output);
$progressBar->start();
try {
$this->geoLiteDbUpdater->downloadFreshCopy(function (int $total, int $downloaded) use ($progressBar) {
$progressBar->setMaxSteps($total);
$progressBar->setProgress($downloaded);
});
$progressBar->finish();
$io->newLine();
$io->success('GeoLite2 database properly updated');
return ExitCodes::EXIT_SUCCESS;
} catch (RuntimeException $e) {
$progressBar->finish();
$io->newLine();
return $this->handleError($e, $io, $input);
}
}
private function handleError(RuntimeException $e, SymfonyStyle $io, InputInterface $input): int
{
$ignoreErrors = $input->getOption('ignoreErrors');
$baseErrorMsg = 'An error occurred while updating GeoLite2 database';
if ($ignoreErrors) {
$io->warning(sprintf('%s, but it was ignored', $baseErrorMsg));
return ExitCodes::EXIT_SUCCESS;
}
$io->error($baseErrorMsg);
if ($io->isVerbose()) {
$this->getApplication()->renderThrowable($e, $io);
}
return ExitCodes::EXIT_FAILURE;
}
}

View file

@ -1,47 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Config;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use function implode;
use function sort;
use function str_split;
class GenerateCharsetCommandTest extends TestCase
{
private CommandTester $commandTester;
public function setUp(): void
{
$command = new GenerateCharsetCommand();
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/** @test */
public function charactersAreGeneratedFromDefault()
{
$prefix = 'Character set: ';
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
// Both default character set and the new one should have the same length
$this->assertStringContainsString($prefix, $output);
}
protected function orderStringLetters($string)
{
$letters = str_split($string);
sort($letters);
return implode('', $letters);
}
}

View file

@ -1,75 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Visit\UpdateDbCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
class UpdateDbCommandTest extends TestCase
{
private CommandTester $commandTester;
private ObjectProphecy $dbUpdater;
public function setUp(): void
{
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
$command = new UpdateDbCommand($this->dbUpdater->reveal());
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/** @test */
public function successMessageIsPrintedIfEverythingWorks(): void
{
$download = $this->dbUpdater->downloadFreshCopy(Argument::type('callable'))->will(function () {
});
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$exitCode = $this->commandTester->getStatusCode();
$this->assertStringContainsString('GeoLite2 database properly updated', $output);
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
$download->shouldHaveBeenCalledOnce();
}
/** @test */
public function errorMessageIsPrintedIfAnExceptionIsThrown(): void
{
$download = $this->dbUpdater->downloadFreshCopy(Argument::type('callable'))->willThrow(RuntimeException::class);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$exitCode = $this->commandTester->getStatusCode();
$this->assertStringContainsString('An error occurred while updating GeoLite2 database', $output);
$this->assertEquals(ExitCodes::EXIT_FAILURE, $exitCode);
$download->shouldHaveBeenCalledOnce();
}
/** @test */
public function warningMessageIsPrintedIfAnExceptionIsThrownAndErrorsAreIgnored(): void
{
$download = $this->dbUpdater->downloadFreshCopy(Argument::type('callable'))->willThrow(RuntimeException::class);
$this->commandTester->execute(['--ignoreErrors' => true]);
$output = $this->commandTester->getDisplay();
$exitCode = $this->commandTester->getStatusCode();
$this->assertStringContainsString('ignored', $output);
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
$download->shouldHaveBeenCalledOnce();
}
}

View file

@ -1,9 +0,0 @@
<?php
declare(strict_types=1);
return [
'app_options' => [],
];

View file

@ -22,7 +22,6 @@ class SimplifiedConfigParser
'short_domain_schema' => ['url_shortener', 'domain', 'schema'],
'short_domain_host' => ['url_shortener', 'domain', 'hostname'],
'validate_url' => ['url_shortener', 'validate_url'],
'not_found_redirect_to' => ['not_found_redirects', 'invalid_short_url'], // Deprecated
'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_path'],

View file

@ -90,9 +90,6 @@ class Visit extends AbstractEntity implements JsonSerializable
'date' => $this->date->toAtomString(),
'userAgent' => $this->userAgent,
'visitLocation' => $this->visitLocation,
// Deprecated
'remoteAddr' => null,
];
}

View file

@ -78,7 +78,7 @@ class NotifyVisitToWebHooks
'User-Agent' => (string) $this->appOptions,
],
RequestOptions::JSON => [
'shortUrl' => $this->transformer->transform($visit->getShortUrl(), false),
'shortUrl' => $this->transformer->transform($visit->getShortUrl()),
'visit' => $visit->jsonSerialize(),
],
];

View file

@ -15,8 +15,6 @@ class AppOptions extends AbstractOptions
private string $name = '';
private string $version = '1.0';
/** @deprecated */
private string $secretKey = '';
private ?string $disableTrackParam = null;
public function getName(): string
@ -41,23 +39,6 @@ class AppOptions extends AbstractOptions
return $this;
}
/**
* @deprecated
*/
public function getSecretKey(): string
{
return $this->secretKey;
}
/**
* @deprecated
*/
protected function setSecretKey(string $secretKey): self
{
$this->secretKey = $secretKey;
return $this;
}
/**
* @return string|null
*/

View file

@ -22,11 +22,11 @@ class ShortUrlDataTransformer implements DataTransformerInterface
/**
* @param ShortUrl $shortUrl
*/
public function transform($shortUrl, bool $includeDeprecated = true): array
public function transform($shortUrl): array
{
$longUrl = $shortUrl->getLongUrl();
$rawData = [
return [
'shortCode' => $shortUrl->getShortCode(),
'shortUrl' => $shortUrl->toString($this->domainConfig),
'longUrl' => $longUrl,
@ -35,12 +35,6 @@ class ShortUrlDataTransformer implements DataTransformerInterface
'tags' => invoke($shortUrl->getTags(), '__toString'),
'meta' => $this->buildMeta($shortUrl),
];
if ($includeDeprecated) {
$rawData['originalUrl'] = $longUrl;
}
return $rawData;
}
private function buildMeta(ShortUrl $shortUrl): array

View file

@ -11,7 +11,7 @@ use function array_merge;
class SimplifiedConfigParserTest extends TestCase
{
private $postProcessor;
private SimplifiedConfigParser $postProcessor;
public function setUp(): void
{
@ -40,7 +40,7 @@ class SimplifiedConfigParserTest extends TestCase
'short_domain_host' => 'doma.in',
'validate_url' => false,
'delete_short_url_threshold' => 50,
'not_found_redirect_to' => 'foobar.com',
'invalid_short_url_redirect_to' => 'foobar.com',
'redis_servers' => [
'tcp://1.1.1.1:1111',
'tcp://1.2.2.2:2222',
@ -125,28 +125,4 @@ class SimplifiedConfigParserTest extends TestCase
$this->assertEquals(array_merge($expected, $simplified), $result);
}
/**
* @test
* @dataProvider provideConfigWithDeprecates
*/
public function properlyMapsDeprecatedConfigs(array $config, string $expected): void
{
$result = ($this->postProcessor)($config);
$this->assertEquals($expected, $result['not_found_redirects']['invalid_short_url']);
}
public function provideConfigWithDeprecates(): iterable
{
yield 'only deprecated config' => [['not_found_redirect_to' => 'old_value'], 'old_value'];
yield 'only new config' => [['invalid_short_url_redirect_to' => 'new_value'], 'new_value'];
yield 'both configs, new first' => [
['invalid_short_url_redirect_to' => 'new_value', 'not_found_redirect_to' => 'old_value'],
'new_value',
];
yield 'both configs, deprecated first' => [
['not_found_redirect_to' => 'old_value', 'invalid_short_url_redirect_to' => 'new_value'],
'new_value',
];
}
}

View file

@ -25,9 +25,6 @@ class VisitTest extends TestCase
'date' => ($date ?? $visit->getDate())->toAtomString(),
'userAgent' => 'Chrome',
'visitLocation' => null,
// Deprecated
'remoteAddr' => null,
], $visit->jsonSerialize());
}

View file

@ -10,7 +10,6 @@ return [
'auth' => [
'routes_whitelist' => [
Action\AuthenticateAction::class,
Action\HealthAction::class,
Action\ShortUrl\SingleStepCreateShortUrlAction::class,
],

View file

@ -20,7 +20,6 @@ return [
Authentication\JWTService::class => ConfigAbstractFactory::class,
ApiKeyService::class => ConfigAbstractFactory::class,
Action\AuthenticateAction::class => ConfigAbstractFactory::class,
Action\HealthAction::class => ConfigAbstractFactory::class,
Action\ShortUrl\CreateShortUrlAction::class => ConfigAbstractFactory::class,
Action\ShortUrl\SingleStepCreateShortUrlAction::class => ConfigAbstractFactory::class,
@ -39,9 +38,7 @@ return [
Middleware\BodyParserMiddleware::class => InvokableFactory::class,
Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
Middleware\PathVersionMiddleware::class => InvokableFactory::class,
Middleware\BackwardsCompatibleProblemDetailsMiddleware::class => ConfigAbstractFactory::class,
Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class => InvokableFactory::class,
Middleware\ShortUrl\ShortCodePathMiddleware::class => InvokableFactory::class,
],
],
@ -49,7 +46,6 @@ return [
Authentication\JWTService::class => [AppOptions::class],
ApiKeyService::class => ['em'],
Action\AuthenticateAction::class => [ApiKeyService::class, Authentication\JWTService::class, 'Logger_Shlink'],
Action\HealthAction::class => [Connection::class, AppOptions::class, 'Logger_Shlink'],
Action\ShortUrl\CreateShortUrlAction::class => [
Service\UrlShortener::class,
@ -76,10 +72,6 @@ return [
Action\Tag\DeleteTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class],
Action\Tag\CreateTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class],
Action\Tag\UpdateTagAction::class => [Service\Tag\TagService::class, LoggerInterface::class],
Middleware\BackwardsCompatibleProblemDetailsMiddleware::class => [
'config.backwards_compatible_problem_details.json_flags',
],
],
];

View file

@ -7,7 +7,6 @@ namespace Shlinkio\Shlink\Rest;
return [
'routes' => [
Action\AuthenticateAction::getRouteDef(),
Action\HealthAction::getRouteDef(),
// Short codes

View file

@ -1,64 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Zend\Diactoros\Response\JsonResponse;
/** @deprecated */
class AuthenticateAction extends AbstractRestAction
{
protected const ROUTE_PATH = '/authenticate';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_POST];
/** @var ApiKeyService|ApiKeyServiceInterface */
private $apiKeyService;
/** @var JWTServiceInterface */
private $jwtService;
public function __construct(
ApiKeyServiceInterface $apiKeyService,
JWTServiceInterface $jwtService,
?LoggerInterface $logger = null
) {
parent::__construct($logger);
$this->apiKeyService = $apiKeyService;
$this->jwtService = $jwtService;
}
/**
* @param Request $request
* @return Response
* @throws \InvalidArgumentException
*/
public function handle(Request $request): Response
{
$authData = $request->getParsedBody();
if (! isset($authData['apiKey'])) {
return new JsonResponse([
'error' => 'INVALID_ARGUMENT',
'message' => 'You have to provide a valid API key under the "apiKey" param name.',
], self::STATUS_BAD_REQUEST);
}
// Authenticate using provided API key
$apiKey = $this->apiKeyService->getByKey($authData['apiKey']);
if ($apiKey === null || ! $apiKey->isValid()) {
return new JsonResponse([
'error' => 'INVALID_API_KEY',
'message' => 'Provided API key does not exist or is invalid.',
], self::STATUS_UNAUTHORIZED);
}
// Generate a JSON Web Token that will be used for authorization in next requests
$token = $this->jwtService->create($apiKey);
return new JsonResponse(['token' => $token]);
}
}

View file

@ -97,7 +97,7 @@ class JWTService implements JWTServiceInterface
*/
private function encode(array $data): string
{
return JWT::encode($data, $this->appOptions->getSecretKey(), self::DEFAULT_ENCRYPTION_ALG);
return JWT::encode($data, '', self::DEFAULT_ENCRYPTION_ALG);
}
/**
@ -106,6 +106,6 @@ class JWTService implements JWTServiceInterface
*/
private function decode(string $jwt): array
{
return (array) JWT::decode($jwt, $this->appOptions->getSecretKey(), [self::DEFAULT_ENCRYPTION_ALG]);
return (array) JWT::decode($jwt, '', [self::DEFAULT_ENCRYPTION_ALG]);
}
}

View file

@ -1,63 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
use Zend\Diactoros\Response\JsonResponse;
use function Functional\reduce_left;
use function Shlinkio\Shlink\Common\json_decode;
use function strpos;
/** @deprecated */
class BackwardsCompatibleProblemDetailsMiddleware implements MiddlewareInterface
{
private const BACKWARDS_COMPATIBLE_FIELDS = [
'error' => 'type',
'message' => 'detail',
];
private int $jsonFlags;
public function __construct(int $jsonFlags)
{
$this->jsonFlags = $jsonFlags;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$resp = $handler->handle($request);
if ($resp->getHeaderLine('Content-type') !== 'application/problem+json' || ! $this->isVersionOne($request)) {
return $resp;
}
try {
$body = (string) $resp->getBody();
$payload = $this->makePayloadBackwardsCompatible(json_decode($body));
} catch (Throwable $e) {
return $resp;
}
return new JsonResponse($payload, $resp->getStatusCode(), $resp->getHeaders(), $this->jsonFlags);
}
private function isVersionOne(ServerRequestInterface $request): bool
{
$path = $request->getUri()->getPath();
return strpos($path, '/v') === false || strpos($path, '/v1') === 0;
}
private function makePayloadBackwardsCompatible(array $payload): array
{
return reduce_left(self::BACKWARDS_COMPATIBLE_FIELDS, function (string $newKey, string $oldKey, $c, $acc) {
$acc[$oldKey] = $acc[$newKey];
return $acc;
}, $payload);
}
}

View file

@ -1,34 +0,0 @@
<?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 function str_replace;
/** @deprecated */
class ShortCodePathMiddleware implements MiddlewareInterface
{
private const OLD_PATH_PREFIX = '/short-codes'; // Old path is deprecated. Remove this middleware on v2
private const NEW_PATH_PREFIX = '/short-urls';
/**
* Process an incoming server request and return a response, optionally delegating
* response creation to a handler.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$uri = $request->getUri();
$path = $uri->getPath();
// If the path starts with the old prefix, replace it by the new one
return $handler->handle(
$request->withUri($uri->withPath(str_replace(self::OLD_PATH_PREFIX, self::NEW_PATH_PREFIX, $path)))
);
}
}

View file

@ -49,9 +49,7 @@ class CreateShortUrlActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_BAD_REQUEST, $statusCode);
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
$this->assertEquals($detail, $payload['detail']);
$this->assertEquals($detail, $payload['message']); // Deprecated
$this->assertEquals('INVALID_SLUG', $payload['type']);
$this->assertEquals('INVALID_SLUG', $payload['error']); // Deprecated
$this->assertEquals('Invalid custom slug', $payload['title']);
$this->assertEquals($slug, $payload['customSlug']);
@ -215,7 +213,7 @@ class CreateShortUrlActionTest extends ApiTestCase
}
/** @test */
public function failsToCreateShortUrlWithInvalidOriginalUrl(): void
public function failsToCreateShortUrlWithInvalidLongUrl(): void
{
$url = 'https://this-has-to-be-invalid.com';
$expectedDetail = sprintf('Provided URL %s is invalid. Try with a different one.', $url);
@ -225,9 +223,7 @@ class CreateShortUrlActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_BAD_REQUEST, $statusCode);
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
$this->assertEquals('INVALID_URL', $payload['type']);
$this->assertEquals('INVALID_URL', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid URL', $payload['title']);
$this->assertEquals($url, $payload['url']);
}

View file

@ -19,9 +19,7 @@ class DeleteShortUrlActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Short URL not found', $payload['title']);
$this->assertEquals('invalid', $payload['shortCode']);
}
@ -41,9 +39,7 @@ class DeleteShortUrlActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_UNPROCESSABLE_ENTITY, $resp->getStatusCode());
$this->assertEquals(self::STATUS_UNPROCESSABLE_ENTITY, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE_DELETION', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE_DELETION', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Cannot delete short URL', $payload['title']);
}
}

View file

@ -20,9 +20,7 @@ class EditShortUrlActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Short URL not found', $payload['title']);
$this->assertEquals('invalid', $payload['shortCode']);
}
@ -40,9 +38,7 @@ class EditShortUrlActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode());
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
$this->assertEquals('INVALID_ARGUMENT', $payload['type']);
$this->assertEquals('INVALID_ARGUMENT', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid data', $payload['title']);
}
}

View file

@ -20,9 +20,7 @@ class EditShortUrlTagsActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode());
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
$this->assertEquals('INVALID_ARGUMENT', $payload['type']);
$this->assertEquals('INVALID_ARGUMENT', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid data', $payload['title']);
}
@ -39,9 +37,7 @@ class EditShortUrlTagsActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Short URL not found', $payload['title']);
$this->assertEquals('invalid', $payload['shortCode']);
}

View file

@ -19,9 +19,7 @@ class GetVisitsActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Short URL not found', $payload['title']);
$this->assertEquals('invalid', $payload['shortCode']);
}

View file

@ -24,7 +24,6 @@ class ListShortUrlsTest extends ApiTestCase
'validUntil' => null,
'maxVisits' => null,
],
'originalUrl' => 'https://shlink.io',
];
private const SHORT_URL_CUSTOM_SLUG_AND_DOMAIN = [
'shortCode' => 'custom-with-domain',
@ -38,7 +37,6 @@ class ListShortUrlsTest extends ApiTestCase
'validUntil' => null,
'maxVisits' => null,
],
'originalUrl' => 'https://google.com',
];
private const SHORT_URL_META = [
'shortCode' => 'def456',
@ -54,9 +52,6 @@ class ListShortUrlsTest extends ApiTestCase
'validUntil' => null,
'maxVisits' => null,
],
'originalUrl' =>
'https://blog.alejandrocelaya.com/2017/12/09'
. '/acmailer-7-0-the-most-important-release-in-a-long-time/',
];
private const SHORT_URL_CUSTOM_SLUG = [
'shortCode' => 'custom',
@ -70,7 +65,6 @@ class ListShortUrlsTest extends ApiTestCase
'validUntil' => null,
'maxVisits' => 2,
],
'originalUrl' => 'https://shlink.io',
];
private const SHORT_URL_CUSTOM_DOMAIN = [
'shortCode' => 'ghi789',
@ -86,9 +80,6 @@ class ListShortUrlsTest extends ApiTestCase
'validUntil' => null,
'maxVisits' => null,
],
'originalUrl' =>
'https://blog.alejandrocelaya.com/2019/04/27'
. '/considerations-to-properly-use-open-source-software-projects/',
];
/**

View file

@ -19,9 +19,7 @@ class ResolveShortUrlActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Short URL not found', $payload['title']);
$this->assertEquals('invalid', $payload['shortCode']);
}

View file

@ -23,9 +23,7 @@ class UpdateTagActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode());
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
$this->assertEquals('INVALID_ARGUMENT', $payload['type']);
$this->assertEquals('INVALID_ARGUMENT', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid data', $payload['title']);
}
@ -50,9 +48,7 @@ class UpdateTagActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('TAG_NOT_FOUND', $payload['type']);
$this->assertEquals('TAG_NOT_FOUND', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Tag not found', $payload['title']);
}
@ -70,9 +66,7 @@ class UpdateTagActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_CONFLICT, $resp->getStatusCode());
$this->assertEquals(self::STATUS_CONFLICT, $payload['status']);
$this->assertEquals('TAG_CONFLICT', $payload['type']);
$this->assertEquals('TAG_CONFLICT', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Tag conflict', $payload['title']);
}

View file

@ -21,15 +21,13 @@ class AuthenticationTest extends ApiTestCase
implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS)
);
$resp = $this->callApi(self::METHOD_GET, '/short-codes');
$resp = $this->callApi(self::METHOD_GET, '/short-urls');
$payload = $this->getJsonResponsePayload($resp);
$this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode());
$this->assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']);
$this->assertEquals('INVALID_AUTHORIZATION', $payload['type']);
$this->assertEquals('INVALID_AUTHORIZATION', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid authorization', $payload['title']);
}
@ -41,7 +39,7 @@ class AuthenticationTest extends ApiTestCase
{
$expectedDetail = 'Provided API key does not exist or is invalid.';
$resp = $this->callApi(self::METHOD_GET, '/short-codes', [
$resp = $this->callApi(self::METHOD_GET, '/short-urls', [
'headers' => [
Plugin\ApiKeyHeaderPlugin::HEADER_NAME => $apiKey,
],
@ -51,9 +49,7 @@ class AuthenticationTest extends ApiTestCase
$this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode());
$this->assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']);
$this->assertEquals('INVALID_API_KEY', $payload['type']);
$this->assertEquals('INVALID_API_KEY', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid API key', $payload['title']);
}
@ -74,7 +70,7 @@ class AuthenticationTest extends ApiTestCase
string $expectedType,
string $expectedTitle
): void {
$resp = $this->callApi(self::METHOD_GET, '/short-codes', [
$resp = $this->callApi(self::METHOD_GET, '/short-urls', [
'headers' => [
Plugin\AuthorizationHeaderPlugin::HEADER_NAME => $authValue,
],
@ -84,9 +80,7 @@ class AuthenticationTest extends ApiTestCase
$this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode());
$this->assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']);
$this->assertEquals($expectedType, $payload['type']);
$this->assertEquals($expectedType, $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals($expectedTitle, $payload['title']);
}

View file

@ -1,72 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Rest\Action\AuthenticateAction;
use Shlinkio\Shlink\Rest\Authentication\JWTService;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Zend\Diactoros\ServerRequest;
use function strpos;
/** @deprecated */
class AuthenticateActionTest extends TestCase
{
/** @var AuthenticateAction */
private $action;
/** @var ObjectProphecy */
private $apiKeyService;
/** @var ObjectProphecy */
private $jwtService;
public function setUp(): void
{
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
$this->jwtService = $this->prophesize(JWTService::class);
$this->jwtService->create(Argument::cetera())->willReturn('');
$this->action = new AuthenticateAction($this->apiKeyService->reveal(), $this->jwtService->reveal());
}
/** @test */
public function notProvidingAuthDataReturnsError()
{
$resp = $this->action->handle(new ServerRequest());
$this->assertEquals(400, $resp->getStatusCode());
}
/** @test */
public function properApiKeyReturnsTokenInResponse()
{
$this->apiKeyService->getByKey('foo')->willReturn((new ApiKey())->setId('5'))
->shouldBeCalledOnce();
$request = (new ServerRequest())->withParsedBody([
'apiKey' => 'foo',
]);
$response = $this->action->handle($request);
$this->assertEquals(200, $response->getStatusCode());
$response->getBody()->rewind();
$this->assertTrue(strpos($response->getBody()->getContents(), '"token"') > 0);
}
/** @test */
public function invalidApiKeyReturnsErrorResponse()
{
$this->apiKeyService->getByKey('foo')->willReturn((new ApiKey())->disable())
->shouldBeCalledOnce();
$request = (new ServerRequest())->withParsedBody([
'apiKey' => 'foo',
]);
$response = $this->action->handle($request);
$this->assertEquals(401, $response->getStatusCode());
}
}

View file

@ -1,86 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Authentication;
use Firebase\JWT\JWT;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Rest\Authentication\JWTService;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
use function time;
/** @deprecated */
class JWTServiceTest extends TestCase
{
/** @var JWTService */
private $service;
public function setUp(): void
{
$this->service = new JWTService(new AppOptions([
'name' => 'ShlinkTest',
'version' => '10000.3.1',
'secret_key' => 'foo',
]));
}
/** @test */
public function tokenIsProperlyCreated()
{
$id = '34';
$token = $this->service->create((new ApiKey())->setId($id));
$payload = (array) JWT::decode($token, 'foo', [JWTService::DEFAULT_ENCRYPTION_ALG]);
$this->assertGreaterThanOrEqual($payload['iat'], time());
$this->assertGreaterThan(time(), $payload['exp']);
$this->assertEquals($id, $payload['key']);
$this->assertEquals('auth', $payload['sub']);
$this->assertEquals('ShlinkTest:v10000.3.1', $payload['iss']);
}
/** @test */
public function refreshIncreasesExpiration()
{
$originalLifetime = 10;
$newLifetime = 30;
$originalPayload = ['exp' => time() + $originalLifetime];
$token = JWT::encode($originalPayload, 'foo');
$newToken = $this->service->refresh($token, $newLifetime);
$newPayload = (array) JWT::decode($newToken, 'foo', [JWTService::DEFAULT_ENCRYPTION_ALG]);
$this->assertGreaterThan($originalPayload['exp'], $newPayload['exp']);
}
/** @test */
public function verifyReturnsTrueWhenTheTokenIsCorrect()
{
$this->assertTrue($this->service->verify(JWT::encode([], 'foo')));
}
/** @test */
public function verifyReturnsFalseWhenTheTokenIsCorrect()
{
$this->assertFalse($this->service->verify('invalidToken'));
}
/** @test */
public function getPayloadWorksWithCorrectTokens()
{
$originalPayload = [
'exp' => time() + 10,
'sub' => 'testing',
];
$token = JWT::encode($originalPayload, 'foo');
$this->assertEquals($originalPayload, $this->service->getPayload($token));
}
/** @test */
public function getPayloadThrowsExceptionWithIncorrectTokens()
{
$this->expectException(AuthenticationException::class);
$this->service->getPayload('invalidToken');
}
}

View file

@ -13,7 +13,7 @@ use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Rest\Action\AuthenticateAction;
use Shlinkio\Shlink\Rest\Action\HealthAction;
use Shlinkio\Shlink\Rest\Authentication\Plugin\AuthenticationPluginInterface;
use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPluginInterface;
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
@ -37,7 +37,7 @@ class AuthenticationMiddlewareTest extends TestCase
$this->middleware = new AuthenticationMiddleware(
$this->requestToPlugin->reveal(),
[AuthenticateAction::class],
[HealthAction::class],
$this->logger->reveal()
);
}
@ -72,7 +72,7 @@ class AuthenticationMiddlewareTest extends TestCase
yield 'with whitelisted route' => [(new ServerRequest())->withAttribute(
RouteResult::class,
RouteResult::fromRoute(
new Route('foo', $dummyMiddleware, Route::HTTP_METHOD_ANY, AuthenticateAction::class)
new Route('foo', $dummyMiddleware, Route::HTTP_METHOD_ANY, HealthAction::class)
)
)];
yield 'with OPTIONS method' => [(new ServerRequest())->withAttribute(

View file

@ -1,94 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Middleware;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Rest\Middleware\BackwardsCompatibleProblemDetailsMiddleware;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequest;
use Zend\Diactoros\ServerRequestFactory;
use Zend\Diactoros\Uri;
class BackwardsCompatibleProblemDetailsMiddlewareTest extends TestCase
{
private BackwardsCompatibleProblemDetailsMiddleware $middleware;
private ObjectProphecy $handler;
public function setUp(): void
{
$this->handler = $this->prophesize(RequestHandlerInterface::class);
$this->middleware = new BackwardsCompatibleProblemDetailsMiddleware(0);
}
/**
* @test
* @dataProvider provideNonProcessableResponses
*/
public function nonProblemDetailsOrInvalidResponsesAreReturnedAsTheyAre(
Response $response,
?ServerRequest $request = null
): void {
$request = $request ?? ServerRequestFactory::fromGlobals();
$handle = $this->handler->handle($request)->willReturn($response);
$result = $this->middleware->process($request, $this->handler->reveal());
$this->assertSame($response, $result);
$handle->shouldHaveBeenCalledOnce();
}
public function provideNonProcessableResponses(): iterable
{
yield 'no problem details' => [new Response()];
yield 'invalid JSON' => [(new Response('data://text/plain,{invalid-json'))->withHeader(
'Content-Type',
'application/problem+json'
)];
yield 'version 2' => [
(new Response())->withHeader('Content-type', 'application/problem+json'),
ServerRequestFactory::fromGlobals()->withUri(new Uri('/v2/something')),
];
}
/**
* @test
* @dataProvider provideRequestPath
*/
public function mapsDeprecatedPropertiesWhenRequestIsPerformedToVersionOne(string $requestPath): void
{
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri($requestPath));
$response = $this->jsonResponse([
'type' => 'the_type',
'detail' => 'the_detail',
]);
$handle = $this->handler->handle($request)->willReturn($response);
/** @var Response\JsonResponse $result */
$result = $this->middleware->process($request, $this->handler->reveal());
$payload = $result->getPayload();
$this->assertEquals([
'type' => 'the_type',
'detail' => 'the_detail',
'error' => 'the_type',
'message' => 'the_detail',
], $payload);
$handle->shouldHaveBeenCalledOnce();
}
public function provideRequestPath(): iterable
{
yield 'no version' => ['/foo'];
yield 'version one' => ['/v1/foo'];
}
private function jsonResponse(array $payload, int $status = 200): Response\JsonResponse
{
$headers = ['Content-Type' => 'application/problem+json'];
return new Response\JsonResponse($payload, $status, $headers);
}
}

View file

@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Middleware\ShortUrl;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Rest\Middleware\ShortUrl\ShortCodePathMiddleware;
use Zend\Diactoros\Response;
use Zend\Diactoros\Uri;
class ShortCodePathMiddlewareTest extends TestCase
{
private $middleware;
private $requestHandler;
public function setUp(): void
{
$this->middleware = new ShortCodePathMiddleware();
$this->requestHandler = $this->prophesize(RequestHandlerInterface::class);
$this->requestHandler->handle(Argument::type(ServerRequestInterface::class))->willReturn(new Response());
}
/** @test */
public function properlyReplacesTheOldPathByTheNewOne()
{
$uri = new Uri('/short-codes/foo');
$request = $this->prophesize(ServerRequestInterface::class);
$request->getUri()->willReturn($uri);
$withUri = $request->withUri(Argument::that(function (UriInterface $uri) {
$path = $uri->getPath();
Assert::assertStringContainsString('/short-urls', $path);
Assert::assertStringNotContainsString('/short-codes', $path);
return $uri;
}))->willReturn($request->reveal());
$this->middleware->process($request->reveal(), $this->requestHandler->reveal());
$withUri->shouldHaveBeenCalledOnce();
}
}