diff options
author | Joas Schilling <coding@schilljs.com> | 2023-04-21 15:08:55 +0200 |
---|---|---|
committer | Joas Schilling <coding@schilljs.com> | 2023-04-24 12:24:48 +0200 |
commit | 89c3c3140210c854fd1bee7507288ba216495220 (patch) | |
tree | 3f517b7714661a64dd986ae074f59077d2471194 | |
parent | 95c098170defa9362d09915a22a11e67cbead1b8 (diff) | |
download | nextcloud-server-89c3c3140210c854fd1bee7507288ba216495220.tar.gz nextcloud-server-89c3c3140210c854fd1bee7507288ba216495220.zip |
feat(ratelimit): Add Attributes support to rate limit middleware
Signed-off-by: Joas Schilling <coding@schilljs.com>
7 files changed, 386 insertions, 144 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 3e1b9559d5e..a4bab729592 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -35,8 +35,11 @@ return array( 'OCP\\AppFramework\\Db\\QBMapper' => $baseDir . '/lib/public/AppFramework/Db/QBMapper.php', 'OCP\\AppFramework\\Db\\TTransactional' => $baseDir . '/lib/public/AppFramework/Db/TTransactional.php', 'OCP\\AppFramework\\Http' => $baseDir . '/lib/public/AppFramework/Http.php', + 'OCP\\AppFramework\\Http\\Attribute\\ARateLimit' => $baseDir . '/lib/public/AppFramework/Http/Attribute/ARateLimit.php', + 'OCP\\AppFramework\\Http\\Attribute\\AnonRateLimit' => $baseDir . '/lib/public/AppFramework/Http/Attribute/AnonRateLimit.php', 'OCP\\AppFramework\\Http\\Attribute\\BruteForceProtection' => $baseDir . '/lib/public/AppFramework/Http/Attribute/BruteForceProtection.php', 'OCP\\AppFramework\\Http\\Attribute\\UseSession' => $baseDir . '/lib/public/AppFramework/Http/Attribute/UseSession.php', + 'OCP\\AppFramework\\Http\\Attribute\\UserRateLimit' => $baseDir . '/lib/public/AppFramework/Http/Attribute/UserRateLimit.php', 'OCP\\AppFramework\\Http\\ContentSecurityPolicy' => $baseDir . '/lib/public/AppFramework/Http/ContentSecurityPolicy.php', 'OCP\\AppFramework\\Http\\DataDisplayResponse' => $baseDir . '/lib/public/AppFramework/Http/DataDisplayResponse.php', 'OCP\\AppFramework\\Http\\DataDownloadResponse' => $baseDir . '/lib/public/AppFramework/Http/DataDownloadResponse.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 1a0d68fd204..8a8de63fc4c 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -68,8 +68,11 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\AppFramework\\Db\\QBMapper' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/QBMapper.php', 'OCP\\AppFramework\\Db\\TTransactional' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/TTransactional.php', 'OCP\\AppFramework\\Http' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http.php', + 'OCP\\AppFramework\\Http\\Attribute\\ARateLimit' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/ARateLimit.php', + 'OCP\\AppFramework\\Http\\Attribute\\AnonRateLimit' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/AnonRateLimit.php', 'OCP\\AppFramework\\Http\\Attribute\\BruteForceProtection' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/BruteForceProtection.php', 'OCP\\AppFramework\\Http\\Attribute\\UseSession' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/UseSession.php', + 'OCP\\AppFramework\\Http\\Attribute\\UserRateLimit' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Attribute/UserRateLimit.php', 'OCP\\AppFramework\\Http\\ContentSecurityPolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/ContentSecurityPolicy.php', 'OCP\\AppFramework\\Http\\DataDisplayResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/DataDisplayResponse.php', 'OCP\\AppFramework\\Http\\DataDownloadResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/DataDownloadResponse.php', diff --git a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php index 5f683fa38ac..6f84a0c94d0 100644 --- a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php @@ -1,5 +1,9 @@ <?php + +declare(strict_types=1); + /** + * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> * * @author Christoph Wurst <christoph@winzerhof-wurst.at> @@ -27,11 +31,17 @@ namespace OC\AppFramework\Middleware\Security; use OC\AppFramework\Utility\ControllerMethodReflector; use OC\Security\RateLimiting\Exception\RateLimitExceededException; use OC\Security\RateLimiting\Limiter; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\AnonRateLimit; +use OCP\AppFramework\Http\Attribute\ARateLimit; +use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Middleware; use OCP\IRequest; use OCP\IUserSession; +use ReflectionMethod; /** * Class RateLimitingMiddleware is the middleware responsible for implementing the @@ -42,7 +52,12 @@ use OCP\IUserSession; * @UserRateThrottle(limit=5, period=100) * @AnonRateThrottle(limit=1, period=100) * - * Those annotations above would mean that logged-in users can access the page 5 + * Or attributes such as: + * + * #[UserRateLimit(limit: 5, period: 100)] + * #[AnonRateLimit(limit: 1, period: 100)] + * + * Both sets would mean that logged-in users can access the page 5 * times within 100 seconds, and anonymous users 1 time within 100 seconds. If * only an AnonRateThrottle is specified that one will also be applied to logged-in * users. @@ -50,64 +65,85 @@ use OCP\IUserSession; * @package OC\AppFramework\Middleware\Security */ class RateLimitingMiddleware extends Middleware { - /** @var IRequest $request */ - private $request; - /** @var IUserSession */ - private $userSession; - /** @var ControllerMethodReflector */ - private $reflector; - /** @var Limiter */ - private $limiter; - - /** - * @param IRequest $request - * @param IUserSession $userSession - * @param ControllerMethodReflector $reflector - * @param Limiter $limiter - */ - public function __construct(IRequest $request, - IUserSession $userSession, - ControllerMethodReflector $reflector, - Limiter $limiter) { - $this->request = $request; - $this->userSession = $userSession; - $this->reflector = $reflector; - $this->limiter = $limiter; + public function __construct( + protected IRequest $request, + protected IUserSession $userSession, + protected ControllerMethodReflector $reflector, + protected Limiter $limiter, + ) { } /** * {@inheritDoc} * @throws RateLimitExceededException */ - public function beforeController($controller, $methodName) { + public function beforeController(Controller $controller, string $methodName): void { parent::beforeController($controller, $methodName); - - $anonLimit = $this->reflector->getAnnotationParameter('AnonRateThrottle', 'limit'); - $anonPeriod = $this->reflector->getAnnotationParameter('AnonRateThrottle', 'period'); - $userLimit = $this->reflector->getAnnotationParameter('UserRateThrottle', 'limit'); - $userPeriod = $this->reflector->getAnnotationParameter('UserRateThrottle', 'period'); $rateLimitIdentifier = get_class($controller) . '::' . $methodName; - if ($userLimit !== '' && $userPeriod !== '' && $this->userSession->isLoggedIn()) { - $this->limiter->registerUserRequest( - $rateLimitIdentifier, - $userLimit, - $userPeriod, - $this->userSession->getUser() - ); - } elseif ($anonLimit !== '' && $anonPeriod !== '') { + + if ($this->userSession->isLoggedIn()) { + $rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'UserRateThrottle', UserRateLimit::class); + + if ($rateLimit !== null) { + $this->limiter->registerUserRequest( + $rateLimitIdentifier, + $rateLimit->getLimit(), + $rateLimit->getPeriod(), + $this->userSession->getUser() + ); + return; + } + + // If not user specific rate limit is found the Anon rate limit applies! + } + + $rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'AnonRateThrottle', AnonRateLimit::class); + + if ($rateLimit !== null) { $this->limiter->registerAnonRequest( $rateLimitIdentifier, - $anonLimit, - $anonPeriod, + $rateLimit->getLimit(), + $rateLimit->getPeriod(), $this->request->getRemoteAddress() ); } } /** + * @template T of ARateLimit + * + * @param Controller $controller + * @param string $methodName + * @param string $annotationName + * @param class-string<T> $attributeClass + * @return ?ARateLimit + */ + protected function readLimitFromAnnotationOrAttribute(Controller $controller, string $methodName, string $annotationName, string $attributeClass): ?ARateLimit { + $annotationLimit = $this->reflector->getAnnotationParameter($annotationName, 'limit'); + $annotationPeriod = $this->reflector->getAnnotationParameter($annotationName, 'period'); + + if ($annotationLimit !== '' && $annotationPeriod !== '') { + return new $attributeClass( + (int) $annotationLimit, + (int) $annotationPeriod, + ); + } + + $reflectionMethod = new ReflectionMethod($controller, $methodName); + $attributes = $reflectionMethod->getAttributes($attributeClass); + $attribute = current($attributes); + + if ($attribute !== false) { + return $attribute->newInstance(); + } + + return null; + } + + /** * {@inheritDoc} */ - public function afterException($controller, $methodName, \Exception $exception) { + public function afterException(Controller $controller, string $methodName, \Exception $exception): Response { if ($exception instanceof RateLimitExceededException) { if (stripos($this->request->getHeader('Accept'), 'html') === false) { $response = new DataResponse([], $exception->getCode()); diff --git a/lib/public/AppFramework/Http/Attribute/ARateLimit.php b/lib/public/AppFramework/Http/Attribute/ARateLimit.php new file mode 100644 index 00000000000..1a779d07518 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/ARateLimit.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCP\AppFramework\Http\Attribute; + +/** + * Attribute for controller methods that want to limit the times a logged-in + * user can call the endpoint in a given time period. + * + * @since 27.0.0 + */ +abstract class ARateLimit { + /** + * @since 27.0.0 + */ + public function __construct( + protected int $limit, + protected int $period, + ) { + } + + /** + * @since 27.0.0 + */ + public function getLimit(): int { + return $this->limit; + } + + /** + * @since 27.0.0 + */ + public function getPeriod(): int { + return $this->period; + } +} diff --git a/lib/public/AppFramework/Http/Attribute/AnonRateLimit.php b/lib/public/AppFramework/Http/Attribute/AnonRateLimit.php new file mode 100644 index 00000000000..039a2022ebb --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/AnonRateLimit.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for controller methods that want to limit the times a not logged-in + * guest can call the endpoint in a given time period. + * + * @since 27.0.0 + */ +#[Attribute(Attribute::TARGET_METHOD)] +class AnonRateLimit extends ARateLimit { +} diff --git a/lib/public/AppFramework/Http/Attribute/UserRateLimit.php b/lib/public/AppFramework/Http/Attribute/UserRateLimit.php new file mode 100644 index 00000000000..e34551ea256 --- /dev/null +++ b/lib/public/AppFramework/Http/Attribute/UserRateLimit.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCP\AppFramework\Http\Attribute; + +use Attribute; + +/** + * Attribute for controller methods that want to limit the times a logged-in + * user can call the endpoint in a given time period. + * + * @since 27.0.0 + */ +#[Attribute(Attribute::TARGET_METHOD)] +class UserRateLimit extends ARateLimit { +} diff --git a/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php index 38d01950f6a..47d479b18a1 100644 --- a/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php @@ -1,7 +1,13 @@ <?php + +declare(strict_types=1); + /** + * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> * + * @author Joas Schilling <coding@schilljs.com> + * * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify @@ -26,34 +32,59 @@ use OC\AppFramework\Utility\ControllerMethodReflector; use OC\Security\RateLimiting\Exception\RateLimitExceededException; use OC\Security\RateLimiting\Limiter; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\AnonRateLimit; +use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\IRequest; use OCP\IUser; use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; +class TestRateLimitController extends Controller { + /** + * @UserRateThrottle(limit=20, period=200) + * @AnonRateThrottle(limit=10, period=100) + */ + public function testMethodWithAnnotation() { + } + + /** + * @AnonRateThrottle(limit=10, period=100) + */ + public function testMethodWithAnnotationFallback() { + } + + public function testMethodWithoutAnnotation() { + } + + #[UserRateLimit(limit: 20, period: 200)] + #[AnonRateLimit(limit: 10, period: 100)] + public function testMethodWithAttributes() { + } + + #[AnonRateLimit(limit: 10, period: 100)] + public function testMethodWithAttributesFallback() { + } +} + /** * @group DB */ class RateLimitingMiddlewareTest extends TestCase { - /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ - private $request; - /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */ - private $userSession; - /** @var ControllerMethodReflector|\PHPUnit\Framework\MockObject\MockObject */ - private $reflector; - /** @var Limiter|\PHPUnit\Framework\MockObject\MockObject */ - private $limiter; - /** @var RateLimitingMiddleware */ - private $rateLimitingMiddleware; + private IRequest|MockObject $request; + private IUserSession|MockObject $userSession; + private ControllerMethodReflector $reflector; + private Limiter|MockObject $limiter; + private RateLimitingMiddleware $rateLimitingMiddleware; protected function setUp(): void { parent::setUp(); $this->request = $this->createMock(IRequest::class); $this->userSession = $this->createMock(IUserSession::class); - $this->reflector = $this->createMock(ControllerMethodReflector::class); + $this->reflector = new ControllerMethodReflector(); $this->limiter = $this->createMock(Limiter::class); $this->rateLimitingMiddleware = new RateLimitingMiddleware( @@ -64,23 +95,25 @@ class RateLimitingMiddlewareTest extends TestCase { ); } - public function testBeforeControllerWithoutAnnotation() { - $this->reflector - ->expects($this->exactly(4)) - ->method('getAnnotationParameter') - ->withConsecutive( - ['AnonRateThrottle', 'limit'], - ['AnonRateThrottle', 'period'], - ['UserRateThrottle', 'limit'], - ['UserRateThrottle', 'period'] - ) - ->willReturnMap([ - ['AnonRateThrottle', 'limit', ''], - ['AnonRateThrottle', 'period', ''], - ['UserRateThrottle', 'limit', ''], - ['UserRateThrottle', 'period', ''], - ]); + public function testBeforeControllerWithoutAnnotationForAnon(): void { + $this->limiter + ->expects($this->never()) + ->method('registerUserRequest'); + $this->limiter + ->expects($this->never()) + ->method('registerAnonRequest'); + + $this->userSession->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + + /** @var TestRateLimitController|MockObject $controller */ + $controller = $this->createMock(TestRateLimitController::class); + $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithoutAnnotation'); + } + public function testBeforeControllerWithoutAnnotationForLoggedIn(): void { $this->limiter ->expects($this->never()) ->method('registerUserRequest'); @@ -88,34 +121,79 @@ class RateLimitingMiddlewareTest extends TestCase { ->expects($this->never()) ->method('registerAnonRequest'); - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethod'); + $this->userSession->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + + /** @var TestRateLimitController|MockObject $controller */ + $controller = $this->createMock(TestRateLimitController::class); + $this->reflector->reflect($controller, 'testMethodWithoutAnnotation'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithoutAnnotation'); } - public function testBeforeControllerForAnon() { - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); + public function testBeforeControllerForAnon(): void { + $controller = new TestRateLimitController('test', $this->request); + + $this->request + ->method('getRemoteAddress') + ->willReturn('127.0.0.1'); + + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + + $this->limiter + ->expects($this->never()) + ->method('registerUserRequest'); + $this->limiter + ->expects($this->once()) + ->method('registerAnonRequest') + ->with(get_class($controller) . '::testMethodWithAnnotation', '10', '100', '127.0.0.1'); + + $this->reflector->reflect($controller, 'testMethodWithAnnotation'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAnnotation'); + } + + public function testBeforeControllerForLoggedIn(): void { + $controller = new TestRateLimitController('test', $this->request); + /** @var IUser|MockObject $user */ + $user = $this->createMock(IUser::class); + + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->willReturn($user); + + $this->limiter + ->expects($this->never()) + ->method('registerAnonRequest'); + $this->limiter + ->expects($this->once()) + ->method('registerUserRequest') + ->with(get_class($controller) . '::testMethodWithAnnotation', '20', '200', $user); + + + $this->reflector->reflect($controller, 'testMethodWithAnnotation'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAnnotation'); + } + + public function testBeforeControllerAnonWithFallback(): void { + $controller = new TestRateLimitController('test', $this->request); $this->request ->expects($this->once()) ->method('getRemoteAddress') ->willReturn('127.0.0.1'); - $this->reflector - ->expects($this->exactly(4)) - ->method('getAnnotationParameter') - ->withConsecutive( - ['AnonRateThrottle', 'limit'], - ['AnonRateThrottle', 'period'], - ['UserRateThrottle', 'limit'], - ['UserRateThrottle', 'period'] - ) - ->willReturnMap([ - ['AnonRateThrottle', 'limit', '100'], - ['AnonRateThrottle', 'period', '10'], - ['UserRateThrottle', 'limit', ''], - ['UserRateThrottle', 'period', ''], - ]); + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->limiter ->expects($this->never()) @@ -123,16 +201,39 @@ class RateLimitingMiddlewareTest extends TestCase { $this->limiter ->expects($this->once()) ->method('registerAnonRequest') - ->with(get_class($controller) . '::testMethod', '100', '10', '127.0.0.1'); + ->with(get_class($controller) . '::testMethodWithAnnotationFallback', '10', '100', '127.0.0.1'); + + $this->reflector->reflect($controller, 'testMethodWithAnnotationFallback'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAnnotationFallback'); + } + + public function testBeforeControllerAttributesForAnon(): void { + $controller = new TestRateLimitController('test', $this->request); + + $this->request + ->method('getRemoteAddress') + ->willReturn('127.0.0.1'); + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethod'); + $this->limiter + ->expects($this->never()) + ->method('registerUserRequest'); + $this->limiter + ->expects($this->once()) + ->method('registerAnonRequest') + ->with(get_class($controller) . '::testMethodWithAttributes', '10', '100', '127.0.0.1'); + + $this->reflector->reflect($controller, 'testMethodWithAttributes'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAttributes'); } - public function testBeforeControllerForLoggedIn() { - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); - /** @var IUser|\PHPUnit\Framework\MockObject\MockObject $user */ + public function testBeforeControllerAttributesForLoggedIn(): void { + $controller = new TestRateLimitController('test', $this->request); + /** @var IUser|MockObject $user */ $user = $this->createMock(IUser::class); $this->userSession @@ -144,37 +245,21 @@ class RateLimitingMiddlewareTest extends TestCase { ->method('getUser') ->willReturn($user); - $this->reflector - ->expects($this->exactly(4)) - ->method('getAnnotationParameter') - ->withConsecutive( - ['AnonRateThrottle', 'limit'], - ['AnonRateThrottle', 'period'], - ['UserRateThrottle', 'limit'], - ['UserRateThrottle', 'period'] - ) - ->willReturnMap([ - ['AnonRateThrottle', 'limit', ''], - ['AnonRateThrottle', 'period', ''], - ['UserRateThrottle', 'limit', '100'], - ['UserRateThrottle', 'period', '10'], - ]); - $this->limiter ->expects($this->never()) ->method('registerAnonRequest'); $this->limiter ->expects($this->once()) ->method('registerUserRequest') - ->with(get_class($controller) . '::testMethod', '100', '10', $user); + ->with(get_class($controller) . '::testMethodWithAttributes', '20', '200', $user); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethod'); + $this->reflector->reflect($controller, 'testMethodWithAttributes'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAttributes'); } - public function testBeforeControllerAnonWithFallback() { - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); + public function testBeforeControllerAttributesAnonWithFallback(): void { + $controller = new TestRateLimitController('test', $this->request); $this->request ->expects($this->once()) ->method('getRemoteAddress') @@ -183,23 +268,8 @@ class RateLimitingMiddlewareTest extends TestCase { $this->userSession ->expects($this->once()) ->method('isLoggedIn') - ->willReturn(false); + ->willReturn(true); - $this->reflector - ->expects($this->exactly(4)) - ->method('getAnnotationParameter') - ->withConsecutive( - ['AnonRateThrottle', 'limit'], - ['AnonRateThrottle', 'period'], - ['UserRateThrottle', 'limit'], - ['UserRateThrottle', 'period'] - ) - ->willReturnMap([ - ['AnonRateThrottle', 'limit', '200'], - ['AnonRateThrottle', 'period', '20'], - ['UserRateThrottle', 'limit', '100'], - ['UserRateThrottle', 'period', '10'], - ]); $this->limiter ->expects($this->never()) @@ -207,25 +277,23 @@ class RateLimitingMiddlewareTest extends TestCase { $this->limiter ->expects($this->once()) ->method('registerAnonRequest') - ->with(get_class($controller) . '::testMethod', '200', '20', '127.0.0.1'); + ->with(get_class($controller) . '::testMethodWithAttributesFallback', '10', '100', '127.0.0.1'); - $this->rateLimitingMiddleware->beforeController($controller, 'testMethod'); + $this->reflector->reflect($controller, 'testMethodWithAttributesFallback'); + $this->rateLimitingMiddleware->beforeController($controller, 'testMethodWithAttributesFallback'); } - - public function testAfterExceptionWithOtherException() { + public function testAfterExceptionWithOtherException(): void { $this->expectException(\Exception::class); $this->expectExceptionMessage('My test exception'); - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); + $controller = new TestRateLimitController('test', $this->request); $this->rateLimitingMiddleware->afterException($controller, 'testMethod', new \Exception('My test exception')); } - public function testAfterExceptionWithJsonBody() { - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); + public function testAfterExceptionWithJsonBody(): void { + $controller = new TestRateLimitController('test', $this->request); $this->request ->expects($this->once()) ->method('getHeader') @@ -238,9 +306,8 @@ class RateLimitingMiddlewareTest extends TestCase { $this->assertEquals($expected, $result); } - public function testAfterExceptionWithHtmlBody() { - /** @var Controller|\PHPUnit\Framework\MockObject\MockObject $controller */ - $controller = $this->createMock(Controller::class); + public function testAfterExceptionWithHtmlBody(): void { + $controller = new TestRateLimitController('test', $this->request); $this->request ->expects($this->once()) ->method('getHeader') |