diff --git a/.gitattributes b/.gitattributes new file mode 100755 index 0000000..ebb9287 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitignore export-ignore diff --git a/.gitignore b/.gitignore index fb8281c..e2b5910 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ composer.lock composer.phar .php_cs.cache /build +.phpunit.result.cache diff --git a/.scrutinizer.yml b/.scrutinizer.yml index f56d612..2aaafc2 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,3 +1,10 @@ +build: + nodes: + analysis: + tests: + override: + - php-scrutinizer-run + filter: excluded_paths: [vendor/*, Tests/*] tools: diff --git a/.travis.yml b/.travis.yml index 19c0211..80d4839 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,19 @@ language: php -sudo: false +os: linux cache: directories: - $HOME/.composer/cache -matrix: +jobs: include: - - php: '7.0' - php: '7.1' - php: '7.2' - php: '7.3' - php: '7.3' env: deps=low + - php: '7.4' before_install: - composer self-update diff --git a/CHANGELOG.md b/CHANGELOG.md index 317f3b7..74395a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,21 @@ CHANGELOG ========= +v0.4.0 +------ + +* Symfony 5 compatibility (#14) + +v0.3.0 +------ + +* Feature. Configuration per using annotations (#9) +* Feature. Rate limit to specific methods (#12) +* Fix null ApiRateLimit instance in RateLimitHandler (#11) + v0.2.1 ------ + * Adding Symfony4 support (#5) * Fix dependencies for Symfony Flex diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 59cf7ed..22973ac 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -28,13 +28,17 @@ final class Configuration implements ConfigurationInterface */ public function getConfigTreeBuilder() { - $treeBuilder = new TreeBuilder(); - $rootNode = $treeBuilder->root('indragunawan_api_rate_limit'); + if (method_exists(TreeBuilder::class, 'getRootNode')) { + $treeBuilder = new TreeBuilder('indragunawan_api_rate_limit'); + $rootNode = $treeBuilder->getRootNode(); + } else { + $treeBuilder = new TreeBuilder(); + $rootNode = $treeBuilder->root('indragunawan_api_rate_limit'); + } $rootNode ->children() ->booleanNode('enabled')->defaultTrue()->end() - ->scalarNode('storage')->defaultNull()->cannotBeEmpty()->end() ->scalarNode('cache')->defaultNull()->cannotBeEmpty()->end() ->arrayNode('header') ->addDefaultsIfNotSet() @@ -51,29 +55,8 @@ public function getConfigTreeBuilder() ->end() ->end() ->arrayNode('throttle') - ->beforeNormalization() - ->ifTrue(function ($v) { return is_array($v) && (isset($v['limit']) || isset($v['period'])); }) - ->then(function ($v) { - $v['default'] = []; - if (isset($v['limit'])) { - @trigger_error('The indragunawan_api_rate_limit.throttle.limit configuration key is deprecated since version v0.2.0 and will be removed in v0.3.0. Use the indragunawan_api_rate_limit.throttle.default.limit configuration key instead.', E_USER_DEPRECATED); - - $v['default']['limit'] = $v['limit']; - } - - if (isset($v['period'])) { - @trigger_error('The indragunawan_api_rate_limit.throttle.period configuration key is deprecated since version v0.2.0 and will be removed in v0.3.0. Use the indragunawan_api_rate_limit.throttle.default.period configuration key instead.', E_USER_DEPRECATED); - - $v['default']['period'] = $v['period']; - } - - return $v; - }) - ->end() ->addDefaultsIfNotSet() ->children() - ->integerNode('limit')->min(1)->defaultValue(60)->end() - ->integerNode('period')->min(1)->defaultValue(60)->end() ->arrayNode('default') ->addDefaultsIfNotSet() ->children() diff --git a/DependencyInjection/IndragunawanApiRateLimitExtension.php b/DependencyInjection/IndragunawanApiRateLimitExtension.php index 4fe947e..0a698df 100644 --- a/DependencyInjection/IndragunawanApiRateLimitExtension.php +++ b/DependencyInjection/IndragunawanApiRateLimitExtension.php @@ -56,10 +56,6 @@ private function registerServiceConfig(ContainerBuilder $container, array $confi { if (null !== $config['cache']) { $cache = new Reference($config['cache']); - } elseif (null !== $config['storage']) { - @trigger_error('The indragunawan_api_rate_limit.storage configuration key is deprecated since version v0.2.0 and will be removed in v0.3.0. Use the indragunawan_api_rate_limit.cache configuration key instead.', E_USER_DEPRECATED); - - $cache = new Definition(DoctrineAdapter::class, [new Reference($config['storage']), 'api_rate_limit']); } else { $cache = new Definition(FilesystemAdapter::class, ['api_rate_limit', 0, $container->getParameter('kernel.cache_dir')]); } diff --git a/EventListener/HeaderModificationListener.php b/EventListener/HeaderModificationListener.php index edde03d..03a9a8e 100644 --- a/EventListener/HeaderModificationListener.php +++ b/EventListener/HeaderModificationListener.php @@ -11,7 +11,7 @@ namespace Indragunawan\ApiRateLimitBundle\EventListener; -use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\Event\ResponseEvent; /** * @author Indra Gunawan @@ -29,9 +29,9 @@ public function __construct(array $header) } /** - * @param FilterResponseEvent $event + * @param ResponseEvent $event */ - public function onKernelResponse(FilterResponseEvent $event) + public function onKernelResponse(ResponseEvent $event) { if (false === $this->header['display']) { return; diff --git a/EventListener/RateLimitListener.php b/EventListener/RateLimitListener.php index c2517a2..a6503a4 100644 --- a/EventListener/RateLimitListener.php +++ b/EventListener/RateLimitListener.php @@ -14,7 +14,7 @@ use Indragunawan\ApiRateLimitBundle\Exception\RateLimitExceededException; use Indragunawan\ApiRateLimitBundle\Service\RateLimitHandler; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; /** @@ -50,7 +50,7 @@ public function __construct(bool $enabled, RateLimitHandler $rateLimitHandler, a $this->tokenStorage = $tokenStorage; } - public function onKernelRequest(GetResponseEvent $event) + public function onKernelRequest(RequestEvent $event) { if (!$this->enabled) { return; diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index a739bc7..d6dbd70 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -31,7 +31,7 @@ class ConfigurationTest extends TestCase /** * {@inheritdoc} */ - protected function setUp() + protected function setUp(): void { $this->configuration = new Configuration(false); $this->processor = new Processor(); @@ -115,21 +115,4 @@ public function testValidExceptionClass() $this->assertSame(ValidRateLimitExceededException::class, $config['exception']['custom_exception']); } - - public function testDeprecatedConfiguration() - { - $config = $this->processor->processConfiguration( - $this->configuration, - [ - [ - 'throttle' => [ - 'limit' => 10, - 'period' => 10, - ], - ], - ] - ); - - $this->assertSame(['limit' => 10, 'period' => 10], $config['throttle']['default']); - } } diff --git a/Tests/DependencyInjection/IndragunawanApiRateLimitExtensionTest.php b/Tests/DependencyInjection/IndragunawanApiRateLimitExtensionTest.php index 0c04480..152a2be 100644 --- a/Tests/DependencyInjection/IndragunawanApiRateLimitExtensionTest.php +++ b/Tests/DependencyInjection/IndragunawanApiRateLimitExtensionTest.php @@ -30,7 +30,7 @@ class IndragunawanApiRateLimitExtensionTest extends TestCase /** * {@inheritdoc} */ - protected function setUp() + protected function setUp(): void { $this->container = new ContainerBuilder(); $this->container->setParameter('kernel.cache_dir', '../../var/cache'); @@ -41,7 +41,7 @@ protected function setUp() /** * {@inheritdoc} */ - protected function tearDown() + protected function tearDown(): void { unset($this->container, $this->extension); } @@ -79,22 +79,6 @@ public function testServiceConfig() $this->assertSame('custom_cache', (string) $storageDefinition->getArgument(0)); } - public function testDeprecatedServiceConfig() - { - $config = [ - [ - 'storage' => 'custom_storage', - ], - ]; - - $this->extension->load($config, $this->container); - - $this->assertTrue($this->container->hasDefinition('indragunawan_api_rate_limit.service.rate_limit_handler')); - - $storageDefinition = $this->container->getDefinition('indragunawan_api_rate_limit.service.rate_limit_handler'); - $this->assertSame('custom_storage', (string) $storageDefinition->getArgument(0)->getArgument(0)); - } - public function testSortFirstMatch() { $roles = [ diff --git a/Tests/EventListener/HeaderModificationListenerTest.php b/Tests/EventListener/HeaderModificationListenerTest.php index 9a763ea..0e72dd1 100644 --- a/Tests/EventListener/HeaderModificationListenerTest.php +++ b/Tests/EventListener/HeaderModificationListenerTest.php @@ -13,20 +13,36 @@ use Indragunawan\ApiRateLimitBundle\EventListener\HeaderModificationListener; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; +use Symfony\Component\HttpKernel\Event\ResponseEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; class HeaderModificationListenerTest extends TestCase { + private $kernel; + + public function setUp(): void + { + $this->kernel = $this->createMock(HttpKernelInterface::class); + } + + public function tearDown(): void + { + $this->kernel = null; + } + public function testNotDisplayHeader() { - $event = $this->getMockBuilder(FilterResponseEvent::class) - ->disableOriginalConstructor() - ->getMock(); + $responseHeaderBag = $this->prophesize(ResponseHeaderBag::class); + $responseHeaderBag->set()->shouldNotHaveBeenCalled(); - $event->expects($this->never()) - ->method('getRequest'); + $response = $this->prophesize(Response::class); + $response->headers = $responseHeaderBag->reveal(); + + $event = new ResponseEvent($this->kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, $response->reveal()); $headerConfig = [ 'display' => false, @@ -38,21 +54,13 @@ public function testNotDisplayHeader() public function testNoRateLimitInfoAttributes() { - $event = $this->getMockBuilder(FilterResponseEvent::class) - ->disableOriginalConstructor() - ->getMock(); - - $resetTime = gmdate('U'); + $attributes = $this->prophesize(ParameterBag::class); + $attributes->get('_api_rate_limit_info', null)->willReturn(null)->shouldBeCalledTimes(1); - $request = Request::create('/api/me'); - $request->attributes->set('_api_rate_limit_info', null); + $request = $this->prophesize(Request::class); + $request->attributes = $attributes->reveal(); - $event->expects($this->once()) - ->method('getRequest') - ->will($this->returnValue($request)); - - $event->expects($this->never()) - ->method('getResponse'); + $event = new ResponseEvent($this->kernel, $request->reveal(), HttpKernelInterface::MASTER_REQUEST, new Response()); $headerConfig = [ 'display' => true, @@ -64,27 +72,21 @@ public function testNoRateLimitInfoAttributes() public function testSetResponseHeaders() { - $event = $this->getMockBuilder(FilterResponseEvent::class) - ->disableOriginalConstructor() - ->getMock(); - $resetTime = gmdate('U'); - $request = Request::create('/api/me'); - $request->attributes->set('_api_rate_limit_info', [ + $attributes = $this->prophesize(ParameterBag::class); + $attributes->get('_api_rate_limit_info', null)->willReturn([ 'limit' => 60, 'remaining' => 59, 'reset' => $resetTime, - ]); + ])->shouldBeCalledTimes(1); - $event->expects($this->once()) - ->method('getRequest') - ->will($this->returnValue($request)); + $request = $this->prophesize(Request::class); + $request->attributes = $attributes->reveal(); $response = new Response(); - $event->expects($this->once()) - ->method('getResponse') - ->will($this->returnValue($response)); + + $event = new ResponseEvent($this->kernel, $request->reveal(), HttpKernelInterface::MASTER_REQUEST, $response); $headerConfig = [ 'display' => true, diff --git a/Tests/EventListener/RateLimitListenerTest.php b/Tests/EventListener/RateLimitListenerTest.php index 5176dce..350af0d 100644 --- a/Tests/EventListener/RateLimitListenerTest.php +++ b/Tests/EventListener/RateLimitListenerTest.php @@ -15,7 +15,7 @@ use Indragunawan\ApiRateLimitBundle\Service\RateLimitHandler; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\User\UserInterface; @@ -24,13 +24,8 @@ class RateLimitListenerTest extends TestCase { public function testDisabledApiRateLimit() { - $rateLimitHandler = $this->getMockBuilder(RateLimitHandler::class) - ->disableOriginalConstructor() - ->getMock(); - - $event = $this->getMockBuilder(GetResponseEvent::class) - ->disableOriginalConstructor() - ->getMock(); + $rateLimitHandler = $this->createMock(RateLimitHandler::class); + $event = $this->createMock(RequestEvent::class); $event->expects($this->never()) ->method('isMasterRequest') @@ -44,13 +39,8 @@ public function testDisabledApiRateLimit() public function testNotMasterRequest() { - $rateLimitHandler = $this->getMockBuilder(RateLimitHandler::class) - ->disableOriginalConstructor() - ->getMock(); - - $event = $this->getMockBuilder(GetResponseEvent::class) - ->disableOriginalConstructor() - ->getMock(); + $rateLimitHandler = $this->createMock(RateLimitHandler::class); + $event = $this->createMock(RequestEvent::class); $event->expects($this->once()) ->method('isMasterRequest') @@ -68,13 +58,8 @@ public function testNotMasterRequest() public function testNoApiResourceClass() { - $rateLimitHandler = $this->getMockBuilder(RateLimitHandler::class) - ->disableOriginalConstructor() - ->getMock(); - - $event = $this->getMockBuilder(GetResponseEvent::class) - ->disableOriginalConstructor() - ->getMock(); + $rateLimitHandler = $this->createMock(RateLimitHandler::class); + $event = $this->createMock(RequestEvent::class); $event->expects($this->once()) ->method('isMasterRequest') @@ -94,9 +79,7 @@ public function testNoApiResourceClass() public function testRateLimitHandlerDisabled() { - $rateLimitHandler = $this->getMockBuilder(RateLimitHandler::class) - ->disableOriginalConstructor() - ->getMock(); + $rateLimitHandler = $this->createMock(RateLimitHandler::class); $rateLimitHandler->expects($this->once()) ->method('handle'); @@ -108,9 +91,7 @@ public function testRateLimitHandlerDisabled() $rateLimitHandler->expects($this->never()) ->method('isRateLimitExceeded'); - $event = $this->getMockBuilder(GetResponseEvent::class) - ->disableOriginalConstructor() - ->getMock(); + $event = $this->createMock(RequestEvent::class); $event->expects($this->once()) ->method('isMasterRequest') @@ -136,9 +117,7 @@ public function testRateLimitExceededForAnonymousUser() $this->expectException(\Indragunawan\ApiRateLimitBundle\Exception\RateLimitExceededException::class); $this->expectExceptionMessage('API rate limit exceeded for 127.0.0.1.'); - $rateLimitHandler = $this->getMockBuilder(RateLimitHandler::class) - ->disableOriginalConstructor() - ->getMock(); + $rateLimitHandler = $this->createMock(RateLimitHandler::class); $rateLimitHandler->expects($this->once()) ->method('handle'); @@ -151,9 +130,7 @@ public function testRateLimitExceededForAnonymousUser() ->method('isRateLimitExceeded') ->will($this->returnValue(true)); - $event = $this->getMockBuilder(GetResponseEvent::class) - ->disableOriginalConstructor() - ->getMock(); + $event = $this->createMock(RequestEvent::class); $event->expects($this->once()) ->method('isMasterRequest') @@ -184,9 +161,7 @@ public function testRateLimitExceededForAuthenticatedUser() $this->expectException(\Indragunawan\ApiRateLimitBundle\Exception\RateLimitExceededException::class); $this->expectExceptionMessage('API rate limit exceeded for user.'); - $rateLimitHandler = $this->getMockBuilder(RateLimitHandler::class) - ->disableOriginalConstructor() - ->getMock(); + $rateLimitHandler = $this->createMock(RateLimitHandler::class); $rateLimitHandler->expects($this->once()) ->method('handle'); @@ -199,9 +174,7 @@ public function testRateLimitExceededForAuthenticatedUser() ->method('isRateLimitExceeded') ->will($this->returnValue(true)); - $event = $this->getMockBuilder(GetResponseEvent::class) - ->disableOriginalConstructor() - ->getMock(); + $event = $this->createMock(RequestEvent::class); $event->expects($this->once()) ->method('isMasterRequest') diff --git a/Tests/Service/RateLimitHandlerTest.php b/Tests/Service/RateLimitHandlerTest.php index 7857959..ae0b073 100644 --- a/Tests/Service/RateLimitHandlerTest.php +++ b/Tests/Service/RateLimitHandlerTest.php @@ -40,9 +40,7 @@ public function testDisabledApiRateLimit() ->method('getItem'); $tokenStorage = $this->createMock(TokenStorage::class); - $authorizationChecker = $this->getMockBuilder(AuthorizationChecker::class) - ->disableOriginalConstructor() - ->getMock(); + $authorizationChecker = $this->createMock(AuthorizationChecker::class); $throttleConfig = [ 'default' => [ @@ -75,9 +73,7 @@ public function testEnabledApiRateLimit() ->will($this->returnValue($cacheItem)); $tokenStorage = $this->createMock(TokenStorage::class); - $authorizationChecker = $this->getMockBuilder(AuthorizationChecker::class) - ->disableOriginalConstructor() - ->getMock(); + $authorizationChecker = $this->createMock(AuthorizationChecker::class); $throttleConfig = [ 'default' => [ @@ -123,9 +119,7 @@ public function testRateLimitIsExceeded() ->will($this->returnValue($cacheItem)); $tokenStorage = $this->createMock(TokenStorage::class); - $authorizationChecker = $this->getMockBuilder(AuthorizationChecker::class) - ->disableOriginalConstructor() - ->getMock(); + $authorizationChecker = $this->createMock(AuthorizationChecker::class); $throttleConfig = [ 'default' => [ @@ -170,9 +164,7 @@ public function testRateLimitIsNotExceeded() ->will($this->returnValue($cacheItem)); $tokenStorage = $this->createMock(TokenStorage::class); - $authorizationChecker = $this->getMockBuilder(AuthorizationChecker::class) - ->disableOriginalConstructor() - ->getMock(); + $authorizationChecker = $this->createMock(AuthorizationChecker::class); $throttleConfig = [ 'default' => [ @@ -216,9 +208,7 @@ public function testRateLimitByRole() $user = $this->createMock(UserInterface::class); - $token = $this->getMockBuilder(UsernamePasswordToken::class) - ->disableOriginalConstructor() - ->getMock(); + $token = $this->createMock(UsernamePasswordToken::class); $token->expects($this->once()) ->method('getUser') @@ -243,7 +233,7 @@ public function testRateLimitByRole() ->method('decide') ->will($this->returnValue(true)); - $authorizationChecker = $this->getMockBuilder(AuthorizationChecker::class) + $authorizationChecker = $this->getMockBuilder(AuthorizationChecker::class) ->setConstructorArgs([$tokenStorage, $authenticationManager, $accessDecisionManager]) ->getMock(); @@ -292,9 +282,7 @@ public function testTokenNotFound() ->method('getItem') ->will($this->returnValue($cacheItem)); - $token = $this->getMockBuilder(UsernamePasswordToken::class) - ->disableOriginalConstructor() - ->getMock(); + $token = $this->createMock(UsernamePasswordToken::class); $tokenStorage = $this->createMock(TokenStorage::class); $tokenStorage->expects($this->any()) @@ -349,9 +337,7 @@ public function testAnnotationDefaultConfiguration() { ->will($this->returnValue($cacheItem)); $tokenStorage = $this->createMock(TokenStorage::class); - $authorizationChecker = $this->getMockBuilder(AuthorizationChecker::class) - ->disableOriginalConstructor() - ->getMock(); + $authorizationChecker = $this->createMock(AuthorizationChecker::class); $throttleConfig = [ 'default' => [ @@ -394,9 +380,7 @@ public function testAnnotationRateLimitByRole() $user = $this->createMock(UserInterface::class); - $token = $this->getMockBuilder(UsernamePasswordToken::class) - ->disableOriginalConstructor() - ->getMock(); + $token = $this->createMock(UsernamePasswordToken::class); $token->expects($this->once()) ->method('getUser') diff --git a/composer.json b/composer.json index ccc4db4..94dea6d 100644 --- a/composer.json +++ b/composer.json @@ -11,19 +11,19 @@ } ], "require": { - "php": ">=7.0", - "api-platform/core": "^2.0", + "php": "^7.1.3", + "api-platform/core": "^2.4", "doctrine/annotations": "^1.4", - "symfony/cache": "^3.1.0 || ^4.0", - "symfony/config": "^2.7 || ^3.0 || ^4.0", - "symfony/dependency-injection": "^2.7 || ^3.0 || ^4.0", - "symfony/http-foundation": "^2.7 || ^3.0 || ^4.0", - "symfony/http-kernel": "^2.7 || ^3.0 || ^4.0", - "symfony/security-bundle": "^2.7 || ^3.0 || ^4.0", - "symfony/twig-bundle": "^2.7 || ^3.0 || ^4.0" + "symfony/cache": "^3.4 || ^4.0 || ^5.0", + "symfony/config": "^3.4 || ^4.0 || ^5.0", + "symfony/dependency-injection": "^3.4 || ^4.0 || ^5.0", + "symfony/http-foundation": "^3.4 || ^4.0 || ^5.0", + "symfony/http-kernel": "^4.3 || ^5.0", + "symfony/security-bundle": "^3.4 || ^4.0 || ^5.0", + "symfony/twig-bundle": "^3.0 || ^4.0 || ^5.0" }, "require-dev": { - "phpunit/phpunit": "^6.2" + "phpunit/phpunit": "^7.5.2 || ^8.0" }, "autoload": { "psr-4": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3f8c632..9b59086 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,9 @@ - +