diff --git a/Attribute/RateLimit.php b/Attribute/RateLimit.php
index a4cf08a..fb81400 100644
--- a/Attribute/RateLimit.php
+++ b/Attribute/RateLimit.php
@@ -26,7 +26,15 @@ public function __construct(
/**
* @var mixed Generic payload
*/
- public mixed $payload = null
+ public mixed $payload = null,
+
+ /**
+ * @var bool|null Defines if the rate limiter blocks the request when a technical problem occurs (default).
+ * For example, when the Redis database which is used as a rate limit storage is down.
+ * If set to `false`, the request is allowed to proceed even if the rate limiter cannot determine if the rate limit has been exceeded.
+ * `null` means that the globally-configured default should be used
+ */
+ public ?bool $failOpen = null
) {
// @RateLimit annotation used to support single method passed as string, keep that for retrocompatibility
if (!is_array($methods)) {
@@ -75,4 +83,9 @@ public function setPayload(mixed $payload): void
{
$this->payload = $payload;
}
+
+ public function setFailOpen(bool $failOpen): void
+ {
+ $this->failOpen = $failOpen;
+ }
}
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index b16a16e..9dc325d 100755
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -135,6 +135,11 @@ public function getConfigTreeBuilder(): TreeBuilder
->end()
->end()
->end()
+ ->booleanNode('fail_open')
+ ->defaultFalse()
+ ->treatNullLike(false)
+ ->info('Defines if the rate limiter blocks the request when a technical problem occurs')
+ ->end()
->booleanNode('fos_oauth_key_listener')
->defaultTrue()
->info('Enabled the FOS OAuthServerBundle listener')
diff --git a/DependencyInjection/NoxlogicRateLimitExtension.php b/DependencyInjection/NoxlogicRateLimitExtension.php
index 077a399..f79a9a8 100755
--- a/DependencyInjection/NoxlogicRateLimitExtension.php
+++ b/DependencyInjection/NoxlogicRateLimitExtension.php
@@ -45,6 +45,8 @@ private function loadServices(ContainerBuilder $container, array $config): void
$container->setParameter('noxlogic_rate_limit.path_limits', $config['path_limits']);
+ $container->setParameter('noxlogic_rate_limit.fail_open', $config['fail_open']);
+
switch ($config['storage_engine']) {
case 'memcache':
$container->setParameter('noxlogic_rate_limit.storage.class', 'Noxlogic\RateLimitBundle\Service\Storage\Memcache');
diff --git a/EventListener/RateLimitAnnotationListener.php b/EventListener/RateLimitAnnotationListener.php
index 7ea8440..56766b5 100644
--- a/EventListener/RateLimitAnnotationListener.php
+++ b/EventListener/RateLimitAnnotationListener.php
@@ -7,6 +7,7 @@
use Noxlogic\RateLimitBundle\Events\GenerateKeyEvent;
use Noxlogic\RateLimitBundle\Events\RateLimitEvents;
use Noxlogic\RateLimitBundle\Exception\RateLimitExceptionInterface;
+use Noxlogic\RateLimitBundle\Exception\Storage\RateLimitStorageExceptionInterface;
use Noxlogic\RateLimitBundle\Service\RateLimitService;
use Noxlogic\RateLimitBundle\Util\PathLimitProcessor;
use Symfony\Component\HttpFoundation\Request;
@@ -33,6 +34,9 @@ public function __construct(
$this->pathLimitProcessor = $pathLimitProcessor;
}
+ /**
+ * @throws RateLimitStorageExceptionInterface
+ */
public function onKernelController(ControllerEvent $event): void
{
// Skip if the bundle isn't enabled (for instance in test environment)
@@ -68,31 +72,39 @@ public function onKernelController(ControllerEvent $event): void
$key = $this->getKey($event, $rateLimit, $rateLimits);
- // Ratelimit the call
- $rateLimitInfo = $this->rateLimitService->limitRate($key);
- if (! $rateLimitInfo) {
- // Create new rate limit entry for this call
- $rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
+ $shouldFailOpenOnStorageError = $rateLimit->failOpen ?? $this->getParameter('fail_open', false);
+ try {
+ // Ratelimit the call
+ $rateLimitInfo = $this->rateLimitService->limitRate($key);
if (! $rateLimitInfo) {
- // @codeCoverageIgnoreStart
- return;
- // @codeCoverageIgnoreEnd
+ // Create new rate limit entry for this call
+ $rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
+ if (! $rateLimitInfo) {
+ // @codeCoverageIgnoreStart
+ return;
+ // @codeCoverageIgnoreEnd
+ }
}
- }
-
- // Store the current rating info in the request attributes
- $request->attributes->set('rate_limit_info', $rateLimitInfo);
-
- // Reset the rate limits
- if(time() >= $rateLimitInfo->getResetTimestamp()) {
- $this->rateLimitService->resetRate($key);
- $rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
- if (! $rateLimitInfo) {
- // @codeCoverageIgnoreStart
+ // Store the current rating info in the request attributes
+ $request->attributes->set('rate_limit_info', $rateLimitInfo);
+
+ // Reset the rate limits
+ if(time() >= $rateLimitInfo->getResetTimestamp()) {
+ $this->rateLimitService->resetRate($key);
+ $rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
+ if (! $rateLimitInfo) {
+ // @codeCoverageIgnoreStart
+ return;
+ // @codeCoverageIgnoreEnd
+ }
+ }
+ } catch (RateLimitStorageExceptionInterface $storageException) {
+ if ($shouldFailOpenOnStorageError) {
return;
- // @codeCoverageIgnoreEnd
}
+
+ throw $storageException;
}
// When we exceeded our limit, return a custom error response
diff --git a/Exception/Storage/CreateRateRateLimitStorageException.php b/Exception/Storage/CreateRateRateLimitStorageException.php
new file mode 100644
index 0000000..b7b36e5
--- /dev/null
+++ b/Exception/Storage/CreateRateRateLimitStorageException.php
@@ -0,0 +1,15 @@
+getMessage()) {
+ $message .= \sprintf(': %s', $previous->getMessage());
+ }
+
+ parent::__construct($message, previous: $previous);
+ }
+}
\ No newline at end of file
diff --git a/Exception/Storage/RateLimitStorageExceptionInterface.php b/Exception/Storage/RateLimitStorageExceptionInterface.php
new file mode 100644
index 0000000..e0cfc17
--- /dev/null
+++ b/Exception/Storage/RateLimitStorageExceptionInterface.php
@@ -0,0 +1,12 @@
+ If you set `$failOpen` to `true` or `false`, it takes precedence over the globally configured value (`noxlogic_rate_limit.fail_open`).
+
+```php
+enabled
%noxlogic_rate_limit.enabled%
+
+ fail_open
+ %noxlogic_rate_limit.fail_open%
+
rate_response_code
%noxlogic_rate_limit.rate_response_code%
diff --git a/Service/RateLimitService.php b/Service/RateLimitService.php
index eced9f8..3d9b0cc 100644
--- a/Service/RateLimitService.php
+++ b/Service/RateLimitService.php
@@ -2,6 +2,7 @@
namespace Noxlogic\RateLimitBundle\Service;
+use Noxlogic\RateLimitBundle\Exception\Storage\RateLimitStorageExceptionInterface;
use Noxlogic\RateLimitBundle\Service\Storage\StorageInterface;
class RateLimitService
@@ -32,7 +33,7 @@ public function getStorage()
}
/**
- *
+ * @throws RateLimitStorageExceptionInterface
*/
public function limitRate($key)
{
@@ -40,7 +41,7 @@ public function limitRate($key)
}
/**
- *
+ * @throws RateLimitStorageExceptionInterface
*/
public function createRate($key, $limit, $period)
{
@@ -48,7 +49,7 @@ public function createRate($key, $limit, $period)
}
/**
- *
+ * @throws RateLimitStorageExceptionInterface
*/
public function resetRate($key)
{
diff --git a/Service/Storage/DoctrineCache.php b/Service/Storage/DoctrineCache.php
index 3adf5f3..7dddd37 100644
--- a/Service/Storage/DoctrineCache.php
+++ b/Service/Storage/DoctrineCache.php
@@ -3,6 +3,10 @@
namespace Noxlogic\RateLimitBundle\Service\Storage;
use Doctrine\Common\Cache\Cache;
+use Noxlogic\RateLimitBundle\Exception\Storage\CreateRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\GetRateInfoRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\LimitRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\ResetRateRateLimitStorageException;
use Noxlogic\RateLimitBundle\Service\RateLimitInfo;
class DoctrineCache implements StorageInterface {
@@ -19,7 +23,12 @@ public function __construct(Cache $client)
public function getRateInfo($key)
{
- $info = $this->client->fetch($key);
+ try {
+ $info = $this->client->fetch($key);
+ } catch (\Throwable $e) {
+ throw new GetRateInfoRateLimitStorageException($e);
+ }
+
if ($info === false || !array_key_exists('limit', $info)) {
return false;
}
@@ -29,7 +38,12 @@ public function getRateInfo($key)
public function limitRate($key)
{
- $info = $this->client->fetch($key);
+ try {
+ $info = $this->client->fetch($key);
+ } catch (\Throwable $e) {
+ throw new LimitRateRateLimitStorageException($e);
+ }
+
if ($info === false || !array_key_exists('limit', $info)) {
return false;
}
@@ -38,7 +52,11 @@ public function limitRate($key)
$expire = $info['reset'] - time();
- $this->client->save($key, $info, $expire);
+ try {
+ $this->client->save($key, $info, $expire);
+ } catch (\Throwable $e) {
+ throw new LimitRateRateLimitStorageException($e);
+ }
return $this->createRateInfo($info);
}
@@ -50,14 +68,22 @@ public function createRate($key, $limit, $period)
$info['calls'] = 1;
$info['reset'] = time() + $period;
- $this->client->save($key, $info, $period);
+ try {
+ $this->client->save($key, $info, $period);
+ } catch (\Throwable $e) {
+ throw new CreateRateRateLimitStorageException($e);
+ }
return $this->createRateInfo($info);
}
public function resetRate($key)
{
- $this->client->delete($key);
+ try {
+ $this->client->delete($key);
+ } catch (\Throwable $e) {
+ throw new ResetRateRateLimitStorageException($e);
+ }
return true;
}
diff --git a/Service/Storage/Memcache.php b/Service/Storage/Memcache.php
index 707fd30..566b48c 100644
--- a/Service/Storage/Memcache.php
+++ b/Service/Storage/Memcache.php
@@ -2,6 +2,10 @@
namespace Noxlogic\RateLimitBundle\Service\Storage;
+use Noxlogic\RateLimitBundle\Exception\Storage\CreateRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\GetRateInfoRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\LimitRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\ResetRateRateLimitStorageException;
use Noxlogic\RateLimitBundle\Service\RateLimitInfo;
class Memcache implements StorageInterface
@@ -18,7 +22,11 @@ public function __construct(\Memcached $client)
public function getRateInfo($key)
{
- $info = $this->client->get($key);
+ try {
+ $info = $this->client->get($key);
+ } catch (\Throwable $e) {
+ throw new GetRateInfoRateLimitStorageException($e);
+ }
return $this->createRateInfo($info);
}
@@ -29,18 +37,31 @@ public function limitRate($key)
$i = 0;
do {
if (defined('Memcached::GET_EXTENDED')) {
- $_o = $this->client->get($key, null, \Memcached::GET_EXTENDED);
+ try {
+ $_o = $this->client->get($key, null, \Memcached::GET_EXTENDED);
+ } catch (\Throwable $e) {
+ throw new LimitRateRateLimitStorageException($e);
+ }
+
$info = $_o['value'] ?? null;
$cas = $_o['cas'] ?? null;
} else {
- $info = $this->client->get($key, null, $cas);
+ try {
+ $info = $this->client->get($key, null, $cas);
+ } catch (\Throwable $e) {
+ throw new LimitRateRateLimitStorageException($e);
+ }
}
if (!$info) {
return false;
}
$info['calls']++;
- $this->client->cas($cas, $key, $info);
+ try {
+ $this->client->cas($cas, $key, $info);
+ } catch (\Throwable $e) {
+ throw new LimitRateRateLimitStorageException($e);
+ }
} while ($this->client->getResultCode() == \Memcached::RES_DATA_EXISTS && $i++ < 5);
return $this->createRateInfo($info);
@@ -53,14 +74,23 @@ public function createRate($key, $limit, $period)
$info['calls'] = 1;
$info['reset'] = time() + $period;
- $this->client->set($key, $info, $period);
+ try {
+ $this->client->set($key, $info, $period);
+ } catch (\Throwable $e) {
+ throw new CreateRateRateLimitStorageException($e);
+ }
return $this->createRateInfo($info);
}
public function resetRate($key)
{
- $this->client->delete($key);
+ try {
+ $this->client->delete($key);
+ } catch (\Throwable $e) {
+ throw new ResetRateRateLimitStorageException($e);
+ }
+
return true;
}
diff --git a/Service/Storage/PhpRedis.php b/Service/Storage/PhpRedis.php
index dc1e018..ae80b91 100644
--- a/Service/Storage/PhpRedis.php
+++ b/Service/Storage/PhpRedis.php
@@ -4,6 +4,10 @@
namespace Noxlogic\RateLimitBundle\Service\Storage;
+use Noxlogic\RateLimitBundle\Exception\Storage\CreateRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\GetRateInfoRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\LimitRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\ResetRateRateLimitStorageException;
use Noxlogic\RateLimitBundle\Service\RateLimitInfo;
class PhpRedis implements StorageInterface
@@ -22,7 +26,12 @@ public function getRateInfo($key)
{
$key = $this->sanitizeRedisKey($key);
- $info = $this->client->hgetall($key);
+ try {
+ $info = $this->client->hgetall($key);
+ } catch (\Throwable $e) {
+ throw new GetRateInfoRateLimitStorageException($e);
+ }
+
if (!isset($info['limit']) || !isset($info['calls']) || !isset($info['reset'])) {
return false;
}
@@ -39,12 +48,22 @@ public function limitRate($key)
{
$key = $this->sanitizeRedisKey($key);
- $info = $this->getRateInfo($key);
+ try {
+ $info = $this->getRateInfo($key);
+ // We want to make sure we throw the exception with the proper class
+ } catch (GetRateInfoRateLimitStorageException $e) {
+ throw new LimitRateRateLimitStorageException($e->getPrevious());
+ }
+
if (!$info) {
return false;
}
- $calls = $this->client->hincrby($key, 'calls', 1);
+ try {
+ $calls = $this->client->hincrby($key, 'calls', 1);
+ } catch (\Throwable $e) {
+ throw new LimitRateRateLimitStorageException($e);
+ }
$info->setCalls($calls);
return $info;
@@ -56,10 +75,14 @@ public function createRate($key, $limit, $period)
$reset = time() + $period;
- $this->client->hset($key, 'limit', $limit);
- $this->client->hset($key, 'calls', 1);
- $this->client->hset($key, 'reset', $reset);
- $this->client->expire($key, $period);
+ try {
+ $this->client->hset($key, 'limit', $limit);
+ $this->client->hset($key, 'calls', 1);
+ $this->client->hset($key, 'reset', $reset);
+ $this->client->expire($key, $period);
+ } catch (\Throwable $e) {
+ throw new CreateRateRateLimitStorageException($e);
+ }
$rateLimitInfo = new RateLimitInfo();
$rateLimitInfo->setLimit($limit);
@@ -73,7 +96,11 @@ public function resetRate($key)
{
$key = $this->sanitizeRedisKey($key);
- $this->client->del($key);
+ try {
+ $this->client->del($key);
+ } catch (\Throwable $e) {
+ throw new ResetRateRateLimitStorageException($e);
+ }
return true;
}
diff --git a/Service/Storage/PsrCache.php b/Service/Storage/PsrCache.php
index 0d2b8a7..763ef93 100644
--- a/Service/Storage/PsrCache.php
+++ b/Service/Storage/PsrCache.php
@@ -2,8 +2,11 @@
namespace Noxlogic\RateLimitBundle\Service\Storage;
+use Noxlogic\RateLimitBundle\Exception\Storage\CreateRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\GetRateInfoRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\LimitRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\ResetRateRateLimitStorageException;
use Noxlogic\RateLimitBundle\Service\RateLimitInfo;
-use Noxlogic\RateLimitBundle\Service\Storage\StorageInterface;
use Psr\Cache\CacheItemPoolInterface;
class PsrCache implements StorageInterface
@@ -20,7 +23,12 @@ public function __construct(CacheItemPoolInterface $client)
public function getRateInfo($key)
{
- $item = $this->client->getItem($key);
+ try {
+ $item = $this->client->getItem($key);
+ } catch (\Throwable $e) {
+ throw new GetRateInfoRateLimitStorageException($e);
+ }
+
if (!$item->isHit()) {
return false;
}
@@ -30,7 +38,12 @@ public function getRateInfo($key)
public function limitRate($key)
{
- $item = $this->client->getItem($key);
+ try {
+ $item = $this->client->getItem($key);
+ } catch (\Throwable $e) {
+ throw new LimitRateRateLimitStorageException($e);
+ }
+
if (!$item->isHit()) {
return false;
}
@@ -41,7 +54,11 @@ public function limitRate($key)
$item->set($info);
$item->expiresAfter($info['reset'] - time());
- $this->client->save($item);
+ try {
+ $this->client->save($item);
+ } catch (\Throwable $e) {
+ throw new LimitRateRateLimitStorageException($e);
+ }
return $this->createRateInfo($info);
}
@@ -53,18 +70,32 @@ public function createRate($key, $limit, $period)
'calls' => 1,
'reset' => time() + $period,
];
- $item = $this->client->getItem($key);
+
+ try {
+ $item = $this->client->getItem($key);
+ } catch (\Throwable $e) {
+ throw new CreateRateRateLimitStorageException($e);
+ }
+
$item->set($info);
$item->expiresAfter($period);
- $this->client->save($item);
+ try {
+ $this->client->save($item);
+ } catch (\Throwable $e) {
+ throw new CreateRateRateLimitStorageException($e);
+ }
return $this->createRateInfo($info);
}
public function resetRate($key)
{
- $this->client->deleteItem($key);
+ try {
+ $this->client->deleteItem($key);
+ } catch (\Throwable $e) {
+ throw new ResetRateRateLimitStorageException($e);
+ }
return true;
}
diff --git a/Service/Storage/Redis.php b/Service/Storage/Redis.php
index d510f5b..26b7a1b 100644
--- a/Service/Storage/Redis.php
+++ b/Service/Storage/Redis.php
@@ -2,6 +2,10 @@
namespace Noxlogic\RateLimitBundle\Service\Storage;
+use Noxlogic\RateLimitBundle\Exception\Storage\CreateRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\GetRateInfoRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\LimitRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\ResetRateRateLimitStorageException;
use Noxlogic\RateLimitBundle\Service\RateLimitInfo;
use Predis\ClientInterface;
@@ -21,7 +25,12 @@ public function getRateInfo($key)
{
$key = $this->sanitizeRedisKey($key);
- $info = $this->client->hgetall($key);
+ try {
+ $info = $this->client->hgetall($key);
+ } catch (\Throwable $e) {
+ throw new GetRateInfoRateLimitStorageException($e);
+ }
+
if (!isset($info['limit']) || !isset($info['calls']) || !isset($info['reset'])) {
return false;
}
@@ -38,12 +47,23 @@ public function limitRate($key)
{
$key = $this->sanitizeRedisKey($key);
- $info = $this->getRateInfo($key);
+ try {
+ $info = $this->getRateInfo($key);
+ // We want to make sure we throw the exception with the proper class
+ } catch (GetRateInfoRateLimitStorageException $e) {
+ throw new LimitRateRateLimitStorageException($e->getPrevious());
+ }
+
if (!$info) {
return false;
}
- $calls = $this->client->hincrby($key, 'calls', 1);
+ try {
+ $calls = $this->client->hincrby($key, 'calls', 1);
+ } catch (\Throwable $e) {
+ throw new LimitRateRateLimitStorageException($e);
+ }
+
$info->setCalls($calls);
return $info;
@@ -55,10 +75,14 @@ public function createRate($key, $limit, $period)
$reset = time() + $period;
- $this->client->hset($key, 'limit', (string) $limit);
- $this->client->hset($key, 'calls', '1');
- $this->client->hset($key, 'reset', (string) $reset);
- $this->client->expire($key, $period);
+ try {
+ $this->client->hset($key, 'limit', (string) $limit);
+ $this->client->hset($key, 'calls', '1');
+ $this->client->hset($key, 'reset', (string) $reset);
+ $this->client->expire($key, $period);
+ } catch (\Throwable $e) {
+ throw new CreateRateRateLimitStorageException($e);
+ }
$rateLimitInfo = new RateLimitInfo();
$rateLimitInfo->setLimit($limit);
@@ -72,7 +96,11 @@ public function resetRate($key)
{
$key = $this->sanitizeRedisKey($key);
- $this->client->del($key);
+ try {
+ $this->client->del($key);
+ } catch (\Throwable $e) {
+ throw new ResetRateRateLimitStorageException($e);
+ }
return true;
}
diff --git a/Service/Storage/SimpleCache.php b/Service/Storage/SimpleCache.php
index 7821633..f443aa9 100644
--- a/Service/Storage/SimpleCache.php
+++ b/Service/Storage/SimpleCache.php
@@ -2,6 +2,10 @@
namespace Noxlogic\RateLimitBundle\Service\Storage;
+use Noxlogic\RateLimitBundle\Exception\Storage\CreateRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\GetRateInfoRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\LimitRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\ResetRateRateLimitStorageException;
use Noxlogic\RateLimitBundle\Service\RateLimitInfo;
use Psr\SimpleCache\CacheInterface;
@@ -19,7 +23,12 @@ public function __construct(CacheInterface $client)
public function getRateInfo($key)
{
- $info = $this->client->get($key);
+ try {
+ $info = $this->client->get($key);
+ } catch (\Throwable $e) {
+ throw new GetRateInfoRateLimitStorageException($e);
+ }
+
if ($info === null || !array_key_exists('limit', $info)) {
return false;
}
@@ -29,7 +38,12 @@ public function getRateInfo($key)
public function limitRate($key)
{
- $info = $this->client->get($key);
+ try {
+ $info = $this->client->get($key);
+ } catch (\Throwable $e) {
+ throw new LimitRateRateLimitStorageException($e);
+ }
+
if ($info === null || !array_key_exists('limit', $info)) {
return false;
}
@@ -37,7 +51,11 @@ public function limitRate($key)
$info['calls']++;
$ttl = $info['reset'] - time();
- $this->client->set($key, $info, $ttl);
+ try {
+ $this->client->set($key, $info, $ttl);
+ } catch (\Throwable $e) {
+ throw new LimitRateRateLimitStorageException($e);
+ }
return $this->createRateInfo($info);
}
@@ -49,19 +67,28 @@ public function createRate($key, $limit, $period)
'calls' => 1,
'reset' => time() + $period,
];
- $this->client->set($key, $info, $period);
+
+ try {
+ $this->client->set($key, $info, $period);
+ } catch (\Throwable $e) {
+ throw new CreateRateRateLimitStorageException($e);
+ }
return $this->createRateInfo($info);
}
public function resetRate($key)
{
- $this->client->delete($key);
+ try {
+ $this->client->delete($key);
+ } catch (\Throwable $e) {
+ throw new ResetRateRateLimitStorageException($e);
+ }
return true;
}
- private function createRateInfo(array $info)
+ private function createRateInfo(array $info): RateLimitInfo
{
$rateLimitInfo = new RateLimitInfo();
$rateLimitInfo->setLimit($info['limit']);
diff --git a/Service/Storage/StorageInterface.php b/Service/Storage/StorageInterface.php
index ec93424..571483c 100644
--- a/Service/Storage/StorageInterface.php
+++ b/Service/Storage/StorageInterface.php
@@ -2,6 +2,7 @@
namespace Noxlogic\RateLimitBundle\Service\Storage;
+use Noxlogic\RateLimitBundle\Exception\Storage\RateLimitStorageExceptionInterface;
use Noxlogic\RateLimitBundle\Service\RateLimitInfo;
interface StorageInterface
@@ -12,7 +13,9 @@ interface StorageInterface
* @param string $key
* @return RateLimitInfo|bool Rate limit information
* @todo: Replace return type with RateLimitInfo|false when PHP 8.2 is the minimum version
- */
+ *
+ * @throws RateLimitStorageExceptionInterface
+ */
public function getRateInfo($key);
/**
@@ -21,6 +24,8 @@ public function getRateInfo($key);
* @param string $key
* @return RateLimitInfo|bool Rate limit info
* @todo: Replace return type with RateLimitInfo|false when PHP 8.2 is the minimum version
+ *
+ * @throws RateLimitStorageExceptionInterface
*/
public function limitRate($key);
@@ -30,6 +35,8 @@ public function limitRate($key);
* @param string $key
* @param integer $limit
* @param integer $period
+ *
+ * @throws RateLimitStorageExceptionInterface
*/
public function createRate($key, $limit, $period);
@@ -37,6 +44,8 @@ public function createRate($key, $limit, $period);
* Reset the rating
*
* @param $key
+ *
+ * @throws RateLimitStorageExceptionInterface
*/
public function resetRate($key);
}
diff --git a/Tests/Attribute/RateLimitTest.php b/Tests/Attribute/RateLimitTest.php
index ddda5fd..3243f68 100644
--- a/Tests/Attribute/RateLimitTest.php
+++ b/Tests/Attribute/RateLimitTest.php
@@ -11,9 +11,10 @@ public function testConstruction(): void
{
$attribute = new RateLimit();
- $this->assertEquals(-1, $attribute->limit);
- $this->assertEmpty($attribute->methods);
- $this->assertEquals(3600, $attribute->period);
+ self::assertSame(-1, $attribute->limit);
+ self::assertEmpty($attribute->methods);
+ self::assertSame(3600, $attribute->period);
+ self::assertNull($attribute->failOpen);
}
public function testConstructionWithValues(): void
@@ -21,19 +22,23 @@ public function testConstructionWithValues(): void
$attribute = new RateLimit(
[],
1234,
- 1000
+ 1000,
+ failOpen: true
);
- $this->assertEquals(1234, $attribute->limit);
- $this->assertEquals(1000, $attribute->period);
+ self::assertSame(1234, $attribute->limit);
+ self::assertSame(1000, $attribute->period);
+ self::assertTrue($attribute->failOpen);
$attribute = new RateLimit(
['POST'],
1234,
- 1000
+ 1000,
+ failOpen: false
);
- $this->assertEquals(1234, $attribute->limit);
- $this->assertEquals(1000, $attribute->period);
- $this->assertEquals(['POST'], $attribute->methods);
+ self::assertSame(1234, $attribute->limit);
+ self::assertSame(1000, $attribute->period);
+ self::assertSame(['POST'], $attribute->methods);
+ self::assertFalse($attribute->failOpen);
}
public function testConstructionWithMethods(): void
diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php
index b881ce4..3577965 100644
--- a/Tests/DependencyInjection/ConfigurationTest.php
+++ b/Tests/DependencyInjection/ConfigurationTest.php
@@ -49,6 +49,7 @@ public function testUnconfiguredConfiguration(): void
'reset' => 'X-RateLimit-Reset',
),
'path_limits' => array(),
+ 'fail_open' => false,
'fos_oauth_key_listener' => true
), $configuration);
}
@@ -177,4 +178,36 @@ public function testMustBeBasedOnExceptionOrNull(): void
# no exception triggered is ok.
$this->expectNotToPerformAssertions();
}
+
+ public function testFailOpen(): void
+ {
+ $config = $this->getConfigs(['fail_open' => true]);
+
+ self::assertTrue($config['fail_open']);
+ }
+
+ public function testFailOpen_false(): void
+ {
+ $config = $this->getConfigs(['fail_open' => false]);
+
+ self::assertFalse($config['fail_open']);
+ }
+
+ public function testFailOpen_nullShouldBeTreatedAsFalse(): void
+ {
+ $config = $this->getConfigs(['fail_open' => null]);
+
+ self::assertFalse($config['fail_open']);
+ }
+
+ /**
+ * @testWith ["not-a-boolean"]
+ * [123]
+ */
+ public function testFailOpen_mustBeBool(mixed $value): void
+ {
+ $this->expectException(InvalidConfigurationException::class);
+
+ $this->getConfigs(['fail_open' => $value]);
+ }
}
diff --git a/Tests/DependencyInjection/NoxlogicRateLimitExtensionTest.php b/Tests/DependencyInjection/NoxlogicRateLimitExtensionTest.php
index c55e94e..71068dd 100644
--- a/Tests/DependencyInjection/NoxlogicRateLimitExtensionTest.php
+++ b/Tests/DependencyInjection/NoxlogicRateLimitExtensionTest.php
@@ -22,10 +22,11 @@ public function testAreParametersSet()
$containerBuilder = new ContainerBuilder(new ParameterBag());
$extension->load(array(), $containerBuilder);
- $this->assertEquals($containerBuilder->getParameter('noxlogic_rate_limit.enabled'), true);
- $this->assertEquals($containerBuilder->getParameter('noxlogic_rate_limit.rate_response_code'), 429);
- $this->assertEquals($containerBuilder->getParameter('noxlogic_rate_limit.display_headers'), true);
- $this->assertEquals($containerBuilder->getParameter('noxlogic_rate_limit.headers.reset.name'), 'X-RateLimit-Reset');
+ self::assertTrue($containerBuilder->getParameter('noxlogic_rate_limit.enabled'));
+ self::assertSame(429, $containerBuilder->getParameter('noxlogic_rate_limit.rate_response_code'));
+ self::assertTrue($containerBuilder->getParameter('noxlogic_rate_limit.display_headers'));
+ self::assertSame('X-RateLimit-Reset', $containerBuilder->getParameter('noxlogic_rate_limit.headers.reset.name'));
+ self::assertFalse($containerBuilder->getParameter('noxlogic_rate_limit.fail_open'));
}
public function testStorageEngineParameterProvider()
@@ -62,7 +63,7 @@ public function testStorageEngineParameterService()
$this->assertEquals('my.redis_cache', (string)($storageDef->getArgument(0)));
}
- public function testParametersWhenDisabled()
+ public function testParametersWhenDisabled(): void
{
$extension = new NoxlogicRateLimitExtension();
$containerBuilder = new ContainerBuilder(new ParameterBag());
@@ -84,8 +85,17 @@ public function testPathLimitsParameter()
$extension = new NoxlogicRateLimitExtension();
$containerBuilder = new ContainerBuilder(new ParameterBag());
- $extension->load(array(array('path_limits' => $pathLimits)), $containerBuilder);
+ $extension->load([['path_limits' => $pathLimits]], $containerBuilder);
- $this->assertEquals($containerBuilder->getParameter('noxlogic_rate_limit.path_limits'), $pathLimits);
+ self::assertSame($pathLimits, $containerBuilder->getParameter('noxlogic_rate_limit.path_limits'));
+ }
+
+ public function testFailOpenParameter(): void
+ {
+ $extension = new NoxlogicRateLimitExtension();
+ $containerBuilder = new ContainerBuilder(new ParameterBag());
+ $extension->load([['fail_open' => true]], $containerBuilder);
+
+ self::assertTrue($containerBuilder->getParameter('noxlogic_rate_limit.fail_open'));
}
}
diff --git a/Tests/EventListener/MockController.php b/Tests/EventListener/MockController.php
index c906b05..c5e7d92 100644
--- a/Tests/EventListener/MockController.php
+++ b/Tests/EventListener/MockController.php
@@ -3,5 +3,7 @@
namespace Noxlogic\RateLimitBundle\Tests\EventListener;
class MockController {
- function mockAction() { }
+ public const RATE_LIMIT_KEY = 'Noxlogic.RateLimitBundle.Tests.EventListener.MockController.mockAction';
+
+ public function mockAction(): void { }
}
\ No newline at end of file
diff --git a/Tests/EventListener/MockControllerWithAttributes.php b/Tests/EventListener/MockControllerWithAttributes.php
index 5f455db..31c2a5b 100644
--- a/Tests/EventListener/MockControllerWithAttributes.php
+++ b/Tests/EventListener/MockControllerWithAttributes.php
@@ -8,5 +8,11 @@
class MockControllerWithAttributes extends MockController {
#[RateLimit(limit: 10, period: 100)]
- function mockAction() { }
+ public function mockAction(): void { }
+
+ #[RateLimit(limit: 10, period: 100, failOpen: true)]
+ public function failOpenMockAction(): void { }
+
+ #[RateLimit(limit: 10, period: 100, failOpen: false)]
+ public function doNotFailOpenMockAction(): void { }
}
\ No newline at end of file
diff --git a/Tests/EventListener/MockStorage.php b/Tests/EventListener/MockStorage.php
index 56d8a92..4472a36 100644
--- a/Tests/EventListener/MockStorage.php
+++ b/Tests/EventListener/MockStorage.php
@@ -2,6 +2,7 @@
namespace Noxlogic\RateLimitBundle\Tests\EventListener;
+use Noxlogic\RateLimitBundle\Exception\Storage\RateLimitStorageExceptionInterface;
use Noxlogic\RateLimitBundle\Service\RateLimitInfo;
use Noxlogic\RateLimitBundle\Service\Storage\StorageInterface;
@@ -19,6 +20,10 @@ public function getRateInfo($key)
{
$info = $this->rates[$key];
+ if ($info instanceof RateLimitStorageExceptionInterface) {
+ throw $info;
+ }
+
$rateLimitInfo = new RateLimitInfo();
$rateLimitInfo->setCalls($info['calls']);
$rateLimitInfo->setResetTimestamp($info['reset']);
@@ -38,6 +43,10 @@ public function limitRate($key)
return null;
}
+ if ($this->rates[$key] instanceof RateLimitStorageExceptionInterface) {
+ throw $this->rates[$key];
+ }
+
$this->rates[$key]['calls']++;
return $this->getRateInfo($key);
}
@@ -66,9 +75,19 @@ public function resetRate($key)
unset($this->rates[$key]);
}
- public function createMockRate($key, $limit, $period, $calls)
+ public function resetAll(): void
+ {
+ $this->rates = [];
+ }
+
+ public function createMockRate($key, $limit, $period, $calls): RateLimitInfo
{
$this->rates[$key] = array('calls' => $calls, 'limit' => $limit, 'reset' => (time() + $period));
return $this->getRateInfo($key);
}
+
+ public function createStorageErrorMockRate($key, RateLimitStorageExceptionInterface $exception): void
+ {
+ $this->rates[$key] = $exception;
+ }
}
diff --git a/Tests/EventListener/RateLimitAnnotationListenerTest.php b/Tests/EventListener/RateLimitAnnotationListenerTest.php
index c4585a6..233504f 100644
--- a/Tests/EventListener/RateLimitAnnotationListenerTest.php
+++ b/Tests/EventListener/RateLimitAnnotationListenerTest.php
@@ -5,34 +5,40 @@
use Noxlogic\RateLimitBundle\Attribute\RateLimit;
use Noxlogic\RateLimitBundle\EventListener\RateLimitAnnotationListener;
use Noxlogic\RateLimitBundle\Events\RateLimitEvents;
+use Noxlogic\RateLimitBundle\Exception\Storage\CreateRateRateLimitStorageException;
use Noxlogic\RateLimitBundle\Service\RateLimitService;
use Noxlogic\RateLimitBundle\Tests\TestCase;
+use PHPUnit\Framework\MockObject\MockObject;
use ReflectionMethod;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Contracts\EventDispatcher\Event;
+use Noxlogic\RateLimitBundle\Util\PathLimitProcessor;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
+use Noxlogic\RateLimitBundle\Tests\Exception\TestException;
class RateLimitAnnotationListenerTest extends TestCase
{
- /**
- * @var MockStorage
- */
- protected $mockStorage;
+ protected MockStorage $mockStorage;
- /** @var \PHPUnit\Framework\MockObject\MockObject */
- protected $mockPathLimitProcessor;
+ protected MockObject $mockPathLimitProcessor;
protected function setUp(): void
{
$this->mockStorage = new MockStorage();
- $this->mockPathLimitProcessor = $this->getMockBuilder('Noxlogic\RateLimitBundle\Util\PathLimitProcessor')
+ $this->mockPathLimitProcessor = $this->getMockBuilder(PathLimitProcessor::class)
->disableOriginalConstructor()
->getMock();
}
- protected function getMockStorage()
+ protected function tearDown(): void
+ {
+ $this->mockStorage->resetAll();
+ }
+
+ protected function getMockStorage(): MockStorage
{
return $this->mockStorage;
}
@@ -40,7 +46,7 @@ protected function getMockStorage()
public function testReturnedWhenNotEnabled(): void
{
- $listener = $this->createListener($this->never());
+ $listener = $this->createListener(self::never());
$listener->setParameter('enabled', false);
$event = $this->createEvent();
@@ -50,7 +56,7 @@ public function testReturnedWhenNotEnabled(): void
public function testReturnedWhenNotAMasterRequest(): void
{
- $listener = $this->createListener($this->never());
+ $listener = $this->createListener(self::never());
$event = $this->createEvent(HttpKernelInterface::SUB_REQUEST);
$listener->onKernelController($event);
@@ -59,9 +65,9 @@ public function testReturnedWhenNotAMasterRequest(): void
public function testReturnedWhenNoControllerFound(): void
{
- $listener = $this->createListener($this->once());
+ $listener = $this->createListener(self::once());
- $kernel = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\HttpKernelInterface')->getMock();
+ $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$request = new Request();
$event = new ControllerEvent($kernel, static function() {}, $request, HttpKernelInterface::MAIN_REQUEST);
@@ -72,7 +78,7 @@ public function testReturnedWhenNoControllerFound(): void
public function testReturnedWhenNoAttributesFound(): void
{
- $listener = $this->createListener($this->once());
+ $listener = $this->createListener(self::once());
$event = $this->createEvent();
$listener->onKernelController($event);
@@ -83,9 +89,9 @@ public function testDelegatesToPathLimitProcessorWhenNoAttributesFound(): void
$request = new Request();
$event = $this->createEvent(HttpKernelInterface::MAIN_REQUEST, $request);
- $listener = $this->createListener($this->once());
+ $listener = $this->createListener(self::once());
- $this->mockPathLimitProcessor->expects($this->once())
+ $this->mockPathLimitProcessor->expects(self::once())
->method('getRateLimit')
->with($request);
@@ -94,7 +100,7 @@ public function testDelegatesToPathLimitProcessorWhenNoAttributesFound(): void
public function testDispatchIsCalled(): void
{
- $listener = $this->createListener($this->exactly(2));
+ $listener = $this->createListener(self::exactly(2));
$event = $this->createEvent();
$event->getRequest()->attributes->set('_x-rate-limit', array(
@@ -106,7 +112,7 @@ public function testDispatchIsCalled(): void
public function testDispatchIsCalledWithAttributes(): void
{
- $listener = $this->createListener($this->exactly(2));
+ $listener = $this->createListener(self::exactly(2));
$event = $this->createEvent(
HttpKernelInterface::MAIN_REQUEST,
@@ -121,7 +127,7 @@ public function testDispatchIsCalledIfThePathLimitProcessorReturnsARateLimit():
{
$event = $this->createEvent(HttpKernelInterface::MAIN_REQUEST);
- $listener = $this->createListener($this->exactly(2));
+ $listener = $this->createListener(self::exactly(2));
$rateLimit = new RateLimit(
[],
100,
@@ -129,7 +135,6 @@ public function testDispatchIsCalledIfThePathLimitProcessorReturnsARateLimit():
);
$this->mockPathLimitProcessor
- ->expects($this->any())
->method('getRateLimit')
->willReturn($rateLimit);
@@ -138,7 +143,7 @@ public function testDispatchIsCalledIfThePathLimitProcessorReturnsARateLimit():
public function testIsRateLimitSetInRequest(): void
{
- $listener = $this->createListener($this->any());
+ $listener = $this->createListener(self::any());
$event = $this->createEvent();
$event->getRequest()->attributes->set('_x-rate-limit', array(
@@ -146,20 +151,20 @@ public function testIsRateLimitSetInRequest(): void
));
- $this->assertNull($event->getRequest()->attributes->get('rate_limit_info'));
+ self::assertNull($event->getRequest()->attributes->get('rate_limit_info'));
// Create initial ratelimit in storage
$listener->onKernelController($event);
- $this->assertArrayHasKey('rate_limit_info', $event->getRequest()->attributes->all());
+ self::assertArrayHasKey('rate_limit_info', $event->getRequest()->attributes->all());
// Add second ratelimit in storage
$listener->onKernelController($event);
- $this->assertArrayHasKey('rate_limit_info', $event->getRequest()->attributes->all());
+ self::assertArrayHasKey('rate_limit_info', $event->getRequest()->attributes->all());
}
public function testRateLimit(): void
{
- $listener = $this->createListener($this->any());
+ $listener = $this->createListener(self::any());
$event = $this->createEvent();
$event->getRequest()->attributes->set('_x-rate-limit', array(
@@ -186,23 +191,24 @@ public function testRateLimit(): void
public function testRateLimitThrottling(): void
{
- $listener = $this->createListener($this->any());
+ $listener = $this->createListener(self::any());
$event = $this->createEvent();
- $event->getRequest()->attributes->set('_x-rate-limit', array(
+ $event->getRequest()->attributes->set('_x-rate-limit', [
new RateLimit([], 5, 3),
- ));
+ ]);
// Throttled
$storage = $this->getMockStorage();
- $storage->createMockRate('Noxlogic.RateLimitBundle.Tests.EventListener.MockController.mockAction', 5, 10, 6);
+ $storage->createMockRate(MockController::RATE_LIMIT_KEY, 5, 10, 6);
$listener->onKernelController($event);
+
self::assertIsNotArray($event->getController());
}
public function testRateLimitExpiring(): void
{
- $listener = $this->createListener($this->any());
+ $listener = $this->createListener(self::any());
$event = $this->createEvent();
$event->getRequest()->attributes->set('_x-rate-limit', array(
@@ -211,14 +217,14 @@ public function testRateLimitExpiring(): void
// Expired
$storage = $this->getMockStorage();
- $storage->createMockRate('Noxlogic.RateLimitBundle.Tests.EventListener.MockController.mockAction', 5, -10, 12);
+ $storage->createMockRate(MockController::RATE_LIMIT_KEY, 5, -10, 12);
$listener->onKernelController($event);
self::assertIsArray($event->getController());
}
public function testBestMethodMatch(): void
{
- $listener = $this->createListener($this->any());
+ $listener = $this->createListener(self::any());
$method = new ReflectionMethod(get_class($listener), 'findBestMethodMatch');
$method->setAccessible(true);
@@ -232,30 +238,29 @@ public function testBestMethodMatch(): void
// Find the method that matches the string
$request->setMethod('GET');
- $this->assertEquals(
+ self::assertSame(
$annotations[1],
$method->invoke($listener, $request, $annotations)
);
// Method not found, use the default one
$request->setMethod('DELETE');
- $this->assertEquals(
+ self::assertSame(
$annotations[0],
$method->invoke($listener, $request, $annotations)
);
// Find best match based in methods in array
$request->setMethod('PUT');
- $this->assertEquals(
+ self::assertSame(
$annotations[2],
$method->invoke($listener, $request, $annotations)
);
}
-
public function testFindNoAttributes(): void
{
- $listener = $this->createListener($this->any());
+ $listener = $this->createListener(self::any());
$method = new ReflectionMethod(get_class($listener), 'findBestMethodMatch');
$method->setAccessible(true);
@@ -264,16 +269,15 @@ public function testFindNoAttributes(): void
$annotations = array();
$request->setMethod('PUT');
- $this->assertNull($method->invoke($listener, $request, $annotations));
+ self::assertNull($method->invoke($listener, $request, $annotations));
$request->setMethod('GET');
- $this->assertNull($method->invoke($listener, $request, $annotations));
+ self::assertNull($method->invoke($listener, $request, $annotations));
}
-
public function testFindBestMethodMatchNotMatchingAnnotations(): void
{
- $listener = $this->createListener($this->any());
+ $listener = $this->createListener(self::any());
$method = new ReflectionMethod(get_class($listener), 'findBestMethodMatch');
$method->setAccessible(true);
@@ -284,10 +288,10 @@ public function testFindBestMethodMatchNotMatchingAnnotations(): void
);
$request->setMethod('PUT');
- $this->assertNull($method->invoke($listener, $request, $annotations));
+ self::assertNull($method->invoke($listener, $request, $annotations));
$request->setMethod('GET');
- $this->assertEquals(
+ self::assertSame(
$annotations[0],
$method->invoke($listener, $request, $annotations)
);
@@ -296,7 +300,7 @@ public function testFindBestMethodMatchNotMatchingAnnotations(): void
public function testFindBestMethodMatchMatchingMultipleAnnotations(): void
{
- $listener = $this->createListener($this->any());
+ $listener = $this->createListener(self::any());
$method = new ReflectionMethod(get_class($listener), 'findBestMethodMatch');
$method->setAccessible(true);
@@ -308,34 +312,33 @@ public function testFindBestMethodMatchMatchingMultipleAnnotations(): void
);
$request->setMethod('PUT');
- $this->assertEquals($annotations[1], $method->invoke($listener, $request, $annotations));
+ self::assertSame($annotations[1], $method->invoke($listener, $request, $annotations));
$request->setMethod('GET');
- $this->assertEquals($annotations[1], $method->invoke($listener, $request, $annotations));
+ self::assertSame($annotations[1], $method->invoke($listener, $request, $annotations));
}
- protected function createEvent(
+ private function createEvent(
int $requestType = HttpKernelInterface::MAIN_REQUEST,
?Request $request = null,
?MockController $controller = null,
+ string $controllerAction = 'mockAction'
): ControllerEvent
{
- $kernel = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\HttpKernelInterface')->getMock();
+ $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$controller = $controller ?? new MockController();
- $action = 'mockAction';
$request = $request ?? new Request();
- return new ControllerEvent($kernel, array($controller, $action), $request, $requestType);
+ return new ControllerEvent($kernel, [$controller, $controllerAction], $request, $requestType);
}
-
- protected function createListener($expects): RateLimitAnnotationListener
+ private function createListener($eventDispatcherExpects): RateLimitAnnotationListener
{
- $mockDispatcher = $this->getMockBuilder('Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface')->getMock();
+ $mockDispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMock();
$mockDispatcher
- ->expects($expects)
+ ->expects($eventDispatcherExpects)
->method('dispatch');
$rateLimitService = new RateLimitService();
@@ -357,19 +360,19 @@ public function testRateLimitKeyGenerationEventHasPayload(): void
));
$generated = false;
- $mockDispatcher = $this->getMockBuilder('Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface')->getMock();
+ $mockDispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMock();
$generatedCallback = function ($name, $event) use ($request, &$generated) {
if ($name !== RateLimitEvents::GENERATE_KEY) {
return;
}
$generated = true;
- $this->assertSame(RateLimitEvents::GENERATE_KEY, $name);
- $this->assertSame($request, $event->getRequest());
- $this->assertSame(['foo'], $event->getPayload());
- $this->assertSame('Noxlogic.RateLimitBundle.Tests.EventListener.MockController.mockAction', $event->getKey());
+ self::assertSame(RateLimitEvents::GENERATE_KEY, $name);
+ self::assertSame($request, $event->getRequest());
+ self::assertSame(['foo'], $event->getPayload());
+ self::assertSame(MockController::RATE_LIMIT_KEY, $event->getKey());
};
$mockDispatcher
- ->expects($this->any())
+ ->expects(self::any())
->method('dispatch')
->willReturnCallback(function ($arg1, $arg2) use ($generatedCallback) {
if ($arg1 instanceof Event) {
@@ -384,19 +387,19 @@ public function testRateLimitKeyGenerationEventHasPayload(): void
$storage = $this->getMockStorage();
$storage->createMockRate('test-key', 5, 10, 1);
- $rateLimitService = $this->getMockBuilder('Noxlogic\RateLimitBundle\Service\RateLimitService')
+ $rateLimitService = $this->getMockBuilder(RateLimitService::class)
->getMock();
$listener = new RateLimitAnnotationListener($mockDispatcher, $rateLimitService, $this->mockPathLimitProcessor);
$listener->onKernelController($event);
- $this->assertTrue($generated, 'Generate key event not dispatched');
+ self::assertTrue($generated, 'Generate key event not dispatched');
}
public function testRateLimitThrottlingWithExceptionAndPayload(): void
{
- $listener = $this->createListener($this->any());
- $listener->setParameter('rate_response_exception', 'Noxlogic\RateLimitBundle\Tests\Exception\TestException');
+ $listener = $this->createListener(self::any());
+ $listener->setParameter('rate_response_exception', TestException::class);
$listener->setParameter('rate_response_code', 123);
$listener->setParameter('rate_response_message', 'a message');
@@ -407,27 +410,118 @@ public function testRateLimitThrottlingWithExceptionAndPayload(): void
// Throttled
$storage = $this->getMockStorage();
- $storage->createMockRate('Noxlogic.RateLimitBundle.Tests.EventListener.MockController.mockAction', 5, 10, 6);
+ $storage->createMockRate(MockController::RATE_LIMIT_KEY, 5, 10, 6);
try {
$listener->onKernelController($event);
- $this->assertFalse(true, 'Exception not being thrown');
+ self::fail('Exception not being thrown');
} catch (\Exception $e) {
- $this->assertInstanceOf('Noxlogic\RateLimitBundle\Tests\Exception\TestException', $e);
- $this->assertSame(123, $e->getCode());
- $this->assertSame('a message', $e->getMessage());
- $this->assertSame(['foo'], $e->payload);
+ self::assertInstanceOf(TestException::class, $e);
+ self::assertSame(123, $e->getCode());
+ self::assertSame('a message', $e->getMessage());
+ self::assertSame(['foo'], $e->payload);
}
}
+ public function testRateLimitThrottling_failOpenFalse_exceptionShouldHappenOnStorageError(): void
+ {
+ $listener = $this->createListener(self::any());
+ $listener->setParameter('rate_response_exception', TestException::class);
+ $listener->setParameter('fail_open', false);
+
+ $event = $this->createEvent();
+ $event->getRequest()->attributes->set('_x-rate-limit', [
+ new RateLimit([], 5, 3),
+ ]);
+
+ // Throttled
+ $this->getMockStorage()->createStorageErrorMockRate(
+ MockController::RATE_LIMIT_KEY,
+ new CreateRateRateLimitStorageException(new \Exception('A storage error happened'))
+ );
+
+ $this->expectException(CreateRateRateLimitStorageException::class);
+
+ $listener->onKernelController($event);
+ }
+
+ public function testRateLimitThrottling_failOpenTrueViaConfig_noExceptionShouldHappenOnStorageError(): void
+ {
+ $listener = $this->createListener(self::any());
+ $listener->setParameter('rate_response_exception', TestException::class);
+ $listener->setParameter('fail_open', true);
+
+ $event = $this->createEvent();
+ $event->getRequest()->attributes->set('_x-rate-limit', [
+ new RateLimit([], 5, 3),
+ ]);
+
+ // Throttled
+ $this->getMockStorage()->createStorageErrorMockRate(
+ MockController::RATE_LIMIT_KEY,
+ new CreateRateRateLimitStorageException(new \Exception('A storage error happened'))
+ );
+
+ $listener->onKernelController($event);
+
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testRateLimitThrottling_failOpenTrueViaAttribute_noExceptionShouldHappenOnStorageError(): void
+ {
+ $listener = $this->createListener(self::any());
+ $listener->setParameter('rate_response_exception', TestException::class);
+ // Fail open is configured to be disabled globally, but the attribute overrides it
+ $listener->setParameter('fail_open', false);
+
+ $event = $this->createEvent(
+ controller: new MockControllerWithAttributes(),
+ controllerAction: 'failOpenMockAction'
+ );
+
+ // Throttled
+ $this->getMockStorage()->createStorageErrorMockRate(
+ MockController::RATE_LIMIT_KEY,
+ new CreateRateRateLimitStorageException(new \Exception('A storage error happened'))
+ );
+
+ $listener->onKernelController($event);
+
+ $this->expectNotToPerformAssertions();
+ }
+
+ public function testRateLimitThrottling_failOpenFalseViaAttribute_noExceptionShouldHappenOnStorageError(): void
+ {
+ $listener = $this->createListener(self::any());
+ $listener->setParameter('rate_response_exception', TestException::class);
+ // Fail open is configured to be enabled globally, but the attribute overrides it
+ $listener->setParameter('fail_open', true);
+
+ $event = $this->createEvent(
+ controller: new MockControllerWithAttributes(),
+ controllerAction: 'doNotFailOpenMockAction'
+ );
+
+ // Throttled
+ $this->getMockStorage()->createStorageErrorMockRate(
+ MockController::RATE_LIMIT_KEY,
+ new CreateRateRateLimitStorageException(new \Exception('A storage error happened'))
+ );
+
+ $listener->onKernelController($event);
+
+ $this->expectNotToPerformAssertions();
+ }
+
public function testRateLimitThrottlingWithException(): void
{
$this->expectException(\BadFunctionCallException::class);
$this->expectExceptionCode(123);
$this->expectExceptionMessage('a message');
- $listener = $this->createListener($this->any());
- $listener->setParameter('rate_response_exception', '\BadFunctionCallException');
+
+ $listener = $this->createListener(self::any());
+ $listener->setParameter('rate_response_exception', \BadFunctionCallException::class);
$listener->setParameter('rate_response_code', 123);
$listener->setParameter('rate_response_message', 'a message');
@@ -438,13 +532,13 @@ public function testRateLimitThrottlingWithException(): void
// Throttled
$storage = $this->getMockStorage();
- $storage->createMockRate('Noxlogic.RateLimitBundle.Tests.EventListener.MockController.mockAction', 5, 10, 6);
+ $storage->createMockRate(MockController::RATE_LIMIT_KEY, 5, 10, 6);
$listener->onKernelController($event);
}
public function testRateLimitThrottlingWithMessages(): void
{
- $listener = $this->createListener($this->any());
+ $listener = $this->createListener(self::any());
$listener->setParameter('rate_response_code', 123);
$listener->setParameter('rate_response_message', 'a message');
@@ -455,7 +549,7 @@ public function testRateLimitThrottlingWithMessages(): void
// Throttled
$storage = $this->getMockStorage();
- $storage->createMockRate('Noxlogic.RateLimitBundle.Tests.EventListener.MockController.mockAction', 5, 10, 6);
+ $storage->createMockRate(MockController::RATE_LIMIT_KEY, 5, 10, 6);
/** @var Response $response */
$listener->onKernelController($event);
@@ -464,7 +558,7 @@ public function testRateLimitThrottlingWithMessages(): void
$a = $event->getController();
$response = $a();
- $this->assertEquals($response->getStatusCode(), 123);
- $this->assertEquals($response->getContent(), "a message");
+ self::assertSame(123, $response->getStatusCode());
+ self::assertSame("a message", $response->getContent());
}
}
diff --git a/Tests/Service/Storage/DoctrineCacheTest.php b/Tests/Service/Storage/DoctrineCacheTest.php
index d326c91..f0f2199 100644
--- a/Tests/Service/Storage/DoctrineCacheTest.php
+++ b/Tests/Service/Storage/DoctrineCacheTest.php
@@ -1,10 +1,15 @@
assertEquals(1234, $rli->getResetTimestamp());
}
- public function testCreateRate()
+ public function testGetRateInfo_exception()
{
- $client = $this->getMockBuilder('Doctrine\\Common\\Cache\\Cache')
+ $client = $this->getMockBuilder(Cache::class)
+ ->getMock();
+ $client->expects($this->once())
+ ->method('fetch')
+ ->with('foo')
+ ->willThrowException(new \Exception('Storage error'));
+
+ $storage = new DoctrineCache($client);
+
+ $this->expectException(GetRateInfoRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to get rate limit info: Storage error');
+
+ $storage->getRateInfo('foo');
+ }
+
+ public function testCreateRate(): void
+ {
+ $client = $this->getMockBuilder(Cache::class)
->getMock();
$client->expects($this->once())
->method('save');
@@ -37,9 +59,25 @@ public function testCreateRate()
}
- public function testLimitRateNoKey()
+ public function testCreateRate_exception()
{
- $client = $this->getMockBuilder('Doctrine\\Common\\Cache\\Cache')
+ $client = $this->getMockBuilder(Cache::class)
+ ->getMock();
+ $client->expects($this->once())
+ ->method('save')
+ ->willThrowException(new \Exception('Storage error'));
+
+ $storage = new DoctrineCache($client);
+
+ $this->expectException(CreateRateRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to create rate limit: Storage error');
+
+ $storage->createRate('foo', 100, 123);
+ }
+
+ public function testLimitRateNoKey(): void
+ {
+ $client = $this->getMockBuilder(Cache::class)
->getMock();
$client->expects($this->once())
->method('fetch')
@@ -72,9 +110,26 @@ public function testLimitRateWithKey()
- public function testResetRate()
+ public function testLimitRate_exception()
{
- $client = $this->getMockBuilder('Doctrine\\Common\\Cache\\Cache')
+ $client = $this->getMockBuilder(Cache::class)
+ ->getMock();
+ $client->expects($this->once())
+ ->method('fetch')
+ ->with('foo')
+ ->willThrowException(new \Exception('Storage error'));
+
+ $storage = new DoctrineCache($client);
+
+ $this->expectException(LimitRateRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to apply rate limit: Storage error');
+
+ $storage->limitRate('foo');
+ }
+
+ public function testResetRate(): void
+ {
+ $client = $this->getMockBuilder(Cache::class)
->getMock();
$client->expects($this->once())
->method('delete')
@@ -83,4 +138,21 @@ public function testResetRate()
$storage = new DoctrineCache($client);
$this->assertTrue($storage->resetRate('foo'));
}
+
+ public function testResetRate_exception(): void
+ {
+ $client = $this->getMockBuilder(Cache::class)
+ ->getMock();
+ $client->expects($this->once())
+ ->method('delete')
+ ->with('foo')
+ ->willThrowException(new \Exception('Storage error'));
+
+ $storage = new DoctrineCache($client);
+
+ $this->expectException(ResetRateRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to reset rate: Storage error');
+
+ $storage->resetRate('foo');
+ }
}
diff --git a/Tests/Service/Storage/PsrCacheTest.php b/Tests/Service/Storage/PsrCacheTest.php
index b97d33b..bc2120d 100644
--- a/Tests/Service/Storage/PsrCacheTest.php
+++ b/Tests/Service/Storage/PsrCacheTest.php
@@ -1,15 +1,25 @@
getMockBuilder('Psr\\Cache\\CacheItemInterface')
+ $item = $this->getMockBuilder(CacheItemInterface::class)
->getMock();
$item->expects($this->once())
->method('isHit')
@@ -18,31 +28,50 @@ public function testGetRateInfo()
->method('get')
->willReturn(array('limit' => 100, 'calls' => 50, 'reset' => 1234));
- $client = $this->getMockBuilder('Psr\\Cache\\CacheItemPoolInterface')
+ $client = $this->getMockBuilder(CacheItemPoolInterface::class)
->getMock();
$client->expects($this->once())
->method('getItem')
->with('foo')
- ->will($this->returnValue($item));
+ ->willReturn($item);
$storage = new PsrCache($client);
+
$rli = $storage->getRateInfo('foo');
- $this->assertInstanceOf('Noxlogic\\RateLimitBundle\\Service\\RateLimitInfo', $rli);
+
+ $this->assertInstanceOf(RateLimitInfo::class, $rli);
$this->assertEquals(100, $rli->getLimit());
$this->assertEquals(50, $rli->getCalls());
$this->assertEquals(1234, $rli->getResetTimestamp());
}
- public function testCreateRate()
+ public function testGetRateInfo_exception(): void
+ {
+ $client = $this->getMockBuilder(CacheItemPoolInterface::class)
+ ->getMock();
+ $client->expects($this->once())
+ ->method('getItem')
+ ->with('foo')
+ ->willThrowException(new \Exception('Storage error'));
+
+ $storage = new PsrCache($client);
+
+ $this->expectException(GetRateInfoRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to get rate limit info: Storage error');
+
+ $storage->getRateInfo('foo');
+ }
+
+ public function testCreateRate(): void
{
- $item = $this->getMockBuilder('Psr\\Cache\\CacheItemInterface')
+ $item = $this->getMockBuilder(CacheItemInterface::class)
->getMock();
/**
* psr/cache 3.0 changed the return type of set() and expiresAfter() to return self.
* @TODO NEXT_MAJOR: Remove this check and the first conditional block when psr/cache <3 support is dropped.
*/
- $psrCacheVersion = \Composer\InstalledVersions::getVersion('psr/cache');
+ $psrCacheVersion = InstalledVersions::getVersion('psr/cache');
if (version_compare($psrCacheVersion, '3.0', '<')) {
$item->expects($this->once())
->method('set')
@@ -59,12 +88,12 @@ public function testCreateRate()
->willReturnSelf();
}
- $client = $this->getMockBuilder('Psr\\Cache\\CacheItemPoolInterface')
+ $client = $this->getMockBuilder(CacheItemPoolInterface::class)
->getMock();
$client->expects($this->once())
->method('getItem')
->with('foo')
- ->will($this->returnValue($item));
+ ->willReturn($item);
$client->expects($this->once())
->method('save')
->with($item)
@@ -74,29 +103,62 @@ public function testCreateRate()
$storage->createRate('foo', 100, 123);
}
+ public function testCreateRate_exception(): void
+ {
+ $client = $this->getMockBuilder(CacheItemPoolInterface::class)
+ ->getMock();
+ $client->expects($this->once())
+ ->method('getItem')
+ ->with('foo')
+ ->willThrowException(new \Exception('Storage error'));
+
+ $storage = new PsrCache($client);
+
+ $this->expectException(CreateRateRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to create rate limit: Storage error');
+
+ $storage->createRate('foo', 100, 123);
+ }
- public function testLimitRateNoKey()
+ public function testLimitRateNoKey(): void
{
- $item = $this->getMockBuilder('Psr\\Cache\\CacheItemInterface')
+ $item = $this->getMockBuilder(CacheItemInterface::class)
->getMock();
$item->expects($this->once())
->method('isHit')
->willReturn(false);
- $client = $this->getMockBuilder('Psr\\Cache\\CacheItemPoolInterface')
+ $client = $this->getMockBuilder(CacheItemPoolInterface::class)
->getMock();
$client->expects($this->once())
->method('getItem')
->with('foo')
- ->will($this->returnValue($item));
+ ->willReturn($item);
$storage = new PsrCache($client);
$this->assertFalse($storage->limitRate('foo'));
}
- public function testLimitRateWithKey()
+ public function testLimitRate_exception(): void
{
- $item = $this->getMockBuilder('Psr\\Cache\\CacheItemInterface')
+ $client = $this->getMockBuilder(CacheItemPoolInterface::class)
+ ->getMock();
+ $client->expects($this->once())
+ ->method('getItem')
+ ->with('foo')
+ ->willThrowException(new \Exception('Storage error'));
+
+ $storage = new PsrCache($client);
+
+ $this->expectException(LimitRateRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to apply rate limit: Storage error');
+
+ $storage->limitRate('foo');
+ }
+
+ public function testLimitRateWithKey(): void
+ {
+ $item = $this->getMockBuilder(CacheItemInterface::class)
->getMock();
$item->expects($this->once())
->method('isHit')
@@ -109,12 +171,12 @@ public function testLimitRateWithKey()
$item->expects($this->once())
->method('expiresAfter');
- $client = $this->getMockBuilder('Psr\\Cache\\CacheItemPoolInterface')
+ $client = $this->getMockBuilder(CacheItemPoolInterface::class)
->getMock();
$client->expects($this->once())
->method('getItem')
->with('foo')
- ->will($this->returnValue($item));
+ ->willReturn($item);
$client->expects($this->once())
->method('save')
->with($item)
@@ -124,9 +186,9 @@ public function testLimitRateWithKey()
$storage->limitRate('foo');
}
- public function testResetRate()
+ public function testResetRate(): void
{
- $client = $this->getMockBuilder('Psr\\Cache\\CacheItemPoolInterface')
+ $client = $this->getMockBuilder(CacheItemPoolInterface::class)
->getMock();
$client->expects($this->once())
->method('deleteItem')
@@ -136,4 +198,21 @@ public function testResetRate()
$storage = new PsrCache($client);
$this->assertTrue($storage->resetRate('foo'));
}
+
+ public function testResetRate_exception(): void
+ {
+ $client = $this->getMockBuilder(CacheItemPoolInterface::class)
+ ->getMock();
+ $client->expects($this->once())
+ ->method('deleteItem')
+ ->with('foo')
+ ->willThrowException(new \Exception('Storage error'));
+
+ $storage = new PsrCache($client);
+
+ $this->expectException(ResetRateRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to reset rate: Storage error');
+
+ $storage->resetRate('foo');
+ }
}
diff --git a/Tests/Service/Storage/RedisTest.php b/Tests/Service/Storage/RedisTest.php
index acbdb9a..1fd7fb0 100644
--- a/Tests/Service/Storage/RedisTest.php
+++ b/Tests/Service/Storage/RedisTest.php
@@ -2,8 +2,13 @@
namespace Noxlogic\RateLimitBundle\Tests\Service\Storage;
+use Noxlogic\RateLimitBundle\Exception\Storage\CreateRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\GetRateInfoRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\LimitRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\ResetRateRateLimitStorageException;
use Noxlogic\RateLimitBundle\Service\Storage\Redis;
use Noxlogic\RateLimitBundle\Tests\TestCase;
+use Predis\Client;
class RedisTest extends TestCase
{
@@ -25,10 +30,28 @@ public function testgetRateInfo()
$this->assertEquals(1234, $rli->getResetTimestamp());
}
- public function testcreateRate()
+ public function testGetRateInfo_exception()
{
- $client = $this->getMockBuilder('Predis\\Client')
- ->setMethods(array('hset', 'expire', 'hgetall'))
+ $client = $this->getMockBuilder(Client::class)
+ ->addMethods(['hgetall'])
+ ->getMock();
+ $client->expects($this->once())
+ ->method('hgetall')
+ ->with('foo')
+ ->willThrowException(new \Exception('Storage error'));
+
+ $storage = new Redis($client);
+
+ $this->expectException(GetRateInfoRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to get rate limit info: Storage error');
+
+ $storage->getRateInfo('foo');
+ }
+
+ public function testCreateRate(): void
+ {
+ $client = $this->getMockBuilder(Client::class)
+ ->addMethods(array('hset', 'expire', 'hgetall'))
->getMock();
$client->expects($this->once())
->method('expire')
@@ -46,10 +69,27 @@ public function testcreateRate()
}
- public function testLimitRateNoKey()
+ public function testLimitRateNoKey_exception()
{
- $client = $this->getMockBuilder('Predis\\Client')
- ->setMethods(array('hgetall'))
+ $client = $this->getMockBuilder(Client::class)
+ ->addMethods(array('hset', 'expire', 'hgetall'))
+ ->getMock();
+ $client->expects($this->once())
+ ->method('hset')
+ ->willThrowException(new \Exception('Storage error'));
+
+ $storage = new Redis($client);
+
+ $this->expectException(CreateRateRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to create rate limit: Storage error');
+
+ $storage->createRate('foo', 100, 123);
+ }
+
+ public function testLimitRateNoKey(): void
+ {
+ $client = $this->getMockBuilder(Client::class)
+ ->addMethods(['hgetall'])
->getMock();
$client->expects($this->once())
->method('hgetall')
@@ -60,23 +100,41 @@ public function testLimitRateNoKey()
$this->assertFalse($storage->limitRate('foo'));
}
- public function testLimitRateWithKey()
+ public function testLimitRate_exception(): void
{
- $client = $this->getMockBuilder('Predis\\Client')
- ->setMethods(array('hexists', 'hincrby', 'hgetall'))
+ $client = $this->getMockBuilder(Client::class)
+ ->addMethods(['hgetall'])
->getMock();
$client->expects($this->once())
->method('hgetall')
->with('foo')
- ->will($this->returnValue([
+ ->willThrowException(new \Exception('Storage error'));
+
+ $storage = new Redis($client);
+
+ $this->expectException(LimitRateRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to apply rate limit: Storage error');
+
+ $this->assertFalse($storage->limitRate('foo'));
+ }
+
+ public function testLimitRateWithKey(): void
+ {
+ $client = $this->getMockBuilder(Client::class)
+ ->addMethods(array('hexists', 'hincrby', 'hgetall'))
+ ->getMock();
+ $client->expects($this->once())
+ ->method('hgetall')
+ ->with('foo')
+ ->willReturn([
'limit' => 1,
'calls' => 1,
'reset' => 1,
- ]));
+ ]);
$client->expects($this->once())
->method('hincrby')
->with('foo', 'calls', 1)
- ->will($this->returnValue(2));
+ ->willReturn(2);
$storage = new Redis($client);
$storage->limitRate('foo');
@@ -97,10 +155,28 @@ public function testresetRate()
$this->assertTrue($storage->resetRate('foo'));
}
- public function testSanitizeKey()
+ public function testResetRate_exception(): void
{
- $client = $this->getMockBuilder('Predis\\Client')
- ->setMethods(array('del'))
+ $client = $this->getMockBuilder(Client::class)
+ ->addMethods(['del'])
+ ->getMock();
+ $client->expects($this->once())
+ ->method('del')
+ ->with('foo')
+ ->willThrowException(new \Exception('Storage error'));
+
+ $storage = new Redis($client);
+
+ $this->expectException(ResetRateRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to reset rate: Storage error');
+
+ $storage->resetRate('foo');
+ }
+
+ public function testSanitizeKey(): void
+ {
+ $client = $this->getMockBuilder(Client::class)
+ ->addMethods(array('del'))
->getMock();
$client->expects($this->once())
->method('del')
diff --git a/Tests/Service/Storage/SimpleCacheTest.php b/Tests/Service/Storage/SimpleCacheTest.php
index 1e2ae9d..046ae53 100644
--- a/Tests/Service/Storage/SimpleCacheTest.php
+++ b/Tests/Service/Storage/SimpleCacheTest.php
@@ -2,8 +2,13 @@
namespace Noxlogic\RateLimitBundle\Tests\Service\Storage;
+use Noxlogic\RateLimitBundle\Exception\Storage\CreateRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\GetRateInfoRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\LimitRateRateLimitStorageException;
+use Noxlogic\RateLimitBundle\Exception\Storage\ResetRateRateLimitStorageException;
use Noxlogic\RateLimitBundle\Service\Storage\SimpleCache;
use Noxlogic\RateLimitBundle\Tests\TestCase;
+use Psr\SimpleCache\CacheInterface;
class SimpleCacheTest extends TestCase
{
@@ -24,9 +29,26 @@ public function testGetRateInfo()
$this->assertEquals(1234, $rli->getResetTimestamp());
}
- public function testCreateRate()
+ public function testGetRateInfo_exception(): void
{
- $client = $this->getMockBuilder('Psr\\SimpleCache\\CacheInterface')
+ $client = $this->getMockBuilder(CacheInterface::class)
+ ->getMock();
+ $client->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->willThrowException(new \Exception('Storage error'));
+
+ $storage = new SimpleCache($client);
+
+ $this->expectException(GetRateInfoRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to get rate limit info: Storage error');
+
+ $storage->getRateInfo('foo');
+ }
+
+ public function testCreateRate(): void
+ {
+ $client = $this->getMockBuilder(CacheInterface::class)
->getMock();
$client->expects($this->once())
->method('set');
@@ -35,10 +57,25 @@ public function testCreateRate()
$storage->createRate('foo', 100, 123);
}
+ public function testCreateRate_exception(): void
+ {
+ $client = $this->getMockBuilder(CacheInterface::class)
+ ->getMock();
+ $client->expects($this->once())
+ ->method('set')
+ ->willThrowException(new \Exception('Storage error'));;
+
+ $storage = new SimpleCache($client);
+
+ $this->expectException(CreateRateRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to create rate limit: Storage error');
- public function testLimitRateNoKey()
+ $storage->createRate('foo', 100, 123);
+ }
+
+ public function testLimitRateNoKey(): void
{
- $client = $this->getMockBuilder('Psr\\SimpleCache\\CacheInterface')
+ $client = $this->getMockBuilder(CacheInterface::class)
->getMock();
$client->expects($this->once())
->method('get')
@@ -49,9 +86,26 @@ public function testLimitRateNoKey()
$this->assertFalse($storage->limitRate('foo'));
}
- public function testLimitRateWithKey()
+ public function testLimitRate_exception(): void
{
- $client = $this->getMockBuilder('Psr\\SimpleCache\\CacheInterface')
+ $client = $this->getMockBuilder(CacheInterface::class)
+ ->getMock();
+ $client->expects($this->once())
+ ->method('get')
+ ->with('foo')
+ ->willThrowException(new \Exception('Storage error'));
+
+ $storage = new SimpleCache($client);
+
+ $this->expectException(LimitRateRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to apply rate limit: Storage error');
+
+ $storage->limitRate('foo');
+ }
+
+ public function testLimitRateWithKey(): void
+ {
+ $client = $this->getMockBuilder(CacheInterface::class)
->getMock();
$info['limit'] = 100;
@@ -80,4 +134,21 @@ public function testResetRate()
$storage = new SimpleCache($client);
$this->assertTrue($storage->resetRate('foo'));
}
-}
+
+ public function testResetRate_exception(): void
+ {
+ $client = $this->getMockBuilder(CacheInterface::class)
+ ->getMock();
+ $client->expects($this->once())
+ ->method('delete')
+ ->with('foo')
+ ->willThrowException(new \Exception('Storage error'));
+
+ $storage = new SimpleCache($client);
+
+ $this->expectException(ResetRateRateLimitStorageException::class);
+ $this->expectExceptionMessage('Rate limit storage: Failed to reset rate: Storage error');
+
+ $storage->resetRate('foo');
+ }
+}
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 34a01ef..02d0e2c 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -342,12 +342,6 @@ parameters:
count: 1
path: Service/Storage/SimpleCache.php
- -
- message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\SimpleCache\:\:createRateInfo\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: Service/Storage/SimpleCache.php
-
-
message: '#^Method Noxlogic\\RateLimitBundle\\Service\\Storage\\SimpleCache\:\:createRateInfo\(\) has parameter \$info with no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
diff --git a/phpstan.dist.neon b/phpstan.dist.neon
index 8704079..5d2d7d4 100644
--- a/phpstan.dist.neon
+++ b/phpstan.dist.neon
@@ -1,5 +1,7 @@
parameters:
level: 6
+ treatPhpDocTypesAsCertain: false
+ reportUnmatchedIgnoredErrors: false
paths:
- Attribute/
- DependencyInjection/