Adds tests for middleware

main
Yasen Pramatarov 2025-02-19 15:31:01 +02:00
parent c2f63f6121
commit 5327bde032
17 changed files with 587 additions and 127 deletions

View File

@ -2,7 +2,7 @@
require_once __DIR__ . '/../helpers/security.php';
function verifyCsrfToken() {
function applyCsrfMiddleware() {
$security = SecurityHelper::getInstance();
// Skip CSRF check for GET requests

View File

@ -11,6 +11,7 @@ require_once __DIR__ . '/../classes/ratelimiter.php';
* @return bool True if request is allowed, false if rate limited
*/
function checkRateLimit($database, $endpoint, $userId = null) {
$isTest = defined('PHPUNIT_RUNNING');
$rateLimiter = new RateLimiter($database['db']);
$ipAddress = $_SERVER['REMOTE_ADDR'];
@ -19,6 +20,7 @@ function checkRateLimit($database, $endpoint, $userId = null) {
// Get remaining requests for error message
$remaining = $rateLimiter->getRemainingPageRequests($ipAddress, $endpoint, $userId);
if (!$isTest) {
// Set rate limit headers
header('X-RateLimit-Remaining: ' . $remaining);
header('X-RateLimit-Reset: ' . (time() + 60)); // Reset in 1 minute
@ -43,6 +45,11 @@ function checkRateLimit($database, $endpoint, $userId = null) {
exit;
}
// In test mode, just set the flash message
Feedback::flash('ERROR', 'DEFAULT', 'Too many requests. Please try again in a minute.', true);
return false;
}
// Record this request
$rateLimiter->recordPageRequest($ipAddress, $endpoint);
return true;

View File

@ -7,23 +7,54 @@
* This middleware should be included in all protected pages.
*/
// Start session if not already started
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
function applySessionMiddleware($config, $app_root) {
$isTest = defined('PHPUNIT_RUNNING');
// Check if user is logged in
if (!isset($_SESSION['USER_ID'])) {
// Access $_SESSION directly in test mode
if (!$isTest) {
// Start session if not already started
if (session_status() !== PHP_SESSION_ACTIVE && !headers_sent()) {
session_start([
'cookie_httponly' => 1,
'cookie_secure' => 1,
'cookie_samesite' => 'Strict',
'gc_maxlifetime' => 1440 // 24 minutes
]);
}
}
// Check if user is logged in
if (!isset($_SESSION['USER_ID'])) {
if (!$isTest) {
header('Location: ' . $app_root . '?page=login');
exit();
}
}
return false;
}
// Check session timeout
$session_timeout = isset($_SESSION['REMEMBER_ME']) ? (30 * 24 * 60 * 60) : 1440; // 30 days or 24 minutes
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $session_timeout)) {
// Check session timeout
$session_timeout = isset($_SESSION['REMEMBER_ME']) ? (30 * 24 * 60 * 60) : 1440; // 30 days or 24 minutes
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $session_timeout)) {
// Session has expired
$oldSessionData = $_SESSION;
$_SESSION = array();
if (!$isTest && session_status() === PHP_SESSION_ACTIVE) {
session_unset();
session_destroy();
// Start a new session to prevent errors
if (!headers_sent()) {
session_start([
'cookie_httponly' => 1,
'cookie_secure' => 1,
'cookie_samesite' => 'Strict',
'gc_maxlifetime' => 1440
]);
}
}
if (!$isTest && !headers_sent()) {
setcookie('username', '', [
'expires' => time() - 3600,
'path' => $config['folder'],
@ -32,18 +63,30 @@ if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] >
'httponly' => true,
'samesite' => 'Strict'
]);
}
if (!$isTest) {
header('Location: ' . $app_root . '?page=login&timeout=1');
exit();
}
}
return false;
}
// Update last activity time
$_SESSION['LAST_ACTIVITY'] = time();
// Update last activity time
$_SESSION['LAST_ACTIVITY'] = time();
// Regenerate session ID periodically (every 30 minutes)
if (!isset($_SESSION['CREATED'])) {
// Regenerate session ID periodically (every 30 minutes)
if (!isset($_SESSION['CREATED'])) {
$_SESSION['CREATED'] = time();
} else if (time() - $_SESSION['CREATED'] > 1800) {
} else if (time() - $_SESSION['CREATED'] > 1800) {
// Regenerate session ID and update creation time
if (!$isTest && !headers_sent() && session_status() === PHP_SESSION_ACTIVE) {
$oldData = $_SESSION;
session_regenerate_id(true);
$_SESSION = $oldData;
$_SESSION['CREATED'] = time();
}
}
return true;
}

View File

@ -0,0 +1,182 @@
<?php
require_once dirname(__DIR__, 4) . '/app/classes/database.php';
require_once dirname(__DIR__, 4) . '/app/classes/ratelimiter.php';
require_once dirname(__DIR__, 4) . '/app/includes/rate_limit_middleware.php';
use PHPUnit\Framework\TestCase;
class RateLimitMiddlewareTest extends TestCase
{
private $db;
private $rateLimiter;
protected function setUp(): void
{
parent::setUp();
// Set up test database
$this->db = new Database([
'type' => 'sqlite',
'dbFile' => ':memory:'
]);
// Create rate limiter table
$this->db->getConnection()->exec("CREATE TABLE pages_rate_limits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
endpoint TEXT NOT NULL,
request_time DATETIME DEFAULT CURRENT_TIMESTAMP
)");
// Create ip_whitelist table
$this->db->getConnection()->exec("CREATE TABLE ip_whitelist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
description TEXT,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
)");
// Create ip_blacklist table
$this->db->getConnection()->exec("CREATE TABLE ip_blacklist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)),
reason TEXT,
expiry_time TEXT NULL,
created_at TEXT DEFAULT (DATETIME('now')),
created_by TEXT
)");
$this->rateLimiter = new RateLimiter($this->db);
// Mock $_SERVER variables
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['REQUEST_URI'] = '/login';
$_SERVER['REQUEST_METHOD'] = 'POST';
// Define testing constant
if (!defined('TESTING')) {
define('TESTING', true);
}
}
protected function tearDown(): void
{
// Clean up rate limit records
$this->db->getConnection()->exec('DELETE FROM pages_rate_limits');
parent::tearDown();
}
public function testRateLimitMiddleware()
{
// Test multiple requests
for ($i = 1; $i <= 5; $i++) {
$result = checkRateLimit(['db' => $this->db], '/login');
if ($i <= 5) {
// First 5 requests should pass
$this->assertTrue($result);
} else {
// 6th and subsequent requests should be blocked
$this->assertFalse($result);
}
}
}
public function testRateLimitBypass()
{
// Test AJAX request bypass
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest';
$result = checkRateLimit(['db' => $this->db], '/login');
$this->assertTrue($result);
}
public function testRateLimitReset()
{
// Use up the rate limit
for ($i = 0; $i < 5; $i++) {
checkRateLimit(['db' => $this->db], '/login');
}
// Wait for rate limit to reset (use a short window for testing)
sleep(2);
// Should be able to make request again
$result = checkRateLimit(['db' => $this->db], '/login');
$this->assertTrue($result);
}
public function testDifferentEndpoints()
{
// Use up rate limit for login
for ($i = 0; $i < 5; $i++) {
checkRateLimit(['db' => $this->db], '/login');
}
// Should still be able to access different endpoint
$result = checkRateLimit(['db' => $this->db], '/dashboard');
$this->assertTrue($result);
}
public function testDifferentIpAddresses()
{
// Use up rate limit for first IP
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
for ($i = 0; $i < 5; $i++) {
checkRateLimit(['db' => $this->db], '/login');
}
// Different IP should not be affected
$_SERVER['REMOTE_ADDR'] = '127.0.0.2';
$result = checkRateLimit(['db' => $this->db], '/login');
$this->assertTrue($result);
}
public function testWhitelistedIp()
{
// Add IP to whitelist
$this->db->execute(
'INSERT INTO ip_whitelist (ip_address, description, created_by) VALUES (?, ?, ?)',
['127.0.0.1', 'Test whitelist', 'PHPUnit']
);
// Should be able to make more requests than limit
for ($i = 0; $i < 10; $i++) {
$result = checkRateLimit(['db' => $this->db], '/login');
$this->assertTrue($result);
}
}
public function testBlacklistedIp()
{
// Add IP to blacklist
$this->db->execute(
'INSERT INTO ip_blacklist (ip_address, reason, created_by) VALUES (?, ?, ?)',
['127.0.0.1', 'Test blacklist', 'PHPUnit']
);
// Should be blocked immediately
$result = checkRateLimit(['db' => $this->db], '/login');
$this->assertFalse($result);
}
public function testRateLimitPersistence()
{
// Use up some of the rate limit
for ($i = 0; $i < 2; $i++) {
checkRateLimit(['db' => $this->db], '/login');
}
// Destroy and restart session
//session_destroy();
//session_start();
// Should still count previous requests
$result = checkRateLimit(['db' => $this->db], '/login');
$this->assertTrue($result);
}
}

View File

@ -0,0 +1,110 @@
<?php
require_once dirname(__DIR__, 4) . '/app/includes/session_middleware.php';
use PHPUnit\Framework\TestCase;
class SessionMiddlewareTest extends TestCase
{
protected $config;
protected $app_root;
protected function setUp(): void
{
parent::setUp();
// Mock server variables
$_SERVER['HTTP_USER_AGENT'] = 'PHPUnit Test Browser';
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['HTTPS'] = 'on';
// Set up test config
$this->config = [
'folder' => '/app',
'domain' => 'localhost'
];
$this->app_root = 'https://localhost/app';
}
protected function tearDown(): void
{
parent::tearDown();
}
public function testSessionStart()
{
$_SESSION = ['USER_ID' => 1];
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertTrue($result);
$this->assertArrayHasKey('LAST_ACTIVITY', $_SESSION);
$this->assertArrayHasKey('CREATED', $_SESSION);
$this->assertArrayHasKey('USER_ID', $_SESSION);
$this->assertEquals(1, $_SESSION['USER_ID']);
}
public function testSessionTimeout()
{
$_SESSION = [
'USER_ID' => 1,
'LAST_ACTIVITY' => time() - 1500 // 25 minutes ago
];
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertFalse($result);
$this->assertArrayNotHasKey('USER_ID', $_SESSION, 'Session should be cleared after timeout');
}
public function testSessionRegeneration()
{
$now = time();
$_SESSION = [
'USER_ID' => 1,
'CREATED' => $now - 1900 // 31+ minutes ago
];
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertTrue($result);
$this->assertEquals(1, $_SESSION['USER_ID']);
$this->assertGreaterThanOrEqual($now - 1900, $_SESSION['CREATED']);
$this->assertLessThanOrEqual($now + 10, $_SESSION['CREATED']);
}
public function testRememberMe()
{
$_SESSION = [
'USER_ID' => 1,
'REMEMBER_ME' => true,
'LAST_ACTIVITY' => time() - 86500 // More than 24 hours ago
];
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertTrue($result);
$this->assertArrayHasKey('USER_ID', $_SESSION);
}
public function testNoUserSession()
{
$_SESSION = [];
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertFalse($result);
$this->assertArrayNotHasKey('USER_ID', $_SESSION);
}
public function testSessionHeaders()
{
$_SESSION = [
'USER_ID' => 1,
'LAST_ACTIVITY' => time() - 1500 // 25 minutes ago
];
$result = applySessionMiddleware($this->config, $this->app_root);
$this->assertFalse($result);
$this->assertArrayNotHasKey('USER_ID', $_SESSION, 'Session should be cleared after timeout');
}
}

View File

@ -0,0 +1,126 @@
<?php
/**
* Test Session Handler
*
* Provides session handling functionality for PHPUnit tests.
* This class ensures proper session management during testing.
*/
class TestSessionHandler implements SessionHandlerInterface
{
private static $initialized = false;
private $data = [];
/**
* Initialize session settings
*/
public static function init()
{
if (!self::$initialized && !headers_sent()) {
// Clean up any existing session
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time()-3600, '/');
}
$_SESSION = array();
if (session_status() === PHP_SESSION_ACTIVE) {
session_destroy();
}
// Set session configuration
session_name('jilo');
// Start a new session
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start([
'cookie_httponly' => 1,
'cookie_secure' => 1,
'cookie_samesite' => 'Strict',
'gc_maxlifetime' => 1440 // 24 minutes
]);
}
self::$initialized = true;
}
}
/**
* Start a fresh session
*/
public static function startSession()
{
// Clean up any existing session first
self::cleanupSession();
// Initialize new session
if (session_status() !== PHP_SESSION_ACTIVE && !headers_sent()) {
session_name('jilo');
session_start([
'cookie_httponly' => 1,
'cookie_secure' => 1,
'cookie_samesite' => 'Strict',
'gc_maxlifetime' => 1440
]);
self::$initialized = true;
}
}
/**
* Clean up the current session
*/
public static function cleanupSession()
{
if (session_status() === PHP_SESSION_ACTIVE) {
session_write_close();
}
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time()-3600, '/');
}
$_SESSION = array();
if (session_status() === PHP_SESSION_ACTIVE) {
session_destroy();
}
self::$initialized = false;
}
public function open($path, $name): bool
{
return true;
}
public function close(): bool
{
return true;
}
public function read($id): string|false
{
return $this->data[$id] ?? '';
}
public function write($id, $data): bool
{
$this->data[$id] = $data;
return true;
}
public function destroy($id): bool
{
unset($this->data[$id]);
return true;
}
public function gc($max_lifetime): int|false
{
return 0;
}
}

View File

@ -3,18 +3,14 @@
// Set test environment
define('PHPUNIT_RUNNING', true);
// Configure session before starting it
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1');
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.gc_maxlifetime', 1440);
// Start session if not already started
//if (session_status() === PHP_SESSION_NONE) {
// session_start();
//}
// Configure session before any output
if (!headers_sent()) {
// Configure session settings
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.gc_maxlifetime', 1440); // 24 minutes
}
// Load Composer's autoloader
require_once __DIR__ . '/vendor/autoload.php';
@ -33,26 +29,9 @@ $GLOBALS['config'] = [
'db' => [
'type' => 'sqlite',
'dbFile' => ':memory:'
],
'folder' => '/',
'domain' => 'localhost',
'login' => [
'max_attempts' => 5,
'lockout_time' => 900
]
];
// Initialize system_messages array
$GLOBALS['system_messages'] = [];
// Set up server variables
$_SERVER['PHP_SELF'] = '/index.php';
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['HTTP_USER_AGENT'] = 'PHPUnit Test Browser';
$_SERVER['HTTP_HOST'] = 'localhost';
$_SERVER['REQUEST_URI'] = '/?page=login';
$_SERVER['HTTPS'] = 'on';
// Define global connectDB function
if (!function_exists('connectDB')) {
function connectDB($config) {
@ -62,3 +41,9 @@ if (!function_exists('connectDB')) {
];
}
}
// Set up server variables
$_SERVER['PHP_SELF'] = '/index.php';
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['HTTP_USER_AGENT'] = 'PHPUnit Test Browser';
$_SERVER['HTTPS'] = 'on';

View File

@ -0,0 +1,7 @@
[Session]
session.cookie_httponly = 1
session.use_only_cookies = 1
session.cookie_secure = 1
session.cookie_samesite = "Strict"
session.gc_maxlifetime = 1440
session.use_strict_mode = 1