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); declare(strict_types=1);
use function Shlinkio\Shlink\Common\env;
return [ return [
'app_options' => [ 'app_options' => [
'name' => 'Shlink', 'name' => 'Shlink',
'version' => '%SHLINK_VERSION%', 'version' => '%SHLINK_VERSION%',
'secret_key' => env('SECRET_KEY', ''),
'disable_track_param' => null, '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' => [ 'error_handler' => [
'listeners' => [Logger\ErrorLogger::class], 'listeners' => [Logger\ErrorLogger::class],
], ],

View File

@@ -21,7 +21,6 @@ return [
'path' => '/rest', 'path' => '/rest',
'middleware' => [ 'middleware' => [
Rest\Middleware\CrossDomainMiddleware::class, Rest\Middleware\CrossDomainMiddleware::class,
Rest\Middleware\BackwardsCompatibleProblemDetailsMiddleware::class,
ProblemDetails\ProblemDetailsMiddleware::class, ProblemDetails\ProblemDetailsMiddleware::class,
], ],
], ],
@@ -35,7 +34,6 @@ return [
'path' => '/rest', 'path' => '/rest',
'middleware' => [ 'middleware' => [
Rest\Middleware\PathVersionMiddleware::class, 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. 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)). * `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: 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", "password": "123abc",
"host": "something.rds.amazonaws.com", "host": "something.rds.amazonaws.com",
"port": "3306" "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). > 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: Once created just run shlink with the volume:
```bash ```bash

View File

@@ -8,19 +8,10 @@ use Monolog\Handler\StreamHandler;
use Monolog\Logger; use Monolog\Logger;
use function explode; use function explode;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function Functional\contains; use function Functional\contains;
use function implode;
use function Shlinkio\Shlink\Common\env; 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 { $helper = new class {
private const BASE62 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
private const DB_DRIVERS_MAP = [ private const DB_DRIVERS_MAP = [
'mysql' => 'pdo_mysql', 'mysql' => 'pdo_mysql',
'maria' => 'pdo_mysql', 'maria' => 'pdo_mysql',
@@ -32,40 +23,6 @@ $helper = new class {
'postgres' => '5432', '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 public function getDbConfig(): array
{ {
$driver = env('DB_DRIVER'); $driver = env('DB_DRIVER');
@@ -94,7 +51,7 @@ $helper = new class {
public function getNotFoundRedirectsConfig(): array public function getNotFoundRedirectsConfig(): array
{ {
return [ 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'), 'regular_404' => env('REGULAR_404_REDIRECT_TO'),
'base_url' => env('BASE_URL_REDIRECT_TO'), 'base_url' => env('BASE_URL_REDIRECT_TO'),
]; ];
@@ -112,7 +69,6 @@ return [
'config_cache_enabled' => false, 'config_cache_enabled' => false,
'app_options' => [ 'app_options' => [
'secret_key' => $helper->getSecretKey(),
'disable_track_param' => env('DISABLE_TRACK_PARAM'), 'disable_track_param' => env('DISABLE_TRACK_PARAM'),
], ],

View File

@@ -15,10 +15,6 @@ return [
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class, Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::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\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class,
Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class, Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class,

View File

@@ -36,10 +36,6 @@ return [
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class, Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
Command\Visit\LocateVisitsCommand::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\GenerateKeyCommand::class => ConfigAbstractFactory::class,
Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class, Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class,
@@ -70,7 +66,6 @@ return [
LockFactory::class, LockFactory::class,
GeolocationDbUpdater::class, GeolocationDbUpdater::class,
], ],
Command\Visit\UpdateDbCommand::class => [DbUpdater::class],
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class], Command\Api\GenerateKeyCommand::class => [ApiKeyService::class],
Command\Api\DisableKeyCommand::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_schema' => ['url_shortener', 'domain', 'schema'],
'short_domain_host' => ['url_shortener', 'domain', 'hostname'], 'short_domain_host' => ['url_shortener', 'domain', 'hostname'],
'validate_url' => ['url_shortener', 'validate_url'], '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'], 'invalid_short_url_redirect_to' => ['not_found_redirects', 'invalid_short_url'],
'regular_404_redirect_to' => ['not_found_redirects', 'regular_404'], 'regular_404_redirect_to' => ['not_found_redirects', 'regular_404'],
'base_url_redirect_to' => ['not_found_redirects', 'base_path'], '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(), 'date' => $this->date->toAtomString(),
'userAgent' => $this->userAgent, 'userAgent' => $this->userAgent,
'visitLocation' => $this->visitLocation, 'visitLocation' => $this->visitLocation,
// Deprecated
'remoteAddr' => null,
]; ];
} }

View File

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

View File

@@ -15,8 +15,6 @@ class AppOptions extends AbstractOptions
private string $name = ''; private string $name = '';
private string $version = '1.0'; private string $version = '1.0';
/** @deprecated */
private string $secretKey = '';
private ?string $disableTrackParam = null; private ?string $disableTrackParam = null;
public function getName(): string public function getName(): string
@@ -41,23 +39,6 @@ class AppOptions extends AbstractOptions
return $this; 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 * @return string|null
*/ */

View File

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

View File

@@ -11,7 +11,7 @@ use function array_merge;
class SimplifiedConfigParserTest extends TestCase class SimplifiedConfigParserTest extends TestCase
{ {
private $postProcessor; private SimplifiedConfigParser $postProcessor;
public function setUp(): void public function setUp(): void
{ {
@@ -40,7 +40,7 @@ class SimplifiedConfigParserTest extends TestCase
'short_domain_host' => 'doma.in', 'short_domain_host' => 'doma.in',
'validate_url' => false, 'validate_url' => false,
'delete_short_url_threshold' => 50, 'delete_short_url_threshold' => 50,
'not_found_redirect_to' => 'foobar.com', 'invalid_short_url_redirect_to' => 'foobar.com',
'redis_servers' => [ 'redis_servers' => [
'tcp://1.1.1.1:1111', 'tcp://1.1.1.1:1111',
'tcp://1.2.2.2:2222', 'tcp://1.2.2.2:2222',
@@ -125,28 +125,4 @@ class SimplifiedConfigParserTest extends TestCase
$this->assertEquals(array_merge($expected, $simplified), $result); $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(), 'date' => ($date ?? $visit->getDate())->toAtomString(),
'userAgent' => 'Chrome', 'userAgent' => 'Chrome',
'visitLocation' => null, 'visitLocation' => null,
// Deprecated
'remoteAddr' => null,
], $visit->jsonSerialize()); ], $visit->jsonSerialize());
} }

View File

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

View File

@@ -20,7 +20,6 @@ return [
Authentication\JWTService::class => ConfigAbstractFactory::class, Authentication\JWTService::class => ConfigAbstractFactory::class,
ApiKeyService::class => ConfigAbstractFactory::class, ApiKeyService::class => ConfigAbstractFactory::class,
Action\AuthenticateAction::class => ConfigAbstractFactory::class,
Action\HealthAction::class => ConfigAbstractFactory::class, Action\HealthAction::class => ConfigAbstractFactory::class,
Action\ShortUrl\CreateShortUrlAction::class => ConfigAbstractFactory::class, Action\ShortUrl\CreateShortUrlAction::class => ConfigAbstractFactory::class,
Action\ShortUrl\SingleStepCreateShortUrlAction::class => ConfigAbstractFactory::class, Action\ShortUrl\SingleStepCreateShortUrlAction::class => ConfigAbstractFactory::class,
@@ -39,9 +38,7 @@ return [
Middleware\BodyParserMiddleware::class => InvokableFactory::class, Middleware\BodyParserMiddleware::class => InvokableFactory::class,
Middleware\CrossDomainMiddleware::class => InvokableFactory::class, Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
Middleware\PathVersionMiddleware::class => InvokableFactory::class, Middleware\PathVersionMiddleware::class => InvokableFactory::class,
Middleware\BackwardsCompatibleProblemDetailsMiddleware::class => ConfigAbstractFactory::class,
Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class => InvokableFactory::class, Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class => InvokableFactory::class,
Middleware\ShortUrl\ShortCodePathMiddleware::class => InvokableFactory::class,
], ],
], ],
@@ -49,7 +46,6 @@ return [
Authentication\JWTService::class => [AppOptions::class], Authentication\JWTService::class => [AppOptions::class],
ApiKeyService::class => ['em'], ApiKeyService::class => ['em'],
Action\AuthenticateAction::class => [ApiKeyService::class, Authentication\JWTService::class, 'Logger_Shlink'],
Action\HealthAction::class => [Connection::class, AppOptions::class, 'Logger_Shlink'], Action\HealthAction::class => [Connection::class, AppOptions::class, 'Logger_Shlink'],
Action\ShortUrl\CreateShortUrlAction::class => [ Action\ShortUrl\CreateShortUrlAction::class => [
Service\UrlShortener::class, Service\UrlShortener::class,
@@ -76,10 +72,6 @@ return [
Action\Tag\DeleteTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class], Action\Tag\DeleteTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class],
Action\Tag\CreateTagsAction::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], 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 [ return [
'routes' => [ 'routes' => [
Action\AuthenticateAction::getRouteDef(),
Action\HealthAction::getRouteDef(), Action\HealthAction::getRouteDef(),
// Short codes // 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 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 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, $statusCode);
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']); $this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
$this->assertEquals($detail, $payload['detail']); $this->assertEquals($detail, $payload['detail']);
$this->assertEquals($detail, $payload['message']); // Deprecated
$this->assertEquals('INVALID_SLUG', $payload['type']); $this->assertEquals('INVALID_SLUG', $payload['type']);
$this->assertEquals('INVALID_SLUG', $payload['error']); // Deprecated
$this->assertEquals('Invalid custom slug', $payload['title']); $this->assertEquals('Invalid custom slug', $payload['title']);
$this->assertEquals($slug, $payload['customSlug']); $this->assertEquals($slug, $payload['customSlug']);
@@ -215,7 +213,7 @@ class CreateShortUrlActionTest extends ApiTestCase
} }
/** @test */ /** @test */
public function failsToCreateShortUrlWithInvalidOriginalUrl(): void public function failsToCreateShortUrlWithInvalidLongUrl(): void
{ {
$url = 'https://this-has-to-be-invalid.com'; $url = 'https://this-has-to-be-invalid.com';
$expectedDetail = sprintf('Provided URL %s is invalid. Try with a different one.', $url); $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, $statusCode);
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']); $this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
$this->assertEquals('INVALID_URL', $payload['type']); $this->assertEquals('INVALID_URL', $payload['type']);
$this->assertEquals('INVALID_URL', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']); $this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid URL', $payload['title']); $this->assertEquals('Invalid URL', $payload['title']);
$this->assertEquals($url, $payload['url']); $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, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']); $this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE', $payload['type']); $this->assertEquals('INVALID_SHORTCODE', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']); $this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Short URL not found', $payload['title']); $this->assertEquals('Short URL not found', $payload['title']);
$this->assertEquals('invalid', $payload['shortCode']); $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, $resp->getStatusCode());
$this->assertEquals(self::STATUS_UNPROCESSABLE_ENTITY, $payload['status']); $this->assertEquals(self::STATUS_UNPROCESSABLE_ENTITY, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE_DELETION', $payload['type']); $this->assertEquals('INVALID_SHORTCODE_DELETION', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE_DELETION', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']); $this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Cannot delete short URL', $payload['title']); $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, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']); $this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE', $payload['type']); $this->assertEquals('INVALID_SHORTCODE', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']); $this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Short URL not found', $payload['title']); $this->assertEquals('Short URL not found', $payload['title']);
$this->assertEquals('invalid', $payload['shortCode']); $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, $resp->getStatusCode());
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']); $this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
$this->assertEquals('INVALID_ARGUMENT', $payload['type']); $this->assertEquals('INVALID_ARGUMENT', $payload['type']);
$this->assertEquals('INVALID_ARGUMENT', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']); $this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid data', $payload['title']); $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, $resp->getStatusCode());
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']); $this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
$this->assertEquals('INVALID_ARGUMENT', $payload['type']); $this->assertEquals('INVALID_ARGUMENT', $payload['type']);
$this->assertEquals('INVALID_ARGUMENT', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']); $this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid data', $payload['title']); $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, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']); $this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE', $payload['type']); $this->assertEquals('INVALID_SHORTCODE', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']); $this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Short URL not found', $payload['title']); $this->assertEquals('Short URL not found', $payload['title']);
$this->assertEquals('invalid', $payload['shortCode']); $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, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']); $this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE', $payload['type']); $this->assertEquals('INVALID_SHORTCODE', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']); $this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Short URL not found', $payload['title']); $this->assertEquals('Short URL not found', $payload['title']);
$this->assertEquals('invalid', $payload['shortCode']); $this->assertEquals('invalid', $payload['shortCode']);
} }

View File

@@ -24,7 +24,6 @@ class ListShortUrlsTest extends ApiTestCase
'validUntil' => null, 'validUntil' => null,
'maxVisits' => null, 'maxVisits' => null,
], ],
'originalUrl' => 'https://shlink.io',
]; ];
private const SHORT_URL_CUSTOM_SLUG_AND_DOMAIN = [ private const SHORT_URL_CUSTOM_SLUG_AND_DOMAIN = [
'shortCode' => 'custom-with-domain', 'shortCode' => 'custom-with-domain',
@@ -38,7 +37,6 @@ class ListShortUrlsTest extends ApiTestCase
'validUntil' => null, 'validUntil' => null,
'maxVisits' => null, 'maxVisits' => null,
], ],
'originalUrl' => 'https://google.com',
]; ];
private const SHORT_URL_META = [ private const SHORT_URL_META = [
'shortCode' => 'def456', 'shortCode' => 'def456',
@@ -54,9 +52,6 @@ class ListShortUrlsTest extends ApiTestCase
'validUntil' => null, 'validUntil' => null,
'maxVisits' => 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 = [ private const SHORT_URL_CUSTOM_SLUG = [
'shortCode' => 'custom', 'shortCode' => 'custom',
@@ -70,7 +65,6 @@ class ListShortUrlsTest extends ApiTestCase
'validUntil' => null, 'validUntil' => null,
'maxVisits' => 2, 'maxVisits' => 2,
], ],
'originalUrl' => 'https://shlink.io',
]; ];
private const SHORT_URL_CUSTOM_DOMAIN = [ private const SHORT_URL_CUSTOM_DOMAIN = [
'shortCode' => 'ghi789', 'shortCode' => 'ghi789',
@@ -86,9 +80,6 @@ class ListShortUrlsTest extends ApiTestCase
'validUntil' => null, 'validUntil' => null,
'maxVisits' => 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, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']); $this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE', $payload['type']); $this->assertEquals('INVALID_SHORTCODE', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']); $this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Short URL not found', $payload['title']); $this->assertEquals('Short URL not found', $payload['title']);
$this->assertEquals('invalid', $payload['shortCode']); $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, $resp->getStatusCode());
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']); $this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
$this->assertEquals('INVALID_ARGUMENT', $payload['type']); $this->assertEquals('INVALID_ARGUMENT', $payload['type']);
$this->assertEquals('INVALID_ARGUMENT', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']); $this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid data', $payload['title']); $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, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']); $this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('TAG_NOT_FOUND', $payload['type']); $this->assertEquals('TAG_NOT_FOUND', $payload['type']);
$this->assertEquals('TAG_NOT_FOUND', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']); $this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Tag not found', $payload['title']); $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, $resp->getStatusCode());
$this->assertEquals(self::STATUS_CONFLICT, $payload['status']); $this->assertEquals(self::STATUS_CONFLICT, $payload['status']);
$this->assertEquals('TAG_CONFLICT', $payload['type']); $this->assertEquals('TAG_CONFLICT', $payload['type']);
$this->assertEquals('TAG_CONFLICT', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']); $this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Tag conflict', $payload['title']); $this->assertEquals('Tag conflict', $payload['title']);
} }

View File

@@ -21,15 +21,13 @@ class AuthenticationTest extends ApiTestCase
implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS) 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); $payload = $this->getJsonResponsePayload($resp);
$this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode()); $this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode());
$this->assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']); $this->assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']);
$this->assertEquals('INVALID_AUTHORIZATION', $payload['type']); $this->assertEquals('INVALID_AUTHORIZATION', $payload['type']);
$this->assertEquals('INVALID_AUTHORIZATION', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']); $this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid authorization', $payload['title']); $this->assertEquals('Invalid authorization', $payload['title']);
} }
@@ -41,7 +39,7 @@ class AuthenticationTest extends ApiTestCase
{ {
$expectedDetail = 'Provided API key does not exist or is invalid.'; $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' => [ 'headers' => [
Plugin\ApiKeyHeaderPlugin::HEADER_NAME => $apiKey, 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, $resp->getStatusCode());
$this->assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']); $this->assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']);
$this->assertEquals('INVALID_API_KEY', $payload['type']); $this->assertEquals('INVALID_API_KEY', $payload['type']);
$this->assertEquals('INVALID_API_KEY', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']); $this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid API key', $payload['title']); $this->assertEquals('Invalid API key', $payload['title']);
} }
@@ -74,7 +70,7 @@ class AuthenticationTest extends ApiTestCase
string $expectedType, string $expectedType,
string $expectedTitle string $expectedTitle
): void { ): void {
$resp = $this->callApi(self::METHOD_GET, '/short-codes', [ $resp = $this->callApi(self::METHOD_GET, '/short-urls', [
'headers' => [ 'headers' => [
Plugin\AuthorizationHeaderPlugin::HEADER_NAME => $authValue, 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, $resp->getStatusCode());
$this->assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']); $this->assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']);
$this->assertEquals($expectedType, $payload['type']); $this->assertEquals($expectedType, $payload['type']);
$this->assertEquals($expectedType, $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']); $this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals($expectedTitle, $payload['title']); $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\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface; 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\Plugin\AuthenticationPluginInterface;
use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPluginInterface; use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPluginInterface;
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware; use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
@@ -37,7 +37,7 @@ class AuthenticationMiddlewareTest extends TestCase
$this->middleware = new AuthenticationMiddleware( $this->middleware = new AuthenticationMiddleware(
$this->requestToPlugin->reveal(), $this->requestToPlugin->reveal(),
[AuthenticateAction::class], [HealthAction::class],
$this->logger->reveal() $this->logger->reveal()
); );
} }
@@ -72,7 +72,7 @@ class AuthenticationMiddlewareTest extends TestCase
yield 'with whitelisted route' => [(new ServerRequest())->withAttribute( yield 'with whitelisted route' => [(new ServerRequest())->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRoute( 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( 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();
}
}