Redis + RateLimiting Unit Test
This commit is contained in:
parent
c13516c085
commit
18bb6bffa7
@ -17,6 +17,10 @@ class RedisConnection {
|
|||||||
$this->link = new \Redis();
|
$this->link = new \Redis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getLogger(): Logger {
|
||||||
|
return $this->logger;
|
||||||
|
}
|
||||||
|
|
||||||
public function connect(ConnectionData $connectionData): bool {
|
public function connect(ConnectionData $connectionData): bool {
|
||||||
try {
|
try {
|
||||||
$this->link->connect($connectionData->getHost(), $connectionData->getPort());
|
$this->link->connect($connectionData->getHost(), $connectionData->getPort());
|
||||||
|
@ -21,14 +21,14 @@ class Context {
|
|||||||
private static Context $instance;
|
private static Context $instance;
|
||||||
|
|
||||||
private ?SQL $sql;
|
private ?SQL $sql;
|
||||||
private ?Session $session;
|
protected ?Session $session;
|
||||||
private ?User $user;
|
private ?User $user;
|
||||||
private Configuration $configuration;
|
private Configuration $configuration;
|
||||||
private Language $language;
|
private Language $language;
|
||||||
public ?Router $router;
|
public ?Router $router;
|
||||||
private ?RedisConnection $redis;
|
protected ?RedisConnection $redis;
|
||||||
|
|
||||||
private function __construct() {
|
protected function __construct() {
|
||||||
|
|
||||||
$this->sql = null;
|
$this->sql = null;
|
||||||
$this->session = null;
|
$this->session = null;
|
||||||
|
@ -20,7 +20,7 @@ class Session extends DatabaseEntity {
|
|||||||
private User $user;
|
private User $user;
|
||||||
private DateTime $expires;
|
private DateTime $expires;
|
||||||
#[MaxLength(45)] private string $ipAddress;
|
#[MaxLength(45)] private string $ipAddress;
|
||||||
#[MaxLength(36)] private string $uuid;
|
#[MaxLength(36)] protected string $uuid;
|
||||||
#[DefaultValue(true)] private bool $active;
|
#[DefaultValue(true)] private bool $active;
|
||||||
#[MaxLength(64)] private ?string $os;
|
#[MaxLength(64)] private ?string $os;
|
||||||
#[MaxLength(64)] private ?string $browser;
|
#[MaxLength(64)] private ?string $browser;
|
||||||
|
@ -8,17 +8,28 @@ export default function SettingsSelection(props) {
|
|||||||
const {key_name, value, options, onChangeValue, disabled, ...other} = props;
|
const {key_name, value, options, onChangeValue, disabled, ...other} = props;
|
||||||
const {translate: L} = useContext(LocaleContext);
|
const {translate: L} = useContext(LocaleContext);
|
||||||
|
|
||||||
|
let optionElements = [];
|
||||||
|
if (Array.isArray(options)) {
|
||||||
|
optionElements = options.map(option => <option
|
||||||
|
key={"option-" + option}
|
||||||
|
value={option}>
|
||||||
|
{option}
|
||||||
|
</option>);
|
||||||
|
} else {
|
||||||
|
optionElements = Object.entries(options).map(([value, label]) => <option
|
||||||
|
key={"option-" + value}
|
||||||
|
value={value}>
|
||||||
|
{label}
|
||||||
|
</option>);
|
||||||
|
}
|
||||||
|
|
||||||
return <SpacedFormGroup {...other}>
|
return <SpacedFormGroup {...other}>
|
||||||
<FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel>
|
<FormLabel disabled={disabled}>{L("settings." + key_name)}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select native value={value}
|
<Select native value={value}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
size={"small"} onChange={e => onChangeValue(e.target.value)}>
|
size={"small"} onChange={e => onChangeValue(e.target.value)}>
|
||||||
{options.map(option => <option
|
{optionElements}
|
||||||
key={"option-" + option}
|
|
||||||
value={option}>
|
|
||||||
{option}
|
|
||||||
</option>)}
|
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</SpacedFormGroup>
|
</SpacedFormGroup>
|
||||||
|
@ -284,7 +284,7 @@ export default function SettingsView(props) {
|
|||||||
];
|
];
|
||||||
} else if (selectedTab === "captcha") {
|
} else if (selectedTab === "captcha") {
|
||||||
return [
|
return [
|
||||||
renderSelection("captcha_provider", ["none", "recaptcha", "hcaptcha"]),
|
renderSelection("captcha_provider", {"none": L("settings.none"), "recaptcha": "Google reCaptcha", "hcaptcha": "hCaptcha"}),
|
||||||
renderTextInput("captcha_site_key", settings.captcha_provider === "none"),
|
renderTextInput("captcha_site_key", settings.captcha_provider === "none"),
|
||||||
renderPasswordInput("captcha_secret_key", settings.captcha_provider === "none"),
|
renderPasswordInput("captcha_secret_key", settings.captcha_provider === "none"),
|
||||||
];
|
];
|
||||||
|
173
test/RateLimiting.test.php
Normal file
173
test/RateLimiting.test.php
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Core\Objects\Context;
|
||||||
|
|
||||||
|
function __new_time_impl() {
|
||||||
|
return RateLimitingTest::$CURRENT_TIME;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RateLimitingTest extends \PHPUnit\Framework\TestCase {
|
||||||
|
|
||||||
|
const FUNCTION_OVERRIDES = ["time"];
|
||||||
|
|
||||||
|
static int $CURRENT_TIME;
|
||||||
|
|
||||||
|
public static function setUpBeforeClass(): void {
|
||||||
|
|
||||||
|
if (!function_exists("runkit7_function_rename") || !function_exists("runkit7_function_remove")) {
|
||||||
|
throw new Exception("Request Unit Test requires runkit7 extension");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ini_get("runkit.internal_override") !== "1") {
|
||||||
|
throw new Exception("Request Unit Test requires runkit7 with internal_override enabled to function properly");
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$CURRENT_TIME = time();
|
||||||
|
foreach (self::FUNCTION_OVERRIDES as $functionName) {
|
||||||
|
runkit7_function_rename($functionName, "__orig_{$functionName}_impl");
|
||||||
|
runkit7_function_rename("__new_{$functionName}_impl", $functionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRules() {
|
||||||
|
$secondsRule = new \Core\Objects\RateLimitRule(1, 1, \Core\Objects\RateLimitRule::SECOND);
|
||||||
|
$this->assertEquals(1, $secondsRule->getCount());
|
||||||
|
$this->assertEquals(1, $secondsRule->getWindow());
|
||||||
|
|
||||||
|
$secondsRule2 = new \Core\Objects\RateLimitRule(5, 120, \Core\Objects\RateLimitRule::SECOND);
|
||||||
|
$this->assertEquals(5, $secondsRule2->getCount());
|
||||||
|
$this->assertEquals(120, $secondsRule2->getWindow());
|
||||||
|
|
||||||
|
$minuteRule = new \Core\Objects\RateLimitRule(10, 5, \Core\Objects\RateLimitRule::MINUTE);
|
||||||
|
$this->assertEquals(10, $minuteRule->getCount());
|
||||||
|
$this->assertEquals(5 * 60, $minuteRule->getWindow());
|
||||||
|
|
||||||
|
$hourRule = new \Core\Objects\RateLimitRule(15, 4, \Core\Objects\RateLimitRule::HOUR);
|
||||||
|
$this->assertEquals(15, $hourRule->getCount());
|
||||||
|
$this->assertEquals(4 * 60 * 60, $hourRule->getWindow());
|
||||||
|
|
||||||
|
// should be interpreted as 30 seconds, ignoring invalid unit
|
||||||
|
$invalidUnitRule = new \Core\Objects\RateLimitRule(20, 30, 10);
|
||||||
|
$this->assertEquals(20, $invalidUnitRule->getCount());
|
||||||
|
$this->assertEquals(30, $invalidUnitRule->getWindow());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRateLimiting() {
|
||||||
|
|
||||||
|
$testContext = new TestContext();
|
||||||
|
$redis = $testContext->getRedis();
|
||||||
|
$this->assertTrue($redis->isConnected());
|
||||||
|
|
||||||
|
$_SERVER["REMOTE_ADDR"] = "0.0.0.0";
|
||||||
|
$method = "test/method";
|
||||||
|
$sessionUUID = uuidv4();
|
||||||
|
$windowSize = 10;
|
||||||
|
|
||||||
|
// should pass
|
||||||
|
$noRateLimiting = new \Core\Objects\RateLimiting();
|
||||||
|
for ($i = 0; $i < 100; $i++) {
|
||||||
|
$this->assertTrue($noRateLimiting->check($testContext, $method));
|
||||||
|
}
|
||||||
|
|
||||||
|
$anonymousRateLimiting = new \Core\Objects\RateLimiting(
|
||||||
|
new \Core\Objects\RateLimitRule($windowSize, 5, \Core\Objects\RateLimitRule::SECOND)
|
||||||
|
);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $windowSize; $i++) {
|
||||||
|
$this->assertTrue($anonymousRateLimiting->check($testContext, $method));
|
||||||
|
$this->assertCount($i + 1, json_decode($redis->hGet($method, $_SERVER["REMOTE_ADDR"])));
|
||||||
|
}
|
||||||
|
$this->assertFalse($anonymousRateLimiting->check($testContext, $method));
|
||||||
|
self::$CURRENT_TIME += 4;
|
||||||
|
$this->assertFalse($anonymousRateLimiting->check($testContext, $method));
|
||||||
|
self::$CURRENT_TIME += 2;
|
||||||
|
$this->assertTrue($anonymousRateLimiting->check($testContext, $method));
|
||||||
|
$this->assertCount($windowSize, json_decode($redis->hGet($method, $_SERVER["REMOTE_ADDR"])));
|
||||||
|
|
||||||
|
$testContext->setSession($sessionUUID);
|
||||||
|
for ($i = 0; $i < 100; $i++) {
|
||||||
|
$this->assertTrue($anonymousRateLimiting->check($testContext, $method));
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionBasedRateLimiting = new \Core\Objects\RateLimiting(
|
||||||
|
null,
|
||||||
|
new \Core\Objects\RateLimitRule($windowSize, 5, \Core\Objects\RateLimitRule::SECOND)
|
||||||
|
);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $windowSize; $i++) {
|
||||||
|
$this->assertTrue($sessionBasedRateLimiting->check($testContext, $method));
|
||||||
|
$this->assertCount($i + 1, json_decode($redis->hGet($method, $sessionUUID)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertFalse($sessionBasedRateLimiting->check($testContext, $method));
|
||||||
|
self::$CURRENT_TIME += 10;
|
||||||
|
$this->assertTrue($sessionBasedRateLimiting->check($testContext, $method));
|
||||||
|
$testContext->destroySession();
|
||||||
|
for ($i = 0; $i < 100; $i++) {
|
||||||
|
$this->assertTrue($sessionBasedRateLimiting->check($testContext, $method));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestSession extends \Core\Objects\DatabaseEntity\Session {
|
||||||
|
public function __construct(Context $context, string $uuid) {
|
||||||
|
parent::__construct($context, new \Core\Objects\DatabaseEntity\User());
|
||||||
|
$this->uuid = $uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestContext extends Context {
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct();
|
||||||
|
$this->redis = new TestRedisConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRedis(): ?\Core\Driver\Redis\RedisConnection {
|
||||||
|
return $this->redis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSession(string $sessionUUID): void {
|
||||||
|
$this->session = new TestSession($this, $sessionUUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroySession(): void {
|
||||||
|
$this->session = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestRedisConnection extends \Core\Driver\Redis\RedisConnection {
|
||||||
|
|
||||||
|
private array $redisData;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
parent::__construct(null);
|
||||||
|
$this->getLogger()->unitTestMode();
|
||||||
|
$this->redisData = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function connect(\Core\Objects\ConnectionData $connectionData): bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isConnected(): bool {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hSet(string $hashKey, mixed $key, string $value): bool {
|
||||||
|
if (!isset($this->redisData[$hashKey])) {
|
||||||
|
$this->redisData[$hashKey] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->redisData[$hashKey][$key] = $value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hGet(string $hashKey, string $key): ?string {
|
||||||
|
if (!isset($this->redisData[$hashKey])) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redisData[$hashKey][$key] ?? "";
|
||||||
|
}
|
||||||
|
}
|
@ -163,6 +163,10 @@ abstract class TestRequest extends Request {
|
|||||||
public static function getEndpoint(string $prefix = ""): ?string {
|
public static function getEndpoint(string $prefix = ""): ?string {
|
||||||
return "test";
|
return "test";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getDescription(): string {
|
||||||
|
return "test description";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RequestAllMethods extends TestRequest {
|
class RequestAllMethods extends TestRequest {
|
||||||
|
Loading…
Reference in New Issue
Block a user