feat: Add comprehensive documentation suite and reorganize project structure
- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
This commit is contained in:
531
tests/Unit/AuthTest.php
Normal file
531
tests/Unit/AuthTest.php
Normal file
@@ -0,0 +1,531 @@
|
||||
<?php
|
||||
|
||||
namespace EasyStream\Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use VAuth;
|
||||
|
||||
class AuthTest extends TestCase
|
||||
{
|
||||
private $auth;
|
||||
private $testUserId;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->auth = VAuth::getInstance();
|
||||
|
||||
// Clear session data
|
||||
if (isset($_SESSION)) {
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
// Clear cookies
|
||||
$_COOKIE = [];
|
||||
|
||||
// Mock server variables
|
||||
$_SERVER = [
|
||||
'REQUEST_URI' => '/test',
|
||||
'REQUEST_METHOD' => 'GET',
|
||||
'HTTP_USER_AGENT' => 'PHPUnit Auth Test',
|
||||
'REMOTE_ADDR' => '127.0.0.1',
|
||||
'HTTPS' => '1'
|
||||
];
|
||||
|
||||
// Clean up any existing test data
|
||||
$this->cleanupTestData();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Clean up test data
|
||||
$this->cleanupTestData();
|
||||
|
||||
// Clear session
|
||||
if (isset($_SESSION)) {
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
$_COOKIE = [];
|
||||
}
|
||||
|
||||
private function cleanupTestData()
|
||||
{
|
||||
global $class_database;
|
||||
$db = $class_database->dbConnection();
|
||||
|
||||
// Clean up test users and related data
|
||||
$testEmails = ['test@example.com', 'testuser@example.com', 'newuser@example.com'];
|
||||
$testUsernames = ['testuser', 'newuser', 'authtest'];
|
||||
|
||||
foreach ($testEmails as $email) {
|
||||
$db->Execute("DELETE FROM db_sessions WHERE user_id IN (SELECT user_id FROM db_users WHERE email = ?)", [$email]);
|
||||
$db->Execute("DELETE FROM db_login_history WHERE email = ?", [$email]);
|
||||
$db->Execute("DELETE FROM db_users WHERE email = ?", [$email]);
|
||||
}
|
||||
|
||||
foreach ($testUsernames as $username) {
|
||||
$db->Execute("DELETE FROM db_sessions WHERE user_id IN (SELECT user_id FROM db_users WHERE username = ?)", [$username]);
|
||||
$db->Execute("DELETE FROM db_login_history WHERE username = ?", [$username]);
|
||||
$db->Execute("DELETE FROM db_users WHERE username = ?", [$username]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test VAuth singleton pattern
|
||||
*/
|
||||
public function testSingletonPattern()
|
||||
{
|
||||
$auth1 = VAuth::getInstance();
|
||||
$auth2 = VAuth::getInstance();
|
||||
|
||||
$this->assertSame($auth1, $auth2);
|
||||
$this->assertInstanceOf(VAuth::class, $auth1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test user registration with valid data
|
||||
*/
|
||||
public function testUserRegistrationSuccess()
|
||||
{
|
||||
$userData = [
|
||||
'username' => 'testuser',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'TestPassword123!'
|
||||
];
|
||||
|
||||
$result = $this->auth->register($userData);
|
||||
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertStringContainsString('Registration successful', $result['message']);
|
||||
$this->assertArrayHasKey('user_id', $result);
|
||||
|
||||
$this->testUserId = $result['user_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test user registration with invalid data
|
||||
*/
|
||||
public function testUserRegistrationValidation()
|
||||
{
|
||||
// Test missing username
|
||||
$result = $this->auth->register([
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'TestPassword123!'
|
||||
]);
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('username', $result['message']);
|
||||
|
||||
// Test missing email
|
||||
$result = $this->auth->register([
|
||||
'username' => 'testuser',
|
||||
'password' => 'TestPassword123!'
|
||||
]);
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('email', $result['message']);
|
||||
|
||||
// Test missing password
|
||||
$result = $this->auth->register([
|
||||
'username' => 'testuser',
|
||||
'email' => 'test@example.com'
|
||||
]);
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('password', $result['message']);
|
||||
|
||||
// Test invalid email
|
||||
$result = $this->auth->register([
|
||||
'username' => 'testuser',
|
||||
'email' => 'invalid-email',
|
||||
'password' => 'TestPassword123!'
|
||||
]);
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('Invalid email', $result['message']);
|
||||
|
||||
// Test weak password
|
||||
$result = $this->auth->register([
|
||||
'username' => 'testuser',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'weak'
|
||||
]);
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('8 characters', $result['message']);
|
||||
|
||||
// Test password without special characters
|
||||
$result = $this->auth->register([
|
||||
'username' => 'testuser',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'TestPassword123'
|
||||
]);
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('special character', $result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test duplicate user registration
|
||||
*/
|
||||
public function testDuplicateUserRegistration()
|
||||
{
|
||||
$userData = [
|
||||
'username' => 'testuser',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'TestPassword123!'
|
||||
];
|
||||
|
||||
// First registration should succeed
|
||||
$result1 = $this->auth->register($userData);
|
||||
$this->assertTrue($result1['success']);
|
||||
|
||||
// Second registration with same username should fail
|
||||
$result2 = $this->auth->register($userData);
|
||||
$this->assertFalse($result2['success']);
|
||||
$this->assertStringContainsString('already exists', $result2['message']);
|
||||
|
||||
// Registration with same email but different username should fail
|
||||
$userData['username'] = 'differentuser';
|
||||
$result3 = $this->auth->register($userData);
|
||||
$this->assertFalse($result3['success']);
|
||||
$this->assertStringContainsString('already exists', $result3['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test email verification
|
||||
*/
|
||||
public function testEmailVerification()
|
||||
{
|
||||
// Register a user first
|
||||
$userData = [
|
||||
'username' => 'testuser',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'TestPassword123!'
|
||||
];
|
||||
|
||||
$registerResult = $this->auth->register($userData);
|
||||
$this->assertTrue($registerResult['success']);
|
||||
|
||||
// Get verification token from database
|
||||
global $class_database;
|
||||
$db = $class_database->dbConnection();
|
||||
$sql = "SELECT verification_token FROM db_users WHERE email = ?";
|
||||
$result = $db->Execute($sql, ['test@example.com']);
|
||||
|
||||
$this->assertFalse($result->EOF);
|
||||
$token = $result->fields['verification_token'];
|
||||
$this->assertNotEmpty($token);
|
||||
|
||||
// Test email verification
|
||||
$verifyResult = $this->auth->verifyEmail($token);
|
||||
$this->assertTrue($verifyResult['success']);
|
||||
$this->assertStringContainsString('verified successfully', $verifyResult['message']);
|
||||
|
||||
// Test verification with invalid token
|
||||
$invalidResult = $this->auth->verifyEmail('invalid_token');
|
||||
$this->assertFalse($invalidResult['success']);
|
||||
$this->assertStringContainsString('Invalid', $invalidResult['message']);
|
||||
|
||||
// Test verification with already used token
|
||||
$usedResult = $this->auth->verifyEmail($token);
|
||||
$this->assertFalse($usedResult['success']);
|
||||
$this->assertStringContainsString('Invalid', $usedResult['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test user login with valid credentials
|
||||
*/
|
||||
public function testLoginSuccess()
|
||||
{
|
||||
// Create and verify a test user
|
||||
$this->createVerifiedTestUser();
|
||||
|
||||
// Test login with username
|
||||
$result = $this->auth->login('testuser', 'TestPassword123!');
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertStringContainsString('Login successful', $result['message']);
|
||||
$this->assertArrayHasKey('user', $result);
|
||||
$this->assertEquals('testuser', $result['user']['username']);
|
||||
|
||||
// Logout for next test
|
||||
$this->auth->logout();
|
||||
|
||||
// Test login with email
|
||||
$result = $this->auth->login('test@example.com', 'TestPassword123!');
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertEquals('test@example.com', $result['user']['email']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test login with invalid credentials
|
||||
*/
|
||||
public function testLoginFailure()
|
||||
{
|
||||
$this->createVerifiedTestUser();
|
||||
|
||||
// Test with wrong password
|
||||
$result = $this->auth->login('testuser', 'WrongPassword123!');
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('Invalid credentials', $result['message']);
|
||||
|
||||
// Test with non-existent user
|
||||
$result = $this->auth->login('nonexistent', 'TestPassword123!');
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('Invalid credentials', $result['message']);
|
||||
|
||||
// Test with empty credentials
|
||||
$result = $this->auth->login('', '');
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('required', $result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test login rate limiting
|
||||
*/
|
||||
public function testLoginRateLimiting()
|
||||
{
|
||||
$this->createVerifiedTestUser();
|
||||
|
||||
// Make multiple failed login attempts
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
$result = $this->auth->login('testuser', 'WrongPassword');
|
||||
}
|
||||
|
||||
// Next attempt should be rate limited
|
||||
$result = $this->auth->login('testuser', 'TestPassword123!');
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('Too many', $result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test session management
|
||||
*/
|
||||
public function testSessionManagement()
|
||||
{
|
||||
$this->createVerifiedTestUser();
|
||||
|
||||
// Test not authenticated initially
|
||||
$this->assertFalse($this->auth->isAuthenticated());
|
||||
$this->assertNull($this->auth->getCurrentUser());
|
||||
|
||||
// Login
|
||||
$result = $this->auth->login('testuser', 'TestPassword123!');
|
||||
$this->assertTrue($result['success']);
|
||||
|
||||
// Test authenticated after login
|
||||
$this->assertTrue($this->auth->isAuthenticated());
|
||||
|
||||
$currentUser = $this->auth->getCurrentUser();
|
||||
$this->assertNotNull($currentUser);
|
||||
$this->assertEquals('testuser', $currentUser['username']);
|
||||
$this->assertEquals('test@example.com', $currentUser['email']);
|
||||
|
||||
// Test logout
|
||||
$logoutResult = $this->auth->logout();
|
||||
$this->assertTrue($logoutResult['success']);
|
||||
|
||||
// Test not authenticated after logout
|
||||
$this->assertFalse($this->auth->isAuthenticated());
|
||||
$this->assertNull($this->auth->getCurrentUser());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test remember me functionality
|
||||
*/
|
||||
public function testRememberMeFunctionality()
|
||||
{
|
||||
$this->createVerifiedTestUser();
|
||||
|
||||
// Login with remember me
|
||||
$result = $this->auth->login('testuser', 'TestPassword123!', true);
|
||||
$this->assertTrue($result['success']);
|
||||
|
||||
// Check if remember token cookie would be set (we can't actually test cookie setting in unit tests)
|
||||
$this->assertTrue($this->auth->isAuthenticated());
|
||||
|
||||
// Logout
|
||||
$this->auth->logout();
|
||||
$this->assertFalse($this->auth->isAuthenticated());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test password reset request
|
||||
*/
|
||||
public function testPasswordResetRequest()
|
||||
{
|
||||
$this->createVerifiedTestUser();
|
||||
|
||||
// Test valid email
|
||||
$result = $this->auth->requestPasswordReset('test@example.com');
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertStringContainsString('reset link', $result['message']);
|
||||
|
||||
// Test invalid email format
|
||||
$result = $this->auth->requestPasswordReset('invalid-email');
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('Invalid email', $result['message']);
|
||||
|
||||
// Test non-existent email (should still return success for security)
|
||||
$result = $this->auth->requestPasswordReset('nonexistent@example.com');
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertStringContainsString('reset link', $result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test password reset with token
|
||||
*/
|
||||
public function testPasswordReset()
|
||||
{
|
||||
$this->createVerifiedTestUser();
|
||||
|
||||
// Request password reset
|
||||
$resetResult = $this->auth->requestPasswordReset('test@example.com');
|
||||
$this->assertTrue($resetResult['success']);
|
||||
|
||||
// Get reset token from database
|
||||
global $class_database;
|
||||
$db = $class_database->dbConnection();
|
||||
$sql = "SELECT reset_token FROM db_users WHERE email = ?";
|
||||
$result = $db->Execute($sql, ['test@example.com']);
|
||||
|
||||
$this->assertFalse($result->EOF);
|
||||
$token = $result->fields['reset_token'];
|
||||
$this->assertNotEmpty($token);
|
||||
|
||||
// Test password reset with valid token
|
||||
$newPassword = 'NewPassword123!';
|
||||
$resetResult = $this->auth->resetPassword($token, $newPassword);
|
||||
$this->assertTrue($resetResult['success']);
|
||||
$this->assertStringContainsString('reset successfully', $resetResult['message']);
|
||||
|
||||
// Test login with new password
|
||||
$loginResult = $this->auth->login('testuser', $newPassword);
|
||||
$this->assertTrue($loginResult['success']);
|
||||
|
||||
// Test old password no longer works
|
||||
$this->auth->logout();
|
||||
$oldLoginResult = $this->auth->login('testuser', 'TestPassword123!');
|
||||
$this->assertFalse($oldLoginResult['success']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test password reset validation
|
||||
*/
|
||||
public function testPasswordResetValidation()
|
||||
{
|
||||
// Test invalid token
|
||||
$result = $this->auth->resetPassword('invalid_token', 'NewPassword123!');
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('Invalid', $result['message']);
|
||||
|
||||
// Test weak password
|
||||
$result = $this->auth->resetPassword('some_token', 'weak');
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('8 characters', $result['message']);
|
||||
|
||||
// Test password without special characters
|
||||
$result = $this->auth->resetPassword('some_token', 'NewPassword123');
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('special character', $result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test password reset rate limiting
|
||||
*/
|
||||
public function testPasswordResetRateLimiting()
|
||||
{
|
||||
$this->createVerifiedTestUser();
|
||||
|
||||
// Make multiple password reset requests
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
$this->auth->requestPasswordReset('test@example.com');
|
||||
}
|
||||
|
||||
// Next request should be rate limited
|
||||
$result = $this->auth->requestPasswordReset('test@example.com');
|
||||
$this->assertFalse($result['success']);
|
||||
$this->assertStringContainsString('Too many', $result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test session security features
|
||||
*/
|
||||
public function testSessionSecurity()
|
||||
{
|
||||
$this->createVerifiedTestUser();
|
||||
|
||||
// Login
|
||||
$this->auth->login('testuser', 'TestPassword123!');
|
||||
$this->assertTrue($this->auth->isAuthenticated());
|
||||
|
||||
$originalSessionId = session_id();
|
||||
|
||||
// Simulate session regeneration (happens during login)
|
||||
$this->assertNotEmpty($originalSessionId);
|
||||
|
||||
// Test session data integrity
|
||||
$this->assertEquals('testuser', $_SESSION['USERNAME']);
|
||||
$this->assertEquals('test@example.com', $_SESSION['EMAIL']);
|
||||
$this->assertArrayHasKey('SESSION_TOKEN', $_SESSION);
|
||||
$this->assertArrayHasKey('LOGIN_TIME', $_SESSION);
|
||||
$this->assertArrayHasKey('IP_ADDRESS', $_SESSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a verified test user
|
||||
*/
|
||||
private function createVerifiedTestUser()
|
||||
{
|
||||
$userData = [
|
||||
'username' => 'testuser',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'TestPassword123!'
|
||||
];
|
||||
|
||||
$registerResult = $this->auth->register($userData);
|
||||
$this->assertTrue($registerResult['success']);
|
||||
|
||||
// Get and use verification token
|
||||
global $class_database;
|
||||
$db = $class_database->dbConnection();
|
||||
$sql = "SELECT verification_token FROM db_users WHERE email = ?";
|
||||
$result = $db->Execute($sql, ['test@example.com']);
|
||||
|
||||
if (!$result->EOF) {
|
||||
$token = $result->fields['verification_token'];
|
||||
$this->auth->verifyEmail($token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test edge cases and error handling
|
||||
*/
|
||||
public function testEdgeCases()
|
||||
{
|
||||
// Test registration with null values
|
||||
$result = $this->auth->register([
|
||||
'username' => null,
|
||||
'email' => null,
|
||||
'password' => null
|
||||
]);
|
||||
$this->assertFalse($result['success']);
|
||||
|
||||
// Test login with null values
|
||||
$result = $this->auth->login(null, null);
|
||||
$this->assertFalse($result['success']);
|
||||
|
||||
// Test very long username
|
||||
$longUsername = str_repeat('a', 100);
|
||||
$result = $this->auth->register([
|
||||
'username' => $longUsername,
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'TestPassword123!'
|
||||
]);
|
||||
$this->assertFalse($result['success']);
|
||||
|
||||
// Test SQL injection attempts
|
||||
$maliciousUsername = "admin'; DROP TABLE db_users; --";
|
||||
$result = $this->auth->register([
|
||||
'username' => $maliciousUsername,
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'TestPassword123!'
|
||||
]);
|
||||
// Should either fail validation or be safely escaped
|
||||
$this->assertIsArray($result);
|
||||
}
|
||||
}
|
||||
157
tests/Unit/ContentUploadTest.php
Normal file
157
tests/Unit/ContentUploadTest.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ContentUploadTest extends TestCase
|
||||
{
|
||||
private $storageDir;
|
||||
private $originalUploadDir;
|
||||
private $originalUploadUrl;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->storageDir = __DIR__ . '/../temp/uploads';
|
||||
if (!is_dir($this->storageDir)) {
|
||||
mkdir($this->storageDir, 0755, true);
|
||||
}
|
||||
|
||||
global $cfg;
|
||||
$this->originalUploadDir = $cfg['upload_files_dir'] ?? null;
|
||||
$this->originalUploadUrl = $cfg['upload_files_url'] ?? null;
|
||||
$cfg['upload_files_dir'] = $this->storageDir;
|
||||
$cfg['upload_files_url'] = 'https://example.com/uploads';
|
||||
|
||||
// Reset singleton for clean state between tests
|
||||
$reflection = new ReflectionProperty(VContent::class, 'instance');
|
||||
$reflection->setAccessible(true);
|
||||
$reflection->setValue(null);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
global $cfg;
|
||||
if ($this->originalUploadDir !== null) {
|
||||
$cfg['upload_files_dir'] = $this->originalUploadDir;
|
||||
}
|
||||
if ($this->originalUploadUrl !== null) {
|
||||
$cfg['upload_files_url'] = $this->originalUploadUrl;
|
||||
}
|
||||
|
||||
if (is_dir($this->storageDir)) {
|
||||
$this->removeDirectory($this->storageDir);
|
||||
}
|
||||
|
||||
$reflection = new ReflectionProperty(VContent::class, 'instance');
|
||||
$reflection->setAccessible(true);
|
||||
$reflection->setValue(null);
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testHandleUploadStoresFileSuccessfully(): void
|
||||
{
|
||||
$content = VContent::getInstance();
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'upload');
|
||||
file_put_contents($tmpFile, str_repeat('A', 1024));
|
||||
|
||||
$file = [
|
||||
'name' => 'test-video.mp4',
|
||||
'tmp_name' => $tmpFile,
|
||||
'size' => filesize($tmpFile),
|
||||
'error' => UPLOAD_ERR_OK,
|
||||
'type' => 'video/mp4'
|
||||
];
|
||||
|
||||
$result = $content->handleUpload($file, 123, [
|
||||
'type' => 'video',
|
||||
'store_metadata' => false
|
||||
]);
|
||||
|
||||
$this->assertTrue($result['success'], 'Expected upload to succeed');
|
||||
$this->assertArrayHasKey('data', $result);
|
||||
|
||||
$data = $result['data'];
|
||||
$this->assertEquals('video', $data['type']);
|
||||
$this->assertNotEmpty($data['stored_path']);
|
||||
|
||||
$storedPath = $this->storageDir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $data['stored_path']);
|
||||
$this->assertFileExists($storedPath, 'Stored file should exist on disk');
|
||||
|
||||
$progress = $content->getUploadProgress($data['upload_id']);
|
||||
$this->assertEquals('completed', $progress['status']);
|
||||
$this->assertEquals(100, $progress['progress']);
|
||||
}
|
||||
|
||||
public function testHandleUploadRejectsInvalidMimeType(): void
|
||||
{
|
||||
$content = VContent::getInstance();
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'upload');
|
||||
file_put_contents($tmpFile, "<?php echo 'malicious'; ?>");
|
||||
|
||||
$file = [
|
||||
'name' => 'malware.php',
|
||||
'tmp_name' => $tmpFile,
|
||||
'size' => filesize($tmpFile),
|
||||
'error' => UPLOAD_ERR_OK,
|
||||
'type' => 'text/x-php'
|
||||
];
|
||||
|
||||
$result = $content->handleUpload($file, 321, [
|
||||
'type' => 'document',
|
||||
'store_metadata' => false
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['success'], 'Upload should fail for dangerous MIME types');
|
||||
$this->assertNotEmpty($result['message']);
|
||||
}
|
||||
|
||||
public function testProgressLifecycle(): void
|
||||
{
|
||||
$content = VContent::getInstance();
|
||||
$uploadId = 'progress_test_' . bin2hex(random_bytes(4));
|
||||
|
||||
$content->initProgress($uploadId, 2000);
|
||||
$progress = $content->getUploadProgress($uploadId);
|
||||
$this->assertEquals('in_progress', $progress['status']);
|
||||
$this->assertEquals(0, $progress['progress']);
|
||||
|
||||
$content->updateProgress($uploadId, 1000, 2000);
|
||||
$progress = $content->getUploadProgress($uploadId);
|
||||
$this->assertEquals(50, $progress['progress']);
|
||||
|
||||
$content->completeProgress($uploadId);
|
||||
$progress = $content->getUploadProgress($uploadId);
|
||||
$this->assertEquals('completed', $progress['status']);
|
||||
$this->assertEquals(100, $progress['progress']);
|
||||
|
||||
$content->cleanupProgress($uploadId);
|
||||
$progress = $content->getUploadProgress($uploadId);
|
||||
$this->assertEquals('unknown', $progress['status']);
|
||||
}
|
||||
|
||||
private function removeDirectory($dir): void
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$items = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item->isDir()) {
|
||||
@rmdir($item->getRealPath());
|
||||
} else {
|
||||
@unlink($item->getRealPath());
|
||||
}
|
||||
}
|
||||
|
||||
@rmdir($dir);
|
||||
}
|
||||
}
|
||||
224
tests/Unit/ErrorHandlerTest.php
Normal file
224
tests/Unit/ErrorHandlerTest.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace EasyStream\Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use VErrorHandler;
|
||||
|
||||
class ErrorHandlerTest extends TestCase
|
||||
{
|
||||
private $errorHandler;
|
||||
private $originalErrorReporting;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->errorHandler = VErrorHandler::getInstance();
|
||||
$this->originalErrorReporting = error_reporting();
|
||||
|
||||
// Set up test environment
|
||||
global $cfg;
|
||||
$cfg['debug_mode'] = false; // Test production mode
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Restore original error reporting
|
||||
error_reporting($this->originalErrorReporting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test error handler singleton pattern
|
||||
*/
|
||||
public function testSingletonPattern()
|
||||
{
|
||||
$handler1 = VErrorHandler::getInstance();
|
||||
$handler2 = VErrorHandler::getInstance();
|
||||
|
||||
$this->assertSame($handler1, $handler2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test error type detection
|
||||
*/
|
||||
public function testErrorTypeDetection()
|
||||
{
|
||||
// Test that error handler can be instantiated without errors
|
||||
$this->assertInstanceOf(VErrorHandler::class, $this->errorHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test error logging functionality
|
||||
*/
|
||||
public function testErrorLogging()
|
||||
{
|
||||
// Test application error logging
|
||||
$this->errorHandler->logApplicationError('Test application error', ['test' => true]);
|
||||
|
||||
// Test validation error logging
|
||||
$this->errorHandler->logValidationError('email', 'invalid-email', 'email_format', ['form' => 'registration']);
|
||||
|
||||
// Test authentication error logging
|
||||
$this->errorHandler->logAuthError('Invalid credentials', 'testuser', ['ip' => '127.0.0.1']);
|
||||
|
||||
// Verify no exceptions were thrown
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test error handling in production mode
|
||||
*/
|
||||
public function testProductionMode()
|
||||
{
|
||||
global $cfg;
|
||||
$cfg['debug_mode'] = false;
|
||||
|
||||
// Create new instance to test production mode
|
||||
$handler = VErrorHandler::getInstance();
|
||||
|
||||
$this->assertInstanceOf(VErrorHandler::class, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test error handling in debug mode
|
||||
*/
|
||||
public function testDebugMode()
|
||||
{
|
||||
global $cfg;
|
||||
$cfg['debug_mode'] = true;
|
||||
|
||||
// Create new instance to test debug mode
|
||||
$handler = VErrorHandler::getInstance();
|
||||
|
||||
$this->assertInstanceOf(VErrorHandler::class, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test error severity mapping
|
||||
*/
|
||||
public function testErrorSeverityMapping()
|
||||
{
|
||||
// Test different error severities are handled properly
|
||||
$severities = [
|
||||
E_ERROR,
|
||||
E_WARNING,
|
||||
E_NOTICE,
|
||||
E_USER_ERROR,
|
||||
E_USER_WARNING,
|
||||
E_USER_NOTICE
|
||||
];
|
||||
|
||||
foreach ($severities as $severity) {
|
||||
// Test that each severity can be processed
|
||||
$this->assertIsInt($severity);
|
||||
}
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test custom error contexts
|
||||
*/
|
||||
public function testCustomErrorContexts()
|
||||
{
|
||||
$contexts = [
|
||||
['user_id' => 123, 'action' => 'upload'],
|
||||
['ip' => '192.168.1.1', 'user_agent' => 'Test Browser'],
|
||||
['request_id' => 'req_123', 'session_id' => 'sess_456']
|
||||
];
|
||||
|
||||
foreach ($contexts as $context) {
|
||||
$this->errorHandler->logApplicationError('Test error with context', $context);
|
||||
}
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test error message sanitization
|
||||
*/
|
||||
public function testErrorMessageSanitization()
|
||||
{
|
||||
$maliciousMessages = [
|
||||
'Error with <script>alert("xss")</script>',
|
||||
'Database error: SELECT * FROM users WHERE password = "secret"',
|
||||
'Path traversal: ../../../etc/passwd'
|
||||
];
|
||||
|
||||
foreach ($maliciousMessages as $message) {
|
||||
$this->errorHandler->logApplicationError($message);
|
||||
}
|
||||
|
||||
// Verify no exceptions were thrown
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test error rate limiting
|
||||
*/
|
||||
public function testErrorRateLimiting()
|
||||
{
|
||||
// Log multiple similar errors rapidly
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$this->errorHandler->logApplicationError('Repeated error message');
|
||||
}
|
||||
|
||||
// Verify system handles repeated errors gracefully
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test memory usage during error handling
|
||||
*/
|
||||
public function testMemoryUsageDuringErrorHandling()
|
||||
{
|
||||
$initialMemory = memory_get_usage();
|
||||
|
||||
// Generate multiple errors
|
||||
for ($i = 0; $i < 100; $i++) {
|
||||
$this->errorHandler->logApplicationError("Memory test error {$i}");
|
||||
}
|
||||
|
||||
$finalMemory = memory_get_usage();
|
||||
|
||||
// Verify memory usage didn't grow excessively
|
||||
$memoryIncrease = $finalMemory - $initialMemory;
|
||||
$this->assertLessThan(10 * 1024 * 1024, $memoryIncrease); // Less than 10MB increase
|
||||
}
|
||||
|
||||
/**
|
||||
* Test error handling with invalid data
|
||||
*/
|
||||
public function testErrorHandlingWithInvalidData()
|
||||
{
|
||||
// Test with null values
|
||||
$this->errorHandler->logApplicationError(null);
|
||||
|
||||
// Test with empty strings
|
||||
$this->errorHandler->logApplicationError('');
|
||||
|
||||
// Test with very long messages
|
||||
$longMessage = str_repeat('A', 10000);
|
||||
$this->errorHandler->logApplicationError($longMessage);
|
||||
|
||||
// Test with special characters
|
||||
$specialMessage = "Error with unicode: 世界 and emojis: 🚨";
|
||||
$this->errorHandler->logApplicationError($specialMessage);
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test concurrent error handling
|
||||
*/
|
||||
public function testConcurrentErrorHandling()
|
||||
{
|
||||
// Simulate concurrent errors
|
||||
for ($i = 0; $i < 50; $i++) {
|
||||
$this->errorHandler->logApplicationError("Concurrent error {$i}");
|
||||
$this->errorHandler->logValidationError("field{$i}", "value{$i}", 'required');
|
||||
$this->errorHandler->logAuthError("Auth error {$i}", "user{$i}");
|
||||
}
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
0
tests/Unit/LoggerTest.php
Normal file
0
tests/Unit/LoggerTest.php
Normal file
448
tests/Unit/RBACTest.php
Normal file
448
tests/Unit/RBACTest.php
Normal file
@@ -0,0 +1,448 @@
|
||||
<?php
|
||||
|
||||
namespace EasyStream\Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use VRBAC;
|
||||
use VAuth;
|
||||
|
||||
class RBACTest extends TestCase
|
||||
{
|
||||
private $rbac;
|
||||
private $auth;
|
||||
private $testUserId;
|
||||
private $adminUserId;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->rbac = VRBAC::getInstance();
|
||||
$this->auth = VAuth::getInstance();
|
||||
|
||||
// Clear session data
|
||||
if (isset($_SESSION)) {
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
// Mock server variables
|
||||
$_SERVER = [
|
||||
'REQUEST_URI' => '/test',
|
||||
'REQUEST_METHOD' => 'GET',
|
||||
'HTTP_USER_AGENT' => 'PHPUnit RBAC Test',
|
||||
'REMOTE_ADDR' => '127.0.0.1',
|
||||
'HTTP_ACCEPT' => 'text/html'
|
||||
];
|
||||
|
||||
// Clean up any existing test data
|
||||
$this->cleanupTestData();
|
||||
|
||||
// Create test users
|
||||
$this->createTestUsers();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Clean up test data
|
||||
$this->cleanupTestData();
|
||||
|
||||
// Clear session
|
||||
if (isset($_SESSION)) {
|
||||
$_SESSION = [];
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupTestData()
|
||||
{
|
||||
global $class_database;
|
||||
$db = $class_database->dbConnection();
|
||||
|
||||
// Clean up test users and related data
|
||||
$testEmails = ['rbactest@example.com', 'rbacadmin@example.com', 'rbacmember@example.com'];
|
||||
$testUsernames = ['rbactest', 'rbacadmin', 'rbacmember'];
|
||||
|
||||
foreach ($testEmails as $email) {
|
||||
$userId = $db->GetOne("SELECT user_id FROM db_users WHERE email = ?", [$email]);
|
||||
if ($userId) {
|
||||
$db->Execute("DELETE FROM db_user_permissions WHERE user_id = ?", [$userId]);
|
||||
$db->Execute("DELETE FROM db_role_history WHERE user_id = ?", [$userId]);
|
||||
$db->Execute("DELETE FROM db_user_suspensions WHERE user_id = ?", [$userId]);
|
||||
$db->Execute("DELETE FROM db_user_bans WHERE user_id = ?", [$userId]);
|
||||
$db->Execute("DELETE FROM db_sessions WHERE user_id = ?", [$userId]);
|
||||
$db->Execute("DELETE FROM db_users WHERE user_id = ?", [$userId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createTestUsers()
|
||||
{
|
||||
// Create regular test user
|
||||
$userData = [
|
||||
'username' => 'rbactest',
|
||||
'email' => 'rbactest@example.com',
|
||||
'password' => 'TestPassword123!'
|
||||
];
|
||||
|
||||
$result = $this->auth->register($userData);
|
||||
if ($result['success']) {
|
||||
$this->testUserId = $result['user_id'];
|
||||
|
||||
// Verify email
|
||||
global $class_database;
|
||||
$db = $class_database->dbConnection();
|
||||
$db->Execute("UPDATE db_users SET email_verified = 1 WHERE user_id = ?", [$this->testUserId]);
|
||||
}
|
||||
|
||||
// Create admin test user
|
||||
$adminData = [
|
||||
'username' => 'rbacadmin',
|
||||
'email' => 'rbacadmin@example.com',
|
||||
'password' => 'AdminPassword123!'
|
||||
];
|
||||
|
||||
$adminResult = $this->auth->register($adminData);
|
||||
if ($adminResult['success']) {
|
||||
$this->adminUserId = $adminResult['user_id'];
|
||||
|
||||
// Set as admin and verify
|
||||
global $class_database;
|
||||
$db = $class_database->dbConnection();
|
||||
$db->Execute("UPDATE db_users SET role = 'admin', email_verified = 1 WHERE user_id = ?", [$this->adminUserId]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test RBAC singleton pattern
|
||||
*/
|
||||
public function testSingletonPattern()
|
||||
{
|
||||
$rbac1 = VRBAC::getInstance();
|
||||
$rbac2 = VRBAC::getInstance();
|
||||
|
||||
$this->assertSame($rbac1, $rbac2);
|
||||
$this->assertInstanceOf(VRBAC::class, $rbac1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test role hierarchy
|
||||
*/
|
||||
public function testRoleHierarchy()
|
||||
{
|
||||
// Test role levels
|
||||
$this->assertTrue($this->rbac->hasRole('guest', $this->testUserId));
|
||||
$this->assertTrue($this->rbac->hasRole('member', $this->testUserId));
|
||||
$this->assertFalse($this->rbac->hasRole('admin', $this->testUserId));
|
||||
|
||||
$this->assertTrue($this->rbac->hasRole('guest', $this->adminUserId));
|
||||
$this->assertTrue($this->rbac->hasRole('member', $this->adminUserId));
|
||||
$this->assertTrue($this->rbac->hasRole('admin', $this->adminUserId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test basic permission checking
|
||||
*/
|
||||
public function testBasicPermissions()
|
||||
{
|
||||
// Test member permissions
|
||||
$this->assertTrue($this->rbac->hasPermission('content.view', $this->testUserId));
|
||||
$this->assertTrue($this->rbac->hasPermission('content.create', $this->testUserId));
|
||||
$this->assertFalse($this->rbac->hasPermission('admin.dashboard', $this->testUserId));
|
||||
|
||||
// Test admin permissions
|
||||
$this->assertTrue($this->rbac->hasPermission('content.view', $this->adminUserId));
|
||||
$this->assertTrue($this->rbac->hasPermission('admin.dashboard', $this->adminUserId));
|
||||
$this->assertTrue($this->rbac->hasPermission('user.ban', $this->adminUserId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test guest permissions
|
||||
*/
|
||||
public function testGuestPermissions()
|
||||
{
|
||||
// Test guest permissions without user ID (not logged in)
|
||||
$this->assertTrue($this->rbac->hasPermission('content.view'));
|
||||
$this->assertTrue($this->rbac->hasPermission('comment.view'));
|
||||
$this->assertFalse($this->rbac->hasPermission('content.create'));
|
||||
$this->assertFalse($this->rbac->hasPermission('admin.dashboard'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test custom user permissions
|
||||
*/
|
||||
public function testCustomUserPermissions()
|
||||
{
|
||||
// Grant custom permission
|
||||
$result = $this->rbac->grantPermission($this->testUserId, 'feature.beta', $this->adminUserId);
|
||||
$this->assertTrue($result);
|
||||
|
||||
// Test custom permission
|
||||
$this->assertTrue($this->rbac->hasPermission('feature.beta', $this->testUserId));
|
||||
|
||||
// Revoke custom permission
|
||||
$revokeResult = $this->rbac->revokePermission($this->testUserId, 'feature.beta', $this->adminUserId);
|
||||
$this->assertTrue($revokeResult);
|
||||
|
||||
// Test permission is revoked
|
||||
$this->assertFalse($this->rbac->hasPermission('feature.beta', $this->testUserId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test permission expiration
|
||||
*/
|
||||
public function testPermissionExpiration()
|
||||
{
|
||||
// Grant permission with expiration
|
||||
$expiresAt = date('Y-m-d H:i:s', time() + 3600); // 1 hour from now
|
||||
$result = $this->rbac->grantPermission($this->testUserId, 'upload.large_files', $this->adminUserId, $expiresAt);
|
||||
$this->assertTrue($result);
|
||||
|
||||
// Test permission is active
|
||||
$this->assertTrue($this->rbac->hasPermission('upload.large_files', $this->testUserId));
|
||||
|
||||
// Test with expired permission (simulate by setting past date)
|
||||
global $class_database;
|
||||
$db = $class_database->dbConnection();
|
||||
$db->Execute("UPDATE db_user_permissions SET expires_at = ? WHERE user_id = ? AND permission = ?",
|
||||
[date('Y-m-d H:i:s', time() - 3600), $this->testUserId, 'upload.large_files']);
|
||||
|
||||
// Test permission is expired
|
||||
$this->assertFalse($this->rbac->hasPermission('upload.large_files', $this->testUserId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test multiple permission checking
|
||||
*/
|
||||
public function testMultiplePermissions()
|
||||
{
|
||||
$permissions = ['content.view', 'content.create', 'comment.create'];
|
||||
|
||||
// Test hasAnyPermission
|
||||
$this->assertTrue($this->rbac->hasAnyPermission($permissions, $this->testUserId));
|
||||
$this->assertTrue($this->rbac->hasAnyPermission(['admin.dashboard', 'content.view'], $this->testUserId));
|
||||
$this->assertFalse($this->rbac->hasAnyPermission(['admin.dashboard', 'admin.system'], $this->testUserId));
|
||||
|
||||
// Test hasAllPermissions
|
||||
$this->assertTrue($this->rbac->hasAllPermissions($permissions, $this->testUserId));
|
||||
$this->assertFalse($this->rbac->hasAllPermissions(['content.view', 'admin.dashboard'], $this->testUserId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test role changes
|
||||
*/
|
||||
public function testRoleChanges()
|
||||
{
|
||||
// Change user role
|
||||
$result = $this->rbac->changeUserRole($this->testUserId, 'verified', $this->adminUserId, 'Test role change');
|
||||
$this->assertTrue($result);
|
||||
|
||||
// Test new role permissions
|
||||
$this->assertTrue($this->rbac->hasRole('verified', $this->testUserId));
|
||||
$this->assertTrue($this->rbac->hasPermission('content.publish', $this->testUserId));
|
||||
|
||||
// Test invalid role change
|
||||
$invalidResult = $this->rbac->changeUserRole($this->testUserId, 'invalid_role', $this->adminUserId);
|
||||
$this->assertFalse($invalidResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test user suspension
|
||||
*/
|
||||
public function testUserSuspension()
|
||||
{
|
||||
// Suspend user
|
||||
$result = $this->rbac->suspendUser($this->testUserId, 'Test suspension', $this->adminUserId);
|
||||
$this->assertTrue($result);
|
||||
|
||||
// Test suspended user has no permissions
|
||||
$this->assertFalse($this->rbac->hasPermission('content.view', $this->testUserId));
|
||||
$this->assertFalse($this->rbac->hasPermission('content.create', $this->testUserId));
|
||||
|
||||
// Reinstate user
|
||||
$reinstateResult = $this->rbac->reinstateUser($this->testUserId, 'Test reinstatement', $this->adminUserId);
|
||||
$this->assertTrue($reinstateResult);
|
||||
|
||||
// Test reinstated user has permissions again
|
||||
$this->assertTrue($this->rbac->hasPermission('content.view', $this->testUserId));
|
||||
$this->assertTrue($this->rbac->hasPermission('content.create', $this->testUserId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test user banning
|
||||
*/
|
||||
public function testUserBanning()
|
||||
{
|
||||
// Ban user
|
||||
$result = $this->rbac->banUser($this->testUserId, 'Test ban', $this->adminUserId, false);
|
||||
$this->assertTrue($result);
|
||||
|
||||
// Test banned user has no permissions
|
||||
$this->assertFalse($this->rbac->hasPermission('content.view', $this->testUserId));
|
||||
$this->assertFalse($this->rbac->hasRole('member', $this->testUserId));
|
||||
|
||||
// Reinstate user
|
||||
$reinstateResult = $this->rbac->reinstateUser($this->testUserId, 'Test unban', $this->adminUserId);
|
||||
$this->assertTrue($reinstateResult);
|
||||
|
||||
// Test unbanned user has permissions again
|
||||
$this->assertTrue($this->rbac->hasPermission('content.view', $this->testUserId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test context-based permissions
|
||||
*/
|
||||
public function testContextPermissions()
|
||||
{
|
||||
// Test content ownership context
|
||||
$context = ['content_owner_id' => $this->testUserId];
|
||||
|
||||
// User should be able to edit their own content
|
||||
$this->assertTrue($this->rbac->hasPermission('content.edit', $this->testUserId, $context));
|
||||
$this->assertTrue($this->rbac->hasPermission('content.delete', $this->testUserId, $context));
|
||||
|
||||
// User should not be able to edit others' content without permission
|
||||
$otherContext = ['content_owner_id' => $this->adminUserId];
|
||||
$this->assertFalse($this->rbac->hasPermission('content.moderate', $this->testUserId, $otherContext));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test permission middleware
|
||||
*/
|
||||
public function testPermissionMiddleware()
|
||||
{
|
||||
// Mock current user session
|
||||
$_SESSION['USER_ID'] = $this->testUserId;
|
||||
$_SESSION['USERNAME'] = 'rbactest';
|
||||
$_SESSION['EMAIL'] = 'rbactest@example.com';
|
||||
$_SESSION['ROLE'] = 'member';
|
||||
$_SESSION['LOGIN_TIME'] = time();
|
||||
$_SESSION['LAST_ACTIVITY'] = time();
|
||||
|
||||
// Test successful permission check
|
||||
$this->expectOutputString(''); // No output expected for successful check
|
||||
ob_start();
|
||||
$result = $this->rbac->requirePermission('content.view');
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertTrue($result);
|
||||
$this->assertEmpty($output);
|
||||
|
||||
// Clear session for next test
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test role middleware
|
||||
*/
|
||||
public function testRoleMiddleware()
|
||||
{
|
||||
// Mock current user session
|
||||
$_SESSION['USER_ID'] = $this->adminUserId;
|
||||
$_SESSION['USERNAME'] = 'rbacadmin';
|
||||
$_SESSION['EMAIL'] = 'rbacadmin@example.com';
|
||||
$_SESSION['ROLE'] = 'admin';
|
||||
$_SESSION['LOGIN_TIME'] = time();
|
||||
$_SESSION['LAST_ACTIVITY'] = time();
|
||||
|
||||
// Test successful role check
|
||||
$this->expectOutputString(''); // No output expected for successful check
|
||||
ob_start();
|
||||
$result = $this->rbac->requireRole('admin');
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertTrue($result);
|
||||
$this->assertEmpty($output);
|
||||
|
||||
// Clear session
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting user permissions
|
||||
*/
|
||||
public function testGetUserPermissions()
|
||||
{
|
||||
$permissions = $this->rbac->getUserPermissions($this->testUserId);
|
||||
|
||||
$this->assertIsArray($permissions);
|
||||
$this->assertContains('content.view', $permissions);
|
||||
$this->assertContains('content.create', $permissions);
|
||||
$this->assertNotContains('admin.dashboard', $permissions);
|
||||
|
||||
// Test admin permissions
|
||||
$adminPermissions = $this->rbac->getUserPermissions($this->adminUserId);
|
||||
$this->assertContains('admin.dashboard', $adminPermissions);
|
||||
$this->assertContains('user.ban', $adminPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting role permissions
|
||||
*/
|
||||
public function testGetRolePermissions()
|
||||
{
|
||||
$memberPermissions = $this->rbac->getRolePermissions('member');
|
||||
$this->assertIsArray($memberPermissions);
|
||||
$this->assertContains('content.view', $memberPermissions);
|
||||
$this->assertContains('content.create', $memberPermissions);
|
||||
|
||||
$adminPermissions = $this->rbac->getRolePermissions('admin');
|
||||
$this->assertContains('admin.dashboard', $adminPermissions);
|
||||
$this->assertContains('user.ban', $adminPermissions);
|
||||
|
||||
$guestPermissions = $this->rbac->getRolePermissions('guest');
|
||||
$this->assertContains('content.view', $guestPermissions);
|
||||
$this->assertNotContains('content.create', $guestPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test permission validation edge cases
|
||||
*/
|
||||
public function testPermissionEdgeCases()
|
||||
{
|
||||
// Test with non-existent user
|
||||
$this->assertFalse($this->rbac->hasPermission('content.view', 99999));
|
||||
|
||||
// Test with invalid permission
|
||||
$this->assertFalse($this->rbac->hasPermission('invalid.permission', $this->testUserId));
|
||||
|
||||
// Test with null user ID and no session
|
||||
$this->assertTrue($this->rbac->hasPermission('content.view')); // Should check guest permissions
|
||||
|
||||
// Test with empty permission
|
||||
$this->assertFalse($this->rbac->hasPermission('', $this->testUserId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test permission caching
|
||||
*/
|
||||
public function testPermissionCaching()
|
||||
{
|
||||
// First call should query database
|
||||
$permissions1 = $this->rbac->getUserPermissions($this->testUserId);
|
||||
|
||||
// Second call should use cache
|
||||
$permissions2 = $this->rbac->getUserPermissions($this->testUserId);
|
||||
|
||||
$this->assertEquals($permissions1, $permissions2);
|
||||
|
||||
// Grant new permission (should clear cache)
|
||||
$this->rbac->grantPermission($this->testUserId, 'feature.beta', $this->adminUserId);
|
||||
|
||||
// Should get updated permissions
|
||||
$permissions3 = $this->rbac->getUserPermissions($this->testUserId);
|
||||
$this->assertContains('feature.beta', $permissions3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test error handling
|
||||
*/
|
||||
public function testErrorHandling()
|
||||
{
|
||||
// Test with invalid parameters
|
||||
$result = $this->rbac->grantPermission(null, 'test.permission', $this->adminUserId);
|
||||
$this->assertFalse($result);
|
||||
|
||||
$result = $this->rbac->changeUserRole($this->testUserId, null, $this->adminUserId);
|
||||
$this->assertFalse($result);
|
||||
|
||||
$result = $this->rbac->suspendUser(null, 'test', $this->adminUserId);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
}
|
||||
364
tests/Unit/SecurityTest.php
Normal file
364
tests/Unit/SecurityTest.php
Normal file
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
|
||||
namespace EasyStream\Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use VSecurity;
|
||||
|
||||
class SecurityTest extends TestCase
|
||||
{
|
||||
private $security;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->security = VSecurity::getInstance();
|
||||
|
||||
// Clear any existing session data
|
||||
if (isset($_SESSION)) {
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
// Clear superglobals for clean testing
|
||||
$_GET = [];
|
||||
$_POST = [];
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Clean up after each test
|
||||
if (isset($_SESSION)) {
|
||||
$_SESSION = [];
|
||||
}
|
||||
$_GET = [];
|
||||
$_POST = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test input validation with various data types
|
||||
*/
|
||||
public function testInputValidationWithEdgeCases()
|
||||
{
|
||||
// Test integer validation
|
||||
$this->assertEquals(123, VSecurity::validateInput('123', 'int'));
|
||||
$this->assertEquals(0, VSecurity::validateInput('0', 'int'));
|
||||
$this->assertEquals(-123, VSecurity::validateInput('-123', 'int'));
|
||||
$this->assertNull(VSecurity::validateInput('abc', 'int'));
|
||||
$this->assertNull(VSecurity::validateInput('123.45', 'int'));
|
||||
$this->assertNull(VSecurity::validateInput('999999999999999999999', 'int'));
|
||||
|
||||
// Test integer with min/max constraints
|
||||
$this->assertEquals(50, VSecurity::validateInput('50', 'int', null, ['min' => 10, 'max' => 100]));
|
||||
$this->assertNull(VSecurity::validateInput('5', 'int', null, ['min' => 10, 'max' => 100]));
|
||||
$this->assertNull(VSecurity::validateInput('150', 'int', null, ['min' => 10, 'max' => 100]));
|
||||
|
||||
// Test float validation
|
||||
$this->assertEquals(123.45, VSecurity::validateInput('123.45', 'float'));
|
||||
$this->assertEquals(0.0, VSecurity::validateInput('0.0', 'float'));
|
||||
$this->assertNull(VSecurity::validateInput('abc', 'float'));
|
||||
|
||||
// Test email validation
|
||||
$this->assertEquals('test@example.com', VSecurity::validateInput('test@example.com', 'email'));
|
||||
$this->assertEquals('user+tag@domain.co.uk', VSecurity::validateInput('user+tag@domain.co.uk', 'email'));
|
||||
$this->assertNull(VSecurity::validateInput('invalid-email', 'email'));
|
||||
$this->assertNull(VSecurity::validateInput('test@', 'email'));
|
||||
$this->assertNull(VSecurity::validateInput('@example.com', 'email'));
|
||||
|
||||
// Test URL validation
|
||||
$this->assertEquals('https://example.com', VSecurity::validateInput('https://example.com', 'url'));
|
||||
$this->assertEquals('http://localhost:8080/path', VSecurity::validateInput('http://localhost:8080/path', 'url'));
|
||||
$this->assertNull(VSecurity::validateInput('not-a-url', 'url'));
|
||||
$this->assertNull(VSecurity::validateInput('ftp://invalid', 'url'));
|
||||
|
||||
// Test alpha validation
|
||||
$this->assertEquals('abcDEF', VSecurity::validateInput('abcDEF', 'alpha'));
|
||||
$this->assertEquals('test', VSecurity::validateInput('test123!@#', 'alpha'));
|
||||
$this->assertNull(VSecurity::validateInput('123', 'alpha'));
|
||||
|
||||
// Test alphanum validation
|
||||
$this->assertEquals('abc123', VSecurity::validateInput('abc123', 'alphanum'));
|
||||
$this->assertEquals('test123', VSecurity::validateInput('test123!@#', 'alphanum'));
|
||||
$this->assertNull(VSecurity::validateInput('!@#', 'alphanum'));
|
||||
|
||||
// Test slug validation
|
||||
$this->assertEquals('test-slug_123', VSecurity::validateInput('test-slug_123', 'slug'));
|
||||
$this->assertEquals('test-slug', VSecurity::validateInput('test-slug!@#', 'slug'));
|
||||
|
||||
// Test filename validation
|
||||
$this->assertEquals('test.txt', VSecurity::validateInput('test.txt', 'filename'));
|
||||
$this->assertEquals('file_name.pdf', VSecurity::validateInput('file_name.pdf', 'filename'));
|
||||
$this->assertEquals('test.txt', VSecurity::validateInput('test/path.txt', 'filename'));
|
||||
|
||||
// Test boolean validation
|
||||
$this->assertTrue(VSecurity::validateInput('true', 'boolean'));
|
||||
$this->assertTrue(VSecurity::validateInput('1', 'boolean'));
|
||||
$this->assertTrue(VSecurity::validateInput('yes', 'boolean'));
|
||||
$this->assertFalse(VSecurity::validateInput('false', 'boolean'));
|
||||
$this->assertFalse(VSecurity::validateInput('0', 'boolean'));
|
||||
$this->assertFalse(VSecurity::validateInput('no', 'boolean'));
|
||||
|
||||
// Test string validation with length constraints
|
||||
$this->assertEquals('test', VSecurity::validateInput('test', 'string', null, ['min_length' => 2, 'max_length' => 10]));
|
||||
$this->assertNull(VSecurity::validateInput('a', 'string', null, ['min_length' => 2, 'max_length' => 10]));
|
||||
$this->assertNull(VSecurity::validateInput('verylongstring', 'string', null, ['min_length' => 2, 'max_length' => 10]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test GET parameter handling
|
||||
*/
|
||||
public function testGetParameterHandling()
|
||||
{
|
||||
$_GET['test_int'] = '123';
|
||||
$_GET['test_string'] = 'hello world';
|
||||
$_GET['test_email'] = 'test@example.com';
|
||||
$_GET['test_invalid'] = 'invalid_email';
|
||||
|
||||
$this->assertEquals(123, VSecurity::getParam('test_int', 'int'));
|
||||
$this->assertEquals('hello world', VSecurity::getParam('test_string', 'string'));
|
||||
$this->assertEquals('test@example.com', VSecurity::getParam('test_email', 'email'));
|
||||
$this->assertEquals('default', VSecurity::getParam('test_invalid', 'email', 'default'));
|
||||
$this->assertNull(VSecurity::getParam('nonexistent', 'string'));
|
||||
$this->assertEquals('default', VSecurity::getParam('nonexistent', 'string', 'default'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test POST parameter handling
|
||||
*/
|
||||
public function testPostParameterHandling()
|
||||
{
|
||||
$_POST['username'] = 'testuser';
|
||||
$_POST['age'] = '25';
|
||||
$_POST['email'] = 'user@example.com';
|
||||
$_POST['malicious'] = '<script>alert("xss")</script>';
|
||||
|
||||
$this->assertEquals('testuser', VSecurity::postParam('username', 'string'));
|
||||
$this->assertEquals(25, VSecurity::postParam('age', 'int'));
|
||||
$this->assertEquals('user@example.com', VSecurity::postParam('email', 'email'));
|
||||
$this->assertEquals('<script>alert("xss")</script>', VSecurity::postParam('malicious', 'string'));
|
||||
$this->assertNull(VSecurity::postParam('nonexistent', 'string'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test CSRF token generation and validation
|
||||
*/
|
||||
public function testCSRFProtection()
|
||||
{
|
||||
// Test token generation
|
||||
$token1 = VSecurity::generateCSRFToken('test_action');
|
||||
$this->assertIsString($token1);
|
||||
$this->assertEquals(64, strlen($token1)); // 32 bytes = 64 hex chars
|
||||
|
||||
// Test token validation
|
||||
$this->assertTrue(VSecurity::validateCSRFToken($token1, 'test_action'));
|
||||
|
||||
// Test invalid token
|
||||
$this->assertFalse(VSecurity::validateCSRFToken('invalid_token', 'test_action'));
|
||||
|
||||
// Test token is one-time use
|
||||
$this->assertFalse(VSecurity::validateCSRFToken($token1, 'test_action'));
|
||||
|
||||
// Test different actions have different tokens
|
||||
$token2 = VSecurity::generateCSRFToken('different_action');
|
||||
$this->assertNotEquals($token1, $token2);
|
||||
|
||||
// Test default action
|
||||
$defaultToken = VSecurity::generateCSRFToken();
|
||||
$this->assertTrue(VSecurity::validateCSRFToken($defaultToken, 'default'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test CSRF field generation
|
||||
*/
|
||||
public function testCSRFFieldGeneration()
|
||||
{
|
||||
$field = VSecurity::getCSRFField('test_form');
|
||||
|
||||
$this->assertStringContainsString('<input type="hidden"', $field);
|
||||
$this->assertStringContainsString('name="csrf_token"', $field);
|
||||
$this->assertStringContainsString('value="', $field);
|
||||
|
||||
// Extract token from field
|
||||
preg_match('/value="([^"]+)"/', $field, $matches);
|
||||
$token = $matches[1] ?? '';
|
||||
|
||||
$this->assertNotEmpty($token);
|
||||
$this->assertEquals(64, strlen($token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test CSRF validation from POST data
|
||||
*/
|
||||
public function testCSRFValidationFromPost()
|
||||
{
|
||||
$token = VSecurity::generateCSRFToken('form_submit');
|
||||
$_POST['csrf_token'] = $token;
|
||||
|
||||
$this->assertTrue(VSecurity::validateCSRFFromPost('form_submit'));
|
||||
|
||||
// Test with invalid token
|
||||
$_POST['csrf_token'] = 'invalid_token';
|
||||
$this->assertFalse(VSecurity::validateCSRFFromPost('form_submit'));
|
||||
|
||||
// Test with missing token
|
||||
unset($_POST['csrf_token']);
|
||||
$this->assertFalse(VSecurity::validateCSRFFromPost('form_submit'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test output escaping
|
||||
*/
|
||||
public function testOutputEscaping()
|
||||
{
|
||||
$maliciousInput = '<script>alert("xss")</script>';
|
||||
$escaped = VSecurity::escapeOutput($maliciousInput);
|
||||
|
||||
$this->assertEquals('<script>alert("xss")</script>', $escaped);
|
||||
|
||||
// Test with quotes and ampersands
|
||||
$input = 'Hello "world" & <friends>';
|
||||
$escaped = VSecurity::escapeOutput($input);
|
||||
$this->assertEquals('Hello "world" & <friends>', $escaped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test JavaScript escaping
|
||||
*/
|
||||
public function testJavaScriptEscaping()
|
||||
{
|
||||
$input = 'Hello "world" & <script>';
|
||||
$escaped = VSecurity::escapeJS($input);
|
||||
|
||||
$this->assertIsString($escaped);
|
||||
$this->assertStringContainsString('\u003C', $escaped); // < should be escaped
|
||||
$this->assertStringContainsString('\u0022', $escaped); // " should be escaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Test file upload validation
|
||||
*/
|
||||
public function testFileUploadValidation()
|
||||
{
|
||||
// Create mock uploaded file
|
||||
$validFile = createMockUploadedFile('test.txt', 'Hello World', 'text/plain');
|
||||
|
||||
// Test valid file
|
||||
$result = VSecurity::validateFileUpload($validFile, ['text/plain'], 1024);
|
||||
$this->assertTrue($result['valid']);
|
||||
$this->assertEquals('text/plain', $result['mime_type']);
|
||||
|
||||
// Test file too large
|
||||
$result = VSecurity::validateFileUpload($validFile, ['text/plain'], 5);
|
||||
$this->assertFalse($result['valid']);
|
||||
$this->assertStringContainsString('too large', $result['error']);
|
||||
|
||||
// Test invalid MIME type
|
||||
$result = VSecurity::validateFileUpload($validFile, ['image/jpeg'], 1024);
|
||||
$this->assertFalse($result['valid']);
|
||||
$this->assertStringContainsString('Invalid file type', $result['error']);
|
||||
|
||||
// Test invalid upload (no file)
|
||||
$invalidFile = ['tmp_name' => '', 'size' => 0];
|
||||
$result = VSecurity::validateFileUpload($invalidFile);
|
||||
$this->assertFalse($result['valid']);
|
||||
$this->assertStringContainsString('No file uploaded', $result['error']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test rate limiting functionality
|
||||
*/
|
||||
public function testRateLimiting()
|
||||
{
|
||||
$key = 'test_user_' . uniqid();
|
||||
|
||||
// Test within limits
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$this->assertTrue(VSecurity::checkRateLimit($key, 5, 60));
|
||||
}
|
||||
|
||||
// Test exceeding limits
|
||||
$this->assertFalse(VSecurity::checkRateLimit($key, 5, 60));
|
||||
|
||||
// Test different key
|
||||
$key2 = 'test_user_' . uniqid();
|
||||
$this->assertTrue(VSecurity::checkRateLimit($key2, 5, 60));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test XSS prevention in various contexts
|
||||
*/
|
||||
public function testXSSPrevention()
|
||||
{
|
||||
$xssPayloads = [
|
||||
'<script>alert("XSS")</script>',
|
||||
'javascript:alert("XSS")',
|
||||
'<img src="x" onerror="alert(\'XSS\')">',
|
||||
'<svg onload="alert(1)">',
|
||||
'"><script>alert("XSS")</script>',
|
||||
'\';alert(String.fromCharCode(88,83,83))//\';alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//--></SCRIPT>">\'><SCRIPT>alert(String.fromCharCode(88,83,83))</SCRIPT>'
|
||||
];
|
||||
|
||||
foreach ($xssPayloads as $payload) {
|
||||
$sanitized = VSecurity::validateInput($payload, 'string');
|
||||
|
||||
// Should not contain dangerous elements
|
||||
$this->assertStringNotContainsString('<script>', strtolower($sanitized));
|
||||
$this->assertStringNotContainsString('javascript:', strtolower($sanitized));
|
||||
$this->assertStringNotContainsString('onerror=', strtolower($sanitized));
|
||||
$this->assertStringNotContainsString('onload=', strtolower($sanitized));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test SQL injection prevention patterns
|
||||
*/
|
||||
public function testSQLInjectionPrevention()
|
||||
{
|
||||
$sqlInjectionPayloads = [
|
||||
"'; DROP TABLE users; --",
|
||||
"1' OR '1'='1",
|
||||
"1; UPDATE users SET password='hacked' WHERE 1=1; --",
|
||||
"' UNION SELECT * FROM users --",
|
||||
"admin'--",
|
||||
"admin'/*",
|
||||
"' OR 1=1#"
|
||||
];
|
||||
|
||||
foreach ($sqlInjectionPayloads as $payload) {
|
||||
// These should be safely handled by input validation
|
||||
$result = VSecurity::validateInput($payload, 'string');
|
||||
|
||||
// The result should be escaped and safe
|
||||
$this->assertIsString($result);
|
||||
$this->assertStringNotContainsString('DROP TABLE', strtoupper($result));
|
||||
$this->assertStringNotContainsString('UNION SELECT', strtoupper($result));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test edge cases and boundary conditions
|
||||
*/
|
||||
public function testEdgeCases()
|
||||
{
|
||||
// Test null input
|
||||
$this->assertEquals('default', VSecurity::validateInput(null, 'string', 'default'));
|
||||
|
||||
// Test empty string
|
||||
$this->assertEquals('', VSecurity::validateInput('', 'string'));
|
||||
|
||||
// Test whitespace handling
|
||||
$this->assertEquals('test', VSecurity::validateInput(' test ', 'string'));
|
||||
|
||||
// Test very long strings
|
||||
$longString = str_repeat('a', 10000);
|
||||
$result = VSecurity::validateInput($longString, 'string', null, ['max_length' => 100]);
|
||||
$this->assertNull($result);
|
||||
|
||||
// Test unicode handling
|
||||
$unicode = 'Hello 世界 🌍';
|
||||
$result = VSecurity::validateInput($unicode, 'string');
|
||||
$this->assertStringContainsString('Hello', $result);
|
||||
|
||||
// Test array input (should be handled gracefully)
|
||||
$result = VSecurity::validateInput(['array', 'input'], 'string', 'default');
|
||||
$this->assertEquals('default', $result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user