2025-02-19 13:31:01 +00:00
|
|
|
<?php
|
|
|
|
|
2025-02-20 08:41:14 +00:00
|
|
|
require_once dirname(__DIR__, 3) . '/app/classes/database.php';
|
|
|
|
require_once dirname(__DIR__, 3) . '/app/classes/ratelimiter.php';
|
2025-02-21 09:44:52 +00:00
|
|
|
require_once dirname(__DIR__, 3) . '/app/classes/log.php';
|
2025-02-20 08:41:14 +00:00
|
|
|
require_once dirname(__DIR__, 3) . '/app/includes/rate_limit_middleware.php';
|
2025-02-19 13:31:01 +00:00
|
|
|
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
|
|
|
|
class RateLimitMiddlewareTest extends TestCase
|
|
|
|
{
|
|
|
|
private $db;
|
|
|
|
private $rateLimiter;
|
|
|
|
|
|
|
|
protected function setUp(): void
|
|
|
|
{
|
|
|
|
parent::setUp();
|
|
|
|
|
2025-04-25 14:15:56 +00:00
|
|
|
// Set global IP for rate limiting
|
|
|
|
global $user_IP;
|
|
|
|
$user_IP = '8.8.8.8';
|
|
|
|
|
|
|
|
// Prepare DB for Github CI
|
|
|
|
$host = defined('CI_DB_HOST') ? CI_DB_HOST : '127.0.0.1';
|
|
|
|
$password = defined('CI_DB_PASSWORD') ? CI_DB_PASSWORD : '';
|
|
|
|
|
2025-02-19 13:31:01 +00:00
|
|
|
// Set up test database
|
|
|
|
$this->db = new Database([
|
2025-04-25 14:15:56 +00:00
|
|
|
'type' => 'mariadb',
|
|
|
|
'host' => $host,
|
|
|
|
'port' => '3306',
|
2025-04-25 15:30:24 +00:00
|
|
|
'dbname' => 'jilo_test',
|
|
|
|
'user' => 'test_jilo',
|
2025-04-25 14:15:56 +00:00
|
|
|
'password' => $password
|
2025-02-19 13:31:01 +00:00
|
|
|
]);
|
|
|
|
|
2025-04-25 14:15:56 +00:00
|
|
|
// Create rate limiter instance
|
2025-02-19 13:31:01 +00:00
|
|
|
$this->rateLimiter = new RateLimiter($this->db);
|
|
|
|
|
2025-04-25 14:15:56 +00:00
|
|
|
// Drop tables if they exist
|
|
|
|
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_rate_auth");
|
|
|
|
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_rate_page");
|
|
|
|
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_ip_blacklist");
|
|
|
|
$this->db->getConnection()->exec("DROP TABLE IF EXISTS security_ip_whitelist");
|
|
|
|
$this->db->getConnection()->exec("DROP TABLE IF EXISTS log");
|
|
|
|
|
|
|
|
// Create required tables with correct names from RateLimiter class
|
|
|
|
$this->db->getConnection()->exec("
|
|
|
|
CREATE TABLE IF NOT EXISTS security_rate_auth (
|
|
|
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
|
|
ip_address VARCHAR(45) NOT NULL,
|
|
|
|
username VARCHAR(255) NOT NULL,
|
|
|
|
attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
INDEX idx_ip_username (ip_address, username)
|
|
|
|
)
|
|
|
|
");
|
|
|
|
|
|
|
|
$this->db->getConnection()->exec("
|
|
|
|
CREATE TABLE IF NOT EXISTS security_rate_page (
|
|
|
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
|
|
ip_address VARCHAR(45) NOT NULL,
|
|
|
|
endpoint VARCHAR(255) NOT NULL,
|
|
|
|
request_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
INDEX idx_ip_endpoint (ip_address, endpoint),
|
|
|
|
INDEX idx_request_time (request_time)
|
|
|
|
)
|
|
|
|
");
|
|
|
|
|
|
|
|
$this->db->getConnection()->exec("
|
|
|
|
CREATE TABLE IF NOT EXISTS security_ip_blacklist (
|
|
|
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
|
|
ip_address VARCHAR(45) NOT NULL,
|
|
|
|
is_network BOOLEAN DEFAULT FALSE,
|
|
|
|
reason VARCHAR(255),
|
|
|
|
expiry_time TIMESTAMP NULL,
|
|
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
created_by VARCHAR(255),
|
|
|
|
UNIQUE KEY unique_ip (ip_address)
|
|
|
|
)
|
|
|
|
");
|
2025-02-19 13:31:01 +00:00
|
|
|
|
2025-04-25 14:15:56 +00:00
|
|
|
$this->db->getConnection()->exec("
|
|
|
|
CREATE TABLE IF NOT EXISTS security_ip_whitelist (
|
|
|
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
|
|
ip_address VARCHAR(45) NOT NULL,
|
|
|
|
is_network BOOLEAN DEFAULT FALSE,
|
|
|
|
description VARCHAR(255),
|
|
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
created_by VARCHAR(255),
|
|
|
|
UNIQUE KEY unique_ip (ip_address)
|
|
|
|
)
|
|
|
|
");
|
|
|
|
|
|
|
|
// Create log table
|
|
|
|
$this->db->getConnection()->exec("
|
|
|
|
CREATE TABLE IF NOT EXISTS log (
|
|
|
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
|
|
user_id INT,
|
|
|
|
scope VARCHAR(50) NOT NULL,
|
|
|
|
message TEXT NOT NULL,
|
|
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
|
|
)
|
|
|
|
");
|
|
|
|
|
|
|
|
// Mock $_SERVER['REMOTE_ADDR'] with a non-whitelisted IP
|
|
|
|
$_SERVER['REMOTE_ADDR'] = '8.8.8.8';
|
|
|
|
|
|
|
|
// Define PHPUNIT_RUNNING constant
|
|
|
|
if (!defined('PHPUNIT_RUNNING')) {
|
|
|
|
define('PHPUNIT_RUNNING', true);
|
2025-02-19 13:31:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function tearDown(): void
|
|
|
|
{
|
2025-04-25 14:15:56 +00:00
|
|
|
// Clean up all rate limit records
|
|
|
|
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
|
|
|
|
$this->db->getConnection()->exec("TRUNCATE TABLE security_ip_blacklist");
|
|
|
|
$this->db->getConnection()->exec("TRUNCATE TABLE security_ip_whitelist");
|
|
|
|
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_auth");
|
|
|
|
$this->db->getConnection()->exec("TRUNCATE TABLE log");
|
2025-02-19 13:31:01 +00:00
|
|
|
parent::tearDown();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testRateLimitMiddleware()
|
|
|
|
{
|
2025-04-25 14:15:56 +00:00
|
|
|
// Clean any existing rate limit records
|
|
|
|
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
|
|
|
|
|
|
|
|
// Make 60 requests to reach the limit
|
|
|
|
for ($i = 0; $i < 60; $i++) {
|
2025-02-21 09:44:52 +00:00
|
|
|
$result = checkRateLimit($this->db, '/login');
|
2025-04-25 14:15:56 +00:00
|
|
|
$this->assertTrue($result, "Request $i should be allowed");
|
2025-02-19 13:31:01 +00:00
|
|
|
|
2025-04-25 14:15:56 +00:00
|
|
|
// Verify request was recorded
|
|
|
|
$stmt = $this->db->getConnection()->prepare("
|
|
|
|
SELECT COUNT(*) as count
|
|
|
|
FROM security_rate_page
|
|
|
|
WHERE ip_address = ?
|
|
|
|
AND endpoint = ?
|
|
|
|
AND request_time >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)
|
|
|
|
");
|
|
|
|
$stmt->execute(['8.8.8.8', '/login']);
|
|
|
|
$count = $stmt->fetch(PDO::FETCH_ASSOC)['count'];
|
|
|
|
$this->assertEquals($i + 1, $count, "Expected " . ($i + 1) . " requests to be recorded, got {$count}");
|
2025-02-19 13:31:01 +00:00
|
|
|
}
|
2025-04-25 14:15:56 +00:00
|
|
|
|
|
|
|
// The 61st request should be blocked
|
|
|
|
$result = checkRateLimit($this->db, '/login');
|
|
|
|
$this->assertFalse($result, "Request should be blocked after 60 requests");
|
2025-02-19 13:31:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function testRateLimitBypass()
|
|
|
|
{
|
2025-04-25 14:15:56 +00:00
|
|
|
// Clean any existing rate limit records and lists
|
|
|
|
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
|
|
|
|
$this->db->getConnection()->exec("TRUNCATE TABLE security_ip_whitelist");
|
|
|
|
$this->db->getConnection()->exec("TRUNCATE TABLE security_ip_blacklist");
|
|
|
|
|
|
|
|
// Add IP to whitelist and verify it was added
|
|
|
|
$stmt = $this->db->getConnection()->prepare("INSERT INTO security_ip_whitelist (ip_address, is_network, description, created_by) VALUES (?, 0, ?, 'PHPUnit')");
|
|
|
|
$stmt->execute(['8.8.8.8', 'Test whitelist']);
|
|
|
|
|
|
|
|
// Verify IP is in whitelist
|
|
|
|
$stmt = $this->db->getConnection()->prepare("SELECT COUNT(*) as count FROM security_ip_whitelist WHERE ip_address = ?");
|
|
|
|
$stmt->execute(['8.8.8.8']);
|
|
|
|
$count = $stmt->fetch(PDO::FETCH_ASSOC)['count'];
|
|
|
|
$this->assertEquals(1, $count, "IP should be in whitelist");
|
|
|
|
|
|
|
|
// Should be able to make more requests than limit
|
|
|
|
for ($i = 0; $i < 100; $i++) {
|
|
|
|
$result = checkRateLimit($this->db, '/login');
|
|
|
|
$this->assertTrue($result, "Request $i should be allowed for whitelisted IP");
|
|
|
|
}
|
2025-02-19 13:31:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function testRateLimitReset()
|
|
|
|
{
|
2025-04-25 14:15:56 +00:00
|
|
|
// Clean any existing rate limit records
|
|
|
|
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
|
|
|
|
|
|
|
|
// Make some requests
|
|
|
|
for ($i = 0; $i < 15; $i++) {
|
|
|
|
$result = checkRateLimit($this->db, '/login');
|
2025-02-19 13:31:01 +00:00
|
|
|
}
|
|
|
|
|
2025-04-25 14:15:56 +00:00
|
|
|
// Manually expire old requests
|
|
|
|
$this->db->getConnection()->exec("
|
|
|
|
DELETE FROM security_rate_page
|
|
|
|
WHERE request_time < DATE_SUB(NOW(), INTERVAL 1 MINUTE)
|
|
|
|
");
|
2025-02-19 13:31:01 +00:00
|
|
|
|
2025-04-25 14:15:56 +00:00
|
|
|
// Should be able to make requests again
|
2025-02-21 09:44:52 +00:00
|
|
|
$result = checkRateLimit($this->db, '/login');
|
2025-02-19 13:31:01 +00:00
|
|
|
$this->assertTrue($result);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testDifferentEndpoints()
|
|
|
|
{
|
2025-04-25 14:15:56 +00:00
|
|
|
// Clean any existing rate limit records
|
|
|
|
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
|
|
|
|
|
|
|
|
// Make requests to login endpoint (default limit: 60)
|
|
|
|
for ($i = 0; $i < 30; $i++) {
|
|
|
|
$result = checkRateLimit($this->db, '/login');
|
|
|
|
$this->assertTrue($result, "Request $i to /login should be allowed");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clean up between endpoint tests
|
|
|
|
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
|
|
|
|
|
|
|
|
// Make requests to register endpoint (limit: 5)
|
2025-02-19 13:31:01 +00:00
|
|
|
for ($i = 0; $i < 5; $i++) {
|
2025-04-25 14:15:56 +00:00
|
|
|
$result = checkRateLimit($this->db, '/register');
|
|
|
|
$this->assertTrue($result, "Request $i to /register should be allowed");
|
2025-02-19 13:31:01 +00:00
|
|
|
}
|
|
|
|
|
2025-04-25 14:15:56 +00:00
|
|
|
// The 6th request to register should be blocked
|
|
|
|
$result = checkRateLimit($this->db, '/register');
|
|
|
|
$this->assertFalse($result, "Request should be blocked after 5 requests to /register");
|
2025-02-19 13:31:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function testDifferentIpAddresses()
|
|
|
|
{
|
2025-04-25 14:15:56 +00:00
|
|
|
// Make requests from first IP
|
|
|
|
for ($i = 0; $i < 30; $i++) {
|
|
|
|
$result = checkRateLimit($this->db, '/login');
|
|
|
|
$this->assertTrue($result);
|
2025-02-19 13:31:01 +00:00
|
|
|
}
|
|
|
|
|
2025-04-25 14:15:56 +00:00
|
|
|
// Change IP
|
|
|
|
$_SERVER['REMOTE_ADDR'] = '8.8.4.4';
|
|
|
|
|
|
|
|
// Should be able to make requests from different IP
|
|
|
|
for ($i = 0; $i < 30; $i++) {
|
|
|
|
$result = checkRateLimit($this->db, '/login');
|
|
|
|
$this->assertTrue($result);
|
|
|
|
}
|
2025-02-19 13:31:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function testWhitelistedIp()
|
|
|
|
{
|
|
|
|
// Add IP to whitelist
|
2025-04-25 14:15:56 +00:00
|
|
|
$this->rateLimiter->addToWhitelist('8.8.8.8', false, 'Test whitelist', 'PHPUnit');
|
2025-02-19 13:31:01 +00:00
|
|
|
|
|
|
|
// Should be able to make more requests than limit
|
2025-04-25 14:15:56 +00:00
|
|
|
for ($i = 0; $i < 50; $i++) {
|
2025-02-21 09:44:52 +00:00
|
|
|
$result = checkRateLimit($this->db, '/login');
|
2025-02-19 13:31:01 +00:00
|
|
|
$this->assertTrue($result);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function testBlacklistedIp()
|
|
|
|
{
|
2025-04-25 14:15:56 +00:00
|
|
|
// Add IP to blacklist and verify it was added
|
|
|
|
$this->db->getConnection()->exec("INSERT INTO security_ip_blacklist (ip_address, is_network, reason, created_by) VALUES ('8.8.8.8', 0, 'Test blacklist', 'system')");
|
2025-02-19 13:31:01 +00:00
|
|
|
|
2025-04-25 14:15:56 +00:00
|
|
|
// Request should be blocked
|
2025-02-21 09:44:52 +00:00
|
|
|
$result = checkRateLimit($this->db, '/login');
|
2025-04-25 14:15:56 +00:00
|
|
|
$this->assertFalse($result, "Blacklisted IP should be blocked");
|
2025-02-19 13:31:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function testRateLimitPersistence()
|
|
|
|
{
|
2025-04-25 14:15:56 +00:00
|
|
|
// Clean any existing rate limit records
|
|
|
|
$this->db->getConnection()->exec("TRUNCATE TABLE security_rate_page");
|
2025-02-19 13:31:01 +00:00
|
|
|
|
2025-04-25 14:15:56 +00:00
|
|
|
// Make 60 requests to reach the limit
|
|
|
|
for ($i = 0; $i < 60; $i++) {
|
|
|
|
$result = checkRateLimit($this->db, '/login');
|
|
|
|
$this->assertTrue($result, "Request $i should be allowed");
|
2025-02-19 13:31:01 +00:00
|
|
|
|
2025-04-25 14:15:56 +00:00
|
|
|
// Verify request was recorded
|
|
|
|
$stmt = $this->db->getConnection()->prepare("
|
|
|
|
SELECT COUNT(*) as count
|
|
|
|
FROM security_rate_page
|
|
|
|
WHERE ip_address = ?
|
|
|
|
AND endpoint = ?
|
|
|
|
AND request_time >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)
|
|
|
|
");
|
|
|
|
$stmt->execute(['8.8.8.8', '/login']);
|
|
|
|
$count = $stmt->fetch(PDO::FETCH_ASSOC)['count'];
|
|
|
|
$this->assertEquals($i + 1, $count, "Expected " . ($i + 1) . " requests to be recorded, got {$count}");
|
|
|
|
}
|
|
|
|
|
|
|
|
// The 61st request should be blocked
|
2025-02-21 09:44:52 +00:00
|
|
|
$result = checkRateLimit($this->db, '/login');
|
2025-04-25 14:15:56 +00:00
|
|
|
$this->assertFalse($result, "Request should be blocked after 60 requests");
|
2025-02-19 13:31:01 +00:00
|
|
|
}
|
|
|
|
}
|