diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2acbd29..3f6fa57 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,6 +10,14 @@ jobs: test: runs-on: ubuntu-latest + env: + DB_TYPE: mariadb + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + DB_DATABASE: jilo_test + DB_USERNAME: test_jilo + DB_PASSWORD: test_password + services: mariadb: image: mariadb:10.6 diff --git a/tests/Unit/Classes/LogTest.php b/tests/Unit/Classes/LogTest.php index 0b434d7..2f96e72 100644 --- a/tests/Unit/Classes/LogTest.php +++ b/tests/Unit/Classes/LogTest.php @@ -121,44 +121,40 @@ class LogTest extends TestCase ]); $connection = $this->db->getConnection(); + + // Ensure fresh log table schema (drop old schema if present) $connection->exec("DROP TABLE IF EXISTS log"); - $connection->exec("DROP TABLE IF EXISTS user"); - // Create user table - $connection->exec(" - CREATE TABLE IF NOT EXISTS user ( - id INT AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(255) NOT NULL, - password VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - "); + // Use centralized schema setup + setupTestDatabaseSchema($connection); - // Create log table with the expected schema from Log class - $connection->exec(" - CREATE TABLE IF NOT EXISTS log ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT, - scope VARCHAR(50) NOT NULL DEFAULT 'user', - message TEXT NOT NULL, - time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES user(id) - ) - "); + // Load logs plugin migration to create log table + $logMigrationPath = __DIR__ . '/../../../plugins/logs/migrations/create_log_table.sql'; + if (file_exists($logMigrationPath)) { + $sql = file_get_contents($logMigrationPath); + $statements = array_filter(array_map('trim', explode(';', $sql))); + foreach ($statements as $statement) { + if (!empty($statement)) { + $connection->exec($statement); + } + } + } - // Create test users with all required fields + // Clean up any existing test data + $connection->exec("DELETE FROM log WHERE user_id >= 1000"); + $connection->exec("DELETE FROM user WHERE id >= 1000"); + + // Create test users + $timestamp = time(); $connection->exec(" - INSERT INTO user (username, password, email) + INSERT INTO user (id, username, password) VALUES - ('testuser', 'password123', 'testuser@example.com'), - ('testuser2', 'password123', 'testuser2@example.com') + (1000, 'testuser_log_{$timestamp}', 'password123'), + (1001, 'testuser2_log_{$timestamp}', 'password123') "); // Store test user ID for later use - $stmt = $this->db->getConnection()->query("SELECT id FROM user WHERE username = 'testuser' LIMIT 1"); - $user = $stmt->fetch(PDO::FETCH_ASSOC); - $this->testUserId = $user['id']; + $this->testUserId = 1000; // Create a TestLogger instance that will be used by the app's Log wrapper $this->log = new TestLogger($this->db); @@ -166,15 +162,15 @@ class LogTest extends TestCase protected function tearDown(): void { - // Drop tables in correct order (respect foreign key constraints) - $this->db->getConnection()->exec("DROP TABLE IF EXISTS log"); - $this->db->getConnection()->exec("DROP TABLE IF EXISTS user"); + // Clean up test data + $this->db->getConnection()->exec("DELETE FROM log WHERE user_id >= 1000"); + $this->db->getConnection()->exec("DELETE FROM user WHERE id >= 1000"); parent::tearDown(); } public function testInsertLog() { - $result = $this->log->insertLog($this->testUserId, 'Test message', 'test'); + $result = $this->log->insertLog($this->testUserId, 'Test message', 'user'); $this->assertTrue($result); // Verify the log was inserted @@ -182,7 +178,7 @@ class LogTest extends TestCase $log = $stmt->fetch(PDO::FETCH_ASSOC); $this->assertEquals('Test message', $log['message']); - $this->assertEquals('test', $log['scope']); + $this->assertEquals('user', $log['scope']); } public function testReadLog() diff --git a/tests/Unit/Classes/UserRegisterTest.php b/tests/Unit/Classes/UserRegisterTest.php index 3f1db59..8e72d1f 100644 --- a/tests/Unit/Classes/UserRegisterTest.php +++ b/tests/Unit/Classes/UserRegisterTest.php @@ -35,53 +35,14 @@ class UserRegisterTest extends TestCase // Set up App::db() for Register class to use App::set('db', $this->db->getConnection()); - // Create user table with MariaDB syntax - $this->db->getConnection()->exec(" - CREATE TABLE IF NOT EXISTS user ( - id INT PRIMARY KEY AUTO_INCREMENT, - username VARCHAR(255) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - "); + // Use centralized schema setup + setupTestDatabaseSchema($this->db->getConnection()); - // Create user_meta table with MariaDB syntax - $this->db->getConnection()->exec(" - CREATE TABLE IF NOT EXISTS user_meta ( - id INT PRIMARY KEY AUTO_INCREMENT, - user_id INT NOT NULL, - name VARCHAR(255), - email VARCHAR(255), - timezone VARCHAR(100), - bio TEXT, - avatar VARCHAR(255), - FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE - ) - "); - - // Create security_rate_auth table for rate limiting - $this->db->getConnection()->exec(" - CREATE TABLE IF NOT EXISTS security_rate_auth ( - id INT PRIMARY KEY AUTO_INCREMENT, - ip_address VARCHAR(45) NOT NULL, - username VARCHAR(255) NOT NULL, - attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - INDEX idx_ip_username (ip_address, username) - ) - "); - - // Create user_2fa table for two-factor authentication - $this->db->getConnection()->exec(" - CREATE TABLE IF NOT EXISTS user_2fa ( - id INT PRIMARY KEY AUTO_INCREMENT, - user_id INT NOT NULL, - secret_key VARCHAR(255) NOT NULL, - backup_codes TEXT, - enabled TINYINT(1) NOT NULL DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE - ) - "); + // Clean up any test users from previous runs + $this->db->getConnection()->exec("DELETE FROM user_2fa WHERE user_id >= 1000"); + $this->db->getConnection()->exec("DELETE FROM security_rate_auth WHERE username LIKE 'testuser%'"); + $this->db->getConnection()->exec("DELETE FROM user_meta WHERE user_id >= 1000"); + $this->db->getConnection()->exec("DELETE FROM user WHERE id >= 1000"); $this->register = new Register(); $this->user = new User($this->db); @@ -91,19 +52,20 @@ class UserRegisterTest extends TestCase { // Clean up App state App::reset('db'); - - // Drop tables in correct order - $this->db->getConnection()->exec("DROP TABLE IF EXISTS user_2fa"); - $this->db->getConnection()->exec("DROP TABLE IF EXISTS security_rate_auth"); - $this->db->getConnection()->exec("DROP TABLE IF EXISTS user_meta"); - $this->db->getConnection()->exec("DROP TABLE IF EXISTS user"); + + // Clean up test data + $this->db->getConnection()->exec("DELETE FROM user_2fa WHERE user_id >= 1000"); + $this->db->getConnection()->exec("DELETE FROM security_rate_auth WHERE username LIKE 'testuser%'"); + $this->db->getConnection()->exec("DELETE FROM user_meta WHERE user_id >= 1000"); + $this->db->getConnection()->exec("DELETE FROM user WHERE id >= 1000"); + parent::tearDown(); } public function testRegister() { - // Register a new user - $username = 'testuser'; + // Register a new user with unique username + $username = 'testuser_reg_' . time() . '_' . rand(1000, 9999); $password = 'password123'; $result = $this->register->register($username, $password); @@ -129,8 +91,8 @@ class UserRegisterTest extends TestCase public function testLogin() { - // First register a user - $username = 'testuser'; + // First register a user with unique username + $username = 'testuser_login_' . time() . '_' . rand(1000, 9999); $password = 'password123'; $this->register->register($username, $password); @@ -163,8 +125,8 @@ class UserRegisterTest extends TestCase public function testGetUserDetails() { - // Register a test user first - $username = 'testuser'; + // First register a user with unique username + $username = 'testuser_details_' . time() . '_' . rand(1000, 9999); $password = 'password123'; $result = $this->register->register($username, $password); $this->assertTrue($result); diff --git a/tests/Unit/Classes/UserTest.php b/tests/Unit/Classes/UserTest.php index ef74f2b..b72ac92 100644 --- a/tests/Unit/Classes/UserTest.php +++ b/tests/Unit/Classes/UserTest.php @@ -35,53 +35,14 @@ class UserTest extends TestCase // Set up App::db() for Register class to use App::set('db', $this->db->getConnection()); - // Create user table with MariaDB syntax - $this->db->getConnection()->exec(" - CREATE TABLE IF NOT EXISTS user ( - id INT PRIMARY KEY AUTO_INCREMENT, - username VARCHAR(255) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - "); + // Use centralized schema setup + setupTestDatabaseSchema($this->db->getConnection()); - // Create user_meta table with MariaDB syntax - $this->db->getConnection()->exec(" - CREATE TABLE IF NOT EXISTS user_meta ( - id INT PRIMARY KEY AUTO_INCREMENT, - user_id INT NOT NULL, - name VARCHAR(255), - email VARCHAR(255), - timezone VARCHAR(100), - bio TEXT, - avatar VARCHAR(255), - FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE - ) - "); - - // Create security_rate_auth table for rate limiting - $this->db->getConnection()->exec(" - CREATE TABLE IF NOT EXISTS security_rate_auth ( - id INT PRIMARY KEY AUTO_INCREMENT, - ip_address VARCHAR(45) NOT NULL, - username VARCHAR(255) NOT NULL, - attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - INDEX idx_ip_username (ip_address, username) - ) - "); - - // Create user_2fa table for two-factor authentication - $this->db->getConnection()->exec(" - CREATE TABLE IF NOT EXISTS user_2fa ( - id INT PRIMARY KEY AUTO_INCREMENT, - user_id INT NOT NULL, - secret_key VARCHAR(255) NOT NULL, - backup_codes TEXT, - enabled TINYINT(1) NOT NULL DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE - ) - "); + // Clean up any test users from previous runs + $this->db->getConnection()->exec("DELETE FROM user_2fa WHERE user_id >= 1000"); + $this->db->getConnection()->exec("DELETE FROM security_rate_auth WHERE username LIKE 'testuser%'"); + $this->db->getConnection()->exec("DELETE FROM user_meta WHERE user_id >= 1000"); + $this->db->getConnection()->exec("DELETE FROM user WHERE id >= 1000"); $this->user = new User($this->db); $this->register = new Register(); @@ -91,19 +52,20 @@ class UserTest extends TestCase { // Clean up App state App::reset('db'); - - // Drop tables in correct order - $this->db->getConnection()->exec("DROP TABLE IF EXISTS user_2fa"); - $this->db->getConnection()->exec("DROP TABLE IF EXISTS security_rate_auth"); - $this->db->getConnection()->exec("DROP TABLE IF EXISTS user_meta"); - $this->db->getConnection()->exec("DROP TABLE IF EXISTS user"); + + // Clean up test data + $this->db->getConnection()->exec("DELETE FROM user_2fa WHERE user_id >= 1000"); + $this->db->getConnection()->exec("DELETE FROM security_rate_auth WHERE username LIKE 'testuser%'"); + $this->db->getConnection()->exec("DELETE FROM user_meta WHERE user_id >= 1000"); + $this->db->getConnection()->exec("DELETE FROM user WHERE id >= 1000"); + parent::tearDown(); } public function testLogin() { - // First register a user - $username = 'testuser'; + // First register a user with unique username + $username = 'testuser_login_' . time() . '_' . rand(1000, 9999); $password = 'password123'; $this->register->register($username, $password); @@ -136,8 +98,8 @@ class UserTest extends TestCase public function testGetUserDetails() { - // Register a test user first - $username = 'testuser'; + // First register a user with unique username + $username = 'testuser_details_' . time() . '_' . rand(1000, 9999); $password = 'password123'; $result = $this->register->register($username, $password); $this->assertTrue($result); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 6e34ec9..8cebb0a 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -21,6 +21,13 @@ if (!defined('APP_PATH')) { require_once __DIR__ . '/../app/core/App.php'; require_once __DIR__ . '/../app/core/PluginRouteRegistry.php'; +// Define plugin route registration function used by plugin bootstraps +if (!function_exists('register_plugin_route_prefix')) { + function register_plugin_route_prefix(string $prefix, array $definition = []): void { + \App\Core\PluginRouteRegistry::registerPrefix($prefix, $definition); + } +} + // Load plugin Log model and IP helper early so fallback wrapper is bypassed require_once __DIR__ . '/../app/helpers/ip_helper.php'; require_once __DIR__ . '/../app/helpers/logger_loader.php'; @@ -72,3 +79,62 @@ $_SERVER['PHP_SELF'] = '/index.php'; $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $_SERVER['HTTP_USER_AGENT'] = 'PHPUnit Test Browser'; $_SERVER['HTTPS'] = 'on'; + +/** + * Setup test database schema by applying main.sql and migrations + * + * @param PDO $pdo Database connection + * @return void + */ +function setupTestDatabaseSchema(PDO $pdo): void +{ + // Apply main.sql schema + $mainSqlPath = __DIR__ . '/../doc/database/main.sql'; + if (file_exists($mainSqlPath)) { + $sql = file_get_contents($mainSqlPath); + // Add IF NOT EXISTS to CREATE TABLE statements + $sql = preg_replace('/CREATE TABLE `/', 'CREATE TABLE IF NOT EXISTS `', $sql); + $statements = array_filter(array_map('trim', explode(';', $sql))); + foreach ($statements as $statement) { + if (!empty($statement)) { + try { + $pdo->exec($statement); + } catch (PDOException $e) { + // Skip errors for INSERT statements on existing data + if (strpos($statement, 'INSERT') === false) { + throw $e; + } + } + } + } + } + + // Apply migrations from doc/database/migrations/ (excluding subfolders) + $migrationsDir = __DIR__ . '/../doc/database/migrations'; + if (is_dir($migrationsDir)) { + $files = glob($migrationsDir . '/*.sql'); + sort($files); // Apply in chronological order + foreach ($files as $file) { + $sql = file_get_contents($file); + // Add IF NOT EXISTS to CREATE TABLE statements + $sql = preg_replace('/CREATE TABLE `/', 'CREATE TABLE IF NOT EXISTS `', $sql); + $statements = array_filter(array_map('trim', explode(';', $sql))); + foreach ($statements as $statement) { + if (!empty($statement)) { + try { + $pdo->exec($statement); + } catch (PDOException $e) { + // Skip errors for: + // - Duplicate columns (already exists) + // - Table doesn't exist (plugin tables not yet created) + $errorMsg = $e->getMessage(); + if (strpos($errorMsg, 'Duplicate column') === false && + strpos($errorMsg, "doesn't exist") === false) { + throw $e; + } + } + } + } + } + } +}