diff --git a/lib/chibi-sql b/lib/chibi-sql index 22910a18..cf0b4bd2 160000 --- a/lib/chibi-sql +++ b/lib/chibi-sql @@ -1 +1 @@ -Subproject commit 22910a186efbcb9bc86a3ae3eb6f4aff34096406 +Subproject commit cf0b4bd2253d0d3240a87ce31f77ee587b306638 diff --git a/public_html/dispatch.php b/public_html/dispatch.php index da953feb..6bc48d0f 100644 --- a/public_html/dispatch.php +++ b/public_html/dispatch.php @@ -1,13 +1,8 @@ startTime = $startTime; +$context = getContext(); $context->query = $query; function renderView() @@ -16,12 +11,6 @@ function renderView() \Chibi\View::render($context->layoutName, $context); } -function getContext() -{ - global $context; - return $context; -} - $context->simpleControllerName = null; $context->simpleActionName = null; diff --git a/src/Access.php b/src/Access.php index 9df388d4..44e85cec 100644 --- a/src/Access.php +++ b/src/Access.php @@ -101,5 +101,3 @@ class Access }); } } - -Access::init(); diff --git a/src/Assert.php b/src/Assert.php new file mode 100644 index 00000000..a056a544 --- /dev/null +++ b/src/Assert.php @@ -0,0 +1,67 @@ +getMessage(), $expectedMessage) === false) + $this->fail('Assertion failed. Expected: "' . $expectedMessage . '", got: "' . $e->getMessage() . '"'); + } + if ($success) + $this->fail('Assertion failed. Expected exception, got nothing'); + } + + public function doesNotThrow($callback) + { + try + { + $ret = $callback(); + } + catch (Exception $e) + { + $this->fail('Assertion failed. Expected nothing, got exception: "' . $e->getMessage() . '"'); + } + return $ret; + } + + public function isNotNull($actual) + { + if ($actual === null or $actual === false) + $this->fail('Assertion failed. Expected: NULL, got: "' . $actual . '"'); + } + + public function isTrue($actual) + { + return $this->areEqual(1, intval(boolval($actual))); + } + + public function isFalse($actual) + { + return $this->areEqual(0, intval(boolval($actual))); + } + + public function areEqual($expected, $actual) + { + if ($expected != $actual) + $this->fail('Assertion failed. Expected: "' . $this->dumpVar($expected) . '", got: "' . $this->dumpVar($actual) . '"'); + } + + public function dumpVar($var) + { + ob_start(); + var_dump($var); + return trim(ob_get_clean()); + } + + public function fail($message) + { + throw new SimpleException($message); + } +} diff --git a/src/Logger.php b/src/Logger.php index 6dd9b947..9b46d629 100644 --- a/src/Logger.php +++ b/src/Logger.php @@ -57,5 +57,3 @@ class Logger self::$buffer = $buffer; } } - -Logger::init(); diff --git a/src/core.php b/src/core.php index 753eaa58..d5cb9f61 100644 --- a/src/core.php +++ b/src/core.php @@ -1,4 +1,6 @@ loadIni($path); -$config->rootDir = $rootDir; - function getConfig() { global $config; return $config; } +function getContext() +{ + global $context; + return $context; +} -//extension sanity checks -$requiredExtensions = ['pdo', 'pdo_' . $config->main->dbDriver, 'gd', 'openssl', 'fileinfo']; -foreach ($requiredExtensions as $ext) - if (!extension_loaded($ext)) - die('PHP extension "' . $ext . '" must be enabled to continue.' . PHP_EOL); +function resetEnvironment() +{ + //load config manually + global $config; + global $rootDir; + global $startTime; -\Chibi\Database::connect( - $config->main->dbDriver, - TextHelper::absolutePath($config->main->dbLocation), - $config->main->dbUser, - $config->main->dbPass); + $configPaths = + [ + $rootDir . DS . 'data' . DS . 'config.ini', + $rootDir . DS . 'data' . DS . 'local.ini', + $rootDir . DS . 'tests' . DS . 'test.ini', + ]; + $config = new \Chibi\Config(); + foreach ($configPaths as $path) + if (file_exists($path)) + $config->loadIni($path); + $config->rootDir = $rootDir; -//wire models -foreach (\Chibi\AutoLoader::getAllIncludablePaths() as $path) - if (preg_match('/Model/', $path)) - \Chibi\AutoLoader::safeInclude($path); + //prepare context + global $context; + $context = new StdClass; + $context->startTime = $startTime; + + //extension sanity checks + $requiredExtensions = ['pdo', 'pdo_' . $config->main->dbDriver, 'gd', 'openssl', 'fileinfo']; + foreach ($requiredExtensions as $ext) + if (!extension_loaded($ext)) + die('PHP extension "' . $ext . '" must be enabled to continue.' . PHP_EOL); + + if (\Chibi\Database::connected()) + \Chibi\Database::disconnect(); + + Auth::setCurrentUser(null); + Access::init(); + Logger::init(); + + \Chibi\Database::connect( + $config->main->dbDriver, + TextHelper::absolutePath($config->main->dbLocation), + $config->main->dbUser, + $config->main->dbPass); +} + +resetEnvironment(); diff --git a/src/upgrade.php b/src/upgrade.php new file mode 100644 index 00000000..24e6bd38 --- /dev/null +++ b/src/upgrade.php @@ -0,0 +1,85 @@ +rootDir + . DS . 'src' . DS . 'Upgrades' . DS . $config->main->dbDriver); + + $upgrades = glob($upgradesPath . DS . '*.sql'); + natcasesort($upgrades); + + foreach ($upgrades as $upgradePath) + { + preg_match('/(\d+)\.sql/', $upgradePath, $matches); + $upgradeVersionMajor = intval($matches[1]); + + list ($dbVersionMajor, $dbVersionMinor) = getDbVersion(); + + if (($upgradeVersionMajor > $dbVersionMajor) + or ($upgradeVersionMajor == $dbVersionMajor and $dbVersionMinor !== null)) + { + printf('%s: executing' . PHP_EOL, $upgradePath); + $upgradeSql = file_get_contents($upgradePath); + $upgradeSql = preg_replace('/^[ \t]+(.*);/m', '\0--', $upgradeSql); + $queries = preg_split('/;\s*[\r\n]+/s', $upgradeSql); + $queries = array_map('trim', $queries); + $queries = array_filter($queries); + $upgradeVersionMinor = 0; + foreach ($queries as $query) + { + $query = preg_replace('/\s*--(.*?)$/m', '', $query); + ++ $upgradeVersionMinor; + if ($upgradeVersionMinor > $dbVersionMinor) + { + try + { + \Chibi\Database::execUnprepared(new \Chibi\Sql\RawStatement($query)); + } + catch (Exception $e) + { + echo $e . PHP_EOL; + echo $query . PHP_EOL; + die; + } + PropertyModel::set(PropertyModel::DbVersion, $upgradeVersionMajor . '.' . $upgradeVersionMinor); + } + } + PropertyModel::set(PropertyModel::DbVersion, $upgradeVersionMajor); + } + else + { + printf('%s: no need to execute' . PHP_EOL, $upgradePath); + } + } + + list ($dbVersionMajor, $dbVersionMinor) = getDbVersion(); + printf('Database version: %d.%d' . PHP_EOL, $dbVersionMajor, $dbVersionMinor); +} + diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 00000000..2fa69c24 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +db.sqlite diff --git a/tests/AbstractTest.php b/tests/AbstractTest.php new file mode 100644 index 00000000..80e58676 --- /dev/null +++ b/tests/AbstractTest.php @@ -0,0 +1,10 @@ +assert = new Assert(); + } +} diff --git a/tests/BasicAuthTest.php b/tests/BasicAuthTest.php new file mode 100644 index 00000000..3bd6ee09 --- /dev/null +++ b/tests/BasicAuthTest.php @@ -0,0 +1,138 @@ +prepareValidUser(); + UserModel::save($user); + + $this->assert->doesNotThrow(function() + { + Auth::login('existing', 'ble', false); + }); + } + + public function testLogout() + { + $this->assert->isFalse(Auth::isLoggedIn()); + $this->testValidPassword(); + $this->assert->isTrue(Auth::isLoggedIn()); + Auth::setCurrentUser(null); + $this->assert->isFalse(Auth::isLoggedIn()); + } + + public function testInvalidUser() + { + $this->assert->throws(function() + { + Auth::login('non-existing', 'wrong-password', false); + }, 'invalid username'); + } + + public function testInvalidPassword() + { + $user = $this->prepareValidUser(); + $user->passHash = UserModel::hashPassword('ble2', $user->passSalt); + UserModel::save($user); + + $this->assert->throws(function() + { + Auth::login('existing', 'wrong-password', false); + }, 'invalid password'); + } + + public function testBanned() + { + $user = $this->prepareValidUser(); + $user->ban(); + UserModel::save($user); + + $this->assert->throws(function() + { + Auth::login('existing', 'ble', false); + }, 'You are banned'); + } + + public function testStaffConfirmationEnabled() + { + getConfig()->registration->staffActivation = true; + + $user = $this->prepareValidUser(); + $user->staffConfirmed = false; + UserModel::save($user); + + $this->assert->throws(function() + { + Auth::login('existing', 'ble', false); + }, 'staff hasn\'t confirmed'); + } + + public function testStaffConfirmationDisabled() + { + getConfig()->registration->staffActivation = false; + + $user = $this->prepareValidUser(); + $user->staffConfirmed = false; + UserModel::save($user); + + $this->assert->doesNotThrow(function() + { + Auth::login('existing', 'ble', false); + }); + } + + public function testMailConfirmationEnabledFail1() + { + getConfig()->registration->needEmailForRegistering = true; + + $user = $this->prepareValidUser(); + $user->staffConfirmed = false; + UserModel::save($user); + + $this->assert->throws(function() + { + Auth::login('existing', 'ble', false); + }, 'need e-mail address confirmation'); + } + + public function testMailConfirmationEnabledFail2() + { + getConfig()->registration->needEmailForRegistering = true; + + $user = $this->prepareValidUser(); + $user->staffConfirmed = false; + $user->emailUnconfirmed = 'test@example.com'; + UserModel::save($user); + + $this->assert->throws(function() + { + Auth::login('existing', 'ble', false); + }, 'need e-mail address confirmation'); + } + + public function testMailConfirmationEnabledPass() + { + getConfig()->registration->needEmailForRegistering = true; + + $user = $this->prepareValidUser(); + $user->staffConfirmed = false; + $user->emailConfirmed = 'test@example.com'; + UserModel::save($user); + + $this->assert->doesNotThrow(function() + { + Auth::login('existing', 'ble', false); + }); + } + + + + protected function prepareValidUser() + { + $user = UserModel::spawn(); + $user->setAccessRank(new AccessRank(AccessRank::Registered)); + $user->setName('existing'); + $user->passHash = UserModel::hashPassword('ble', $user->passSalt); + return $user; + } +} diff --git a/tests/run-all.php b/tests/run-all.php new file mode 100644 index 00000000..a36db31e --- /dev/null +++ b/tests/run-all.php @@ -0,0 +1,135 @@ +getMethods() as $method) + { + if (preg_match('/test/i', $method->name)) + { + $testMethods []= $method; + } + } + } + + return $testMethods; +} + +function runAll() +{ + $startTime = microtime(true); + $testMethods = getTestMethods(); + + echo 'Starting tests' . PHP_EOL; + + //get display names of the methods + $labels = []; + foreach ($testMethods as $key => $method) + $labels[$key] = $method->class . '::' . $method->name; + + //ensure every label has the same length + $maxLabelLength = max(array_map('strlen', $labels)); + foreach ($labels as &$label) + $label = str_pad($label, $maxLabelLength + 1, ' '); + + $pad = count($testMethods) ? ceil(log10(count($testMethods))) : 0; + + //run all the methods + $success = true; + foreach ($testMethods as $key => $method) + { + $instance = new $method->class(); + $testStartTime = microtime(true); + + echo str_pad($key + 1, $pad, ' ', STR_PAD_LEFT) . ' '; + echo $labels[$key] . '... '; + + unset($e); + try + { + runSingle(function() use ($method, $instance) + { + $method->invoke($instance); + }); + echo 'OK'; + } + catch (Exception $e) + { + $success = false; + echo 'FAIL'; + } + + printf(' [%.03fs]' . PHP_EOL, microtime(true) - $testStartTime); + if (isset($e)) + { + echo '---' . PHP_EOL; + echo $e->getMessage() . PHP_EOL; + echo $e->getTraceAsString() . PHP_EOL; + echo '---' . PHP_EOL . PHP_EOL; + } + } + + printf('%s %s... %s [%.03fs]' . PHP_EOL, + str_pad('', $pad, ' '), + str_pad('All tests', $maxLabelLength + 1, ' '), + $success ? 'OK' : 'FAIL', + microtime(true) - $startTime); + + return $success; +} + +function runSingle($callback) +{ + resetEnvironment(); + \Chibi\Database::rollback(function() use ($callback) + { + $callback(); + }); +} diff --git a/upgrade.php b/upgrade.php index a9b4b790..0776cdb3 100644 --- a/upgrade.php +++ b/upgrade.php @@ -1,80 +1,4 @@ rootDir . DS . 'src' . DS . 'Upgrades' . DS . $config->main->dbDriver); -$upgrades = glob($upgradesPath . DS . '*.sql'); -natcasesort($upgrades); - -foreach ($upgrades as $upgradePath) -{ - preg_match('/(\d+)\.sql/', $upgradePath, $matches); - $upgradeVersionMajor = intval($matches[1]); - - list ($dbVersionMajor, $dbVersionMinor) = getDbVersion(); - - if (($upgradeVersionMajor > $dbVersionMajor) or ($upgradeVersionMajor == $dbVersionMajor and $dbVersionMinor !== null)) - { - printf('%s: executing' . PHP_EOL, $upgradePath); - $upgradeSql = file_get_contents($upgradePath); - $upgradeSql = preg_replace('/^[ \t]+(.*);/m', '\0--', $upgradeSql); - $queries = preg_split('/;\s*[\r\n]+/s', $upgradeSql); - $queries = array_map('trim', $queries); - $queries = array_filter($queries); - $upgradeVersionMinor = 0; - foreach ($queries as $query) - { - $query = preg_replace('/\s*--(.*?)$/m', '', $query); - ++ $upgradeVersionMinor; - if ($upgradeVersionMinor > $dbVersionMinor) - { - try - { - \Chibi\Database::execUnprepared(new \Chibi\Sql\RawStatement($query)); - } - catch (Exception $e) - { - echo $e . PHP_EOL; - echo $query . PHP_EOL; - die; - } - PropertyModel::set(PropertyModel::DbVersion, $upgradeVersionMajor . '.' . $upgradeVersionMinor); - } - } - PropertyModel::set(PropertyModel::DbVersion, $upgradeVersionMajor); - } - else - { - printf('%s: no need to execute' . PHP_EOL, $upgradePath); - } -} - -list ($dbVersionMajor, $dbVersionMinor) = getDbVersion(); -printf('Database version: %d.%d' . PHP_EOL, $dbVersionMajor, $dbVersionMinor); +require_once 'src/upgrade.php'; +upgradeDatabase();