Compare commits

..

55 Commits

Author SHA1 Message Date
Alejandro Celaya
b3664597b0 Merge branch 'develop' 2018-05-07 11:27:13 +02:00
Alejandro Celaya
8cfb4f61ca Merge pull request #148 from acelaya/feature/1.9.0
Version 1.9.0
2018-05-07 11:26:27 +02:00
Alejandro Celaya
b0dbb2dae4 Updated CreateShortCodeContentNegotiationMiddleware so that query parameter takes precedence over Accept header 2018-05-07 11:17:10 +02:00
Alejandro Celaya
7c6da4985d Updated build script to delete more development-specific files 2018-05-07 11:09:32 +02:00
Alejandro Celaya
386b0dfb7b Updated changelog 2018-05-07 11:03:28 +02:00
Alejandro Celaya
1437ff48ce Ensured all core actions log errors 2018-05-07 10:58:49 +02:00
Alejandro Celaya
63294f20ee Updated language files 2018-05-06 12:36:07 +02:00
Alejandro Celaya
d8acc3c247 Removed unused use statement 2018-05-06 12:34:21 +02:00
Alejandro Celaya
52d8ffa212 Improved CreateShortCodeContentNegotiationMiddleware sho that it takes into account the case in which an error is returned from next middleware 2018-05-06 12:28:22 +02:00
Alejandro Celaya
98ad2816e8 Documented new endpoint to create short URLs in a single step 2018-05-06 12:19:08 +02:00
Alejandro Celaya
9d890f4227 Created CreateShortCodeContentNegotiationMiddleware 2018-05-03 19:04:40 +02:00
Alejandro Celaya
0932d04907 Fixed tests namespaces to match their subject under test 2018-05-03 18:34:45 +02:00
Alejandro Celaya
1f78b5c524 Improved CreateShortCodeContentNegotiationMiddleware so that it can determine the format based on a query partameter 2018-05-03 18:32:32 +02:00
Alejandro Celaya
59f10619ba Created middleware used with short codes creation actions to handle content negotiation 2018-05-03 18:26:31 +02:00
Alejandro Celaya
334710e92c Added middleware which injects the content-length header in the response if not present 2018-05-03 18:25:57 +02:00
Alejandro Celaya
75b8175824 Fixed coding styles in config file 2018-05-03 18:05:16 +02:00
Alejandro Celaya
8a74ef2a33 Moved action to subnamespace 2018-05-03 18:04:00 +02:00
Alejandro Celaya
d05ac5ce9d Moved action to subnamespace 2018-05-03 18:03:10 +02:00
Alejandro Celaya
3100fffa2b Moved action to subnamespace 2018-05-03 18:02:45 +02:00
Alejandro Celaya
6bbacb1017 Moved action to subnamespace 2018-05-03 18:01:57 +02:00
Alejandro Celaya
4403dc5df9 Moved action to subnamespace 2018-05-03 18:00:32 +02:00
Alejandro Celaya
fdc637c23d Moved action to subnamespace 2018-05-03 17:59:28 +02:00
Alejandro Celaya
b99d662417 Created SingleStepCreateShortCodeActionTest 2018-05-03 17:57:43 +02:00
Alejandro Celaya
eb9a964c66 Removed unused use statement 2018-05-03 13:34:13 +02:00
Alejandro Celaya
e5ef8d7f8c Created action which allows short URLs to be created on a single API request 2018-05-03 13:21:43 +02:00
Alejandro Celaya
28650aee2b Fixed case sensitivity errors 2018-05-03 12:19:51 +02:00
Alejandro Celaya
a2294704e6 Split try catch to prevent undefined variables 2018-05-01 19:38:44 +02:00
Alejandro Celaya
e5e1aa2ff4 Defined abstract action which handles short codes generations 2018-05-01 19:35:12 +02:00
Alejandro Celaya
2f5290b9d3 Moved whitelisted routes in CheckAuthenticationMiddleware to external configuration 2018-05-01 18:36:42 +02:00
Alejandro Celaya
ef3c4aadf2 Moved most of rest routes config to their actions 2018-05-01 18:28:37 +02:00
Alejandro Celaya
c9ce56eea5 Added public method in AbstractRestAction which builds route definition 2018-05-01 18:16:44 +02:00
Alejandro Celaya
4fee656f96 Prepared version 1.9.0 2018-05-01 10:10:19 +02:00
Alejandro Celaya
d2a04259f5 Merge branch 'develop' 2018-04-07 09:06:45 +02:00
Alejandro Celaya
e504daa1ba Merge pull request #142 from acelaya/develop
Develop
2018-04-07 09:05:56 +02:00
Alejandro Celaya
8793a67ce9 Reduced the number of includes by pointing to dcotrine scripts with extension 2018-04-07 08:37:41 +02:00
Alejandro Celaya
b4ded374e9 Updated changelog 2018-04-07 08:32:06 +02:00
Alejandro Celaya
91d350b12f Removed path workaround in PathVersionMiddleware and simplified code 2018-04-07 08:31:03 +02:00
Alejandro Celaya
b3e25f28fd Added v1.8.1 to changelog 2018-04-07 08:25:01 +02:00
Alejandro Celaya
aca89f9abe Updated links to doctrine CLI scripts to avoid depending on symlinks 2018-04-07 08:21:34 +02:00
Alejandro Celaya
243075dd78 Merge branch 'develop' 2018-03-29 09:52:00 +02:00
Alejandro Celaya
7130425896 Merge pull request #133 from acelaya/feature/1.8.0
1.8.0
2018-03-29 09:50:58 +02:00
Alejandro Celaya
fe9ab20cbb Applied some improvements 2018-03-27 23:57:29 +02:00
Alejandro Celaya
6935b2ebe2 Updated system so that NotFoundDelegate is used 2018-03-26 20:37:04 +02:00
Alejandro Celaya
3dcc510da1 Updated to symfony 4 2018-03-26 20:32:12 +02:00
Alejandro Celaya
2f26c82fa6 Removed expressive migration tool from dev dependencies 2018-03-26 20:25:30 +02:00
Alejandro Celaya
9ddb60a882 Updated changelog including v1.8.0 2018-03-26 20:22:57 +02:00
Alejandro Celaya
210b08b61f Created PixelActionTest 2018-03-26 20:17:38 +02:00
Alejandro Celaya
42fe4bd5ce Created new action to track visits, which returns an empty pixel 2018-03-26 20:13:03 +02:00
Alejandro Celaya
1b2a0820e5 Updated to phpunit 7 and dropped dbunit dependency 2018-03-26 19:09:10 +02:00
Alejandro Celaya
6cf0155417 Updated minimum required MSI 2018-03-26 19:06:49 +02:00
Alejandro Celaya
9b8be3e5b8 Fixed phpstan errors 2018-03-26 19:05:26 +02:00
Alejandro Celaya
a27b01b895 Fixed tests 2018-03-26 19:02:41 +02:00
Alejandro Celaya
16dd1838aa Updated to expressive 3 2018-03-26 18:49:28 +02:00
Alejandro Celaya
f788d6872f Added infection to the build matrix 2018-03-26 18:16:59 +02:00
Alejandro Celaya
d0df007812 Dropped support for PHP 7.0 2018-03-26 18:16:59 +02:00
90 changed files with 1400 additions and 642 deletions

View File

@@ -5,7 +5,6 @@ branches:
- /.*/
php:
- 7
- 7.1
- 7.2
@@ -16,12 +15,10 @@ before_install:
before_script:
- composer self-update
- composer install --no-interaction
- if [[ $TRAVIS_PHP_VERSION = 7.1 ]] || [[ $TRAVIS_PHP_VERSION = 7.2 ]]; then composer global require --dev phpstan/phpstan:0.9.*; fi
script:
- mkdir build
- composer check
- if [[ $TRAVIS_PHP_VERSION = 7.1 ]] || [[ $TRAVIS_PHP_VERSION = 7.2 ]]; then ~/.composer/vendor/bin/phpstan analyse module/*/src/ --level=6 -c phpstan.neon; fi
after_script:
- vendor/bin/phpcov merge build --clover build/clover.xml

View File

@@ -1,5 +1,41 @@
## CHANGELOG
### 1.9.0
**Features**
* [147: Allow short URLs to be created on the fly with query param authentication](https://github.com/shlinkio/shlink/issues/147)
**Bugs:**
* [139: Make sure all core actions log exceptions](https://github.com/shlinkio/shlink/issues/139)
### 1.8.1
**Tasks**
* [141: Remove workaround used in PathVersionMiddleware](https://github.com/shlinkio/shlink/issues/141)
**Bugs:**
* [140: Installation failed. Warning thrown while trying to include doctrine script](https://github.com/shlinkio/shlink/issues/140)
### 1.8.0
**Features**
* [125: Implement a path which returns a 1px image instead of a redirection](https://github.com/shlinkio/shlink/issues/125)
**Enhancements:**
* [130: Update to Expressive 3](https://github.com/shlinkio/shlink/issues/130)
* [137: Update symfony packages to v4](https://github.com/shlinkio/shlink/issues/137)
**Tasks**
* [131: Drop support for PHP 7](https://github.com/shlinkio/shlink/issues/131)
* [132: Add infection to improve tests](https://github.com/shlinkio/shlink/issues/132)
### 1.7.2
**Bugs:**

View File

@@ -33,8 +33,12 @@ rm composer.*
rm LICENSE
rm indocker
rm docker-compose.yml
rm docker-compose.override.yml
rm docker-compose.override.yml.dist
rm func_tests_bootstrap.php
rm php*
rm README.md
rm infection.json
rm -rf build
rm -ff data/database.sqlite
rm -rf data/infra

View File

@@ -12,8 +12,8 @@
}
],
"require": {
"php": "^7.0",
"acelaya/ze-content-based-error-handler": "^2.0",
"php": "^7.1",
"acelaya/ze-content-based-error-handler": "^2.2",
"cocur/slugify": "^3.0",
"doctrine/annotations": "^1.4",
"doctrine/cache": "^1.6",
@@ -22,23 +22,23 @@
"doctrine/dbal": "^2.5",
"doctrine/migrations": "^1.4",
"doctrine/orm": "^2.5",
"endroid/qrcode": "^1.7",
"endroid/qr-code": "^1.7",
"firebase/php-jwt": "^4.0",
"guzzlehttp/guzzle": "^6.2",
"http-interop/http-middleware": "^0.4.1",
"mikehaertl/phpwkhtmltopdf": "^2.2",
"monolog/monolog": "^1.21",
"roave/security-advisories": "dev-master",
"symfony/console": "^3.4",
"symfony/filesystem": "^3.0",
"symfony/process": "^3.0",
"symfony/console": "^4.0",
"symfony/filesystem": "^4.0",
"symfony/process": "^4.0",
"theorchard/monolog-cascade": "^0.4",
"zendframework/zend-config": "^3.0",
"zendframework/zend-config-aggregator": "^1.0",
"zendframework/zend-expressive": "^2.0",
"zendframework/zend-expressive-fastroute": "^2.0",
"zendframework/zend-expressive-helpers": "^4.2",
"zendframework/zend-expressive-platesrenderer": "^1.3",
"zendframework/zend-diactoros": "^1.7",
"zendframework/zend-expressive": "^3.0",
"zendframework/zend-expressive-fastroute": "^3.0",
"zendframework/zend-expressive-helpers": "^5.0",
"zendframework/zend-expressive-platesrenderer": "^2.0",
"zendframework/zend-i18n": "^2.7",
"zendframework/zend-inputfilter": "^2.8",
"zendframework/zend-paginator": "^2.6",
@@ -47,14 +47,16 @@
},
"require-dev": {
"filp/whoops": "^2.0",
"phpunit/dbunit": "^3.0",
"phpunit/phpcov": "^4.0",
"phpunit/phpunit": "^6.0",
"infection/infection": "^0.8.1",
"phpstan/phpstan": "0.9",
"phpunit/phpcov": "^5.0",
"phpunit/phpunit": "^7.0",
"slevomat/coding-standard": "^4.0",
"squizlabs/php_codesniffer": "^3.1 <3.2",
"symfony/dotenv": "^3.4",
"symfony/var-dumper": "^3.0",
"zendframework/zend-expressive-tooling": "^0.4"
"symfony/dotenv": "^4.0",
"symfony/var-dumper": "^4.0",
"zendframework/zend-component-installer": "^2.1",
"zendframework/zend-expressive-tooling": "^1.0"
},
"autoload": {
"psr-4": {
@@ -84,8 +86,10 @@
"scripts": {
"check": [
"@cs",
"@stan",
"@test",
"@func-test"
"@func-test",
"@infect"
],
"cs": "phpcs",
"cs-fix": "phpcbf",
@@ -97,13 +101,17 @@
"@test",
"@func-test",
"phpcov merge build --html build/html"
]
],
"stan": "phpstan analyse module/*/src/ --level=6 -c phpstan.neon",
"infect": "infection --threads=4 --min-msi=65 --only-covered --log-verbosity=2",
"infect-show": "infection --threads=4 --min-msi=65 --only-covered --log-verbosity=2 --show-mutations",
"expressive": "expressive"
},
"config": {
"process-timeout": 0,
"sort-packages": true,
"platform": {
"php": "7.0.8"
"php": "7.1.8"
}
}
}

View File

@@ -5,30 +5,23 @@ use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory
use Zend\Expressive;
use Zend\Expressive\Container;
use Zend\Expressive\Helper;
use Zend\Expressive\Middleware;
use Zend\Expressive\Plates;
use Zend\Expressive\Router;
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
use Zend\Expressive\Template;
use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\Stratigility\Middleware\ErrorHandler;
return [
'dependencies' => [
'factories' => [
Expressive\Application::class => Container\ApplicationFactory::class,
Template\TemplateRendererInterface::class => Plates\PlatesRendererFactory::class,
Router\RouterInterface::class => Router\FastRouteRouterFactory::class,
ErrorHandler::class => Container\ErrorHandlerFactory::class,
ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class,
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
Helper\ServerUrlHelper::class => InvokableFactory::class,
],
'aliases' => [
Middleware\ImplicitOptionsMiddleware::class => ImplicitOptionsMiddleware::class,
'delegators' => [
Expressive\Application::class => [
Container\ApplicationConfigInjectionDelegator::class,
],
],
],

View File

@@ -2,6 +2,7 @@
declare(strict_types=1);
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware;
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware;
@@ -15,12 +16,13 @@ return [
'pre-routing' => [
'middleware' => [
ErrorHandler::class,
Expressive\Helper\ContentLengthMiddleware::class,
LocaleMiddleware::class,
],
'priority' => 11,
],
'pre-routing-rest' => [
// 'path' => '/rest',
'path' => '/rest',
'middleware' => [
PathVersionMiddleware::class,
],
@@ -48,6 +50,7 @@ return [
'post-routing' => [
'middleware' => [
Expressive\Router\Middleware\DispatchMiddleware::class,
NotFoundHandler::class,
],
'priority' => 1,
],

View File

@@ -7,6 +7,7 @@ use Shlinkio\Shlink\Common;
use Shlinkio\Shlink\Core;
use Shlinkio\Shlink\Rest;
use Zend\ConfigAggregator;
use Zend\Expressive;
/**
* Configuration files are loaded in a specific order. First ``global.php``, then ``*.global.php``.
@@ -18,8 +19,11 @@ use Zend\ConfigAggregator;
*/
return (new ConfigAggregator\ConfigAggregator([
Zend\Expressive\ConfigProvider::class,
Zend\Expressive\Router\ConfigProvider::class,
Expressive\ConfigProvider::class,
Expressive\Router\ConfigProvider::class,
Expressive\Router\FastRouteRouter\ConfigProvider::class,
Expressive\Plates\ConfigProvider::class,
Expressive\Helper\ConfigProvider::class,
ExpressiveErrorHandler\ConfigProvider::class,
Common\ConfigProvider::class,
Core\ConfigProvider::class,

0
data/infra/database/.gitignore vendored Normal file → Executable file
View File

0
data/infra/nginx/.gitignore vendored Normal file → Executable file
View File

View File

@@ -0,0 +1,125 @@
{
"get": {
"tags": [
"ShortCodes"
],
"summary": "Create a short URL",
"description": "Creates a short URL in a single API call. Useful for third party integrations",
"parameters": [
{
"name": "apiKey",
"in": "query",
"description": "The API key used to authenticate the request",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "longUrl",
"in": "query",
"description": "The URL to be shortened",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "format",
"in": "query",
"description": "The format in which you want the response to be returned. You can also use the \"Accept\" header instead of this",
"required": false,
"schema": {
"type": "string",
"enum": [
"txt",
"json"
]
}
}
],
"responses": {
"200": {
"description": "The list of short URLs",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"longUrl": {
"type": "string",
"description": "The original long URL that has been shortened"
},
"shortUrl": {
"type": "string",
"description": "The generated short URL"
},
"shortCode": {
"type": "string",
"description": "the short code that is being used in the short URL"
}
}
}
},
"text/plain": {
"schema": {
"type": "string"
}
}
},
"examples": {
"application/json": {
"longUrl": "https://github.com/shlinkio/shlink",
"shortUrl": "https://dom.ain/abc123",
"shortCode": "abc123"
},
"text/plain": "https://dom.ain/abc123"
}
},
"400": {
"description": "The long URL was not provided or is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
},
"text/plain": {
"schema": {
"type": "string"
}
}
},
"examples": {
"application/json": {
"error": "INVALID_URL",
"message": "Provided URL foo is invalid. Try with a different one."
},
"text/plain": "INVALID_URL"
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
},
"text/plain": {
"schema": {
"type": "string"
}
}
},
"examples": {
"application/json": {
"error": "UNKNOWN_ERROR",
"message": "Unexpected error occurred"
},
"text/plain": "UNKNOWN_ERROR"
}
}
}
}
}

View File

@@ -40,6 +40,9 @@
"/v1/short-codes": {
"$ref": "paths/v1_short-codes.json"
},
"/v1/short-codes/shorten": {
"$ref": "paths/v1_short-codes_shorten.json"
},
"/v1/short-codes/{shortCode}": {
"$ref": "paths/v1_short-codes_{shortCode}.json"
},

18
infection.json Normal file
View File

@@ -0,0 +1,18 @@
{
"source": {
"directories": [
"module/*/src"
],
"excludes": []
},
"timeout": 10,
"logs": {
"text": "build/infection/infection-log.txt",
"summary": "build/infection/summary-log.txt",
"debug": "build/infection/debug-log.txt"
},
"tmpDir": "build/infection/temp",
"phpUnit": {
"configDir": "."
}
}

View File

@@ -134,7 +134,7 @@ class InstallCommand extends Command
if (! $this->isUpdate) {
$this->io->write('Initializing database...');
if (! $this->runCommand(
'php vendor/bin/doctrine.php orm:schema-tool:create',
'php vendor/doctrine/orm/bin/doctrine.php orm:schema-tool:create',
'Error generating database.',
$output
)) {
@@ -145,7 +145,7 @@ class InstallCommand extends Command
// Run database migrations
$this->io->write('Updating database...');
if (! $this->runCommand(
'php vendor/bin/doctrine-migrations migrations:migrate',
'php vendor/doctrine/migrations/bin/doctrine-migrations.php migrations:migrate',
'Error updating database.',
$output
)) {
@@ -155,7 +155,7 @@ class InstallCommand extends Command
// Generate proxies
$this->io->write('Generating proxies...');
if (! $this->runCommand(
'php vendor/bin/doctrine.php orm:generate-proxies',
'php vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies',
'Error generating proxies.',
$output
)) {

View File

@@ -27,6 +27,8 @@ class EmptyResponseImplicitOptionsMiddlewareFactory implements FactoryInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new ImplicitOptionsMiddleware(new EmptyResponse());
return new ImplicitOptionsMiddleware(function () {
return new EmptyResponse();
});
}
}

View File

@@ -3,10 +3,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Middleware;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as DelegateInterface;
use Zend\I18n\Translator\Translator;
class LocaleMiddleware implements MiddlewareInterface
@@ -32,15 +32,15 @@ class LocaleMiddleware implements MiddlewareInterface
*
* @return Response
*/
public function process(Request $request, DelegateInterface $delegate)
public function process(Request $request, DelegateInterface $delegate): Response
{
if (! $request->hasHeader('Accept-Language')) {
return $delegate->process($request);
return $delegate->handle($request);
}
$locale = $request->getHeaderLine('Accept-Language');
$this->translator->setLocale($this->normalizeLocale($locale));
return $delegate->process($request);
return $delegate->handle($request);
}
/**

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Response;
use Psr\Http\Message\StreamInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
class PixelResponse extends Response
{
private const BASE_64_IMAGE = 'R0lGODlhAQABAJAAAP8AAAAAACH5BAUQAAAALAAAAAABAAEAAAICBAEAOw==';
private const CONTENT_TYPE = 'image/gif';
public function __construct(int $status = 200, array $headers = [])
{
$headers['content-type'] = self::CONTENT_TYPE;
parent::__construct($this->createBody(), $status, $headers);
}
/**
* Create the message body.
*
* @return StreamInterface
*/
private function createBody(): StreamInterface
{
$body = new Stream('php://temp', 'wb+');
$body->write(\base64_decode(self::BASE_64_IMAGE));
$body->rewind();
return $body;
}
}

View File

@@ -3,40 +3,17 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\DbUnit;
use Doctrine\DBAL\Driver\PDOConnection;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\DbUnit\Database\Connection as DbConn;
use PHPUnit\DbUnit\DataSet\IDataSet as DataSet;
use PHPUnit\DbUnit\TestCase;
use PHPUnit\Framework\TestCase;
abstract class DatabaseTestCase extends TestCase
{
const ENTITIES_TO_EMPTY = [];
protected const ENTITIES_TO_EMPTY = [];
/**
* @var EntityManagerInterface
*/
public static $em;
/**
* @var DbConn
*/
private static $conn;
public function getConnection(): DbConn
{
if (isset(self::$conn)) {
return self::$conn;
}
/** @var PDOConnection $pdo */
$pdo = static::$em->getConnection()->getWrappedConnection();
return self::$conn = $this->createDefaultDBConnection($pdo, static::$em->getConnection()->getDatabase());
}
public function getDataSet(): DataSet
{
return $this->createArrayDataSet([]);
}
protected function getEntityManager(): EntityManagerInterface
{

View File

@@ -38,8 +38,8 @@ class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase
$instance = $this->factory->__invoke(new ServiceManager(), '');
$ref = new \ReflectionObject($instance);
$prop = $ref->getProperty('response');
$prop = $ref->getProperty('responseFactory');
$prop->setAccessible(true);
$this->assertInstanceOf(EmptyResponse::class, $prop->getValue($instance));
$this->assertInstanceOf(EmptyResponse::class, $prop->getValue($instance)());
}
}

View File

@@ -32,7 +32,7 @@ class LocaleMiddlewareTest extends TestCase
public function whenNoHeaderIsPresentLocaleIsNotChanged()
{
$this->assertEquals('ru', $this->translator->getLocale());
$this->middleware->process(ServerRequestFactory::fromGlobals(), TestUtils::createDelegateMock()->reveal());
$this->middleware->process(ServerRequestFactory::fromGlobals(), TestUtils::createReqHandlerMock()->reveal());
$this->assertEquals('ru', $this->translator->getLocale());
}
@@ -43,7 +43,7 @@ class LocaleMiddlewareTest extends TestCase
{
$this->assertEquals('ru', $this->translator->getLocale());
$request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'es');
$this->middleware->process($request, TestUtils::createDelegateMock()->reveal());
$this->middleware->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertEquals('es', $this->translator->getLocale());
}
@@ -52,7 +52,7 @@ class LocaleMiddlewareTest extends TestCase
*/
public function localeGetsNormalized()
{
$delegate = TestUtils::createDelegateMock();
$delegate = TestUtils::createReqHandlerMock();
$this->assertEquals('ru', $this->translator->getLocale());

View File

@@ -3,22 +3,22 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Util;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Prophecy\Argument;
use Prophecy\Prophet;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response;
class TestUtils
{
private static $prophet;
public static function createDelegateMock(ResponseInterface $response = null, RequestInterface $request = null)
public static function createReqHandlerMock(ResponseInterface $response = null, RequestInterface $request = null)
{
$argument = $request ?: Argument::any();
$delegate = static::getProphet()->prophesize(DelegateInterface::class);
$delegate->process($argument)->willReturn($response ?: new Response());
$delegate = static::getProphet()->prophesize(RequestHandlerInterface::class);
$delegate->handle($argument)->willReturn($response ?: new Response());
return $delegate;
}

View File

@@ -6,7 +6,7 @@ use Shlinkio\Shlink\Common\Service\PreviewGenerator;
use Shlinkio\Shlink\Core\Action;
use Shlinkio\Shlink\Core\Middleware;
use Shlinkio\Shlink\Core\Options;
use Shlinkio\Shlink\Core\Response\NotFoundDelegate;
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
use Shlinkio\Shlink\Core\Service;
use Zend\Expressive\Router\RouterInterface;
use Zend\Expressive\Template\TemplateRendererInterface;
@@ -17,7 +17,7 @@ return [
'dependencies' => [
'factories' => [
Options\AppOptions::class => Options\AppOptionsFactory::class,
NotFoundDelegate::class => ConfigAbstractFactory::class,
NotFoundHandler::class => ConfigAbstractFactory::class,
// Services
Service\UrlShortener::class => ConfigAbstractFactory::class,
@@ -28,18 +28,15 @@ return [
// Middleware
Action\RedirectAction::class => ConfigAbstractFactory::class,
Action\PixelAction::class => ConfigAbstractFactory::class,
Action\QrCodeAction::class => ConfigAbstractFactory::class,
Action\PreviewAction::class => ConfigAbstractFactory::class,
Middleware\QrCodeCacheMiddleware::class => ConfigAbstractFactory::class,
],
'aliases' => [
'Zend\Expressive\Delegate\DefaultDelegate' => NotFoundDelegate::class,
],
],
ConfigAbstractFactory::class => [
NotFoundDelegate::class => [TemplateRendererInterface::class],
NotFoundHandler::class => [TemplateRendererInterface::class],
// Services
Service\UrlShortener::class => [
@@ -59,9 +56,16 @@ return [
Service\UrlShortener::class,
Service\VisitsTracker::class,
Options\AppOptions::class,
'Logger_Shlink',
],
Action\PixelAction::class => [
Service\UrlShortener::class,
Service\VisitsTracker::class,
Options\AppOptions::class,
'Logger_Shlink',
],
Action\QrCodeAction::class => [RouterInterface::class, Service\UrlShortener::class, 'Logger_Shlink'],
Action\PreviewAction::class => [PreviewGenerator::class, Service\UrlShortener::class],
Action\PreviewAction::class => [PreviewGenerator::class, Service\UrlShortener::class, 'Logger_Shlink'],
Middleware\QrCodeCacheMiddleware::class => [Cache::class],
],

View File

@@ -13,6 +13,12 @@ return [
'middleware' => Action\RedirectAction::class,
'allowed_methods' => ['GET'],
],
[
'name' => 'pixel-tracking',
'path' => '/{shortCode}/track',
'middleware' => Action\PixelAction::class,
'allowed_methods' => ['GET'],
],
[
'name' => 'short-url-qr-code',
'path' => '/{shortCode}/qr-code[/{size:[0-9]+}]',

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
abstract class AbstractTrackingAction implements MiddlewareInterface
{
use ErrorResponseBuilderTrait;
/**
* @var UrlShortenerInterface
*/
private $urlShortener;
/**
* @var VisitsTrackerInterface
*/
private $visitTracker;
/**
* @var AppOptions
*/
private $appOptions;
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(
UrlShortenerInterface $urlShortener,
VisitsTrackerInterface $visitTracker,
AppOptions $appOptions,
LoggerInterface $logger = null
) {
$this->urlShortener = $urlShortener;
$this->visitTracker = $visitTracker;
$this->appOptions = $appOptions;
$this->logger = $logger ?: new NullLogger();
}
/**
* Process an incoming server request and return a response, optionally delegating
* to the next middleware component to create the response.
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
*
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$shortCode = $request->getAttribute('shortCode', '');
$query = $request->getQueryParams();
$disableTrackParam = $this->appOptions->getDisableTrackParam();
try {
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
// Track visit to this short code
if ($disableTrackParam === null || ! \array_key_exists($disableTrackParam, $query)) {
$this->visitTracker->track($shortCode, $request);
}
return $this->createResp($longUrl);
} catch (InvalidShortCodeException | EntityDoesNotExistException $e) {
$this->logger->warning('An error occurred while tracking short code.' . PHP_EOL . $e);
return $this->buildErrorResponse($request, $handler);
}
}
abstract protected function createResp(string $longUrl): ResponseInterface;
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action;
use Psr\Http\Message\ResponseInterface;
use Shlinkio\Shlink\Common\Response\PixelResponse;
class PixelAction extends AbstractTrackingAction
{
protected function createResp(string $longUrl): ResponseInterface
{
return new PixelResponse();
}
}

View File

@@ -3,10 +3,12 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
use Shlinkio\Shlink\Common\Util\ResponseUtilsTrait;
@@ -28,11 +30,19 @@ class PreviewAction implements MiddlewareInterface
* @var UrlShortenerInterface
*/
private $urlShortener;
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(PreviewGeneratorInterface $previewGenerator, UrlShortenerInterface $urlShortener)
{
public function __construct(
PreviewGeneratorInterface $previewGenerator,
UrlShortenerInterface $urlShortener,
LoggerInterface $logger = null
) {
$this->previewGenerator = $previewGenerator;
$this->urlShortener = $urlShortener;
$this->logger = $logger ?: new NullLogger();
}
/**
@@ -40,11 +50,11 @@ class PreviewAction implements MiddlewareInterface
* to the next middleware component to create the response.
*
* @param Request $request
* @param DelegateInterface $delegate
* @param RequestHandlerInterface $handler
*
* @return Response
*/
public function process(Request $request, DelegateInterface $delegate)
public function process(Request $request, RequestHandlerInterface $handler): Response
{
$shortCode = $request->getAttribute('shortCode');
@@ -52,12 +62,9 @@ class PreviewAction implements MiddlewareInterface
$url = $this->urlShortener->shortCodeToUrl($shortCode);
$imagePath = $this->previewGenerator->generatePreview($url);
return $this->generateImageResponse($imagePath);
} catch (InvalidShortCodeException $e) {
return $this->buildErrorResponse($request, $delegate);
} catch (EntityDoesNotExistException $e) {
return $this->buildErrorResponse($request, $delegate);
} catch (PreviewGenerationException $e) {
return $this->buildErrorResponse($request, $delegate);
} catch (InvalidShortCodeException | EntityDoesNotExistException | PreviewGenerationException $e) {
$this->logger->warning('An error occurred while generating preview image.' . PHP_EOL . $e);
return $this->buildErrorResponse($request, $handler);
}
}
}

View File

@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action;
use Endroid\QrCode\QrCode;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
@@ -15,6 +15,7 @@ use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Zend\Expressive\Router\Exception\RuntimeException;
use Zend\Expressive\Router\RouterInterface;
class QrCodeAction implements MiddlewareInterface
@@ -49,22 +50,21 @@ class QrCodeAction implements MiddlewareInterface
* to the next middleware component to create the response.
*
* @param Request $request
* @param DelegateInterface $delegate
* @param RequestHandlerInterface $handler
*
* @return Response
* @throws \InvalidArgumentException
* @throws RuntimeException
*/
public function process(Request $request, DelegateInterface $delegate)
public function process(Request $request, RequestHandlerInterface $handler): Response
{
// Make sure the short URL exists for this short code
$shortCode = $request->getAttribute('shortCode');
try {
$this->urlShortener->shortCodeToUrl($shortCode);
} catch (InvalidShortCodeException $e) {
$this->logger->warning('Tried to create a QR code with an invalid short code' . PHP_EOL . $e);
return $this->buildErrorResponse($request, $delegate);
} catch (EntityDoesNotExistException $e) {
$this->logger->warning('Tried to create a QR code with a not found short code' . PHP_EOL . $e);
return $this->buildErrorResponse($request, $delegate);
} catch (InvalidShortCodeException | EntityDoesNotExistException $e) {
$this->logger->warning('An error occurred while creating QR code' . PHP_EOL . $e);
return $this->buildErrorResponse($request, $handler);
}
$path = $this->router->generateUri('long-url-redirect', ['shortCode' => $shortCode]);
@@ -80,7 +80,7 @@ class QrCodeAction implements MiddlewareInterface
* @param Request $request
* @return int
*/
protected function getSizeParam(Request $request)
private function getSizeParam(Request $request): int
{
$size = (int) $request->getAttribute('size', 300);
if ($size < 50) {

View File

@@ -3,75 +3,15 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Zend\Diactoros\Response\RedirectResponse;
class RedirectAction implements MiddlewareInterface
class RedirectAction extends AbstractTrackingAction
{
use ErrorResponseBuilderTrait;
/**
* @var UrlShortenerInterface
*/
private $urlShortener;
/**
* @var VisitsTrackerInterface
*/
private $visitTracker;
/**
* @var AppOptions
*/
private $appOptions;
public function __construct(
UrlShortenerInterface $urlShortener,
VisitsTrackerInterface $visitTracker,
AppOptions $appOptions
) {
$this->urlShortener = $urlShortener;
$this->visitTracker = $visitTracker;
$this->appOptions = $appOptions;
}
/**
* Process an incoming server request and return a response, optionally delegating
* to the next middleware component to create the response.
*
* @param Request $request
* @param DelegateInterface $delegate
*
* @return Response
*/
public function process(Request $request, DelegateInterface $delegate)
protected function createResp(string $longUrl): Response
{
$shortCode = $request->getAttribute('shortCode', '');
$query = $request->getQueryParams();
$disableTrackParam = $this->appOptions->getDisableTrackParam();
try {
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
// Track visit to this short code
if ($disableTrackParam === null || ! \array_key_exists($disableTrackParam, $query)) {
$this->visitTracker->track($shortCode, $request);
}
// Return a redirect response to the long URL.
// Use a temporary redirect to make sure browsers always hit the server for analytics purposes
return new RedirectResponse($longUrl);
} catch (InvalidShortCodeException $e) {
return $this->buildErrorResponse($request, $delegate);
} catch (EntityDoesNotExistException $e) {
return $this->buildErrorResponse($request, $delegate);
}
// Return a redirect response to the long URL.
// Use a temporary redirect to make sure browsers always hit the server for analytics purposes
return new RedirectResponse($longUrl);
}
}

View File

@@ -3,16 +3,18 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action\Util;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\Response\NotFoundDelegate;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
trait ErrorResponseBuilderTrait
{
private function buildErrorResponse(ServerRequestInterface $request, DelegateInterface $delegate): ResponseInterface
{
$request = $request->withAttribute(NotFoundDelegate::NOT_FOUND_TEMPLATE, 'ShlinkCore::invalid-short-code');
return $delegate->process($request);
private function buildErrorResponse(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
$request = $request->withAttribute(NotFoundHandler::NOT_FOUND_TEMPLATE, 'ShlinkCore::invalid-short-code');
return $handler->handle($request);
}
}

View File

@@ -57,7 +57,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
/**
* @return string
*/
public function getReferer()
public function getReferer(): string
{
return $this->referer;
}
@@ -66,7 +66,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
* @param string $referer
* @return $this
*/
public function setReferer($referer)
public function setReferer($referer): self
{
$this->referer = $referer;
return $this;
@@ -75,7 +75,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
/**
* @return \DateTime
*/
public function getDate()
public function getDate(): \DateTime
{
return $this->date;
}
@@ -84,7 +84,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
* @param \DateTime $date
* @return $this
*/
public function setDate($date)
public function setDate($date): self
{
$this->date = $date;
return $this;
@@ -93,7 +93,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
/**
* @return ShortUrl
*/
public function getShortUrl()
public function getShortUrl(): ShortUrl
{
return $this->shortUrl;
}
@@ -102,7 +102,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
* @param ShortUrl $shortUrl
* @return $this
*/
public function setShortUrl($shortUrl)
public function setShortUrl($shortUrl): self
{
$this->shortUrl = $shortUrl;
return $this;
@@ -111,7 +111,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
/**
* @return string
*/
public function getRemoteAddr()
public function getRemoteAddr(): string
{
return $this->remoteAddr;
}
@@ -120,7 +120,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
* @param string $remoteAddr
* @return $this
*/
public function setRemoteAddr($remoteAddr)
public function setRemoteAddr($remoteAddr): self
{
$this->remoteAddr = $remoteAddr;
return $this;
@@ -129,7 +129,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
/**
* @return string
*/
public function getUserAgent()
public function getUserAgent(): string
{
return $this->userAgent;
}
@@ -138,7 +138,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
* @param string $userAgent
* @return $this
*/
public function setUserAgent($userAgent)
public function setUserAgent($userAgent): self
{
$this->userAgent = $userAgent;
return $this;
@@ -147,7 +147,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
/**
* @return VisitLocation
*/
public function getVisitLocation()
public function getVisitLocation(): VisitLocation
{
return $this->visitLocation;
}
@@ -156,7 +156,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
* @param VisitLocation $visitLocation
* @return $this
*/
public function setVisitLocation($visitLocation)
public function setVisitLocation($visitLocation): self
{
$this->visitLocation = $visitLocation;
return $this;
@@ -165,11 +165,11 @@ class Visit extends AbstractEntity implements \JsonSerializable
/**
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* @return array data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize()
public function jsonSerialize(): array
{
return [
'referer' => $this->referer,

View File

@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Middleware;
use Doctrine\Common\Cache\Cache;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response as DiactResp;
class QrCodeCacheMiddleware implements MiddlewareInterface
@@ -27,11 +27,11 @@ class QrCodeCacheMiddleware implements MiddlewareInterface
* to the next middleware component to create the response.
*
* @param Request $request
* @param DelegateInterface $delegate
* @param RequestHandlerInterface $handler
*
* @return Response
*/
public function process(Request $request, DelegateInterface $delegate)
public function process(Request $request, RequestHandlerInterface $handler): Response
{
$cacheKey = $request->getUri()->getPath();
@@ -45,7 +45,7 @@ class QrCodeCacheMiddleware implements MiddlewareInterface
// If not, call the next middleware and cache it
/** @var Response $resp */
$resp = $delegate->process($request);
$resp = $handler->handle($request);
$this->cache->save($cacheKey, [
'body' => $resp->getBody()->__toString(),
'content-type' => $resp->getHeaderLine('Content-Type'),

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model;
use Psr\Http\Message\UriInterface;
final class CreateShortCodeData
{
/**
* @var UriInterface
*/
private $longUrl;
/**
* @var array
*/
private $tags;
/**
* @var ShortUrlMeta
*/
private $meta;
public function __construct(
UriInterface $longUrl,
array $tags = [],
ShortUrlMeta $meta = null
) {
$this->longUrl = $longUrl;
$this->tags = $tags;
$this->meta = $meta ?? ShortUrlMeta::createFromParams();
}
/**
* @return UriInterface
*/
public function getLongUrl(): UriInterface
{
return $this->longUrl;
}
/**
* @return array
*/
public function getTags(): array
{
return $this->tags;
}
/**
* @return ShortUrlMeta
*/
public function getMeta(): ShortUrlMeta
{
return $this->meta;
}
}

View File

@@ -71,7 +71,7 @@ final class ShortUrlMeta
* @param array $data
* @throws ValidationException
*/
private function validate(array $data)
private function validate(array $data): void
{
$inputFilter = new ShortUrlMetaInputFilter($data);
if (! $inputFilter->isValid()) {

View File

@@ -4,15 +4,15 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Response;
use Fig\Http\Message\StatusCodeInterface;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response;
use Zend\Expressive\Template\TemplateRendererInterface;
class NotFoundDelegate implements DelegateInterface
class NotFoundHandler implements RequestHandlerInterface
{
const NOT_FOUND_TEMPLATE = 'notFoundTemplate';
public const NOT_FOUND_TEMPLATE = 'notFoundTemplate';
/**
* @var TemplateRendererInterface
@@ -37,14 +37,14 @@ class NotFoundDelegate implements DelegateInterface
* @return ResponseInterface
* @throws \InvalidArgumentException
*/
public function process(ServerRequestInterface $request): ResponseInterface
public function handle(ServerRequestInterface $request): ResponseInterface
{
$accepts = explode(',', $request->getHeaderLine('Accept'));
$accept = array_shift($accepts);
$accepts = \explode(',', $request->getHeaderLine('Accept'));
$accept = \array_shift($accepts);
$status = StatusCodeInterface::STATUS_NOT_FOUND;
// If the first accepted type is json, return a json response
if (in_array($accept, ['application/json', 'text/json', 'application/x-json'], true)) {
if (\in_array($accept, ['application/json', 'text/json', 'application/x-json'], true)) {
return new Response\JsonResponse([
'error' => 'NOT_FOUND',
'message' => 'Not found',

View File

@@ -14,7 +14,7 @@ trait TagManagerTrait
* @param string[] $tags
* @return Collections\Collection|Tag[]
*/
protected function tagNamesToEntities(EntityManagerInterface $em, array $tags)
private function tagNamesToEntities(EntityManagerInterface $em, array $tags)
{
$entities = [];
foreach ($tags as $tagName) {
@@ -33,8 +33,8 @@ trait TagManagerTrait
* @param string $tagName
* @return string
*/
protected function normalizeTagName($tagName)
private function normalizeTagName($tagName): string
{
return str_replace(' ', '-', strtolower(trim($tagName)));
return \str_replace(' ', '-', \strtolower(\trim($tagName)));
}
}

View File

@@ -9,7 +9,7 @@ use Zend\InputFilter\Input;
trait InputFactoryTrait
{
public function createInput($name, $required = true): Input
private function createInput($name, $required = true): Input
{
$input = new Input($name);
$input->setRequired($required)

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Action;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Response\PixelResponse;
use Shlinkio\Shlink\Core\Action\PixelAction;
use Shlinkio\Shlink\Core\Action\RedirectAction;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\Service\VisitsTracker;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequestFactory;
class PixelActionTest extends TestCase
{
/**
* @var RedirectAction
*/
protected $action;
/**
* @var ObjectProphecy
*/
protected $urlShortener;
/**
* @var ObjectProphecy
*/
protected $visitTracker;
public function setUp()
{
$this->urlShortener = $this->prophesize(UrlShortener::class);
$this->visitTracker = $this->prophesize(VisitsTracker::class);
$this->action = new PixelAction(
$this->urlShortener->reveal(),
$this->visitTracker->reveal(),
new AppOptions()
);
}
/**
* @test
*/
public function imageIsReturned()
{
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn('http://domain.com/foo/bar')
->shouldBeCalledTimes(1);
$this->visitTracker->track(Argument::cetera())->willReturn(null)
->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertInstanceOf(PixelResponse::class, $response);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('image/gif', $response->getHeaderLine('content-type'));
}
}

View File

@@ -3,11 +3,11 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Action;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
use Shlinkio\Shlink\Core\Action\PreviewAction;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
@@ -47,8 +47,8 @@ class PreviewActionTest extends TestCase
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class)
->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class);
$delegate->process(Argument::cetera())->shouldBeCalledTimes(1)
$delegate = $this->prophesize(RequestHandlerInterface::class);
$delegate->handle(Argument::cetera())->shouldBeCalledTimes(1)
->willReturn(new Response());
$this->action->process(
@@ -70,7 +70,7 @@ class PreviewActionTest extends TestCase
$resp = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
TestUtils::createDelegateMock()->reveal()
TestUtils::createReqHandlerMock()->reveal()
);
$this->assertEquals(filesize($path), $resp->getHeaderLine('Content-length'));
@@ -85,9 +85,9 @@ class PreviewActionTest extends TestCase
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class)
->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class);
$delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */
$process = $delegate->process(Argument::any())->willReturn(new Response());
$process = $delegate->handle(Argument::any())->willReturn(new Response());
$this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),

View File

@@ -3,11 +3,11 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Action;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
use Shlinkio\Shlink\Core\Action\QrCodeAction;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
@@ -46,8 +46,8 @@ class QrCodeActionTest extends TestCase
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class)
->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class);
$process = $delegate->process(Argument::any())->willReturn(new Response());
$delegate = $this->prophesize(RequestHandlerInterface::class);
$process = $delegate->handle(Argument::any())->willReturn(new Response());
$this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
@@ -65,9 +65,9 @@ class QrCodeActionTest extends TestCase
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class)
->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class);
$delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */
$process = $delegate->process(Argument::any())->willReturn(new Response());
$process = $delegate->handle(Argument::any())->willReturn(new Response());
$this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
@@ -84,7 +84,7 @@ class QrCodeActionTest extends TestCase
{
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn('')->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class);
$delegate = $this->prophesize(RequestHandlerInterface::class);
$resp = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
@@ -93,6 +93,6 @@ class QrCodeActionTest extends TestCase
$this->assertInstanceOf(QrCodeResponse::class, $resp);
$this->assertEquals(200, $resp->getStatusCode());
$delegate->process(Argument::any())->shouldHaveBeenCalledTimes(0);
$delegate->handle(Argument::any())->shouldHaveBeenCalledTimes(0);
}
}

View File

@@ -3,11 +3,11 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Action;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Core\Action\RedirectAction;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Options\AppOptions;
@@ -57,7 +57,7 @@ class RedirectActionTest extends TestCase
->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->action->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertInstanceOf(Response\RedirectResponse::class, $response);
$this->assertEquals(302, $response->getStatusCode());
@@ -76,9 +76,9 @@ class RedirectActionTest extends TestCase
$this->visitTracker->track(Argument::cetera())->willReturn(null)
->shouldNotBeCalled();
$delegate = $this->prophesize(DelegateInterface::class);
$delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */
$process = $delegate->process(Argument::any())->willReturn(new Response());
$process = $delegate->handle(Argument::any())->willReturn(new Response());
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$this->action->process($request, $delegate->reveal());
@@ -100,7 +100,7 @@ class RedirectActionTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode)
->withQueryParams(['foobar' => true]);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->action->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertInstanceOf(Response\RedirectResponse::class, $response);
$this->assertEquals(302, $response->getStatusCode());

View File

@@ -5,9 +5,9 @@ namespace ShlinkioTest\Shlink\Core\Middleware;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\Cache;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Core\Middleware\QrCodeCacheMiddleware;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
@@ -35,8 +35,8 @@ class QrCodeCacheMiddlewareTest extends TestCase
*/
public function noCachedPathFallsBackToNextMiddleware()
{
$delegate = $this->prophesize(DelegateInterface::class);
$delegate->process(Argument::any())->willReturn(new Response())->shouldBeCalledTimes(1);
$delegate = $this->prophesize(RequestHandlerInterface::class);
$delegate->handle(Argument::any())->willReturn(new Response())->shouldBeCalledTimes(1);
$this->middleware->process(ServerRequestFactory::fromGlobals()->withUri(
new Uri('/foo/bar')
@@ -53,7 +53,7 @@ class QrCodeCacheMiddlewareTest extends TestCase
$isCalled = false;
$uri = (new Uri())->withPath('/foo');
$this->cache->save('/foo', ['body' => 'the body', 'content-type' => 'image/png']);
$delegate = $this->prophesize(DelegateInterface::class);
$delegate = $this->prophesize(RequestHandlerInterface::class);
$resp = $this->middleware->process(
ServerRequestFactory::fromGlobals()->withUri($uri),
@@ -64,6 +64,6 @@ class QrCodeCacheMiddlewareTest extends TestCase
$resp->getBody()->rewind();
$this->assertEquals('the body', $resp->getBody()->getContents());
$this->assertEquals('image/png', $resp->getHeaderLine('Content-Type'));
$delegate->process(Argument::any())->shouldHaveBeenCalledTimes(0);
$delegate->handle(Argument::any())->shouldHaveBeenCalledTimes(0);
}
}

View File

@@ -7,15 +7,15 @@ use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Response\NotFoundDelegate;
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
use Zend\Expressive\Template\TemplateRendererInterface;
class NotFoundDelegateTest extends TestCase
class NotFoundHandlerTest extends TestCase
{
/**
* @var NotFoundDelegate
* @var NotFoundHandler
*/
private $delegate;
/**
@@ -26,7 +26,7 @@ class NotFoundDelegateTest extends TestCase
public function setUp()
{
$this->renderer = $this->prophesize(TemplateRendererInterface::class);
$this->delegate = new NotFoundDelegate($this->renderer->reveal());
$this->delegate = new NotFoundHandler($this->renderer->reveal());
}
/**
@@ -43,7 +43,7 @@ class NotFoundDelegateTest extends TestCase
/** @var MethodProphecy $render */
$render = $this->renderer->render(Argument::cetera())->willReturn('');
$resp = $this->delegate->process($request);
$resp = $this->delegate->handle($request);
$this->assertInstanceOf($expectedResponse, $resp);
$render->shouldHaveBeenCalledTimes($renderCalls);

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest;
return [
'auth' => [
'routes_whitelist' => [
Action\AuthenticateAction::class,
Action\ShortCode\SingleStepCreateShortCodeAction::class,
],
],
];

View File

@@ -20,12 +20,13 @@ return [
ApiKeyService::class => ConfigAbstractFactory::class,
Action\AuthenticateAction::class => ConfigAbstractFactory::class,
Action\CreateShortcodeAction::class => ConfigAbstractFactory::class,
Action\EditShortCodeAction::class => ConfigAbstractFactory::class,
Action\ResolveUrlAction::class => ConfigAbstractFactory::class,
Action\GetVisitsAction::class => ConfigAbstractFactory::class,
Action\ListShortcodesAction::class => ConfigAbstractFactory::class,
Action\EditShortcodeTagsAction::class => ConfigAbstractFactory::class,
Action\ShortCode\CreateShortCodeAction::class => ConfigAbstractFactory::class,
Action\ShortCode\SingleStepCreateShortCodeAction::class => ConfigAbstractFactory::class,
Action\ShortCode\EditShortCodeAction::class => ConfigAbstractFactory::class,
Action\ShortCode\ResolveUrlAction::class => ConfigAbstractFactory::class,
Action\Visit\GetVisitsAction::class => ConfigAbstractFactory::class,
Action\ShortCode\ListShortCodesAction::class => ConfigAbstractFactory::class,
Action\ShortCode\EditShortCodeTagsAction::class => ConfigAbstractFactory::class,
Action\Tag\ListTagsAction::class => ConfigAbstractFactory::class,
Action\Tag\DeleteTagsAction::class => ConfigAbstractFactory::class,
Action\Tag\CreateTagsAction::class => ConfigAbstractFactory::class,
@@ -35,6 +36,7 @@ return [
Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
Middleware\PathVersionMiddleware::class => InvokableFactory::class,
Middleware\CheckAuthenticationMiddleware::class => ConfigAbstractFactory::class,
Middleware\ShortCode\CreateShortCodeContentNegotiationMiddleware::class => InvokableFactory::class,
],
],
@@ -43,23 +45,39 @@ return [
ApiKeyService::class => ['em'],
Action\AuthenticateAction::class => [ApiKeyService::class, JWTService::class, 'translator', 'Logger_Shlink'],
Action\CreateShortcodeAction::class => [
Action\ShortCode\CreateShortCodeAction::class => [
Service\UrlShortener::class,
'translator',
'config.url_shortener.domain',
'Logger_Shlink',
],
Action\EditShortCodeAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink',],
Action\ResolveUrlAction::class => [Service\UrlShortener::class, 'translator'],
Action\GetVisitsAction::class => [Service\VisitsTracker::class, 'translator', 'Logger_Shlink'],
Action\ListShortcodesAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink'],
Action\EditShortcodeTagsAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink'],
Action\ShortCode\SingleStepCreateShortCodeAction::class => [
Service\UrlShortener::class,
'translator',
ApiKeyService::class,
'config.url_shortener.domain',
'Logger_Shlink',
],
Action\ShortCode\EditShortCodeAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink',],
Action\ShortCode\ResolveUrlAction::class => [Service\UrlShortener::class, 'translator'],
Action\Visit\GetVisitsAction::class => [Service\VisitsTracker::class, 'translator', 'Logger_Shlink'],
Action\ShortCode\ListShortCodesAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink'],
Action\ShortCode\EditShortCodeTagsAction::class => [
Service\ShortUrlService::class,
'translator',
'Logger_Shlink',
],
Action\Tag\ListTagsAction::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\UpdateTagAction::class => [Service\Tag\TagService::class, Translator::class, LoggerInterface::class],
Middleware\CheckAuthenticationMiddleware::class => [JWTService::class, 'translator', 'Logger_Shlink'],
Middleware\CheckAuthenticationMiddleware::class => [
JWTService::class,
'translator',
'config.auth.routes_whitelist',
'Logger_Shlink',
],
],
];

View File

@@ -1,84 +1,35 @@
<?php
declare(strict_types=1);
use Fig\Http\Message\RequestMethodInterface as RequestMethod;
namespace Shlinkio\Shlink\Rest;
use Shlinkio\Shlink\Rest\Action;
return [
'routes' => [
[
'name' => Action\AuthenticateAction::class,
'path' => '/authenticate',
'middleware' => Action\AuthenticateAction::class,
'allowed_methods' => [RequestMethod::METHOD_POST],
],
Action\AuthenticateAction::getRouteDef(),
// Short codes
[
'name' => Action\CreateShortcodeAction::class,
'path' => '/short-codes',
'middleware' => Action\CreateShortcodeAction::class,
'allowed_methods' => [RequestMethod::METHOD_POST],
],
[
'name' => Action\EditShortCodeAction::class,
'path' => '/short-codes/{shortCode}',
'middleware' => Action\EditShortCodeAction::class,
'allowed_methods' => [RequestMethod::METHOD_PUT],
],
[
'name' => Action\ResolveUrlAction::class,
'path' => '/short-codes/{shortCode}',
'middleware' => Action\ResolveUrlAction::class,
'allowed_methods' => [RequestMethod::METHOD_GET],
],
[
'name' => Action\ListShortcodesAction::class,
'path' => '/short-codes',
'middleware' => Action\ListShortcodesAction::class,
'allowed_methods' => [RequestMethod::METHOD_GET],
],
[
'name' => Action\EditShortcodeTagsAction::class,
'path' => '/short-codes/{shortCode}/tags',
'middleware' => Action\EditShortcodeTagsAction::class,
'allowed_methods' => [RequestMethod::METHOD_PUT],
],
Action\ShortCode\CreateShortCodeAction::getRouteDef([
Middleware\ShortCode\CreateShortCodeContentNegotiationMiddleware::class,
]),
Action\ShortCode\SingleStepCreateShortCodeAction::getRouteDef([
Middleware\ShortCode\CreateShortCodeContentNegotiationMiddleware::class,
]),
Action\ShortCode\EditShortCodeAction::getRouteDef(),
Action\ShortCode\ResolveUrlAction::getRouteDef(),
Action\ShortCode\ListShortCodesAction::getRouteDef(),
Action\ShortCode\EditShortCodeTagsAction::getRouteDef(),
// Visits
[
'name' => Action\GetVisitsAction::class,
'path' => '/short-codes/{shortCode}/visits',
'middleware' => Action\GetVisitsAction::class,
'allowed_methods' => [RequestMethod::METHOD_GET],
],
Action\Visit\GetVisitsAction::getRouteDef(),
// Tags
[
'name' => Action\Tag\ListTagsAction::class,
'path' => '/tags',
'middleware' => Action\Tag\ListTagsAction::class,
'allowed_methods' => [RequestMethod::METHOD_GET],
],
[
'name' => Action\Tag\DeleteTagsAction::class,
'path' => '/tags',
'middleware' => Action\Tag\DeleteTagsAction::class,
'allowed_methods' => [RequestMethod::METHOD_DELETE],
],
[
'name' => Action\Tag\CreateTagsAction::class,
'path' => '/tags',
'middleware' => Action\Tag\CreateTagsAction::class,
'allowed_methods' => [RequestMethod::METHOD_POST],
],
[
'name' => Action\Tag\UpdateTagAction::class,
'path' => '/tags',
'middleware' => Action\Tag\UpdateTagAction::class,
'allowed_methods' => [RequestMethod::METHOD_PUT],
],
Action\Tag\ListTagsAction::getRouteDef(),
Action\Tag\DeleteTagsAction::getRouteDef(),
Action\Tag\CreateTagsAction::getRouteDef(),
Action\Tag\UpdateTagAction::getRouteDef(),
],
];

Binary file not shown.

View File

@@ -1,15 +1,15 @@
msgid ""
msgstr ""
"Project-Id-Version: Shlink 1.0\n"
"POT-Creation-Date: 2018-01-21 09:40+0100\n"
"PO-Revision-Date: 2018-01-21 09:40+0100\n"
"POT-Creation-Date: 2018-05-06 12:34+0200\n"
"PO-Revision-Date: 2018-05-06 12:35+0200\n"
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
"Language-Team: \n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.4\n"
"X-Generator: Poedit 2.0.6\n"
"X-Poedit-Basepath: ..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
@@ -25,9 +25,6 @@ msgstr ""
msgid "Provided API key does not exist or is invalid."
msgstr "La clave de API proporcionada no existe o es inválida."
msgid "A URL was not provided"
msgstr "No se ha proporcionado una URL"
#, php-format
msgid "Provided URL %s is invalid. Try with a different one."
msgstr "La URL proporcionada \"%s\" es inválida. Prueba con una diferente."
@@ -39,6 +36,9 @@ msgstr "El slug proporcionado \"%s\" ya está en uso. Prueba con uno diferente."
msgid "Unexpected error occurred"
msgstr "Ocurrió un error inesperado"
msgid "A URL was not provided"
msgstr "No se ha proporcionado una URL"
#, php-format
msgid "No URL found for short code \"%s\""
msgstr "No se ha encontrado ninguna URL para el código corto \"%s\""
@@ -49,14 +49,13 @@ msgstr "Los datos proporcionados son inválidos."
msgid "A list of tags was not provided"
msgstr "No se ha proporcionado una lista de etiquetas"
#, php-format
msgid "Provided short code %s does not exist"
msgstr "El código corto \"%s\" proporcionado no existe"
#, php-format
msgid "Provided short code \"%s\" has an invalid format"
msgstr "El código corto proporcionado \"%s\" tiene un formato no inválido"
msgid "No API key was provided or it is not valid"
msgstr "No se ha proporcionado una clave de API o esta es inválida"
msgid ""
"You have to provide both 'oldName' and 'newName' params in order to properly "
"rename the tag"
@@ -68,6 +67,10 @@ msgstr ""
msgid "It wasn't possible to find a tag with name '%s'"
msgstr "No fue posible encontrar una etiqueta con el nombre '%s'"
#, php-format
msgid "Provided short code %s does not exist"
msgstr "El código corto \"%s\" proporcionado no existe"
#, php-format
msgid "You need to provide the Bearer type in the %s header."
msgstr "Debes proporcionar el typo Bearer en la cabecera %s."

View File

@@ -5,12 +5,15 @@ namespace Shlinkio\Shlink\Rest\Action;
use Fig\Http\Message\RequestMethodInterface;
use Fig\Http\Message\StatusCodeInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
abstract class AbstractRestAction implements MiddlewareInterface, RequestMethodInterface, StatusCodeInterface
abstract class AbstractRestAction implements RequestHandlerInterface, RequestMethodInterface, StatusCodeInterface
{
protected const ROUTE_PATH = '';
protected const ROUTE_ALLOWED_METHODS = [];
/**
* @var LoggerInterface
*/
@@ -20,4 +23,14 @@ abstract class AbstractRestAction implements MiddlewareInterface, RequestMethodI
{
$this->logger = $logger ?: new NullLogger();
}
public static function getRouteDef(array $prevMiddleware = [], array $postMiddleware = []): array
{
return [
'name' => static::class,
'middleware' => \array_merge($prevMiddleware, [static::class], $postMiddleware),
'path' => static::ROUTE_PATH,
'allowed_methods' => static::ROUTE_ALLOWED_METHODS,
];
}
}

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface;
@@ -16,6 +15,9 @@ use Zend\I18n\Translator\TranslatorInterface;
class AuthenticateAction extends AbstractRestAction
{
protected const ROUTE_PATH = '/authenticate';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_POST];
/**
* @var TranslatorInterface
*/
@@ -43,11 +45,10 @@ class AuthenticateAction extends AbstractRestAction
/**
* @param Request $request
* @param DelegateInterface $delegate
* @return null|Response
* @return Response
* @throws \InvalidArgumentException
*/
public function process(Request $request, DelegateInterface $delegate)
public function handle(Request $request): Response
{
$authData = $request->getParsedBody();
if (! isset($authData['apiKey'])) {
@@ -61,7 +62,7 @@ class AuthenticateAction extends AbstractRestAction
// Authenticate using provided API key
$apiKey = $this->apiKeyService->getByKey($authData['apiKey']);
if (! isset($apiKey) || ! $apiKey->isValid()) {
if ($apiKey === null || ! $apiKey->isValid()) {
return new JsonResponse([
'error' => RestUtils::INVALID_API_KEY_ERROR,
'message' => $this->translator->translate('Provided API key does not exist or is invalid.'),

View File

@@ -1,21 +1,23 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action;
namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Model\CreateShortCodeData;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\Uri;
use Zend\I18n\Translator\TranslatorInterface;
class CreateShortcodeAction extends AbstractRestAction
abstract class AbstractCreateShortCodeAction extends AbstractRestAction
{
/**
* @var UrlShortenerInterface
@@ -28,7 +30,7 @@ class CreateShortcodeAction extends AbstractRestAction
/**
* @var TranslatorInterface
*/
private $translator;
protected $translator;
public function __construct(
UrlShortenerInterface $urlShortener,
@@ -44,37 +46,39 @@ class CreateShortcodeAction extends AbstractRestAction
/**
* @param Request $request
* @param DelegateInterface $delegate
* @return null|Response
* @return Response
* @throws \InvalidArgumentException
*/
public function process(Request $request, DelegateInterface $delegate)
public function handle(Request $request): Response
{
$postData = (array) $request->getParsedBody();
if (! isset($postData['longUrl'])) {
try {
$shortCodeData = $this->buildUrlToShortCodeData($request);
$shortCodeMeta = $shortCodeData->getMeta();
$longUrl = $shortCodeData->getLongUrl();
$customSlug = $shortCodeMeta->getCustomSlug();
} catch (InvalidArgumentException $e) {
$this->logger->warning('Provided data is invalid.' . PHP_EOL . $e);
return new JsonResponse([
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
'message' => $this->translator->translate('A URL was not provided'),
'message' => $e->getMessage(),
], self::STATUS_BAD_REQUEST);
}
$longUrl = $postData['longUrl'];
$customSlug = $postData['customSlug'] ?? null;
try {
$shortCode = $this->urlShortener->urlToShortCode(
new Uri($longUrl),
(array) ($postData['tags'] ?? []),
$this->getOptionalDate($postData, 'validSince'),
$this->getOptionalDate($postData, 'validUntil'),
$longUrl,
$shortCodeData->getTags(),
$shortCodeMeta->getValidSince(),
$shortCodeMeta->getValidUntil(),
$customSlug,
isset($postData['maxVisits']) ? (int) $postData['maxVisits'] : null
$shortCodeMeta->getMaxVisits()
);
$shortUrl = (new Uri())->withPath($shortCode)
->withScheme($this->domainConfig['schema'])
->withHost($this->domainConfig['hostname']);
return new JsonResponse([
'longUrl' => $longUrl,
'longUrl' => (string) $longUrl,
'shortUrl' => (string) $shortUrl,
'shortCode' => $shortCode,
]);
@@ -82,7 +86,7 @@ class CreateShortcodeAction extends AbstractRestAction
$this->logger->warning('Provided Invalid URL.' . PHP_EOL . $e);
return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => sprintf(
'message' => \sprintf(
$this->translator->translate('Provided URL %s is invalid. Try with a different one.'),
$longUrl
),
@@ -91,7 +95,7 @@ class CreateShortcodeAction extends AbstractRestAction
$this->logger->warning('Provided non-unique slug.' . PHP_EOL . $e);
return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => sprintf(
'message' => \sprintf(
$this->translator->translate('Provided slug %s is already in use. Try with a different one.'),
$customSlug
),
@@ -105,8 +109,10 @@ class CreateShortcodeAction extends AbstractRestAction
}
}
private function getOptionalDate(array $postData, string $fieldName)
{
return isset($postData[$fieldName]) ? new \DateTime($postData[$fieldName]) : null;
}
/**
* @param Request $request
* @return CreateShortCodeData
* @throws InvalidArgumentException
*/
abstract protected function buildUrlToShortCodeData(Request $request): CreateShortCodeData;
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Psr\Http\Message\ServerRequestInterface as Request;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Model\CreateShortCodeData;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Zend\Diactoros\Uri;
class CreateShortCodeAction extends AbstractCreateShortCodeAction
{
protected const ROUTE_PATH = '/short-codes';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_POST];
/**
* @param Request $request
* @return CreateShortCodeData
* @throws InvalidArgumentException
* @throws \InvalidArgumentException
*/
protected function buildUrlToShortCodeData(Request $request): CreateShortCodeData
{
$postData = (array) $request->getParsedBody();
if (! isset($postData['longUrl'])) {
throw new InvalidArgumentException($this->translator->translate('A URL was not provided'));
}
return new CreateShortCodeData(
new Uri($postData['longUrl']),
(array) ($postData['tags'] ?? []),
ShortUrlMeta::createFromParams(
$this->getOptionalDate($postData, 'validSince'),
$this->getOptionalDate($postData, 'validUntil'),
$postData['customSlug'] ?? null,
isset($postData['maxVisits']) ? (int) $postData['maxVisits'] : null
)
);
}
private function getOptionalDate(array $postData, string $fieldName)
{
return isset($postData[$fieldName]) ? new \DateTime($postData[$fieldName]) : null;
}
}

View File

@@ -1,15 +1,15 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action;
namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\EmptyResponse;
use Zend\Diactoros\Response\JsonResponse;
@@ -17,6 +17,9 @@ use Zend\I18n\Translator\TranslatorInterface;
class EditShortCodeAction extends AbstractRestAction
{
protected const ROUTE_PATH = '/short-codes/{shortCode}';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_PUT];
/**
* @var ShortUrlServiceInterface
*/
@@ -41,12 +44,11 @@ class EditShortCodeAction extends AbstractRestAction
* to the next middleware component to create the response.
*
* @param ServerRequestInterface $request
* @param DelegateInterface $delegate
*
* @return ResponseInterface
* @throws \InvalidArgumentException
*/
public function process(ServerRequestInterface $request, DelegateInterface $delegate): ResponseInterface
public function handle(ServerRequestInterface $request): ResponseInterface
{
$postData = (array) $request->getParsedBody();
$shortCode = $request->getAttribute('shortCode', '');

View File

@@ -1,20 +1,23 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action;
namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface;
class EditShortcodeTagsAction extends AbstractRestAction
class EditShortCodeTagsAction extends AbstractRestAction
{
protected const ROUTE_PATH = '/short-codes/{shortCode}/tags';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_PUT];
/**
* @var ShortUrlServiceInterface
*/
@@ -36,11 +39,10 @@ class EditShortcodeTagsAction extends AbstractRestAction
/**
* @param Request $request
* @param DelegateInterface $delegate
* @return null|Response
* @return Response
* @throws \InvalidArgumentException
*/
public function process(Request $request, DelegateInterface $delegate)
public function handle(Request $request): Response
{
$shortCode = $request->getAttribute('shortCode');
$bodyParams = $request->getParsedBody();

View File

@@ -1,22 +1,25 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action;
namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface;
class ListShortcodesAction extends AbstractRestAction
class ListShortCodesAction extends AbstractRestAction
{
use PaginatorUtilsTrait;
protected const ROUTE_PATH = '/short-codes';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
/**
* @var ShortUrlServiceInterface
*/
@@ -38,11 +41,10 @@ class ListShortcodesAction extends AbstractRestAction
/**
* @param Request $request
* @param DelegateInterface $delegate
* @return null|Response
* @return Response
* @throws \InvalidArgumentException
*/
public function process(Request $request, DelegateInterface $delegate)
public function handle(Request $request): Response
{
try {
$params = $this->queryToListParams($request->getQueryParams());
@@ -61,13 +63,13 @@ class ListShortcodesAction extends AbstractRestAction
* @param array $query
* @return array
*/
public function queryToListParams(array $query)
private function queryToListParams(array $query): array
{
return [
isset($query['page']) ? $query['page'] : 1,
isset($query['searchTerm']) ? $query['searchTerm'] : null,
isset($query['tags']) ? $query['tags'] : [],
isset($query['orderBy']) ? $query['orderBy'] : null,
$query['page'] ?? 1,
$query['searchTerm'] ?? null,
$query['tags'] ?? [],
$query['orderBy'] ?? null,
];
}
}

View File

@@ -1,21 +1,24 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action;
namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface;
class ResolveUrlAction extends AbstractRestAction
{
protected const ROUTE_PATH = '/short-codes/{shortCode}';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
/**
* @var UrlShortenerInterface
*/
@@ -37,11 +40,10 @@ class ResolveUrlAction extends AbstractRestAction
/**
* @param Request $request
* @param DelegateInterface $delegate
* @return null|Response
* @return Response
* @throws \InvalidArgumentException
*/
public function process(Request $request, DelegateInterface $delegate)
public function handle(Request $request): Response
{
$shortCode = $request->getAttribute('shortCode');

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Model\CreateShortCodeData;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Zend\Diactoros\Uri;
use Zend\I18n\Translator\TranslatorInterface;
class SingleStepCreateShortCodeAction extends AbstractCreateShortCodeAction
{
protected const ROUTE_PATH = '/short-codes/shorten';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
/**
* @var ApiKeyServiceInterface
*/
private $apiKeyService;
public function __construct(
UrlShortenerInterface $urlShortener,
TranslatorInterface $translator,
ApiKeyServiceInterface $apiKeyService,
array $domainConfig,
LoggerInterface $logger = null
) {
parent::__construct($urlShortener, $translator, $domainConfig, $logger);
$this->apiKeyService = $apiKeyService;
}
/**
* @param Request $request
* @return CreateShortCodeData
* @throws \InvalidArgumentException
* @throws InvalidArgumentException
*/
protected function buildUrlToShortCodeData(Request $request): CreateShortCodeData
{
$query = $request->getQueryParams();
// Check provided API key
$apiKey = $this->apiKeyService->getByKey($query['apiKey'] ?? '');
if ($apiKey === null || ! $apiKey->isValid()) {
throw new InvalidArgumentException(
$this->translator->translate('No API key was provided or it is not valid')
);
}
if (! isset($query['longUrl'])) {
throw new InvalidArgumentException($this->translator->translate('A URL was not provided'));
}
return new CreateShortCodeData(new Uri($query['longUrl']));
}
}

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\Tag;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
@@ -13,6 +12,9 @@ use Zend\Diactoros\Response\JsonResponse;
class CreateTagsAction extends AbstractRestAction
{
protected const ROUTE_PATH = '/tags';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_POST];
/**
* @var TagServiceInterface
*/
@@ -29,15 +31,14 @@ class CreateTagsAction extends AbstractRestAction
* to the next middleware component to create the response.
*
* @param ServerRequestInterface $request
* @param DelegateInterface $delegate
*
* @return ResponseInterface
* @throws \InvalidArgumentException
*/
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
public function handle(ServerRequestInterface $request): ResponseInterface
{
$body = $request->getParsedBody();
$tags = isset($body['tags']) ? $body['tags'] : [];
$tags = $body['tags'] ?? [];
return new JsonResponse([
'tags' => [

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\Tag;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
@@ -13,6 +12,9 @@ use Zend\Diactoros\Response\EmptyResponse;
class DeleteTagsAction extends AbstractRestAction
{
protected const ROUTE_PATH = '/tags';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_DELETE];
/**
* @var TagServiceInterface
*/
@@ -29,14 +31,13 @@ class DeleteTagsAction extends AbstractRestAction
* to the next middleware component to create the response.
*
* @param ServerRequestInterface $request
* @param DelegateInterface $delegate
*
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
public function handle(ServerRequestInterface $request): ResponseInterface
{
$query = $request->getQueryParams();
$tags = isset($query['tags']) ? $query['tags'] : [];
$tags = $query['tags'] ?? [];
$this->tagService->deleteTags($tags);
return new EmptyResponse();

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\Tag;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
@@ -13,6 +12,9 @@ use Zend\Diactoros\Response\JsonResponse;
class ListTagsAction extends AbstractRestAction
{
protected const ROUTE_PATH = '/tags';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
/**
* @var TagServiceInterface
*/
@@ -29,12 +31,11 @@ class ListTagsAction extends AbstractRestAction
* to the next middleware component to create the response.
*
* @param ServerRequestInterface $request
* @param DelegateInterface $delegate
*
* @return ResponseInterface
* @throws \InvalidArgumentException
*/
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
public function handle(ServerRequestInterface $request): ResponseInterface
{
return new JsonResponse([
'tags' => [

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\Tag;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
@@ -17,6 +16,9 @@ use Zend\I18n\Translator\TranslatorInterface;
class UpdateTagAction extends AbstractRestAction
{
protected const ROUTE_PATH = '/tags';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_PUT];
/**
* @var TagServiceInterface
*/
@@ -41,12 +43,11 @@ class UpdateTagAction extends AbstractRestAction
* to the next middleware component to create the response.
*
* @param ServerRequestInterface $request
* @param DelegateInterface $delegate
*
* @return ResponseInterface
* @throws \InvalidArgumentException
*/
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
public function handle(ServerRequestInterface $request): ResponseInterface
{
$body = $request->getParsedBody();
if (! isset($body['oldName'], $body['newName'])) {

View File

@@ -1,21 +1,24 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action;
namespace Shlinkio\Shlink\Rest\Action\Visit;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface;
class GetVisitsAction extends AbstractRestAction
{
protected const ROUTE_PATH = '/short-codes/{shortCode}/visits';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
/**
* @var VisitsTrackerInterface
*/
@@ -37,11 +40,10 @@ class GetVisitsAction extends AbstractRestAction
/**
* @param Request $request
* @param DelegateInterface $delegate
* @return null|Response
* @return Response
* @throws \InvalidArgumentException
*/
public function process(Request $request, DelegateInterface $delegate)
public function handle(Request $request): Response
{
$shortCode = $request->getAttribute('shortCode');
$startDate = $this->getDateQueryParam($request, 'startDate');

View File

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

View File

@@ -44,7 +44,7 @@ class ApiKey extends AbstractEntity
/**
* @return string
*/
public function getKey()
public function getKey(): string
{
return $this->key;
}
@@ -53,7 +53,7 @@ class ApiKey extends AbstractEntity
* @param string $key
* @return $this
*/
public function setKey($key)
public function setKey($key): self
{
$this->key = $key;
return $this;
@@ -62,7 +62,7 @@ class ApiKey extends AbstractEntity
/**
* @return \DateTime|null
*/
public function getExpirationDate()
public function getExpirationDate(): ?\DateTime
{
return $this->expirationDate;
}
@@ -71,7 +71,7 @@ class ApiKey extends AbstractEntity
* @param \DateTime $expirationDate
* @return $this
*/
public function setExpirationDate($expirationDate)
public function setExpirationDate($expirationDate): self
{
$this->expirationDate = $expirationDate;
return $this;
@@ -80,9 +80,9 @@ class ApiKey extends AbstractEntity
/**
* @return bool
*/
public function isExpired()
public function isExpired(): bool
{
if (! isset($this->expirationDate)) {
if ($this->expirationDate === null) {
return false;
}
@@ -92,7 +92,7 @@ class ApiKey extends AbstractEntity
/**
* @return boolean
*/
public function isEnabled()
public function isEnabled(): bool
{
return $this->enabled;
}
@@ -101,7 +101,7 @@ class ApiKey extends AbstractEntity
* @param boolean $enabled
* @return $this
*/
public function setEnabled($enabled)
public function setEnabled($enabled): self
{
$this->enabled = $enabled;
return $this;
@@ -112,7 +112,7 @@ class ApiKey extends AbstractEntity
*
* @return $this
*/
public function disable()
public function disable(): self
{
return $this->setEnabled(false);
}
@@ -122,17 +122,17 @@ class ApiKey extends AbstractEntity
*
* @return bool
*/
public function isValid()
public function isValid(): bool
{
return $this->isEnabled() && ! $this->isExpired();
}
/**
* The string repesentation of an API key is the key itself
* The string representation of an API key is the key itself
*
* @return string
*/
public function __toString()
public function __toString(): string
{
return $this->getKey();
}

View File

@@ -20,7 +20,7 @@ class JsonErrorResponseGenerator implements ErrorResponseGeneratorInterface, Sta
* @return Response
* @throws \InvalidArgumentException
*/
public function __invoke($e, Request $request, Response $response)
public function __invoke(?\Throwable $e, Request $request, Response $response)
{
$status = $response->getStatusCode();
$responsePhrase = $status < 400 ? 'Internal Server Error' : $response->getReasonPhrase();
@@ -32,8 +32,8 @@ class JsonErrorResponseGenerator implements ErrorResponseGeneratorInterface, Sta
], $status);
}
protected function responsePhraseToCode(string $responsePhrase): string
private function responsePhraseToCode(string $responsePhrase): string
{
return strtoupper(str_replace(' ', '_', $responsePhrase));
return \strtoupper(\str_replace(' ', '_', $responsePhrase));
}
}

View File

@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware;
use Fig\Http\Message\RequestMethodInterface;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Rest\Exception\RuntimeException;
class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterface
@@ -17,11 +17,11 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac
* to the next middleware component to create the response.
*
* @param Request $request
* @param DelegateInterface $delegate
* @param RequestHandlerInterface $handler
*
* @return Response
*/
public function process(Request $request, DelegateInterface $delegate)
public function process(Request $request, RequestHandlerInterface $handler): Response
{
$method = $request->getMethod();
$currentParams = $request->getParsedBody();
@@ -32,16 +32,16 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac
self::METHOD_HEAD,
self::METHOD_OPTIONS,
], true)) {
return $delegate->process($request);
return $handler->handle($request);
}
// If the accepted content is JSON, try to parse the body from JSON
$contentType = $this->getRequestContentType($request);
if (\in_array($contentType, ['application/json', 'text/json', 'application/x-json'], true)) {
return $delegate->process($this->parseFromJson($request));
return $handler->handle($this->parseFromJson($request));
}
return $delegate->process($this->parseFromUrlEncoded($request));
return $handler->handle($this->parseFromUrlEncoded($request));
}
/**

View File

@@ -4,13 +4,12 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware;
use Fig\Http\Message\StatusCodeInterface;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Shlinkio\Shlink\Rest\Action\AuthenticateAction;
use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface;
use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
use Shlinkio\Shlink\Rest\Util\RestUtils;
@@ -21,7 +20,7 @@ use Zend\Stdlib\ErrorHandler;
class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterface
{
const AUTHORIZATION_HEADER = 'Authorization';
public const AUTHORIZATION_HEADER = 'Authorization';
/**
* @var TranslatorInterface
@@ -35,14 +34,20 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeIn
* @var LoggerInterface
*/
private $logger;
/**
* @var array
*/
private $routesWhitelist;
public function __construct(
JWTServiceInterface $jwtService,
TranslatorInterface $translator,
array $routesWhitelist,
LoggerInterface $logger = null
) {
$this->translator = $translator;
$this->jwtService = $jwtService;
$this->routesWhitelist = $routesWhitelist;
$this->logger = $logger ?: new NullLogger();
}
@@ -51,23 +56,23 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeIn
* to the next middleware component to create the response.
*
* @param Request $request
* @param DelegateInterface $delegate
* @param RequestHandlerInterface $handler
*
* @return Response
* @throws \InvalidArgumentException
* @throws \ErrorException
*/
public function process(Request $request, DelegateInterface $delegate)
public function process(Request $request, RequestHandlerInterface $handler): Response
{
// If current route is the authenticate route or an OPTIONS request, continue to the next middleware
/** @var RouteResult|null $routeResult */
$routeResult = $request->getAttribute(RouteResult::class);
if ($routeResult === null
|| $routeResult->isFailure()
|| $routeResult->getMatchedRouteName() === AuthenticateAction::class
|| $request->getMethod() === 'OPTIONS'
|| \in_array($routeResult->getMatchedRouteName(), $this->routesWhitelist, true)
) {
return $delegate->process($request);
return $handler->handle($request);
}
// Check that the auth header was provided, and that it belongs to a non-expired token
@@ -77,22 +82,22 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeIn
// Get token making sure the an authorization type is provided
$authToken = $request->getHeaderLine(self::AUTHORIZATION_HEADER);
$authTokenParts = explode(' ', $authToken);
if (count($authTokenParts) === 1) {
$authTokenParts = \explode(' ', $authToken);
if (\count($authTokenParts) === 1) {
return new JsonResponse([
'error' => RestUtils::INVALID_AUTHORIZATION_ERROR,
'message' => sprintf($this->translator->translate(
'message' => \sprintf($this->translator->translate(
'You need to provide the Bearer type in the %s header.'
), self::AUTHORIZATION_HEADER),
], self::STATUS_UNAUTHORIZED);
}
// Make sure the authorization type is Bearer
list($authType, $jwt) = $authTokenParts;
if (strtolower($authType) !== 'bearer') {
[$authType, $jwt] = $authTokenParts;
if (\strtolower($authType) !== 'bearer') {
return new JsonResponse([
'error' => RestUtils::INVALID_AUTHORIZATION_ERROR,
'message' => sprintf($this->translator->translate(
'message' => \sprintf($this->translator->translate(
'Provided authorization type %s is not supported. Use Bearer instead.'
), $authType),
], self::STATUS_UNAUTHORIZED);
@@ -107,7 +112,7 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeIn
// Update the token expiration and continue to next middleware
$jwt = $this->jwtService->refresh($jwt);
$response = $delegate->process($request);
$response = $handler->handle($request);
// Return the response with the updated token on it
return $response->withHeader(self::AUTHORIZATION_HEADER, 'Bearer ' . $jwt);
@@ -119,11 +124,15 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeIn
}
}
protected function createTokenErrorResponse()
/**
* @return JsonResponse
* @throws \InvalidArgumentException
*/
private function createTokenErrorResponse(): JsonResponse
{
return new JsonResponse([
'error' => RestUtils::INVALID_AUTH_TOKEN_ERROR,
'message' => sprintf(
'message' => \sprintf(
$this->translator->translate(
'Missing or invalid auth token provided. Perform a new authentication request and send provided '
. 'token on every new request on the "%s" header'

View File

@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware;
use Fig\Http\Message\RequestMethodInterface;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class CrossDomainMiddleware implements MiddlewareInterface, RequestMethodInterface
{
@@ -16,15 +16,15 @@ class CrossDomainMiddleware implements MiddlewareInterface, RequestMethodInterfa
* to the next middleware component to create the response.
*
* @param Request $request
* @param DelegateInterface $delegate
* @param RequestHandlerInterface $handler
*
* @return Response
* @throws \InvalidArgumentException
*/
public function process(Request $request, DelegateInterface $delegate)
public function process(Request $request, RequestHandlerInterface $handler): Response
{
/** @var Response $response */
$response = $delegate->process($request);
$response = $handler->handle($request);
if (! $request->hasHeader('Origin')) {
return $response;
}

View File

@@ -3,10 +3,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class PathVersionMiddleware implements MiddlewareInterface
{
@@ -15,32 +15,21 @@ class PathVersionMiddleware implements MiddlewareInterface
* to the next middleware component to create the response.
*
* @param Request $request
* @param DelegateInterface $delegate
* @param RequestHandlerInterface $handler
*
* @return Response
* @throws \InvalidArgumentException
*/
public function process(Request $request, DelegateInterface $delegate)
public function process(Request $request, RequestHandlerInterface $handler): Response
{
$uri = $request->getUri();
$path = $uri->getPath();
// TODO Workaround... Do not process the request if it does not start with rest
if (\strpos($path, '/rest') !== 0) {
return $delegate->process($request);
}
// If the path does not begin with the version number, prepend v1 by default for BC compatibility purposes
if (\strpos($path, '/rest/v') !== 0) {
$parts = \explode('/', $path);
// Remove the first empty part and the rest part
\array_shift($parts);
\array_shift($parts);
// Prepend the version prefix
\array_unshift($parts, '/rest/v1');
$request = $request->withUri($uri->withPath(\implode('/', $parts)));
if (\strpos($path, '/v') !== 0) {
$request = $request->withUri($uri->withPath('/v1' . $uri->getPath()));
}
return $delegate->process($request);
return $handler->handle($request);
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware\ShortCode;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Response\JsonResponse;
class CreateShortCodeContentNegotiationMiddleware implements MiddlewareInterface
{
private const PLAIN_TEXT = 'text';
private const JSON = 'json';
/**
* Process an incoming server request and return a response, optionally delegating
* response creation to a handler.
* @throws \RuntimeException
* @throws \InvalidArgumentException
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
// If the response is not JSON, return it as is
if (! $response instanceof JsonResponse) {
return $response;
}
$query = $request->getQueryParams();
$acceptedType = isset($query['format'])
? $this->determineAcceptTypeFromQuery($query)
: $this->determineAcceptTypeFromHeader($request->getHeaderLine('Accept'));
// If JSON was requested, return the response from next handler as is
if ($acceptedType === self::JSON) {
return $response;
}
// If requested, return a plain text response containing the short URL only
$resp = (new Response())->withHeader('Content-Type', 'text/plain');
$body = $resp->getBody();
$body->write($this->determineBody($response));
$body->rewind();
return $resp;
}
private function determineAcceptTypeFromQuery(array $query): string
{
if (! isset($query['format'])) {
return self::JSON;
}
$format = \strtolower((string) $query['format']);
return $format === 'txt' ? self::PLAIN_TEXT : self::JSON;
}
private function determineAcceptTypeFromHeader(string $acceptValue): string
{
$accepts = \explode(',', $acceptValue);
$accept = \strtolower(\array_shift($accepts));
return \strpos($accept, 'text/plain') !== false ? self::PLAIN_TEXT : self::JSON;
}
private function determineBody(JsonResponse $resp): string
{
$payload = $resp->getPayload();
return $payload['shortUrl'] ?? $payload['error'] ?? '';
}
}

View File

@@ -28,7 +28,7 @@ class ApiKeyService implements ApiKeyServiceInterface
public function create(\DateTime $expirationDate = null)
{
$key = new ApiKey();
if (isset($expirationDate)) {
if ($expirationDate !== null) {
$key->setExpirationDate($expirationDate);
}
@@ -44,7 +44,7 @@ class ApiKeyService implements ApiKeyServiceInterface
* @param string $key
* @return bool
*/
public function check($key)
public function check(string $key)
{
/** @var ApiKey|null $apiKey */
$apiKey = $this->getByKey($key);
@@ -58,7 +58,7 @@ class ApiKeyService implements ApiKeyServiceInterface
* @return ApiKey
* @throws InvalidArgumentException
*/
public function disable($key)
public function disable(string $key)
{
/** @var ApiKey|null $apiKey */
$apiKey = $this->getByKey($key);
@@ -77,7 +77,7 @@ class ApiKeyService implements ApiKeyServiceInterface
* @param bool $enabledOnly Tells if only enabled keys should be returned
* @return ApiKey[]
*/
public function listKeys($enabledOnly = false)
public function listKeys(bool $enabledOnly = false)
{
$conditions = $enabledOnly ? ['enabled' => true] : [];
return $this->em->getRepository(ApiKey::class)->findBy($conditions);
@@ -89,7 +89,7 @@ class ApiKeyService implements ApiKeyServiceInterface
* @param string $key
* @return ApiKey|null
*/
public function getByKey($key)
public function getByKey(string $key)
{
/** @var ApiKey|null $apiKey */
$apiKey = $this->em->getRepository(ApiKey::class)->findOneBy([

View File

@@ -22,7 +22,7 @@ interface ApiKeyServiceInterface
* @param string $key
* @return bool
*/
public function check($key);
public function check(string $key);
/**
* Disables provided api key
@@ -31,7 +31,7 @@ interface ApiKeyServiceInterface
* @return ApiKey
* @throws InvalidArgumentException
*/
public function disable($key);
public function disable(string $key);
/**
* Lists all existing api keys
@@ -39,7 +39,7 @@ interface ApiKeyServiceInterface
* @param bool $enabledOnly Tells if only enabled keys should be returned
* @return ApiKey[]
*/
public function listKeys($enabledOnly = false);
public function listKeys(bool $enabledOnly = false);
/**
* Tries to find one API key by its key string
@@ -47,5 +47,5 @@ interface ApiKeyServiceInterface
* @param string $key
* @return ApiKey|null
*/
public function getByKey($key);
public function getByKey(string $key);
}

View File

@@ -9,16 +9,16 @@ use Shlinkio\Shlink\Rest\Exception as Rest;
class RestUtils
{
const INVALID_SHORTCODE_ERROR = 'INVALID_SHORTCODE';
const INVALID_URL_ERROR = 'INVALID_URL';
const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT';
const INVALID_SLUG_ERROR = 'INVALID_SLUG';
const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS';
const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN';
const INVALID_AUTHORIZATION_ERROR = 'INVALID_AUTHORIZATION';
const INVALID_API_KEY_ERROR = 'INVALID_API_KEY';
const NOT_FOUND_ERROR = 'NOT_FOUND';
const UNKNOWN_ERROR = 'UNKNOWN_ERROR';
public const INVALID_SHORTCODE_ERROR = 'INVALID_SHORTCODE';
public const INVALID_URL_ERROR = 'INVALID_URL';
public const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT';
public const INVALID_SLUG_ERROR = 'INVALID_SLUG';
public const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS';
public const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN';
public const INVALID_AUTHORIZATION_ERROR = 'INVALID_AUTHORIZATION';
public const INVALID_API_KEY_ERROR = 'INVALID_API_KEY';
public const NOT_FOUND_ERROR = 'NOT_FOUND';
public const UNKNOWN_ERROR = 'UNKNOWN_ERROR';
public static function getRestErrorCodeFromException(\Throwable $e)
{
@@ -30,6 +30,7 @@ class RestUtils
case $e instanceof Core\NonUniqueSlugException:
return self::INVALID_SLUG_ERROR;
case $e instanceof Common\InvalidArgumentException:
case $e instanceof Core\InvalidArgumentException:
case $e instanceof Core\ValidationException:
return self::INVALID_ARGUMENT_ERROR;
case $e instanceof Rest\AuthenticationException:

View File

@@ -10,7 +10,6 @@ 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 ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator;
@@ -47,7 +46,7 @@ class AuthenticateActionTest extends TestCase
*/
public function notProvidingAuthDataReturnsError()
{
$resp = $this->action->process(ServerRequestFactory::fromGlobals(), TestUtils::createDelegateMock()->reveal());
$resp = $this->action->handle(ServerRequestFactory::fromGlobals());
$this->assertEquals(400, $resp->getStatusCode());
}
@@ -62,7 +61,7 @@ class AuthenticateActionTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
'apiKey' => 'foo',
]);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->action->handle($request);
$this->assertEquals(200, $response->getStatusCode());
$response->getBody()->rewind();
@@ -80,7 +79,7 @@ class AuthenticateActionTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
'apiKey' => 'foo',
]);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->action->handle($request);
$this->assertEquals(401, $response->getStatusCode());
}
}

View File

@@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action;
namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
@@ -9,17 +9,16 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Rest\Action\CreateShortcodeAction;
use Shlinkio\Shlink\Rest\Action\ShortCode\CreateShortCodeAction;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequestFactory;
use Zend\Diactoros\Uri;
use Zend\I18n\Translator\Translator;
class CreateShortcodeActionTest extends TestCase
class CreateShortCodeActionTest extends TestCase
{
/**
* @var CreateShortcodeAction
* @var CreateShortCodeAction
*/
protected $action;
/**
@@ -30,7 +29,7 @@ class CreateShortcodeActionTest extends TestCase
public function setUp()
{
$this->urlShortener = $this->prophesize(UrlShortener::class);
$this->action = new CreateShortcodeAction($this->urlShortener->reveal(), Translator::factory([]), [
$this->action = new CreateShortCodeAction($this->urlShortener->reveal(), Translator::factory([]), [
'schema' => 'http',
'hostname' => 'foo.com',
]);
@@ -41,10 +40,7 @@ class CreateShortcodeActionTest extends TestCase
*/
public function missingLongUrlParamReturnsError()
{
$response = $this->action->process(
ServerRequestFactory::fromGlobals(),
TestUtils::createDelegateMock()->reveal()
);
$response = $this->action->handle(ServerRequestFactory::fromGlobals());
$this->assertEquals(400, $response->getStatusCode());
}
@@ -60,7 +56,7 @@ class CreateShortcodeActionTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
'longUrl' => 'http://www.domain.com/foo/bar',
]);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->action->handle($request);
$this->assertEquals(200, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), 'http://foo.com/abc123') > 0);
}
@@ -77,7 +73,7 @@ class CreateShortcodeActionTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
'longUrl' => 'http://www.domain.com/foo/bar',
]);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->action->handle($request);
$this->assertEquals(400, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_URL_ERROR) > 0);
}
@@ -100,7 +96,7 @@ class CreateShortcodeActionTest extends TestCase
'longUrl' => 'http://www.domain.com/foo/bar',
'customSlug' => 'foo',
]);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->action->handle($request);
$this->assertEquals(400, $response->getStatusCode());
$this->assertContains(RestUtils::INVALID_SLUG_ERROR, (string) $response->getBody());
}
@@ -117,7 +113,7 @@ class CreateShortcodeActionTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
'longUrl' => 'http://www.domain.com/foo/bar',
]);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->action->handle($request);
$this->assertEquals(500, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::UNKNOWN_ERROR) > 0);
}

View File

@@ -1,16 +1,15 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action;
namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Rest\Action\EditShortCodeAction;
use Shlinkio\Shlink\Rest\Action\ShortCode\EditShortCodeAction;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequestFactory;
@@ -43,7 +42,7 @@ class EditShortCodeActionTest extends TestCase
]);
/** @var JsonResponse $resp */
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
$resp = $this->action->handle($request);
$payload = $resp->getPayload();
$this->assertEquals(400, $resp->getStatusCode());
@@ -65,7 +64,7 @@ class EditShortCodeActionTest extends TestCase
);
/** @var JsonResponse $resp */
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
$resp = $this->action->handle($request);
$payload = $resp->getPayload();
$this->assertEquals(404, $resp->getStatusCode());
@@ -85,7 +84,7 @@ class EditShortCodeActionTest extends TestCase
]);
$updateMeta = $this->shortUrlService->updateMetadataByShortCode(Argument::cetera())->willReturn(new ShortUrl());
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
$resp = $this->action->handle($request);
$this->assertEquals(204, $resp->getStatusCode());
$updateMeta->shouldHaveBeenCalled();

View File

@@ -1,22 +1,21 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action;
namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Shlinkio\Shlink\Rest\Action\EditShortcodeTagsAction;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Shlinkio\Shlink\Rest\Action\ShortCode\EditShortCodeTagsAction;
use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator;
class EditShortcodeTagsActionTest extends TestCase
class EditShortCodeTagsActionTest extends TestCase
{
/**
* @var EditShortcodeTagsAction
* @var EditShortCodeTagsAction
*/
protected $action;
/**
@@ -27,7 +26,7 @@ class EditShortcodeTagsActionTest extends TestCase
public function setUp()
{
$this->shortUrlService = $this->prophesize(ShortUrlService::class);
$this->action = new EditShortcodeTagsAction($this->shortUrlService->reveal(), Translator::factory([]));
$this->action = new EditShortCodeTagsAction($this->shortUrlService->reveal(), Translator::factory([]));
}
/**
@@ -35,10 +34,7 @@ class EditShortcodeTagsActionTest extends TestCase
*/
public function notProvidingTagsReturnsError()
{
$response = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123'),
TestUtils::createDelegateMock()->reveal()
);
$response = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123'));
$this->assertEquals(400, $response->getStatusCode());
}
@@ -51,10 +47,9 @@ class EditShortcodeTagsActionTest extends TestCase
$this->shortUrlService->setTagsByShortCode($shortCode, [])->willThrow(InvalidShortCodeException::class)
->shouldBeCalledTimes(1);
$response = $this->action->process(
$response = $this->action->handle(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123')
->withParsedBody(['tags' => []]),
TestUtils::createDelegateMock()->reveal()
->withParsedBody(['tags' => []])
);
$this->assertEquals(404, $response->getStatusCode());
}
@@ -68,10 +63,9 @@ class EditShortcodeTagsActionTest extends TestCase
$this->shortUrlService->setTagsByShortCode($shortCode, [])->willReturn(new ShortUrl())
->shouldBeCalledTimes(1);
$response = $this->action->process(
$response = $this->action->handle(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123')
->withParsedBody(['tags' => []]),
TestUtils::createDelegateMock()->reveal()
->withParsedBody(['tags' => []])
);
$this->assertEquals(200, $response->getStatusCode());
}

View File

@@ -1,13 +1,12 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action;
namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Shlinkio\Shlink\Rest\Action\ListShortcodesAction;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Shlinkio\Shlink\Rest\Action\ShortCode\ListShortCodesAction;
use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator;
use Zend\Paginator\Adapter\ArrayAdapter;
@@ -16,7 +15,7 @@ use Zend\Paginator\Paginator;
class ListShortCodesActionTest extends TestCase
{
/**
* @var ListShortcodesAction
* @var ListShortCodesAction
*/
protected $action;
/**
@@ -27,7 +26,7 @@ class ListShortCodesActionTest extends TestCase
public function setUp()
{
$this->service = $this->prophesize(ShortUrlService::class);
$this->action = new ListShortcodesAction($this->service->reveal(), Translator::factory([]));
$this->action = new ListShortCodesAction($this->service->reveal(), Translator::factory([]));
}
/**
@@ -39,12 +38,9 @@ class ListShortCodesActionTest extends TestCase
$this->service->listShortUrls($page, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledTimes(1);
$response = $this->action->process(
ServerRequestFactory::fromGlobals()->withQueryParams([
'page' => $page,
]),
TestUtils::createDelegateMock()->reveal()
);
$response = $this->action->handle(ServerRequestFactory::fromGlobals()->withQueryParams([
'page' => $page,
]));
$this->assertEquals(200, $response->getStatusCode());
}
@@ -57,12 +53,9 @@ class ListShortCodesActionTest extends TestCase
$this->service->listShortUrls($page, null, [], null)->willThrow(\Exception::class)
->shouldBeCalledTimes(1);
$response = $this->action->process(
ServerRequestFactory::fromGlobals()->withQueryParams([
'page' => $page,
]),
TestUtils::createDelegateMock()->reveal()
);
$response = $this->action->handle(ServerRequestFactory::fromGlobals()->withQueryParams([
'page' => $page,
]));
$this->assertEquals(500, $response->getStatusCode());
}
}

View File

@@ -1,16 +1,15 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action;
namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Rest\Action\ResolveUrlAction;
use Shlinkio\Shlink\Rest\Action\ShortCode\ResolveUrlAction;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator;
@@ -41,7 +40,7 @@ class ResolveUrlActionTest extends TestCase
->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->action->handle($request);
$this->assertEquals(404, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_ARGUMENT_ERROR) > 0);
}
@@ -56,7 +55,7 @@ class ResolveUrlActionTest extends TestCase
->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->action->handle($request);
$this->assertEquals(200, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), 'http://domain.com/foo/bar') > 0);
}
@@ -71,7 +70,7 @@ class ResolveUrlActionTest extends TestCase
->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->action->handle($request);
$this->assertEquals(400, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_SHORTCODE_ERROR) > 0);
}
@@ -86,7 +85,7 @@ class ResolveUrlActionTest extends TestCase
->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->action->handle($request);
$this->assertEquals(500, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::UNKNOWN_ERROR) > 0);
}

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Message\UriInterface;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Action\ShortCode\SingleStepCreateShortCodeAction;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator;
class SingleStepCreateShortCodeActionTest extends TestCase
{
/**
* @var SingleStepCreateShortCodeAction
*/
private $action;
/**
* @var ObjectProphecy
*/
private $urlShortener;
/**
* @var ObjectProphecy
*/
private $apiKeyService;
public function setUp()
{
$this->urlShortener = $this->prophesize(UrlShortenerInterface::class);
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
$this->action = new SingleStepCreateShortCodeAction(
$this->urlShortener->reveal(),
Translator::factory([]),
$this->apiKeyService->reveal(),
[
'schema' => 'http',
'hostname' => 'foo.com',
]
);
}
/**
* @test
* @dataProvider provideInvalidApiKeys
*/
public function errorResponseIsReturnedIfInvalidApiKeyIsProvided(?ApiKey $apiKey)
{
$request = ServerRequestFactory::fromGlobals()->withQueryParams(['apiKey' => 'abc123']);
$findApiKey = $this->apiKeyService->getByKey('abc123')->willReturn($apiKey);
/** @var JsonResponse $resp */
$resp = $this->action->handle($request);
$payload = $resp->getPayload();
$this->assertEquals(400, $resp->getStatusCode());
$this->assertEquals('INVALID_ARGUMENT', $payload['error']);
$this->assertEquals('No API key was provided or it is not valid', $payload['message']);
$findApiKey->shouldHaveBeenCalled();
}
public function provideInvalidApiKeys(): array
{
return [
[null],
[(new ApiKey())->disable()],
];
}
/**
* @test
*/
public function errorResponseIsReturnedIfNoUrlIsProvided()
{
$request = ServerRequestFactory::fromGlobals()->withQueryParams(['apiKey' => 'abc123']);
$findApiKey = $this->apiKeyService->getByKey('abc123')->willReturn(new ApiKey());
/** @var JsonResponse $resp */
$resp = $this->action->handle($request);
$payload = $resp->getPayload();
$this->assertEquals(400, $resp->getStatusCode());
$this->assertEquals('INVALID_ARGUMENT', $payload['error']);
$this->assertEquals('A URL was not provided', $payload['message']);
$findApiKey->shouldHaveBeenCalled();
}
/**
* @test
*/
public function properDataIsPassedWhenGeneratingShortCode()
{
$request = ServerRequestFactory::fromGlobals()->withQueryParams([
'apiKey' => 'abc123',
'longUrl' => 'http://foobar.com',
]);
$findApiKey = $this->apiKeyService->getByKey('abc123')->willReturn(new ApiKey());
$generateShortCode = $this->urlShortener->urlToShortCode(
Argument::that(function (UriInterface $argument) {
Assert::assertEquals('http://foobar.com', (string) $argument);
return $argument;
}),
[],
null,
null,
null,
null
);
$resp = $this->action->handle($request);
$this->assertEquals(200, $resp->getStatusCode());
$findApiKey->shouldHaveBeenCalled();
$generateShortCode->shouldHaveBeenCalled();
}
}

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action\Tag;
use Doctrine\Common\Collections\ArrayCollection;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
@@ -40,7 +39,7 @@ class CreateTagsActionTest extends TestCase
/** @var MethodProphecy $deleteTags */
$deleteTags = $this->tagService->createTags($tags ?: [])->willReturn(new ArrayCollection());
$response = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
$response = $this->action->handle($request);
$this->assertEquals(200, $response->getStatusCode());
$deleteTags->shouldHaveBeenCalled();

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action\Tag;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
@@ -39,7 +38,7 @@ class DeleteTagsActionTest extends TestCase
/** @var MethodProphecy $deleteTags */
$deleteTags = $this->tagService->deleteTags($tags ?: []);
$response = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
$response = $this->action->handle($request);
$this->assertEquals(204, $response->getStatusCode());
$deleteTags->shouldHaveBeenCalled();

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action\Tag;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
@@ -37,10 +36,7 @@ class ListTagsActionTest extends TestCase
/** @var MethodProphecy $listTags */
$listTags = $this->tagService->listTags()->willReturn([new Tag('foo'), new Tag('bar')]);
$resp = $this->action->process(
ServerRequestFactory::fromGlobals(),
$this->prophesize(DelegateInterface::class)->reveal()
);
$resp = $this->action->handle(ServerRequestFactory::fromGlobals());
$this->assertEquals([
'tags' => [

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action\Tag;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
@@ -39,7 +38,7 @@ class UpdateTagActionTest extends TestCase
public function whenInvalidParamsAreProvidedAnErrorIsReturned(array $bodyParams)
{
$request = ServerRequestFactory::fromGlobals()->withParsedBody($bodyParams);
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
$resp = $this->action->handle($request);
$this->assertEquals(400, $resp->getStatusCode());
}
@@ -65,7 +64,7 @@ class UpdateTagActionTest extends TestCase
/** @var MethodProphecy $rename */
$rename = $this->tagService->renameTag('foo', 'bar')->willThrow(EntityDoesNotExistException::class);
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
$resp = $this->action->handle($request);
$this->assertEquals(404, $resp->getStatusCode());
$rename->shouldHaveBeenCalled();
@@ -83,7 +82,7 @@ class UpdateTagActionTest extends TestCase
/** @var MethodProphecy $rename */
$rename = $this->tagService->renameTag('foo', 'bar')->willReturn(new Tag());
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
$resp = $this->action->handle($request);
$this->assertEquals(204, $resp->getStatusCode());
$rename->shouldHaveBeenCalled();

View File

@@ -1,7 +1,7 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action;
namespace ShlinkioTest\Shlink\Rest\Action\Visit;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
@@ -9,8 +9,7 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Service\VisitsTracker;
use Shlinkio\Shlink\Rest\Action\GetVisitsAction;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Shlinkio\Shlink\Rest\Action\Visit\GetVisitsAction;
use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator;
@@ -40,10 +39,7 @@ class GetVisitsActionTest extends TestCase
$this->visitsTracker->info($shortCode, Argument::type(DateRange::class))->willReturn([])
->shouldBeCalledTimes(1);
$response = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
TestUtils::createDelegateMock()->reveal()
);
$response = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode));
$this->assertEquals(200, $response->getStatusCode());
}
@@ -57,10 +53,7 @@ class GetVisitsActionTest extends TestCase
InvalidArgumentException::class
)->shouldBeCalledTimes(1);
$response = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
TestUtils::createDelegateMock()->reveal()
);
$response = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode));
$this->assertEquals(404, $response->getStatusCode());
}
@@ -74,10 +67,7 @@ class GetVisitsActionTest extends TestCase
\Exception::class
)->shouldBeCalledTimes(1);
$response = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
TestUtils::createDelegateMock()->reveal()
);
$response = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode));
$this->assertEquals(500, $response->getStatusCode());
}
@@ -91,10 +81,9 @@ class GetVisitsActionTest extends TestCase
->willReturn([])
->shouldBeCalledTimes(1);
$response = $this->action->process(
$response = $this->action->handle(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode)
->withQueryParams(['endDate' => '2016-01-01 00:00:00']),
TestUtils::createDelegateMock()->reveal()
->withQueryParams(['endDate' => '2016-01-01 00:00:00'])
);
$this->assertEquals(200, $response->getStatusCode());
}

View File

@@ -3,11 +3,11 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Middleware;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
@@ -31,9 +31,9 @@ class BodyParserMiddlewareTest extends TestCase
public function requestsFromOtherMethodsJustFallbackToNextMiddleware()
{
$request = ServerRequestFactory::fromGlobals()->withMethod('GET');
$delegate = $this->prophesize(DelegateInterface::class);
$delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */
$process = $delegate->process($request)->willReturn(new Response());
$process = $delegate->handle($request)->willReturn(new Response());
$this->middleware->process($request, $delegate->reveal());
@@ -51,9 +51,9 @@ class BodyParserMiddlewareTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withMethod('PUT')
->withBody($body)
->withHeader('content-type', 'application/json');
$delegate = $this->prophesize(DelegateInterface::class);
$delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */
$process = $delegate->process(Argument::type(ServerRequestInterface::class))->will(
$process = $delegate->handle(Argument::type(ServerRequestInterface::class))->will(
function (array $args) use ($test) {
/** @var ServerRequestInterface $req */
$req = array_shift($args);
@@ -82,9 +82,9 @@ class BodyParserMiddlewareTest extends TestCase
$body->write('foo=bar&bar[]=one&bar[]=5');
$request = ServerRequestFactory::fromGlobals()->withMethod('PUT')
->withBody($body);
$delegate = $this->prophesize(DelegateInterface::class);
$delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */
$process = $delegate->process(Argument::type(ServerRequestInterface::class))->will(
$process = $delegate->handle(Argument::type(ServerRequestInterface::class))->will(
function (array $args) use ($test) {
/** @var ServerRequestInterface $req */
$req = array_shift($args);

View File

@@ -3,10 +3,10 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Middleware;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Rest\Action\AuthenticateAction;
use Shlinkio\Shlink\Rest\Authentication\JWTService;
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
@@ -16,7 +16,6 @@ use Zend\Diactoros\ServerRequestFactory;
use Zend\Expressive\Router\Route;
use Zend\Expressive\Router\RouteResult;
use Zend\I18n\Translator\Translator;
use function Zend\Stratigility\middleware;
class CheckAuthenticationMiddlewareTest extends TestCase
@@ -38,9 +37,11 @@ class CheckAuthenticationMiddlewareTest extends TestCase
public function setUp()
{
$this->jwtService = $this->prophesize(JWTService::class);
$this->middleware = new CheckAuthenticationMiddleware($this->jwtService->reveal(), Translator::factory([]));
$this->dummyMiddleware = middleware(function ($request, $handler) {
return new Response\EmptyResponse;
$this->middleware = new CheckAuthenticationMiddleware($this->jwtService->reveal(), Translator::factory([]), [
AuthenticateAction::class,
]);
$this->dummyMiddleware = middleware(function () {
return new Response\EmptyResponse();
});
}
@@ -50,9 +51,9 @@ class CheckAuthenticationMiddlewareTest extends TestCase
public function someWhiteListedSituationsFallbackToNextMiddleware()
{
$request = ServerRequestFactory::fromGlobals();
$delegate = $this->prophesize(DelegateInterface::class);
$delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */
$process = $delegate->process($request)->willReturn(new Response());
$process = $delegate->handle($request)->willReturn(new Response());
$this->middleware->process($request, $delegate->reveal());
$process->shouldHaveBeenCalledTimes(1);
@@ -61,9 +62,9 @@ class CheckAuthenticationMiddlewareTest extends TestCase
RouteResult::class,
RouteResult::fromRouteFailure(['GET'])
);
$delegate = $this->prophesize(DelegateInterface::class);
$delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */
$process = $delegate->process($request)->willReturn(new Response());
$process = $delegate->handle($request)->willReturn(new Response());
$this->middleware->process($request, $delegate->reveal());
$process->shouldHaveBeenCalledTimes(1);
@@ -76,9 +77,9 @@ class CheckAuthenticationMiddlewareTest extends TestCase
AuthenticateAction::class
))
);
$delegate = $this->prophesize(DelegateInterface::class);
$delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */
$process = $delegate->process($request)->willReturn(new Response());
$process = $delegate->handle($request)->willReturn(new Response());
$this->middleware->process($request, $delegate->reveal());
$process->shouldHaveBeenCalledTimes(1);
@@ -86,9 +87,9 @@ class CheckAuthenticationMiddlewareTest extends TestCase
RouteResult::class,
RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), [])
)->withMethod('OPTIONS');
$delegate = $this->prophesize(DelegateInterface::class);
$delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */
$process = $delegate->process($request)->willReturn(new Response());
$process = $delegate->handle($request)->willReturn(new Response());
$this->middleware->process($request, $delegate->reveal());
$process->shouldHaveBeenCalledTimes(1);
}
@@ -102,7 +103,7 @@ class CheckAuthenticationMiddlewareTest extends TestCase
RouteResult::class,
RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), [])
);
$response = $this->middleware->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->middleware->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertEquals(401, $response->getStatusCode());
}
@@ -117,7 +118,7 @@ class CheckAuthenticationMiddlewareTest extends TestCase
RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), [])
)->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, $authToken);
$response = $this->middleware->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->middleware->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertEquals(401, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), 'You need to provide the Bearer type') > 0);
@@ -134,7 +135,7 @@ class CheckAuthenticationMiddlewareTest extends TestCase
RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), [])
)->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'Basic ' . $authToken);
$response = $this->middleware->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->middleware->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertEquals(401, $response->getStatusCode());
$this->assertTrue(
@@ -154,7 +155,7 @@ class CheckAuthenticationMiddlewareTest extends TestCase
)->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'Bearer ' . $authToken);
$this->jwtService->verify($authToken)->willReturn(false)->shouldBeCalledTimes(1);
$response = $this->middleware->process($request, TestUtils::createDelegateMock()->reveal());
$response = $this->middleware->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertEquals(401, $response->getStatusCode());
}
@@ -171,9 +172,9 @@ class CheckAuthenticationMiddlewareTest extends TestCase
$this->jwtService->verify($authToken)->willReturn(true)->shouldBeCalledTimes(1);
$this->jwtService->refresh($authToken)->willReturn($authToken)->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class);
$delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */
$process = $delegate->process($request)->willReturn(new Response());
$process = $delegate->handle($request)->willReturn(new Response());
$resp = $this->middleware->process($request, $delegate->reveal());
$process->shouldHaveBeenCalledTimes(1);

View File

@@ -3,10 +3,10 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Middleware;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
@@ -25,7 +25,7 @@ class CrossDomainMiddlewareTest extends TestCase
public function setUp()
{
$this->middleware = new CrossDomainMiddleware();
$this->delegate = $this->prophesize(DelegateInterface::class);
$this->delegate = $this->prophesize(RequestHandlerInterface::class);
}
/**
@@ -34,7 +34,7 @@ class CrossDomainMiddlewareTest extends TestCase
public function nonCrossDomainRequestsAreNotAffected()
{
$originalResponse = new Response();
$this->delegate->process(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1);
$this->delegate->handle(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1);
$response = $this->middleware->process(ServerRequestFactory::fromGlobals(), $this->delegate->reveal());
$this->assertSame($originalResponse, $response);
@@ -50,7 +50,7 @@ class CrossDomainMiddlewareTest extends TestCase
public function anyRequestIncludesTheAllowAccessHeader()
{
$originalResponse = new Response();
$this->delegate->process(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1);
$this->delegate->handle(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1);
$response = $this->middleware->process(
ServerRequestFactory::fromGlobals()->withHeader('Origin', 'local'),
@@ -70,7 +70,7 @@ class CrossDomainMiddlewareTest extends TestCase
{
$originalResponse = new Response();
$request = ServerRequestFactory::fromGlobals()->withMethod('OPTIONS')->withHeader('Origin', 'local');
$this->delegate->process(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1);
$this->delegate->handle(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1);
$response = $this->middleware->process($request, $this->delegate->reveal());
$this->assertNotSame($originalResponse, $response);

View File

@@ -3,11 +3,11 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Middleware;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Rest\Middleware\PathVersionMiddleware;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
@@ -30,25 +30,10 @@ class PathVersionMiddlewareTest extends TestCase
*/
public function whenVersionIsProvidedRequestRemainsUnchanged()
{
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/rest/v2/foo'));
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/v2/foo'));
$delegate = $this->prophesize(DelegateInterface::class);
$process = $delegate->process($request)->willReturn(new Response());
$this->middleware->process($request, $delegate->reveal());
$process->shouldHaveBeenCalled();
}
/**
* @test
*/
public function whenPathDoesNotStartWithRestRemainsUnchanged()
{
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/foo'));
$delegate = $this->prophesize(DelegateInterface::class);
$process = $delegate->process($request)->willReturn(new Response());
$delegate = $this->prophesize(RequestHandlerInterface::class);
$process = $delegate->handle($request)->willReturn(new Response());
$this->middleware->process($request, $delegate->reveal());
@@ -60,14 +45,14 @@ class PathVersionMiddlewareTest extends TestCase
*/
public function versionOneIsPrependedWhenNoVersionIsDefined()
{
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/rest/bar/baz'));
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/bar/baz'));
$delegate = $this->prophesize(DelegateInterface::class);
$delegate->process(Argument::type(Request::class))->will(function (array $args) use ($request) {
$delegate = $this->prophesize(RequestHandlerInterface::class);
$delegate->handle(Argument::type(Request::class))->will(function (array $args) use ($request) {
$req = \array_shift($args);
Assert::assertNotSame($request, $req);
Assert::assertEquals('/rest/v1/bar/baz', $req->getUri()->getPath());
Assert::assertEquals('/v1/bar/baz', $req->getUri()->getPath());
return new Response();
});

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Middleware\ShortCode;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Rest\Middleware\ShortCode\CreateShortCodeContentNegotiationMiddleware;
use Zend\Diactoros\Response;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequestFactory;
class CreateShortCodeContentNegotiationMiddlewareTest extends TestCase
{
/**
* @var CreateShortCodeContentNegotiationMiddleware
*/
private $middleware;
/**
* @var RequestHandlerInterface
*/
private $requestHandler;
public function setUp()
{
$this->middleware = new CreateShortCodeContentNegotiationMiddleware();
$this->requestHandler = $this->prophesize(RequestHandlerInterface::class);
}
/**
* @test
*/
public function whenNoJsonResponseIsReturnedNoFurtherOperationsArePerformed()
{
$expectedResp = new Response();
$this->requestHandler->handle(Argument::type(ServerRequestInterface::class))->willReturn($expectedResp);
$resp = $this->middleware->process(ServerRequestFactory::fromGlobals(), $this->requestHandler->reveal());
$this->assertSame($expectedResp, $resp);
}
/**
* @test
* @dataProvider provideData
* @param array $query
*/
public function properResponseIsReturned(?string $accept, array $query, string $expectedContentType)
{
$request = ServerRequestFactory::fromGlobals()->withQueryParams($query);
if ($accept !== null) {
$request = $request->withHeader('Accept', $accept);
}
$handle = $this->requestHandler->handle(Argument::type(ServerRequestInterface::class))->willReturn(
new JsonResponse(['shortUrl' => 'http://doma.in/foo'])
);
$response = $this->middleware->process($request, $this->requestHandler->reveal());
$this->assertEquals($expectedContentType, $response->getHeaderLine('Content-type'));
$handle->shouldHaveBeenCalled();
}
public function provideData(): array
{
return [
[null, [], 'application/json'],
[null, ['format' => 'json'], 'application/json'],
[null, ['format' => 'invalid'], 'application/json'],
[null, ['format' => 'txt'], 'text/plain'],
['application/json', [], 'application/json'],
['application/xml', [], 'application/json'],
['text/plain', [], 'text/plain'],
['application/json', ['format' => 'txt'], 'text/plain'],
];
}
/**
* @test
* @dataProvider provideTextBodies
* @param array $json
*/
public function properBodyIsReturnedInPlainTextResponses(array $json, string $expectedBody)
{
$request = ServerRequestFactory::fromGlobals()->withQueryParams(['format' => 'txt']);
$handle = $this->requestHandler->handle(Argument::type(ServerRequestInterface::class))->willReturn(
new JsonResponse($json)
);
$response = $this->middleware->process($request, $this->requestHandler->reveal());
$this->assertEquals($expectedBody, (string) $response->getBody());
$handle->shouldHaveBeenCalled();
}
public function provideTextBodies(): array
{
return [
[['shortUrl' => 'foobar'], 'foobar'],
[['error' => 'FOO_BAR'], 'FOO_BAR'],
[[], ''],
];
}
}

View File

@@ -1,3 +1,4 @@
parameters:
excludes_analyse:
- module/Common/src/Template/Extension/TranslatorExtension.php
- module/Rest/src/Util/RestUtils.php

View File

@@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
use Interop\Container\ContainerInterface;
use Psr\Container\ContainerInterface;
use Zend\Expressive\Application;
/** @var ContainerInterface $container */
$container = include __DIR__ . '/../config/container.php';
$app = $container->get(Application::class)->run();
$container->get(Application::class)->run();