added ratelimiter tests

This commit is contained in:
Daniel Hensby 2017-09-27 15:56:44 +01:00
parent 51ac297c59
commit 5f739c111e
No known key found for this signature in database
GPG Key ID: B00D1E9767F0B06E
5 changed files with 252 additions and 11 deletions

View File

@ -26,6 +26,11 @@ class RateLimitMiddleware implements HTTPMiddleware
*/
private $decay = 1;
/**
* @var RateLimiter|null
*/
private $rateLimiter;
/**
* @param HTTPRequest $request
* @param callable $delegate
@ -33,11 +38,13 @@ class RateLimitMiddleware implements HTTPMiddleware
*/
public function process(HTTPRequest $request, callable $delegate)
{
$limiter = RateLimiter::create(
$this->getKeyFromRequest($request),
$this->getMaxAttempts(),
$this->getDecay()
);
if (!$limiter = $this->getRateLimiter()) {
$limiter = RateLimiter::create(
$this->getKeyFromRequest($request),
$this->getMaxAttempts(),
$this->getDecay()
);
}
if ($limiter->canAccess()) {
$limiter->hit();
$response = $delegate($request);
@ -140,4 +147,22 @@ class RateLimitMiddleware implements HTTPMiddleware
{
return $this->decay;
}
/**
* @param RateLimiter $rateLimiter
* @return $this
*/
public function setRateLimiter($rateLimiter)
{
$this->rateLimiter = $rateLimiter;
return $this;
}
/**
* @return RateLimiter|null
*/
public function getRateLimiter()
{
return $this->rateLimiter;
}
}

View File

@ -190,13 +190,14 @@ class DBDatetime extends DBDate implements TemplateGlobalProvider
*/
public static function set_mock_now($datetime)
{
if ($datetime instanceof DBDatetime) {
self::$mock_now = $datetime;
} elseif (is_string($datetime)) {
self::$mock_now = DBField::create_field('Datetime', $datetime);
} else {
throw new InvalidArgumentException('DBDatetime::set_mock_now(): Wrong format: ' . $datetime);
if (!$datetime instanceof DBDatetime) {
$value = $datetime;
$datetime = DBField::create_field('Datetime', $datetime);
if ($datetime === false) {
throw new InvalidArgumentException('DBDatetime::set_mock_now(): Wrong format: ' . $value);
}
}
self::$mock_now = $datetime;
}
/**

View File

@ -0,0 +1,18 @@
<?php
namespace SilverStripe\Control\Tests\Middleware\Control;
use SilverStripe\Control\Controller;
class TestController extends Controller
{
public function index($request)
{
return "Success";
}
public function Link($action = null)
{
return Controller::join_links('TestController', $action);
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace SilverStripe\Control\Tests\Middleware;
use SilverStripe\Control\Middleware\RateLimitMiddleware;
use SilverStripe\Control\Middleware\RequestHandlerMiddlewareAdapter;
use SilverStripe\Control\Tests\Middleware\Control\TestController;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\ORM\FieldType\DBDatetime;
class RateLimitMiddlewareTest extends FunctionalTest
{
protected static $extra_controllers = [
TestController::class,
];
protected function setUp()
{
parent::setUp();
DBDatetime::set_mock_now('2017-09-27 00:00:00');
Config::modify()->set(Injector::class, 'TestRateLimitMiddleware', [
'class' => RateLimitMiddleware::class,
'properties' => [
'ExtraKey' => 'test',
'MaxAttempts' => 2,
'Decay' => 1,
],
]);
Config::modify()->set(Injector::class, 'RateLimitTestController', [
'class' => RequestHandlerMiddlewareAdapter::class,
'properties' => [
'RequestHandler' => '%$' . TestController::class,
'Middlewares' => [
'%$TestRateLimitMiddleware'
],
],
]);
}
protected function getExtraRoutes()
{
$rules = parent::getExtraRoutes();
$rules['TestController//$Action/$ID/$OtherID'] = '%$RateLimitTestController';
return $rules;
}
public function testRequest()
{
$response = $this->get('TestController');
$this->assertFalse($response->isError());
$this->assertEquals(2, $response->getHeader('X-RateLimit-Limit'));
$this->assertEquals(1, $response->getHeader('X-RateLimit-Remaining'));
$this->assertEquals(DBDatetime::now()->getTimestamp() + 60, $response->getHeader('X-RateLimit-Reset'));
$this->assertEquals('Success', $response->getBody());
$response = $this->get('TestController');
$this->assertFalse($response->isError());
$this->assertEquals(0, $response->getHeader('X-RateLimit-Remaining'));
$response = $this->get('TestController');
$this->assertTrue($response->isError());
$this->assertEquals(429, $response->getStatusCode());
$this->assertEquals(60, $response->getHeader('retry-after'));
$this->assertNotEquals('Success', $response->getBody());
}
}

View File

@ -0,0 +1,129 @@
<?php
namespace SilverStripe\Core\Tests\Cache;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Cache\RateLimiter;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\FieldType\DBDatetime;
use Symfony\Component\Cache\Simple\ArrayCache;
class RateLimiterTest extends SapphireTest
{
protected function setUp()
{
parent::setUp();
DBDatetime::set_mock_now('2017-09-27 00:00:00');
}
public function testConstruct()
{
$cache = new ArrayCache();
$rateLimiter = new RateLimiter(
'test',
5,
1
);
$rateLimiter->setCache($cache);
$this->assertEquals('test', $rateLimiter->getIdentifier());
$this->assertEquals(5, $rateLimiter->getMaxAttempts());
$this->assertEquals(1, $rateLimiter->getDecay());
}
public function testGetNumberOfAttempts()
{
$cache = new ArrayCache();
$rateLimiter = new RateLimiter(
'test',
5,
1
);
$rateLimiter->setCache($cache);
for ($i = 0; $i < 7; ++$i) {
$this->assertEquals($i, $rateLimiter->getNumAttempts());
$rateLimiter->hit();
}
}
public function testGetNumAttemptsRemaining()
{
$cache = new ArrayCache();
$rateLimiter = new RateLimiter(
'test',
1,
1
);
$rateLimiter->setCache($cache);
$this->assertEquals(1, $rateLimiter->getNumAttemptsRemaining());
$rateLimiter->hit();
$this->assertEquals(0, $rateLimiter->getNumAttemptsRemaining());
$rateLimiter->hit();
$this->assertEquals(0, $rateLimiter->getNumAttemptsRemaining());
}
public function testGetTimeToReset()
{
$cache = new ArrayCache();
$rateLimiter = new RateLimiter(
'test',
1,
1
);
$rateLimiter->setCache($cache);
$this->assertEquals(0, $rateLimiter->getTimeToReset());
$rateLimiter->hit();
$this->assertEquals(60, $rateLimiter->getTimeToReset());
DBDatetime::set_mock_now(DBDatetime::now()->getTimestamp() + 30);
$this->assertEquals(30, $rateLimiter->getTimeToReset());
}
public function testClearAttempts()
{
$cache = new ArrayCache();
$rateLimiter = new RateLimiter(
'test',
1,
1
);
$rateLimiter->setCache($cache);
for ($i = 0; $i < 5; ++$i) {
$rateLimiter->hit();
}
$this->assertEquals(5, $rateLimiter->getNumAttempts());
$rateLimiter->clearAttempts();
$this->assertEquals(0, $rateLimiter->getNumAttempts());
}
public function testHit()
{
$cache = new ArrayCache();
$rateLimiter = new RateLimiter(
'test',
1,
1
);
$rateLimiter->setCache($cache);
$this->assertFalse($cache->has('test'));
$this->assertFalse($cache->has('test-timer'));
$rateLimiter->hit();
$this->assertTrue($cache->has('test'));
$this->assertTrue($cache->has('test-timer'));
}
public function testCanAccess()
{
$cache = new ArrayCache();
$rateLimiter = new RateLimiter(
'test',
1,
1
);
$rateLimiter->setCache($cache);
$this->assertTrue($rateLimiter->canAccess());
$rateLimiter->hit();
$this->assertFalse($rateLimiter->canAccess());
}
}