Implemented EditShortCodeAction

This commit is contained in:
Alejandro Celaya 2018-01-07 20:45:05 +01:00
parent 7ba9eb8e2c
commit 84094a51a2
7 changed files with 189 additions and 13 deletions

View file

@ -9,7 +9,7 @@ return [
'routes' => [ 'routes' => [
[ [
'name' => Action\AuthenticateAction::class, 'name' => Action\AuthenticateAction::class,
'path' => '/rest/v{version:1}/authenticate', 'path' => '/authenticate',
'middleware' => Action\AuthenticateAction::class, 'middleware' => Action\AuthenticateAction::class,
'allowed_methods' => [RequestMethod::METHOD_POST], 'allowed_methods' => [RequestMethod::METHOD_POST],
], ],
@ -17,25 +17,31 @@ return [
// Short codes // Short codes
[ [
'name' => Action\CreateShortcodeAction::class, 'name' => Action\CreateShortcodeAction::class,
'path' => '/rest/v{version:1}/short-codes', 'path' => '/short-codes',
'middleware' => Action\CreateShortcodeAction::class, 'middleware' => Action\CreateShortcodeAction::class,
'allowed_methods' => [RequestMethod::METHOD_POST], '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, 'name' => Action\ResolveUrlAction::class,
'path' => '/rest/v{version:1}/short-codes/{shortCode}', 'path' => '/short-codes/{shortCode}',
'middleware' => Action\ResolveUrlAction::class, 'middleware' => Action\ResolveUrlAction::class,
'allowed_methods' => [RequestMethod::METHOD_GET], 'allowed_methods' => [RequestMethod::METHOD_GET],
], ],
[ [
'name' => Action\ListShortcodesAction::class, 'name' => Action\ListShortcodesAction::class,
'path' => '/rest/v{version:1}/short-codes', 'path' => '/short-codes',
'middleware' => Action\ListShortcodesAction::class, 'middleware' => Action\ListShortcodesAction::class,
'allowed_methods' => [RequestMethod::METHOD_GET], 'allowed_methods' => [RequestMethod::METHOD_GET],
], ],
[ [
'name' => Action\EditShortcodeTagsAction::class, 'name' => Action\EditShortcodeTagsAction::class,
'path' => '/rest/v{version:1}/short-codes/{shortCode}/tags', 'path' => '/short-codes/{shortCode}/tags',
'middleware' => Action\EditShortcodeTagsAction::class, 'middleware' => Action\EditShortcodeTagsAction::class,
'allowed_methods' => [RequestMethod::METHOD_PUT], 'allowed_methods' => [RequestMethod::METHOD_PUT],
], ],
@ -43,7 +49,7 @@ return [
// Visits // Visits
[ [
'name' => Action\GetVisitsAction::class, 'name' => Action\GetVisitsAction::class,
'path' => '/rest/v{version:1}/short-codes/{shortCode}/visits', 'path' => '/short-codes/{shortCode}/visits',
'middleware' => Action\GetVisitsAction::class, 'middleware' => Action\GetVisitsAction::class,
'allowed_methods' => [RequestMethod::METHOD_GET], 'allowed_methods' => [RequestMethod::METHOD_GET],
], ],
@ -51,25 +57,25 @@ return [
// Tags // Tags
[ [
'name' => Action\Tag\ListTagsAction::class, 'name' => Action\Tag\ListTagsAction::class,
'path' => '/rest/v{version:1}/tags', 'path' => '/tags',
'middleware' => Action\Tag\ListTagsAction::class, 'middleware' => Action\Tag\ListTagsAction::class,
'allowed_methods' => [RequestMethod::METHOD_GET], 'allowed_methods' => [RequestMethod::METHOD_GET],
], ],
[ [
'name' => Action\Tag\DeleteTagsAction::class, 'name' => Action\Tag\DeleteTagsAction::class,
'path' => '/rest/v{version:1}/tags', 'path' => '/tags',
'middleware' => Action\Tag\DeleteTagsAction::class, 'middleware' => Action\Tag\DeleteTagsAction::class,
'allowed_methods' => [RequestMethod::METHOD_DELETE], 'allowed_methods' => [RequestMethod::METHOD_DELETE],
], ],
[ [
'name' => Action\Tag\CreateTagsAction::class, 'name' => Action\Tag\CreateTagsAction::class,
'path' => '/rest/v{version:1}/tags', 'path' => '/tags',
'middleware' => Action\Tag\CreateTagsAction::class, 'middleware' => Action\Tag\CreateTagsAction::class,
'allowed_methods' => [RequestMethod::METHOD_POST], 'allowed_methods' => [RequestMethod::METHOD_POST],
], ],
[ [
'name' => Action\Tag\UpdateTagAction::class, 'name' => Action\Tag\UpdateTagAction::class,
'path' => '/rest/v{version:1}/tags', 'path' => '/tags',
'middleware' => Action\Tag\UpdateTagAction::class, 'middleware' => Action\Tag\UpdateTagAction::class,
'allowed_methods' => [RequestMethod::METHOD_PUT], 'allowed_methods' => [RequestMethod::METHOD_PUT],
], ],

View file

@ -6,9 +6,36 @@ namespace Shlinkio\Shlink\Rest\Action;
use Interop\Http\ServerMiddleware\DelegateInterface; use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; 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\Util\RestUtils;
use Zend\Diactoros\Response\EmptyResponse;
use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface;
class EditShortCodeAction extends AbstractRestAction class EditShortCodeAction extends AbstractRestAction
{ {
/**
* @var ShortUrlServiceInterface
*/
private $shortUrlService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(
ShortUrlServiceInterface $shortUrlService,
TranslatorInterface $translator,
LoggerInterface $logger = null
) {
parent::__construct($logger);
$this->shortUrlService = $shortUrlService;
$this->translator = $translator;
}
/** /**
* Process an incoming server request and return a response, optionally delegating * Process an incoming server request and return a response, optionally delegating
* to the next middleware component to create the response. * to the next middleware component to create the response.
@ -17,8 +44,31 @@ class EditShortCodeAction extends AbstractRestAction
* @param DelegateInterface $delegate * @param DelegateInterface $delegate
* *
* @return ResponseInterface * @return ResponseInterface
* @throws \InvalidArgumentException
*/ */
public function process(ServerRequestInterface $request, DelegateInterface $delegate): ResponseInterface public function process(ServerRequestInterface $request, DelegateInterface $delegate): ResponseInterface
{ {
$postData = (array) $request->getParsedBody();
$shortCode = $request->getAttribute('shortCode', '');
try {
$this->shortUrlService->updateMetadataByShortCode(
$shortCode,
ShortUrlMeta::createFromRawData($postData)
);
return new EmptyResponse();
} catch (Exception\InvalidShortCodeException $e) {
$this->logger->warning('Provided data is invalid.' . PHP_EOL . $e);
return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => \sprintf($this->translator->translate('No URL found for short code "%s"'), $shortCode),
], self::STATUS_NOT_FOUND);
} catch (Exception\ValidationException $e) {
$this->logger->warning('Provided data is invalid.' . PHP_EOL . $e);
return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => $this->translator->translate('Provided data is invalid.'),
], self::STATUS_BAD_REQUEST);
}
} }
} }

View file

@ -8,8 +8,24 @@ use Zend\Stdlib\Glob;
class ConfigProvider class ConfigProvider
{ {
const ROUTES_PREFIX = '/rest/v{version:1}';
public function __invoke() public function __invoke()
{ {
return Factory::fromFiles(Glob::glob(__DIR__ . '/../config/{,*.}config.php', Glob::GLOB_BRACE)); return $this->applyRoutesPrefix(
Factory::fromFiles(Glob::glob(__DIR__ . '/../config/{,*.}config.php', Glob::GLOB_BRACE))
);
}
private function applyRoutesPrefix(array $config): array
{
$routes =& $config['routes'] ?? [];
// Prepend the routes prefix to every path
foreach ($routes as $key => $route) {
$routes[$key]['path'] = self::ROUTES_PREFIX . $route['path'];
}
return $config;
} }
} }

View file

@ -30,6 +30,7 @@ class RestUtils
case $e instanceof Core\NonUniqueSlugException: case $e instanceof Core\NonUniqueSlugException:
return self::INVALID_SLUG_ERROR; return self::INVALID_SLUG_ERROR;
case $e instanceof Common\InvalidArgumentException: case $e instanceof Common\InvalidArgumentException:
case $e instanceof Core\ValidationException:
return self::INVALID_ARGUMENT_ERROR; return self::INVALID_ARGUMENT_ERROR;
case $e instanceof Rest\AuthenticationException: case $e instanceof Rest\AuthenticationException:
return self::INVALID_CREDENTIALS_ERROR; return self::INVALID_CREDENTIALS_ERROR;

View file

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action;
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\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator;
class EditShortCodeActionTest extends TestCase
{
/**
* @var EditShortCodeAction
*/
private $action;
/**
* @var ObjectProphecy
*/
private $shortUrlService;
public function setUp()
{
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
$this->action = new EditShortCodeAction($this->shortUrlService->reveal(), Translator::factory([]));
}
/**
* @test
*/
public function invalidDataReturnsError()
{
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
'maxVisits' => 'invalid',
]);
/** @var JsonResponse $resp */
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
$payload = $resp->getPayload();
$this->assertEquals(400, $resp->getStatusCode());
$this->assertEquals(RestUtils::INVALID_ARGUMENT_ERROR, $payload['error']);
$this->assertEquals('Provided data is invalid.', $payload['message']);
}
/**
* @test
*/
public function incorrectShortCodeReturnsError()
{
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123')
->withParsedBody([
'maxVisits' => 5,
]);
$updateMeta = $this->shortUrlService->updateMetadataByShortCode(Argument::cetera())->willThrow(
InvalidShortCodeException::class
);
/** @var JsonResponse $resp */
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
$payload = $resp->getPayload();
$this->assertEquals(404, $resp->getStatusCode());
$this->assertEquals(RestUtils::INVALID_SHORTCODE_ERROR, $payload['error']);
$this->assertEquals('No URL found for short code "abc123"', $payload['message']);
$updateMeta->shouldHaveBeenCalled();
}
/**
* @test
*/
public function correctShortCodeReturnsSuccess()
{
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123')
->withParsedBody([
'maxVisits' => 5,
]);
$updateMeta = $this->shortUrlService->updateMetadataByShortCode(Argument::cetera())->willReturn(new ShortUrl());
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
$this->assertEquals(204, $resp->getStatusCode());
$updateMeta->shouldHaveBeenCalled();
}
}

View file

@ -1,4 +1,9 @@
<phpunit bootstrap="./func_tests_bootstrap.php" colors="true"> <?xml version="1.0"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.5/phpunit.xsd"
bootstrap="./func_tests_bootstrap.php"
colors="true">
<testsuites> <testsuites>
<testsuite name="Shlink functional database tests"> <testsuite name="Shlink functional database tests">
<directory>./module/*/test-func</directory> <directory>./module/*/test-func</directory>

View file

@ -1,4 +1,9 @@
<phpunit bootstrap="./vendor/autoload.php" colors="true"> <?xml version="1.0"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.5/phpunit.xsd"
bootstrap="./vendor/autoload.php"
colors="true">
<testsuites> <testsuites>
<testsuite name="Common"> <testsuite name="Common">
<directory>./module/Common/test</directory> <directory>./module/Common/test</directory>