diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000..eb2ac88
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,40 @@
+# Jilo Web Test Suite
+
+This directory contains the test suite for the Jilo Web application. All testing-related files are isolated here to keep the main application clean.
+
+## Structure
+
+```
+tests/
+├── framework/ # Test framework files
+│ ├── composer.json # Composer configuration for tests
+│ ├── phpunit.xml # PHPUnit configuration
+│ ├── Unit/ # Unit tests
+│ ├── Integration/ # Integration tests
+│ └── TestCase.php # Base test case class
+└── README.md # This file
+```
+
+## Running Tests
+
+1. Change to the framework directory:
+```bash
+cd tests/framework
+```
+
+2. Install dependencies (first time only):
+```bash
+composer install
+```
+
+3. Run all tests:
+```bash
+composer test
+```
+
+4. Generate coverage report:
+```bash
+composer test-coverage
+```
+
+The coverage report will be generated in `tests/framework/coverage/`.
diff --git a/tests/framework/.gitignore b/tests/framework/.gitignore
new file mode 100644
index 0000000..bc868f4
--- /dev/null
+++ b/tests/framework/.gitignore
@@ -0,0 +1,4 @@
+/vendor/
+/coverage/
+.phpunit.result.cache
+composer.lock
diff --git a/tests/framework/TestCase.php b/tests/framework/TestCase.php
new file mode 100644
index 0000000..f4dd305
--- /dev/null
+++ b/tests/framework/TestCase.php
@@ -0,0 +1,68 @@
+db = new Database([
+ 'type' => 'sqlite',
+ 'dbFile' => ':memory:'
+ ]);
+
+ // Create jilo_agents table
+ $this->db->getConnection()->exec("
+ CREATE TABLE jilo_agents (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ host_id INTEGER NOT NULL,
+ agent_type_id INTEGER NOT NULL,
+ url TEXT NOT NULL,
+ secret_key TEXT,
+ check_period INTEGER DEFAULT 60,
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
+ )
+ ");
+
+ // Create jilo_agent_types table
+ $this->db->getConnection()->exec("
+ CREATE TABLE jilo_agent_types (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ description TEXT NOT NULL,
+ endpoint TEXT NOT NULL
+ )
+ ");
+
+ // Create hosts table
+ $this->db->getConnection()->exec("
+ CREATE TABLE hosts (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ platform_id INTEGER NOT NULL,
+ name TEXT NOT NULL
+ )
+ ");
+
+ // Insert test host
+ $this->db->getConnection()->exec("
+ INSERT INTO hosts (id, platform_id, name) VALUES (1, 1, 'Test Host')
+ ");
+
+ // Insert test agent type
+ $this->db->getConnection()->exec("
+ INSERT INTO jilo_agent_types (id, description, endpoint)
+ VALUES (1, 'Test Agent Type', '/api/test')
+ ");
+
+ $this->agent = new Agent($this->db);
+ }
+
+ public function testAddAgent()
+ {
+ $hostId = 1;
+ $data = [
+ 'type_id' => 1,
+ 'url' => 'http://test.agent:8080',
+ 'secret_key' => 'test_secret',
+ 'check_period' => 60
+ ];
+
+ try {
+ $result = $this->agent->addAgent($hostId, $data);
+ $this->assertTrue($result);
+
+ // Verify agent was created
+ $stmt = $this->db->getConnection()->prepare('SELECT * FROM jilo_agents WHERE host_id = ?');
+ $stmt->execute([$hostId]);
+ $agent = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ $this->assertEquals($data['url'], $agent['url']);
+ $this->assertEquals($data['secret_key'], $agent['secret_key']);
+ $this->assertEquals($data['check_period'], $agent['check_period']);
+ } catch (Exception $e) {
+ $this->fail('An error occurred while adding agent: ' . $e->getMessage());
+ }
+ }
+
+ public function testGetAgentDetails()
+ {
+ // Add test agent
+ $hostId = 1;
+ $data = [
+ 'type_id' => 1,
+ 'url' => 'http://test.agent:8080',
+ 'secret_key' => 'test_secret',
+ 'check_period' => 60
+ ];
+
+ $this->agent->addAgent($hostId, $data);
+
+ // Test getting agent details
+ $agents = $this->agent->getAgentDetails($hostId);
+ $this->assertIsArray($agents);
+ $this->assertCount(1, $agents);
+ $this->assertEquals($data['url'], $agents[0]['url']);
+ }
+
+ public function testEditAgent()
+ {
+ // Add test agent
+ $hostId = 1;
+ $data = [
+ 'type_id' => 1,
+ 'url' => 'http://test.agent:8080',
+ 'secret_key' => 'test_secret',
+ 'check_period' => 60
+ ];
+
+ $this->agent->addAgent($hostId, $data);
+
+ // Get agent ID
+ $stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agents WHERE host_id = ? LIMIT 1');
+ $stmt->execute([$hostId]);
+ $agentId = $stmt->fetch(PDO::FETCH_COLUMN);
+
+ // Update agent
+ $updateData = [
+ 'type_id' => 1,
+ 'url' => 'http://updated.agent:8080',
+ 'secret_key' => 'updated_secret',
+ 'check_period' => 120,
+ 'agent_type_id' => 1 // Add this field for the update
+ ];
+
+ $result = $this->agent->editAgent($agentId, $updateData);
+ $this->assertTrue($result);
+
+ // Verify update
+ $stmt = $this->db->getConnection()->prepare('SELECT * FROM jilo_agents WHERE id = ?');
+ $stmt->execute([$agentId]);
+ $agent = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ $this->assertEquals($updateData['url'], $agent['url']);
+ $this->assertEquals($updateData['secret_key'], $agent['secret_key']);
+ $this->assertEquals($updateData['check_period'], $agent['check_period']);
+ }
+
+ public function testDeleteAgent()
+ {
+ // Add test agent
+ $hostId = 1;
+ $data = [
+ 'type_id' => 1,
+ 'url' => 'http://test.agent:8080',
+ 'secret_key' => 'test_secret',
+ 'check_period' => 60
+ ];
+
+ $this->agent->addAgent($hostId, $data);
+
+ // Get agent ID
+ $stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agents WHERE host_id = ? LIMIT 1');
+ $stmt->execute([$hostId]);
+ $agentId = $stmt->fetch(PDO::FETCH_COLUMN);
+
+ // Delete agent
+ $result = $this->agent->deleteAgent($agentId);
+ $this->assertTrue($result);
+
+ // Verify deletion
+ $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM jilo_agents WHERE id = ?');
+ $stmt->execute([$agentId]);
+ $count = $stmt->fetch(PDO::FETCH_COLUMN);
+
+ $this->assertEquals(0, $count);
+ }
+
+ public function testFetchAgent()
+ {
+ // Add test agent
+ $hostId = 1;
+ $data = [
+ 'type_id' => 1,
+ 'url' => 'http://test.agent:8080',
+ 'secret_key' => 'test_secret',
+ 'check_period' => 60
+ ];
+
+ $this->agent->addAgent($hostId, $data);
+
+ // Get agent ID
+ $stmt = $this->db->getConnection()->prepare('SELECT id FROM jilo_agents WHERE host_id = ? LIMIT 1');
+ $stmt->execute([$hostId]);
+ $agentId = $stmt->fetch(PDO::FETCH_COLUMN);
+
+ // Mock fetch response
+ $mockAgent = $this->getMockBuilder(Agent::class)
+ ->setConstructorArgs([$this->db])
+ ->onlyMethods(['fetchAgent'])
+ ->getMock();
+
+ $mockResponse = json_encode([
+ 'status' => 'ok',
+ 'metrics' => [
+ 'cpu_usage' => 25.5,
+ 'memory_usage' => 1024,
+ 'uptime' => 3600
+ ]
+ ]);
+
+ $mockAgent->expects($this->once())
+ ->method('fetchAgent')
+ ->willReturn($mockResponse);
+
+ $response = $mockAgent->fetchAgent($agentId);
+ $this->assertJson($response);
+
+ $data = json_decode($response, true);
+ $this->assertEquals('ok', $data['status']);
+ }
+}
diff --git a/tests/framework/Unit/Classes/DatabaseTest.php b/tests/framework/Unit/Classes/DatabaseTest.php
new file mode 100644
index 0000000..e9bcd54
--- /dev/null
+++ b/tests/framework/Unit/Classes/DatabaseTest.php
@@ -0,0 +1,131 @@
+config = [
+ 'type' => 'sqlite',
+ 'dbFile' => ':memory:'
+ ];
+ }
+
+ public function testDatabaseConnection()
+ {
+ $db = new Database($this->config);
+ $this->assertNotNull($db->getConnection());
+ }
+
+ public function testMysqlAndMariadbEquivalence()
+ {
+ // Test that mysql and mariadb are treated the same
+ $mysqlConfig = [
+ 'type' => 'mysql',
+ 'host' => 'invalid-host',
+ 'port' => 3306,
+ 'dbname' => 'test',
+ 'user' => 'test',
+ 'password' => 'test'
+ ];
+
+ $mariadbConfig = [
+ 'type' => 'mariadb',
+ 'host' => 'invalid-host',
+ 'port' => 3306,
+ 'dbname' => 'test',
+ 'user' => 'test',
+ 'password' => 'test'
+ ];
+
+ // Both should fail to connect and return null
+ $mysqlDb = new Database($mysqlConfig);
+ $this->assertNull($mysqlDb->getConnection());
+
+ $mariaDb = new Database($mariadbConfig);
+ $this->assertNull($mariaDb->getConnection());
+ }
+
+ public function testInvalidDatabaseType()
+ {
+ $invalidConfig = [
+ 'type' => 'invalid',
+ 'host' => 'localhost',
+ 'port' => 3306,
+ 'dbname' => 'test',
+ 'user' => 'test',
+ 'password' => 'test'
+ ];
+
+ $invalidDb = new Database($invalidConfig);
+ $this->assertNull($invalidDb->getConnection());
+ }
+
+ public function testMySQLConnectionMissingData()
+ {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('MySQL connection data is missing');
+
+ $config = [
+ 'type' => 'mysql',
+ 'host' => 'localhost',
+ 'port' => 3306,
+ 'dbname' => 'test',
+ // Missing user parameter
+ 'password' => 'test'
+ ];
+ new Database($config);
+ }
+
+ public function testPrepareAndExecute()
+ {
+ $db = new Database($this->config);
+
+ // Create test table
+ $db->execute('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
+
+ // Test prepare and execute
+ $result = $db->execute('INSERT INTO test (name) VALUES (?)', ['test_name']);
+ $this->assertEquals(1, $result->rowCount());
+
+ // Verify insertion
+ $result = $db->execute('SELECT name FROM test WHERE id = ?', [1]);
+ $row = $result->fetch(PDO::FETCH_ASSOC);
+ $this->assertEquals('test_name', $row['name']);
+ }
+
+ public function testTransaction()
+ {
+ $db = new Database($this->config);
+
+ // Create test table
+ $db->execute('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
+
+ // Test successful transaction
+ $db->beginTransaction();
+ $db->execute('INSERT INTO test (name) VALUES (?)', ['transaction_test']);
+ $db->commit();
+
+ $result = $db->execute('SELECT COUNT(*) as count FROM test');
+ $this->assertEquals(1, $result->fetch(PDO::FETCH_ASSOC)['count']);
+
+ // Test rollback
+ $db->beginTransaction();
+ $db->execute('INSERT INTO test (name) VALUES (?)', ['rollback_test']);
+ $db->rollBack();
+
+ $result = $db->execute('SELECT COUNT(*) as count FROM test');
+ $this->assertEquals(1, $result->fetch(PDO::FETCH_ASSOC)['count']);
+ }
+}
diff --git a/tests/framework/Unit/Classes/FeedbackTest.php b/tests/framework/Unit/Classes/FeedbackTest.php
new file mode 100644
index 0000000..44ef9e0
--- /dev/null
+++ b/tests/framework/Unit/Classes/FeedbackTest.php
@@ -0,0 +1,112 @@
+assertIsArray($messages);
+ $this->assertCount(1, $messages);
+
+ $message = $messages[0];
+ $this->assertEquals('LOGIN', $message['category']);
+ $this->assertEquals('LOGIN_SUCCESS', $message['key']);
+ $this->assertEquals('Test message', $message['custom_message']);
+ }
+
+ public function testRender()
+ {
+ // Test success message with custom text
+ $output = Feedback::render('LOGIN', 'LOGIN_SUCCESS', 'Success message');
+ $this->assertStringContainsString('alert-success', $output);
+ $this->assertStringContainsString('Success message', $output);
+ $this->assertStringContainsString('alert-dismissible', $output);
+
+ // Test error message (non-dismissible)
+ $output = Feedback::render('LOGIN', 'LOGIN_FAILED', 'Error message');
+ $this->assertStringContainsString('alert-danger', $output);
+ $this->assertStringContainsString('Error message', $output);
+ $this->assertStringNotContainsString('alert-dismissible', $output);
+
+ // Test small message
+ $output = Feedback::render('LOGIN', 'LOGIN_SUCCESS', 'Small message', true, true);
+ $this->assertStringContainsString('alert-sm', $output);
+ $this->assertStringContainsString('btn-close-sm', $output);
+ }
+
+ public function testGetMessageData()
+ {
+ $data = Feedback::getMessageData('LOGIN', 'LOGIN_SUCCESS', 'Test message');
+
+ $this->assertIsArray($data);
+ $this->assertEquals(Feedback::TYPE_SUCCESS, $data['type']);
+ $this->assertEquals('Test message', $data['message']);
+ $this->assertTrue($data['dismissible']);
+ $this->assertFalse($data['small']);
+
+ // Test with default message
+ $data = Feedback::getMessageData('LOGIN', 'LOGIN_SUCCESS');
+ $this->assertNotNull($data['message']);
+ }
+
+ public function testFlash()
+ {
+ Feedback::flash('LOGIN', 'LOGIN_SUCCESS', 'Test message');
+ $this->assertArrayHasKey('flash_messages', $_SESSION);
+ $this->assertCount(1, $_SESSION['flash_messages']);
+
+ $message = $_SESSION['flash_messages'][0];
+ $this->assertEquals('LOGIN', $message['category']);
+ $this->assertEquals('LOGIN_SUCCESS', $message['key']);
+ $this->assertEquals('Test message', $message['custom_message']);
+ }
+
+ public function testPredefinedMessageTypes()
+ {
+ $this->assertEquals('success', Feedback::TYPE_SUCCESS);
+ $this->assertEquals('danger', Feedback::TYPE_ERROR);
+ $this->assertEquals('info', Feedback::TYPE_INFO);
+ $this->assertEquals('warning', Feedback::TYPE_WARNING);
+ }
+
+ public function testMessageConfigurations()
+ {
+ $config = Feedback::get('LOGIN', 'LOGIN_SUCCESS');
+ $this->assertEquals(Feedback::TYPE_SUCCESS, $config['type']);
+ $this->assertTrue($config['dismissible']);
+
+ $config = Feedback::get('LOGIN', 'LOGIN_FAILED');
+ $this->assertEquals(Feedback::TYPE_ERROR, $config['type']);
+ $this->assertFalse($config['dismissible']);
+ }
+}
diff --git a/tests/framework/Unit/Classes/HostTest.php b/tests/framework/Unit/Classes/HostTest.php
new file mode 100644
index 0000000..e5cce13
--- /dev/null
+++ b/tests/framework/Unit/Classes/HostTest.php
@@ -0,0 +1,183 @@
+db = new \Database([
+ 'type' => 'sqlite',
+ 'dbFile' => ':memory:'
+ ]);
+
+ // Create hosts table
+ $this->db->getConnection()->exec("
+ CREATE TABLE hosts (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ platform_id INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ address TEXT NOT NULL
+ )
+ ");
+
+ // Create jilo_agents table for relationship testing
+ $this->db->getConnection()->exec("
+ CREATE TABLE jilo_agents (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ host_id INTEGER NOT NULL,
+ agent_type_id INTEGER NOT NULL,
+ url TEXT NOT NULL,
+ secret_key TEXT,
+ check_period INTEGER DEFAULT 60
+ )
+ ");
+
+ $this->host = new \Host($this->db);
+ }
+
+ public function testAddHost()
+ {
+ $data = [
+ 'platform_id' => 1,
+ 'name' => 'Test host',
+ 'address' => '192.168.1.100'
+ ];
+
+ $result = $this->host->addHost($data);
+ $this->assertTrue($result);
+
+ // Verify host was created
+ $stmt = $this->db->getConnection()->prepare('SELECT * FROM hosts WHERE platform_id = ? AND name = ?');
+ $stmt->execute([$data['platform_id'], $data['name']]);
+ $host = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+ $this->assertEquals($data['name'], $host['name']);
+ $this->assertEquals($data['address'], $host['address']);
+ }
+
+ public function testGetHostDetails()
+ {
+ // Add test host
+ $data = [
+ 'platform_id' => 1,
+ 'name' => 'Test host',
+ 'address' => '192.168.1.100'
+ ];
+ $this->host->addHost($data);
+
+ // Test getting all hosts
+ $hosts = $this->host->getHostDetails();
+ $this->assertIsArray($hosts);
+ $this->assertCount(1, $hosts);
+
+ // Test getting hosts by platform
+ $hosts = $this->host->getHostDetails(1);
+ $this->assertIsArray($hosts);
+ $this->assertCount(1, $hosts);
+ $this->assertEquals($data['name'], $hosts[0]['name']);
+
+ // Test getting specific host
+ $hosts = $this->host->getHostDetails(1, $hosts[0]['id']);
+ $this->assertIsArray($hosts);
+ $this->assertCount(1, $hosts);
+ $this->assertEquals($data['name'], $hosts[0]['name']);
+ }
+
+ public function testEditHost()
+ {
+ // Add test host
+ $data = [
+ 'platform_id' => 1,
+ 'name' => 'Test host',
+ 'address' => '192.168.1.100'
+ ];
+ $this->host->addHost($data);
+
+ // Get host ID
+ $stmt = $this->db->getConnection()->prepare('SELECT id FROM hosts WHERE platform_id = ? AND name = ?');
+ $stmt->execute([$data['platform_id'], $data['name']]);
+ $hostId = $stmt->fetch(\PDO::FETCH_COLUMN);
+
+ // Update host
+ $updateData = [
+ 'id' => $hostId,
+ 'name' => 'Updated host',
+ 'address' => '192.168.1.200'
+ ];
+
+ $result = $this->host->editHost($data['platform_id'], $updateData);
+ $this->assertTrue($result);
+
+ // Verify update
+ $stmt = $this->db->getConnection()->prepare('SELECT * FROM hosts WHERE id = ?');
+ $stmt->execute([$hostId]);
+ $host = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+ $this->assertEquals($updateData['name'], $host['name']);
+ $this->assertEquals($updateData['address'], $host['address']);
+ }
+
+ public function testDeleteHost()
+ {
+ // Add test host
+ $data = [
+ 'platform_id' => 1,
+ 'name' => 'Test host',
+ 'address' => '192.168.1.100'
+ ];
+ $this->host->addHost($data);
+
+ // Get host ID
+ $stmt = $this->db->getConnection()->prepare('SELECT id FROM hosts WHERE platform_id = ? AND name = ?');
+ $stmt->execute([$data['platform_id'], $data['name']]);
+ $hostId = $stmt->fetch(\PDO::FETCH_COLUMN);
+
+ // Add test agent to the host
+ $this->db->getConnection()->exec("
+ INSERT INTO jilo_agents (host_id, agent_type_id, url, secret_key)
+ VALUES ($hostId, 1, 'http://test:8080', 'secret')
+ ");
+
+ // Delete host
+ $result = $this->host->deleteHost($hostId);
+ $this->assertTrue($result);
+
+ // Verify host deletion
+ $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM hosts WHERE id = ?');
+ $stmt->execute([$hostId]);
+ $hostCount = $stmt->fetch(\PDO::FETCH_COLUMN);
+ $this->assertEquals(0, $hostCount);
+
+ // Verify agent deletion
+ $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) FROM jilo_agents WHERE host_id = ?');
+ $stmt->execute([$hostId]);
+ $agentCount = $stmt->fetch(\PDO::FETCH_COLUMN);
+ $this->assertEquals(0, $agentCount);
+ }
+
+ public function testEditNonexistentHost()
+ {
+ $updateData = [
+ 'id' => 999,
+ 'name' => 'Nonexistent host',
+ 'address' => '192.168.1.200'
+ ];
+
+ $result = $this->host->editHost(1, $updateData);
+ $this->assertIsString($result);
+ $this->assertStringContainsString('No host found', $result);
+ }
+}
diff --git a/tests/framework/Unit/Classes/JiloServerTest.php b/tests/framework/Unit/Classes/JiloServerTest.php
new file mode 100644
index 0000000..0edb07d
--- /dev/null
+++ b/tests/framework/Unit/Classes/JiloServerTest.php
@@ -0,0 +1,89 @@
+db = new Database([
+ 'type' => 'sqlite',
+ 'dbFile' => ':memory:'
+ ]);
+
+ // Create servers table
+ $this->db->getConnection()->exec("
+ CREATE TABLE servers (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ host_id INTEGER NOT NULL,
+ port INTEGER NOT NULL,
+ status TEXT DEFAULT 'offline',
+ last_seen INTEGER,
+ version TEXT,
+ created_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL
+ )
+ ");
+
+ $this->server = new Server($this->db);
+ }
+
+ public function testGetServerStatus()
+ {
+ // Create mock server that overrides file_get_contents
+ $mockServer = $this->getMockBuilder(Server::class)
+ ->setConstructorArgs([$this->db])
+ ->onlyMethods(['getServerStatus'])
+ ->getMock();
+
+ // Test successful response
+ $mockServer->expects($this->exactly(2))
+ ->method('getServerStatus')
+ ->willReturnMap([
+ ['localhost', 8080, '/health', true],
+ ['localhost', 8081, '/health', false]
+ ]);
+
+ $this->assertTrue($mockServer->getServerStatus('localhost', 8080));
+ $this->assertFalse($mockServer->getServerStatus('localhost', 8081));
+ }
+
+ public function testGetServerStatusWithCustomEndpoint()
+ {
+ $mockServer = $this->getMockBuilder(Server::class)
+ ->setConstructorArgs([$this->db])
+ ->onlyMethods(['getServerStatus'])
+ ->getMock();
+
+ $mockServer->expects($this->once())
+ ->method('getServerStatus')
+ ->with('localhost', 8080, '/custom/health')
+ ->willReturn(true);
+
+ $this->assertTrue($mockServer->getServerStatus('localhost', 8080, '/custom/health'));
+ }
+
+ public function testGetServerStatusWithDefaults()
+ {
+ $mockServer = $this->getMockBuilder(Server::class)
+ ->setConstructorArgs([$this->db])
+ ->onlyMethods(['getServerStatus'])
+ ->getMock();
+
+ $mockServer->expects($this->once())
+ ->method('getServerStatus')
+ ->with('127.0.0.1', 8080, '/health')
+ ->willReturn(true);
+
+ $this->assertTrue($mockServer->getServerStatus());
+ }
+}
diff --git a/tests/framework/Unit/Classes/LogTest.php b/tests/framework/Unit/Classes/LogTest.php
new file mode 100644
index 0000000..c82333c
--- /dev/null
+++ b/tests/framework/Unit/Classes/LogTest.php
@@ -0,0 +1,138 @@
+db = new Database([
+ 'type' => 'sqlite',
+ 'dbFile' => ':memory:'
+ ]);
+
+ // Create users table
+ $this->db->getConnection()->exec("
+ CREATE TABLE users (
+ id INTEGER PRIMARY KEY,
+ username TEXT NOT NULL
+ )
+ ");
+
+ // Create test user
+ $this->db->getConnection()->exec("
+ INSERT INTO users (id, username) VALUES (1, 'testuser'), (2, 'testuser2')
+ ");
+
+ // Create logs table
+ $this->db->getConnection()->exec("
+ CREATE TABLE logs (
+ id INTEGER PRIMARY KEY,
+ user_id INTEGER,
+ scope TEXT NOT NULL,
+ message TEXT NOT NULL,
+ time DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+ )
+ ");
+
+ $this->log = new Log($this->db);
+ }
+
+ public function testInsertLog()
+ {
+ $result = $this->log->insertLog(1, 'Test message', 'test');
+ $this->assertTrue($result);
+
+ $stmt = $this->db->getConnection()->prepare("SELECT * FROM logs WHERE scope = ?");
+ $stmt->execute(['test']);
+ $log = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ $this->assertEquals(1, $log['user_id']);
+ $this->assertEquals('Test message', $log['message']);
+ $this->assertEquals('test', $log['scope']);
+ }
+
+ public function testReadLog()
+ {
+ // Insert test logs
+ $this->log->insertLog(1, 'Test message 1', 'user');
+ $this->log->insertLog(1, 'Test message 2', 'user');
+
+ $logs = $this->log->readLog(1, 'user');
+ $this->assertCount(2, $logs);
+ $this->assertEquals('Test message 1', $logs[0]['message']);
+ $this->assertEquals('Test message 2', $logs[1]['message']);
+ }
+
+ public function testReadLogWithTimeFilter()
+ {
+ // Insert test logs with different times
+ $this->log->insertLog(1, 'Old message', 'user');
+ sleep(1); // Ensure different timestamps
+ $this->log->insertLog(1, 'New message', 'user');
+
+ $now = date('Y-m-d H:i:s');
+ $oneHourAgo = date('Y-m-d H:i:s', strtotime('-1 hour'));
+
+ $logs = $this->log->readLog(1, 'user', 0, '', [
+ 'from_time' => $oneHourAgo,
+ 'until_time' => $now
+ ]);
+
+ $this->assertCount(2, $logs);
+ }
+
+ public function testReadLogWithPagination()
+ {
+ // Insert test logs
+ $this->log->insertLog(1, 'Message 1', 'user');
+ $this->log->insertLog(1, 'Message 2', 'user');
+ $this->log->insertLog(1, 'Message 3', 'user');
+
+ // Test with limit
+ $logs = $this->log->readLog(1, 'user', 0, 2);
+ $this->assertCount(2, $logs);
+
+ // Test with offset
+ $logs = $this->log->readLog(1, 'user', 2, 2);
+ $this->assertCount(1, $logs);
+ }
+
+ public function testReadLogWithMessageFilter()
+ {
+ // Insert test logs
+ $this->log->insertLog(1, 'Test message', 'user');
+ $this->log->insertLog(1, 'Another message', 'user');
+
+ $logs = $this->log->readLog(1, 'user', 0, '', [
+ 'message' => 'Test'
+ ]);
+
+ $this->assertCount(1, $logs);
+ $this->assertEquals('Test message', $logs[0]['message']);
+ }
+
+ public function testReadLogWithUserFilter()
+ {
+ // Insert test logs for different users
+ $this->log->insertLog(1, 'User 1 message', 'user');
+ $this->log->insertLog(2, 'User 2 message', 'user');
+
+ $logs = $this->log->readLog(1, 'user', 0, '', [
+ 'id' => 1
+ ]);
+
+ $this->assertCount(1, $logs);
+ $this->assertEquals('User 1 message', $logs[0]['message']);
+ }
+}
diff --git a/tests/framework/Unit/Classes/PlatformTest.php b/tests/framework/Unit/Classes/PlatformTest.php
new file mode 100644
index 0000000..539aa62
--- /dev/null
+++ b/tests/framework/Unit/Classes/PlatformTest.php
@@ -0,0 +1,222 @@
+db = new Database([
+ 'type' => 'sqlite',
+ 'dbFile' => ':memory:'
+ ]);
+
+ // Create hosts table
+ $this->db->getConnection()->exec("
+ CREATE TABLE hosts (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ platform_id INTEGER NOT NULL,
+ name TEXT NOT NULL
+ )
+ ");
+
+ // Create jilo_agents table
+ $this->db->getConnection()->exec("
+ CREATE TABLE jilo_agents (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ host_id INTEGER NOT NULL
+ )
+ ");
+
+ // Create platforms table
+ $this->db->getConnection()->exec("
+ CREATE TABLE platforms (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ jitsi_url TEXT NOT NULL,
+ jilo_database TEXT NOT NULL,
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
+ )
+ ");
+
+ $this->platform = new Platform($this->db);
+ }
+
+ public function testAddPlatform()
+ {
+ $data = [
+ 'name' => 'Test platform',
+ 'jitsi_url' => 'https://jitsi.example.com',
+ 'jilo_database' => '/path/to/jilo.db'
+ ];
+
+ $result = $this->platform->addPlatform($data);
+ $this->assertTrue($result);
+
+ // Verify platform was created
+ $stmt = $this->db->getConnection()->prepare('SELECT * FROM platforms WHERE name = ?');
+ $stmt->execute([$data['name']]);
+ $platform = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ $this->assertNotNull($platform);
+ $this->assertEquals($data['name'], $platform['name']);
+ $this->assertEquals($data['jitsi_url'], $platform['jitsi_url']);
+ $this->assertEquals($data['jilo_database'], $platform['jilo_database']);
+ }
+
+ public function testGetPlatformDetails()
+ {
+ // Create test platform
+ $stmt = $this->db->getConnection()->prepare('INSERT INTO platforms (name, jitsi_url, jilo_database) VALUES (?, ?, ?)');
+ $stmt->execute(['Test platform', 'https://jitsi.example.com', '/path/to/jilo.db']);
+ $platformId = $this->db->getConnection()->lastInsertId();
+
+ // Test getting specific platform
+ $platform = $this->platform->getPlatformDetails($platformId);
+ $this->assertIsArray($platform);
+ $this->assertEquals('Test platform', $platform[0]['name']);
+
+ // Test getting all platforms
+ $platforms = $this->platform->getPlatformDetails();
+ $this->assertIsArray($platforms);
+ $this->assertCount(1, $platforms);
+ }
+
+ public function testEditPlatform()
+ {
+ // Create test platform
+ $stmt = $this->db->getConnection()->prepare('INSERT INTO platforms (name, jitsi_url, jilo_database) VALUES (?, ?, ?)');
+ $stmt->execute(['Test platform', 'https://jitsi.example.com', '/path/to/jilo.db']);
+ $platformId = $this->db->getConnection()->lastInsertId();
+
+ $updateData = [
+ 'name' => 'Updated platform',
+ 'jitsi_url' => 'https://new.example.com',
+ 'jilo_database' => '/path/to/jilo.db'
+ ];
+
+ $result = $this->platform->editPlatform($platformId, $updateData);
+ $this->assertTrue($result);
+
+ // Verify update
+ $stmt = $this->db->getConnection()->prepare('SELECT * FROM platforms WHERE id = ?');
+ $stmt->execute([$platformId]);
+ $platform = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ $this->assertEquals($updateData['name'], $platform['name']);
+ $this->assertEquals($updateData['jitsi_url'], $platform['jitsi_url']);
+ }
+
+ public function testDeletePlatform()
+ {
+ // Create test platform
+ $stmt = $this->db->getConnection()->prepare('INSERT INTO platforms (name, jitsi_url, jilo_database) VALUES (?, ?, ?)');
+ $stmt->execute(['Test platform', 'https://jitsi.example.com', '/path/to/jilo.db']);
+ $platformId = $this->db->getConnection()->lastInsertId();
+
+ // Create test host
+ $stmt = $this->db->getConnection()->prepare('INSERT INTO hosts (platform_id, name) VALUES (?, ?)');
+ $stmt->execute([$platformId, 'Test host']);
+ $hostId = $this->db->getConnection()->lastInsertId();
+
+ // Create test agent
+ $stmt = $this->db->getConnection()->prepare('INSERT INTO jilo_agents (host_id) VALUES (?)');
+ $stmt->execute([$hostId]);
+
+ $result = $this->platform->deletePlatform($platformId);
+ $this->assertTrue($result);
+
+ // Verify platform deletion
+ $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platforms WHERE id = ?');
+ $stmt->execute([$platformId]);
+ $result = $stmt->fetch(PDO::FETCH_ASSOC);
+ $this->assertEquals(0, $result['count']);
+
+ // Verify host deletion
+ $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM hosts WHERE platform_id = ?');
+ $stmt->execute([$platformId]);
+ $result = $stmt->fetch(PDO::FETCH_ASSOC);
+ $this->assertEquals(0, $result['count']);
+
+ // Verify agent deletion
+ $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM jilo_agents WHERE host_id = ?');
+ $stmt->execute([$hostId]);
+ $result = $stmt->fetch(PDO::FETCH_ASSOC);
+ $this->assertEquals(0, $result['count']);
+ }
+
+ public function testValidatePlatformData()
+ {
+ $validData = [
+ 'name' => 'Test platform',
+ 'jitsi_url' => 'https://jitsi.example.com',
+ 'jilo_database' => '/path/to/jilo.db'
+ ];
+
+ $result = $this->platform->addPlatform($validData);
+ $this->assertTrue($result);
+
+ // Verify platform was created
+ $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platforms WHERE name = ?');
+ $stmt->execute([$validData['name']]);
+ $result = $stmt->fetch(PDO::FETCH_ASSOC);
+ $this->assertEquals(1, $result['count']);
+
+ // Test invalid data (missing required fields)
+ $invalidData = [
+ 'name' => 'Test platform 2'
+ // Missing jitsi_url and jilo_database
+ ];
+
+ $result = $this->platform->addPlatform($invalidData);
+ $this->assertIsString($result); // Should return error message
+
+ // Verify platform was not created
+ $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platforms WHERE name = ?');
+ $stmt->execute([$invalidData['name']]);
+ $result = $stmt->fetch(PDO::FETCH_ASSOC);
+ $this->assertEquals(0, $result['count']);
+ }
+
+ public function testCheckJiloDatabaseAccess()
+ {
+ // Create a temporary SQLite database for testing
+ $tempDb = tempnam(sys_get_temp_dir(), 'jilo_test_');
+ $testDb = new \SQLite3($tempDb);
+ $testDb->close();
+
+ $data = [
+ 'name' => 'Test platform',
+ 'jitsi_url' => 'https://jitsi.example.com',
+ 'jilo_database' => $tempDb
+ ];
+
+ $result = $this->platform->addPlatform($data);
+ $this->assertTrue($result);
+
+ // Verify platform was created
+ $stmt = $this->db->getConnection()->prepare('SELECT COUNT(*) as count FROM platforms WHERE jilo_database = ?');
+ $stmt->execute([$tempDb]);
+ $result = $stmt->fetch(PDO::FETCH_ASSOC);
+ $this->assertEquals(1, $result['count']);
+
+ // Test with non-existent database
+ $data['name'] = 'Another platform';
+ $data['jilo_database'] = '/nonexistent/path/db.sqlite';
+ $result = $this->platform->addPlatform($data);
+ $this->assertTrue($result); // No database validation in Platform class
+
+ // Clean up
+ unlink($tempDb);
+ }
+}
diff --git a/tests/framework/Unit/Classes/RateLimiterTest.php b/tests/framework/Unit/Classes/RateLimiterTest.php
new file mode 100644
index 0000000..b5e15cc
--- /dev/null
+++ b/tests/framework/Unit/Classes/RateLimiterTest.php
@@ -0,0 +1,110 @@
+db = new Database([
+ 'type' => 'sqlite',
+ 'dbFile' => ':memory:'
+ ]);
+
+ $this->rateLimiter = new RateLimiter($this->db);
+ }
+
+ public function testGetRecentAttempts()
+ {
+ $ip = '127.0.0.1';
+ $username = 'testuser';
+
+ // Clean up any existing attempts first
+ $stmt = $this->db->getConnection()->prepare("DELETE FROM {$this->rateLimiter->authRatelimitTable} WHERE ip_address = ?");
+ $stmt->execute([$ip]);
+
+ // Initially should have no attempts
+ $attempts = $this->rateLimiter->getRecentAttempts($ip);
+ $this->assertEquals(0, $attempts);
+
+ // Add a login attempt
+ $stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->authRatelimitTable} (ip_address, username) VALUES (?, ?)");
+ $stmt->execute([$ip, $username]);
+
+ // Should now have 1 attempt
+ $attempts = $this->rateLimiter->getRecentAttempts($ip);
+ $this->assertEquals(1, $attempts);
+ }
+
+ public function testIpBlacklisting()
+ {
+ $ip = '192.0.2.1'; // Using TEST-NET-1 range
+
+ // Should be blacklisted by default (TEST-NET-1 range)
+ $this->assertTrue($this->rateLimiter->isIpBlacklisted($ip));
+
+ // Test with non-blacklisted IP
+ $nonBlacklistedIp = '8.8.8.8'; // Google DNS
+ $this->assertFalse($this->rateLimiter->isIpBlacklisted($nonBlacklistedIp));
+
+ // Add IP to blacklist
+ $stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->blacklistTable} (ip_address, reason) VALUES (?, ?)");
+ $stmt->execute([$nonBlacklistedIp, 'Test blacklist']);
+
+ // IP should now be blacklisted
+ $this->assertTrue($this->rateLimiter->isIpBlacklisted($nonBlacklistedIp));
+ }
+
+ public function testIpWhitelisting()
+ {
+ $ip = '127.0.0.1'; // Localhost
+
+ // Clean up any existing whitelist entries
+ $stmt = $this->db->getConnection()->prepare("DELETE FROM {$this->rateLimiter->whitelistTable} WHERE ip_address = ?");
+ $stmt->execute([$ip]);
+
+ // Add to whitelist
+ $stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->whitelistTable} (ip_address, description) VALUES (?, ?)");
+ $stmt->execute([$ip, 'Test whitelist']);
+
+ // Should be whitelisted
+ $this->assertTrue($this->rateLimiter->isIpWhitelisted($ip));
+
+ // Test with non-whitelisted IP
+ $nonWhitelistedIp = '8.8.8.8'; // Google DNS
+ $this->assertFalse($this->rateLimiter->isIpWhitelisted($nonWhitelistedIp));
+
+ // Add to whitelist
+ $stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->whitelistTable} (ip_address, description) VALUES (?, ?)");
+ $stmt->execute([$nonWhitelistedIp, 'Test whitelist']);
+
+ // Should now be whitelisted
+ $this->assertTrue($this->rateLimiter->isIpWhitelisted($nonWhitelistedIp));
+ }
+
+ public function testIpRangeBlacklisting()
+ {
+ $ip = '8.8.8.8'; // Google DNS
+ $networkIp = '8.8.8.0/24'; // Network containing Google DNS
+
+ // Initially IP should not be blacklisted
+ $this->assertFalse($this->rateLimiter->isIpBlacklisted($ip));
+
+ // Add network to blacklist
+ $stmt = $this->db->getConnection()->prepare("INSERT INTO {$this->rateLimiter->blacklistTable} (ip_address, is_network, reason) VALUES (?, 1, ?)");
+ $stmt->execute([$networkIp, 'Test network blacklist']);
+
+ // IP in range should now be blacklisted
+ $this->assertTrue($this->rateLimiter->isIpBlacklisted($ip));
+ }
+}
diff --git a/tests/framework/Unit/Classes/RouterTest.php b/tests/framework/Unit/Classes/RouterTest.php
new file mode 100644
index 0000000..a316c74
--- /dev/null
+++ b/tests/framework/Unit/Classes/RouterTest.php
@@ -0,0 +1,86 @@
+router = new Router('', true); // Empty controller path and dry-run mode
+ }
+
+ public function testAddRoute()
+ {
+ $this->router->add('/test', 'TestController@index');
+ $this->assertTrue(true); // If we get here, no exception was thrown
+ }
+
+ public function testDispatchRoute()
+ {
+ $this->router->add('/users/(\d+)', 'UserController@show');
+
+ $match = $this->router->dispatch('/users/123');
+ $this->assertIsArray($match);
+ $this->assertEquals('UserController@show', $match['callback']);
+ $this->assertEquals(['123'], $match['params']);
+ }
+
+ public function testDispatchRouteWithMultipleParameters()
+ {
+ $this->router->add('/users/(\d+)/posts/(\d+)', 'PostController@show');
+
+ $match = $this->router->dispatch('/users/123/posts/456');
+ $this->assertIsArray($match);
+ $this->assertEquals('PostController@show', $match['callback']);
+ $this->assertEquals(['123', '456'], $match['params']);
+ }
+
+ public function testNoMatchingRoute()
+ {
+ $this->router->add('/test', 'TestController@index');
+
+ $match = $this->router->dispatch('/nonexistent');
+ $this->assertNull($match);
+ }
+
+ public function testDispatchWithQueryString()
+ {
+ $this->router->add('/test', 'TestController@index');
+
+ $match = $this->router->dispatch('/test?param=value');
+ $this->assertIsArray($match);
+ $this->assertEquals('TestController@index', $match['callback']);
+ $this->assertEquals([], $match['params']);
+ }
+
+ public function testOptionalParameters()
+ {
+ $this->router->add('/users(?:/(\d+))?', 'UserController@index');
+
+ // Test with parameter
+ $match1 = $this->router->dispatch('/users/123');
+ $this->assertIsArray($match1);
+ $this->assertEquals('UserController@index', $match1['callback']);
+ $this->assertEquals(['123'], $match1['params']);
+
+ // Test without parameter
+ $match2 = $this->router->dispatch('/users');
+ $this->assertIsArray($match2);
+ $this->assertEquals('UserController@index', $match2['callback']);
+ $this->assertEquals([], $match2['params']);
+ }
+
+ public function testInvokeWithMissingController()
+ {
+ $router = new Router(''); // Empty controller path, not in dry-run mode
+ ob_start();
+ $router->dispatch('/test');
+ $output = ob_get_clean();
+ $this->assertEquals('404 page not found', $output);
+ }
+}
diff --git a/tests/framework/Unit/Classes/SettingsTest.php b/tests/framework/Unit/Classes/SettingsTest.php
new file mode 100644
index 0000000..c288461
--- /dev/null
+++ b/tests/framework/Unit/Classes/SettingsTest.php
@@ -0,0 +1,29 @@
+settings = new Settings(null);
+ }
+
+ public function testGetPlatformJsFileWithInvalidUrl()
+ {
+ $result = $this->settings->getPlatformJsFile('invalid-url', 'test.js');
+ $this->assertEquals('Invalid URL: invalid-url/test.js', $result);
+ }
+
+ public function testGetPlatformJsFileWithValidUrl()
+ {
+ $result = $this->settings->getPlatformJsFile('https://example.com', 'test.js');
+ $this->assertStringContainsString("can't be loaded", $result);
+ }
+}
diff --git a/tests/framework/Unit/Classes/UserTest.php b/tests/framework/Unit/Classes/UserTest.php
new file mode 100644
index 0000000..26a8ba7
--- /dev/null
+++ b/tests/framework/Unit/Classes/UserTest.php
@@ -0,0 +1,164 @@
+db = new Database([
+ 'type' => 'sqlite',
+ 'dbFile' => ':memory:'
+ ]);
+
+ // Create users table
+ $this->db->getConnection()->exec("
+ CREATE TABLE users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ username TEXT NOT NULL UNIQUE,
+ password TEXT NOT NULL
+ )
+ ");
+
+ // Create users_meta table
+ $this->db->getConnection()->exec("
+ CREATE TABLE users_meta (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ name TEXT,
+ email TEXT,
+ timezone TEXT,
+ bio TEXT,
+ avatar TEXT,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+ )
+ ");
+
+ // Create tables for rate limiter
+ $this->db->getConnection()->exec("
+ CREATE TABLE login_attempts (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ ip_address TEXT NOT NULL,
+ username TEXT NOT NULL,
+ attempted_at TEXT DEFAULT (DATETIME('now'))
+ )
+ ");
+
+ $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
+ )
+ ");
+
+ $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->user = new User($this->db);
+ }
+
+ public function testRegister()
+ {
+ $result = $this->user->register('testuser', 'password123');
+ $this->assertTrue($result);
+
+ // Verify user was created
+ $stmt = $this->db->getConnection()->prepare('SELECT * FROM users WHERE username = ?');
+ $stmt->execute(['testuser']);
+ $user = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+ $this->assertEquals('testuser', $user['username']);
+ $this->assertTrue(password_verify('password123', $user['password']));
+
+ // Verify user_meta was created
+ $stmt = $this->db->getConnection()->prepare('SELECT * FROM users_meta WHERE user_id = ?');
+ $stmt->execute([$user['id']]);
+ $meta = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+ $this->assertNotNull($meta);
+ }
+
+ public function testLogin()
+ {
+ // Create a test user
+ $password = 'password123';
+ $hashedPassword = password_hash($password, PASSWORD_DEFAULT);
+
+ $stmt = $this->db->getConnection()->prepare('INSERT INTO users (username, password) VALUES (?, ?)');
+ $stmt->execute(['testuser', $hashedPassword]);
+
+ // Mock $_SERVER['REMOTE_ADDR'] for rate limiter
+ $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
+
+ // Test successful login
+ try {
+ $result = $this->user->login('testuser', $password);
+ $this->assertTrue($result);
+ } catch (Exception $e) {
+ $this->fail('Login should not throw an exception for valid credentials: ' . $e->getMessage());
+ }
+
+ // Test failed login
+ try {
+ $this->user->login('testuser', 'wrongpassword');
+ $this->fail('Login should throw an exception for invalid credentials');
+ } catch (Exception $e) {
+ $this->assertStringContainsString('Invalid credentials', $e->getMessage());
+ }
+
+ // Test nonexistent user
+ try {
+ $this->user->login('nonexistent', $password);
+ $this->fail('Login should throw an exception for nonexistent user');
+ } catch (Exception $e) {
+ $this->assertStringContainsString('Invalid credentials', $e->getMessage());
+ }
+ }
+
+ public function testGetUserDetails()
+ {
+ // Create a test user
+ $stmt = $this->db->getConnection()->prepare('INSERT INTO users (username, password) VALUES (?, ?)');
+ $stmt->execute(['testuser', 'hashedpassword']);
+ $userId = $this->db->getConnection()->lastInsertId();
+
+ // Create user meta with some data
+ $stmt = $this->db->getConnection()->prepare('INSERT INTO users_meta (user_id, name, email) VALUES (?, ?, ?)');
+ $stmt->execute([$userId, 'Test User', 'test@example.com']);
+
+ $userDetails = $this->user->getUserDetails($userId);
+ $this->assertIsArray($userDetails);
+ $this->assertCount(1, $userDetails); // Should return one row
+ $user = $userDetails[0]; // Get the first row
+ $this->assertEquals('testuser', $user['username']);
+ $this->assertEquals('Test User', $user['name']);
+ $this->assertEquals('test@example.com', $user['email']);
+
+ // Test nonexistent user
+ $userDetails = $this->user->getUserDetails(999);
+ $this->assertEmpty($userDetails);
+ }
+}
diff --git a/tests/framework/Unit/Classes/ValidatorTest.php b/tests/framework/Unit/Classes/ValidatorTest.php
new file mode 100644
index 0000000..a31835d
--- /dev/null
+++ b/tests/framework/Unit/Classes/ValidatorTest.php
@@ -0,0 +1,143 @@
+ 'John'];
+ $validator = new Validator($data);
+ $rules = ['name' => ['required' => true]];
+
+ $this->assertTrue($validator->validate($rules));
+ $this->assertEmpty($validator->getErrors());
+
+ // Test invalid data
+ $data = ['name' => ''];
+ $validator = new Validator($data);
+ $this->assertFalse($validator->validate($rules));
+ $this->assertNotEmpty($validator->getErrors());
+ }
+
+ public function testEmail()
+ {
+ // Test valid email
+ $data = ['email' => 'test@example.com'];
+ $validator = new Validator($data);
+ $rules = ['email' => ['email' => true]];
+
+ $this->assertTrue($validator->validate($rules));
+ $this->assertEmpty($validator->getErrors());
+
+ // Test invalid email
+ $data = ['email' => 'invalid-email'];
+ $validator = new Validator($data);
+ $this->assertFalse($validator->validate($rules));
+ $this->assertNotEmpty($validator->getErrors());
+ }
+
+ public function testMinLength()
+ {
+ // Test valid length
+ $data = ['password' => '123456'];
+ $validator = new Validator($data);
+ $rules = ['password' => ['min' => 6]];
+
+ $this->assertTrue($validator->validate($rules));
+ $this->assertEmpty($validator->getErrors());
+
+ // Test invalid length
+ $data = ['password' => '12345'];
+ $validator = new Validator($data);
+ $this->assertFalse($validator->validate($rules));
+ $this->assertNotEmpty($validator->getErrors());
+ }
+
+ public function testMaxLength()
+ {
+ // Test valid length
+ $data = ['username' => '12345'];
+ $validator = new Validator($data);
+ $rules = ['username' => ['max' => 5]];
+
+ $this->assertTrue($validator->validate($rules));
+ $this->assertEmpty($validator->getErrors());
+
+ // Test invalid length
+ $data = ['username' => '123456'];
+ $validator = new Validator($data);
+ $this->assertFalse($validator->validate($rules));
+ $this->assertNotEmpty($validator->getErrors());
+ }
+
+ public function testNumeric()
+ {
+ // Test valid number
+ $data = ['age' => '25'];
+ $validator = new Validator($data);
+ $rules = ['age' => ['numeric' => true]];
+
+ $this->assertTrue($validator->validate($rules));
+ $this->assertEmpty($validator->getErrors());
+
+ // Test invalid number
+ $data = ['age' => 'twenty-five'];
+ $validator = new Validator($data);
+ $this->assertFalse($validator->validate($rules));
+ $this->assertNotEmpty($validator->getErrors());
+ }
+
+ public function testUrl()
+ {
+ // Test valid URL
+ $data = ['website' => 'https://example.com'];
+ $validator = new Validator($data);
+ $rules = ['website' => ['url' => true]];
+
+ $this->assertTrue($validator->validate($rules));
+ $this->assertEmpty($validator->getErrors());
+
+ // Test invalid URL
+ $data = ['website' => 'not-a-url'];
+ $validator = new Validator($data);
+ $this->assertFalse($validator->validate($rules));
+ $this->assertNotEmpty($validator->getErrors());
+ }
+
+ public function testMultipleRules()
+ {
+ // Test valid data
+ $data = ['email' => 'test@example.com'];
+ $validator = new Validator($data);
+ $rules = ['email' => [
+ 'required' => true,
+ 'email' => true,
+ 'max' => 50
+ ]];
+
+ $this->assertTrue($validator->validate($rules));
+ $this->assertEmpty($validator->getErrors());
+
+ // Test invalid data
+ $data = ['email' => str_repeat('a', 51) . '@example.com'];
+ $validator = new Validator($data);
+ $this->assertFalse($validator->validate($rules));
+ $this->assertNotEmpty($validator->getErrors());
+ }
+
+ public function testCustomErrorMessages()
+ {
+ $data = ['age' => 'not-a-number'];
+ $validator = new Validator($data);
+ $rules = ['age' => ['numeric' => true]];
+
+ $this->assertFalse($validator->validate($rules));
+ $errors = $validator->getErrors();
+ $this->assertNotEmpty($errors);
+ $this->assertEquals('Must be a number', $errors['age'][0]);
+ }
+}
diff --git a/tests/framework/Unit/Helpers/SecurityHelperTest.php b/tests/framework/Unit/Helpers/SecurityHelperTest.php
new file mode 100644
index 0000000..33eeb5e
--- /dev/null
+++ b/tests/framework/Unit/Helpers/SecurityHelperTest.php
@@ -0,0 +1,113 @@
+security = SecurityHelper::getInstance();
+ }
+
+ public function testGenerateCsrfToken()
+ {
+ $token = $this->security->generateCsrfToken();
+
+ $this->assertNotEmpty($token);
+ $this->assertEquals(64, strlen($token)); // 32 bytes = 64 hex chars
+ $this->assertEquals($token, $_SESSION['csrf_token']);
+ }
+
+ public function testVerifyCsrfToken()
+ {
+ $token = $this->security->generateCsrfToken();
+
+ $this->assertTrue($this->security->verifyCsrfToken($token));
+ $this->assertFalse($this->security->verifyCsrfToken('invalid_token'));
+ $this->assertFalse($this->security->verifyCsrfToken(''));
+ }
+
+ public function testSanitizeString()
+ {
+ $input = '';
+ $expected = 'alert("xss")';
+
+ $this->assertEquals($expected, $this->security->sanitizeString($input));
+ $this->assertEquals('', $this->security->sanitizeString(null));
+ $this->assertEquals('', $this->security->sanitizeString([]));
+ }
+
+ public function testValidateEmail()
+ {
+ $this->assertTrue($this->security->validateEmail('test@example.com'));
+ $this->assertTrue($this->security->validateEmail('user.name+tag@example.co.uk'));
+ $this->assertFalse($this->security->validateEmail('invalid.email'));
+ $this->assertFalse($this->security->validateEmail('@example.com'));
+ }
+
+ public function testValidateInt()
+ {
+ $this->assertTrue($this->security->validateInt('123'));
+ $this->assertTrue($this->security->validateInt('-123'));
+ $this->assertFalse($this->security->validateInt('12.3'));
+ $this->assertFalse($this->security->validateInt('abc'));
+ }
+
+ public function testValidateUrl()
+ {
+ $this->assertTrue($this->security->validateUrl('https://example.com'));
+ $this->assertTrue($this->security->validateUrl('http://sub.example.co.uk/path?query=1'));
+ $this->assertTrue($this->security->validateUrl('ftp://example.com')); // Any valid URL is accepted
+ $this->assertFalse($this->security->validateUrl('not-a-url'));
+ }
+
+ public function testSanitizeArray()
+ {
+ $input = [
+ 'name' => 'John',
+ 'email' => 'john@example.com',
+ 'nested' => [
+ 'key' => 'value'
+ ]
+ ];
+
+ $allowedKeys = ['name', 'email'];
+ $result = $this->security->sanitizeArray($input, $allowedKeys);
+
+ $this->assertArrayHasKey('name', $result);
+ $this->assertArrayHasKey('email', $result);
+ $this->assertArrayNotHasKey('nested', $result);
+ $this->assertEquals('John', $result['name']); // HTML tags are stripped
+ $this->assertEquals('john@example.com', $result['email']);
+ }
+
+ public function testValidateFormData()
+ {
+ $data = [
+ 'name' => 'John Doe',
+ 'email' => 'invalid-email',
+ 'age' => 'not-a-number',
+ 'website' => 'not-a-url'
+ ];
+
+ $rules = [
+ 'name' => ['type' => 'string', 'required' => true, 'min' => 2, 'max' => 50],
+ 'email' => ['type' => 'email', 'required' => true],
+ 'age' => ['type' => 'integer', 'required' => true],
+ 'website' => ['type' => 'url', 'required' => true]
+ ];
+
+ $errors = $this->security->validateFormData($data, $rules);
+
+ $this->assertIsArray($errors);
+ $this->assertCount(3, $errors);
+ $this->assertArrayHasKey('email', $errors);
+ $this->assertArrayHasKey('age', $errors);
+ $this->assertArrayHasKey('website', $errors);
+ }
+}
diff --git a/tests/framework/bootstrap.php b/tests/framework/bootstrap.php
new file mode 100644
index 0000000..887e203
--- /dev/null
+++ b/tests/framework/bootstrap.php
@@ -0,0 +1,64 @@
+ [
+ '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) {
+ global $dbWeb;
+ return [
+ 'db' => $dbWeb
+ ];
+ }
+}
diff --git a/tests/framework/composer.json b/tests/framework/composer.json
new file mode 100644
index 0000000..4e55f7e
--- /dev/null
+++ b/tests/framework/composer.json
@@ -0,0 +1,50 @@
+{
+ "name": "lindeas/jilo-web-tests",
+ "description": "Test Suite for Jilo Web Application",
+ "type": "project",
+ "require": {
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5",
+ "phpunit/php-code-coverage": "^9.2",
+ "mockery/mockery": "^1.5"
+ },
+ "autoload": {
+ "files": [
+ "../../app/includes/errors.php",
+ "../../app/classes/database.php",
+ "../../app/classes/agent.php",
+ "../../app/classes/host.php",
+ "../../app/classes/platform.php",
+ "../../app/classes/server.php",
+ "../../app/classes/log.php",
+ "../../app/classes/feedback.php",
+ "../../app/classes/settings.php",
+ "../../app/classes/validator.php",
+ "../../app/classes/router.php",
+ "../../app/classes/ratelimiter.php",
+ "../../app/classes/user.php",
+ "../../app/helpers/security.php",
+ "../../app/includes/errors.php"
+ ],
+ "classmap": [
+ "TestCase.php"
+ ]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Tests\\": "./"
+ }
+ },
+ "scripts": {
+ "test": "phpunit",
+ "test-coverage": "phpunit --coverage-html coverage"
+ },
+ "config": {
+ "optimize-autoloader": true,
+ "preferred-install": "dist",
+ "sort-packages": true
+ },
+ "minimum-stability": "stable"
+}
diff --git a/tests/framework/phpunit.xml b/tests/framework/phpunit.xml
new file mode 100644
index 0000000..706b87e
--- /dev/null
+++ b/tests/framework/phpunit.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ ./Unit
+
+
+
+
+ ../../app
+
+
+ ../../app/templates
+ ../../app/includes
+
+
+
+
+
+
+
+