diff --git a/.travis.yml b/.travis.yml index 51363216..7ed7d71f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,11 @@ language: php php: - '5.6' - '7.0' + - '7.1' services: - mysql - + before_install: - rvm use 2.2 --install --binary --fuzzy - ruby --version @@ -27,5 +28,7 @@ before_install: script: - cd client - npm test +- cd ../server +- ./run-tests.sh - cd ../tests - bacon init.rb diff --git a/server/composer.json b/server/composer.json index 4802e3c2..81592050 100755 --- a/server/composer.json +++ b/server/composer.json @@ -9,6 +9,6 @@ "ezyang/htmlpurifier": "^4.8" }, "require-dev": { - "phpunit/phpunit": "5.0.*" + "phpunit/phpunit": "^5.7" } } diff --git a/server/controllers/system/init-settings.php b/server/controllers/system/init-settings.php index 8ec4e583..a4220171 100755 --- a/server/controllers/system/init-settings.php +++ b/server/controllers/system/init-settings.php @@ -93,7 +93,7 @@ class InitSettingsController extends Controller { private function storeMailTemplates() { $mails = InitialMails::retrieve(); - + foreach ($mails as $mailType => $mailLanguages) { foreach ($mailLanguages as $mailLanguage => $mailContent) { $mailTemplate = new MailTemplate(); @@ -124,7 +124,7 @@ class InitSettingsController extends Controller { } private function storeLanguages() { $defaultLanguage = Controller::request('language'); - + foreach(Language::LANGUAGES as $languageCode) { $language = new Language(); $language->setProperties([ diff --git a/server/controllers/user/login.php b/server/controllers/user/login.php index 899cb5fd..adcf792a 100755 --- a/server/controllers/user/login.php +++ b/server/controllers/user/login.php @@ -51,15 +51,14 @@ class LoginController extends Controller { if(!Controller::isUserSystemEnabled() && !Controller::request('staff')) { throw new Exception(ERRORS::USER_SYSTEM_DISABLED); } - + if ($this->isAlreadyLoggedIn()) { throw new Exception(ERRORS::SESSION_EXISTS); } if ($this->checkInputCredentials() || $this->checkRememberToken()) { if($this->userInstance->verificationToken !== null) { - Response::respondError(ERRORS::UNVERIFIED_USER); - return; + throw new Exception(ERRORS::UNVERIFIED_USER); } $this->createUserSession(); @@ -71,7 +70,7 @@ class LoginController extends Controller { Response::respondSuccess($this->getUserData()); } else { - Response::respondError(ERRORS::INVALID_CREDENTIALS); + throw new Exception(ERRORS::INVALID_CREDENTIALS); } } @@ -81,13 +80,13 @@ class LoginController extends Controller { private function checkInputCredentials() { $this->userInstance = $this->getUserByInputCredentials(); - + return !$this->userInstance->isNull(); } private function checkRememberToken() { $this->userInstance = $this->getUserByRememberToken(); - + return !$this->userInstance->isNull(); } @@ -117,7 +116,7 @@ class LoginController extends Controller { return User::authenticate($email, $password); } } - + private function getUserByRememberToken() { $rememberToken = Controller::request('rememberToken'); $userInstance = new NullDataStore(); @@ -131,7 +130,7 @@ class LoginController extends Controller { $sessionCookie->delete(); } } - + return $userInstance; } diff --git a/server/libs/Controller.php b/server/libs/Controller.php index c05b7c33..3d198dbc 100755 --- a/server/libs/Controller.php +++ b/server/libs/Controller.php @@ -27,10 +27,10 @@ abstract class Controller { } }; } - + public function validate() { $validator = new Validator(); - + $validator->validate($this->validations()); } @@ -54,7 +54,7 @@ abstract class Controller { public static function request($key, $secure = false) { $result = call_user_func(self::$dataRequester, $key); - + if($secure) { $config = HTMLPurifier_Config::createDefault(); $purifier = new HTMLPurifier($config); @@ -63,7 +63,7 @@ abstract class Controller { return $result; } } - + public static function getLoggedUser() { $session = Session::getInstance(); @@ -90,7 +90,7 @@ abstract class Controller { public static function getAppInstance() { return \Slim\Slim::getInstance(); } - + public function uploadFile($forceUpload = false) { $allowAttachments = Setting::getSetting('allow-attachments')->getValue(); @@ -114,8 +114,8 @@ abstract class Controller { throw new Exception(ERRORS::INVALID_FILE); } } - + public static function isUserSystemEnabled() { return Setting::getSetting('user-system-enabled')->getValue(); } -} \ No newline at end of file +} diff --git a/server/libs/Hashing.php b/server/libs/Hashing.php index da076181..7b270459 100755 --- a/server/libs/Hashing.php +++ b/server/libs/Hashing.php @@ -30,8 +30,10 @@ class Hashing { $sqrt = sqrt($number); $prime = true; - for($i = 0; $i < $sqrt; $i++) { - if($sqrt % 2 === 0) { + if($number <= 1) return false; + + for($i = 2; $i <= $sqrt; $i++) { + if($number % $i === 0) { $prime = false; break; } @@ -39,4 +41,4 @@ class Hashing { return $prime; } -} \ No newline at end of file +} diff --git a/server/libs/LinearCongruentialGenerator.php b/server/libs/LinearCongruentialGenerator.php index 88df4141..e82d4ed3 100755 --- a/server/libs/LinearCongruentialGenerator.php +++ b/server/libs/LinearCongruentialGenerator.php @@ -4,7 +4,7 @@ class LinearCongruentialGenerator { private $first; private $min = 100000; private $max = 999999; - + public function setRange($min, $max) { $this->min = $min; $this->max = $max; @@ -12,20 +12,22 @@ class LinearCongruentialGenerator { public function setGap($gap) { if(!Hashing::isPrime($gap)) throw new Exception('LinearCongruentialGenerator: gap must be prime'); - + $this->gap = $gap; } public function setFirst($first) { $this->first = $first; } - + public function generate($offset) { - if($offset) return ($this->first - $this->min + $offset * $this->gap) % ($this->max - $this->min + 1) + $this->min; - else return $this->generateFirst(); + if(!$this->first) throw new Exception('LinearCongruentialGenerator: first is not set'); + if(!$this->gap) throw new Exception('LinearCongruentialGenerator: gap is not set'); + + return ($this->first - $this->min + $offset * $this->gap) % ($this->max - $this->min + 1) + $this->min; } - + public function generateFirst() { return Hashing::generateRandomNumber($this->min, $this->max); } -} \ No newline at end of file +} diff --git a/server/models/DataStore.php b/server/models/DataStore.php index e280ac13..43ec9d22 100755 --- a/server/models/DataStore.php +++ b/server/models/DataStore.php @@ -134,7 +134,6 @@ abstract class DataStore { public function delete() { RedBean::trash($this->getBeanInstance()); - unset($this); } public function getBeanInstance() { diff --git a/server/run-tests.sh b/server/run-tests.sh index 251e7164..e8c21cc0 100755 --- a/server/run-tests.sh +++ b/server/run-tests.sh @@ -1,3 +1,3 @@ -phpunit --colors tests/models -phpunit --colors tests/controllers -phpunit --colors tests/libs \ No newline at end of file +./vendor/bin/phpunit --colors tests/models +./vendor/bin/phpunit --colors tests/controllers +./vendor/bin/phpunit --colors tests/libs diff --git a/server/tests/__lib__/Mock.php b/server/tests/__lib__/Mock.php index f15e86c0..b3285349 100755 --- a/server/tests/__lib__/Mock.php +++ b/server/tests/__lib__/Mock.php @@ -44,8 +44,9 @@ class Stub { } class Mock { + public static function stub() { - return new Stub; + return new Stub(); } public static function setStatics($statics) { @@ -55,7 +56,7 @@ class Mock { } public static function __callStatic($key, $arguments) { - if (static::$functionList[$key]) { + if (array_key_exists($key, static::$functionList)) { $function = static::$functionList[$key]; return call_user_func_array($function, $arguments); @@ -87,9 +88,11 @@ class Mock { } public function __call($method, $arguments) { - if (isset($this->{$method}) && is_callable($this->{$method})) { + if ($this->{$method} && is_callable($this->$method)) { return call_user_func_array($this->{$method}, $arguments); - } else if (!self::__callStatic($method, $arguments)) { + } else if (array_key_exists($method, static::$functionList)) { + return self::__callStatic($method, $arguments); + } else { throw new Exception("Fatal error: Call to undefined method stdObject::{$method}()"); } } diff --git a/server/tests/__mocks__/ControllerMock.php b/server/tests/__mocks__/ControllerMock.php index 6f9376c5..7a4e8149 100755 --- a/server/tests/__mocks__/ControllerMock.php +++ b/server/tests/__mocks__/ControllerMock.php @@ -1,12 +1,20 @@ parent::stub()->returns('mockRequestValue'), - 'checkUserLogged' => parent::stub()->returns(true) - )); + public static function request($value) { + if($value === 'staff') return false; + return static::$requestReturnMock; } -} \ No newline at end of file + + public static function checkUserLogged() { + return static::$checkUserLoggedReturnMock; + } + + public static function isUserSystemEnabled() { + return static::$isUserSystemEnabledReturnMock; + } +} diff --git a/server/tests/__mocks__/HashingMock.php b/server/tests/__mocks__/HashingMock.php index ef61e299..65b35d6b 100755 --- a/server/tests/__mocks__/HashingMock.php +++ b/server/tests/__mocks__/HashingMock.php @@ -1,6 +1,6 @@ parent::stub()->returns('TEST_TOKEN') )); } - - public static function mockInstanceFunction($functionName, $functionMock) { - self::getInstance()->{$functionName} = $functionMock; - } - - private static function getInstanceMock() { - return new \Mock(array( - 'initSession' => parent::stub(), - 'closeSession' => parent::stub(), - 'createSession' => parent::stub(), - 'getToken' => parent::stub()->returns('TEST_TOKEN'), - 'sessionExists' => parent::stub()->returns(false), - 'checkAuthentication' => parent::stub()->returns(true), - 'isLoggedWithId' => parent::stub()->returns(true), - )); - } -} \ No newline at end of file +} diff --git a/server/tests/__mocks__/SessionCookieMock.php b/server/tests/__mocks__/SessionCookieMock.php new file mode 100644 index 00000000..0d0e850d --- /dev/null +++ b/server/tests/__mocks__/SessionCookieMock.php @@ -0,0 +1,26 @@ +user = new \Mock(); + $this->user->id = 'MOCK_ID'; + } + + public function isNull() { + return false; + } + + public function setProperties() { + return null; + } + + public function store() { + return null; + } +} diff --git a/server/tests/__mocks__/SessionMock.php b/server/tests/__mocks__/SessionMock.php index de102462..417bf749 100755 --- a/server/tests/__mocks__/SessionMock.php +++ b/server/tests/__mocks__/SessionMock.php @@ -24,4 +24,4 @@ class Session extends \Mock { 'isLoggedWithId' => parent::stub()->returns(true), )); } -} \ No newline at end of file +} diff --git a/server/tests/__mocks__/SettingMock.php b/server/tests/__mocks__/SettingMock.php index 3f1b9562..8ad7b7e1 100755 --- a/server/tests/__mocks__/SettingMock.php +++ b/server/tests/__mocks__/SettingMock.php @@ -24,4 +24,4 @@ class Setting extends \Mock { return $mockUserInstance; } -} \ No newline at end of file +} diff --git a/server/tests/__mocks__/SlimMock.php b/server/tests/__mocks__/SlimMock.php index 740bb4e8..a8199c63 100755 --- a/server/tests/__mocks__/SlimMock.php +++ b/server/tests/__mocks__/SlimMock.php @@ -9,7 +9,10 @@ namespace Slim { if (self::$instance === null) { self::$instance = new \Mock(); self::$instance->setBody = \Mock::stub(); + self::$instance->setStatus = \Mock::stub(); self::$instance->finalize = \Mock::stub(); + self::$instance->headers = new \Mock(); + self::$instance->headers->set = \Mock::stub(); } return self::$instance; @@ -18,6 +21,7 @@ namespace Slim { class Slim extends \Mock { protected static $instance; + public static $functionList = []; public function __construct() { } @@ -25,10 +29,10 @@ namespace Slim { public static function getInstance() { if (self::$instance === null) { self::$instance = new Slim(); - self::$instance->response = \Mock::stub()->returns(Response::getInstance()); + self::$instance->response = Response::getInstance(); } return self::$instance; } } -} \ No newline at end of file +} diff --git a/server/tests/controllers/user/loginTest.php b/server/tests/controllers/user/loginTest.php index 2a1583f7..f86ec8b6 100755 --- a/server/tests/controllers/user/loginTest.php +++ b/server/tests/controllers/user/loginTest.php @@ -6,18 +6,22 @@ include_once 'tests/__mocks__/ResponseMock.php'; include_once 'tests/__mocks__/ControllerMock.php'; include_once 'tests/__mocks__/SessionMock.php'; include_once 'tests/__mocks__/UserMock.php'; +include_once 'tests/__mocks__/HashingMock.php'; +include_once 'tests/__mocks__/SessionCookieMock.php'; include_once 'data/ERRORS.php'; include_once 'controllers/user/login.php'; -class LoginControllerTest extends PHPUnit_Framework_TestCase { +use PHPUnit\Framework\TestCase; + +class LoginControllerTest extends TestCase { private $loginController; protected function setUp() { Session::initStubs(); - Controller::initStubs(); User::initStubs(); Response::initStubs(); + $_SERVER['REMOTE_ADDR'] = 'MOCK_REMOTE'; $this->loginController = new LoginController(); } @@ -25,9 +29,9 @@ class LoginControllerTest extends PHPUnit_Framework_TestCase { public function testShouldRespondErrorIfAlreadyLoggedIn() { Session::mockInstanceFunction('sessionExists', \Mock::stub()->returns(true)); - $this->loginController->handler(); + $this->expectExceptionMessage(ERRORS::SESSION_EXISTS); - $this->assertTrue(Response::get('respondError')->hasBeenCalledWithArgs(ERRORS::SESSION_EXISTS)); + $this->loginController->handler(); } public function testShouldCreateSessionAndRespondSuccessIfCredentialsAreValid() { @@ -35,11 +39,11 @@ class LoginControllerTest extends PHPUnit_Framework_TestCase { $this->loginController->handler(); - $this->assertTrue(Session::getInstance()->createSession->hasBeenCalledWithArgs('MOCK_ID', null)); + $this->assertTrue(!!Session::getInstance()->createSession->hasBeenCalledWithArgs('MOCK_ID', false)); $this->assertTrue(Response::get('respondSuccess')->hasBeenCalledWithArgs(array( 'userId' => 'MOCK_ID', 'userEmail' => 'MOCK_EMAIL', - 'staff' => null, + 'staff' => false, 'token' => 'TEST_TOKEN', 'rememberToken' => null ))); @@ -50,8 +54,10 @@ class LoginControllerTest extends PHPUnit_Framework_TestCase { 'authenticate' => \Mock::stub()->returns(new NullDataStore()) )); - $this->loginController->handler(); + Controller::$requestReturnMock = ''; - $this->assertTrue(Response::get('respondError')->hasBeenCalledWithArgs(ERRORS::INVALID_CREDENTIALS)); + $this->expectExceptionMessage(ERRORS::INVALID_CREDENTIALS); + + $this->loginController->handler(); } } diff --git a/server/tests/libs/HashingTest.php b/server/tests/libs/HashingTest.php new file mode 100644 index 00000000..9b4cfb95 --- /dev/null +++ b/server/tests/libs/HashingTest.php @@ -0,0 +1,73 @@ +assertNotEquals($token1, $token2); + } + + public function testShouldHashAndVerifyPassword() { + $TEST_TIMES = 5; + + for ($i = 0; $i < $TEST_TIMES; $i++) { + $password = Hashing::generateRandomToken(); + $passwordHash = Hashing::hashPassword($password); + + $this->assertTrue(Hashing::verifyPassword($password, $passwordHash)); + $this->assertFalse(Hashing::verifyPassword('', $passwordHash)); + $this->assertFalse(Hashing::verifyPassword($password, '')); + $this->assertFalse(Hashing::verifyPassword('', '')); + $this->assertFalse(Hashing::verifyPassword($password . 'a', $passwordHash)); + $this->assertFalse(Hashing::verifyPassword($password, $passwordHash . 'a')); + } + + } + + public function testShouldGenerateNumber() { + $TEST_TIMES = 10; + + for ($i = 0; $i < $TEST_TIMES; $i++) { + $min = $i*1000; + $max = $TEST_TIMES*6000; + + $number1 = Hashing::generateRandomNumber($min, $max); + $number2 = Hashing::generateRandomNumber($min, $max); + + $this->assertTrue($min < $number1 && $number1 < $max); + $this->assertTrue($min < $number2 && $number2 < $max); + $this->assertNotEquals($number1, $number2); + } + } + + public function testShouldRecognizePrime() { + $primes = [2, 3, 5, 7, 11, 17, 53, 163, 379, 401, 443, 449, 701]; + $nonPrimes = [0, 1, 4, 27, 40, 51, 155, 363, 381, 511, 703, 928]; + + foreach($primes as $number) $this->assertTrue(Hashing::isPrime($number)); + foreach($nonPrimes as $number) $this->assertFalse(Hashing::isPrime($number)); + } + + public function testShouldGenerateRandsomPrime() { + $TEST_TIMES = 10; + + for ($i = 0; $i < $TEST_TIMES; $i++) { + $min = $i*1000; + $max = $TEST_TIMES*6000; + + $number1 = Hashing::generateRandomPrime($min, $max); + $number2 = Hashing::generateRandomPrime($min, $max); + + $this->assertTrue($min < $number1 && $number1 < $max); + $this->assertTrue($min < $number2 && $number2 < $max); + $this->assertNotEquals($number1, $number2); + $this->assertTrue(Hashing::isPrime($number1)); + $this->assertTrue(Hashing::isPrime($number2)); + } + } +} diff --git a/server/tests/libs/LinearCongruentialGeneratorTest.php b/server/tests/libs/LinearCongruentialGeneratorTest.php new file mode 100644 index 00000000..b8addb94 --- /dev/null +++ b/server/tests/libs/LinearCongruentialGeneratorTest.php @@ -0,0 +1,30 @@ +setRange($min, $max); + $linearCongruentialGenerator->setGap(Hashing::generateRandomPrime($min, $max)); + $linearCongruentialGenerator->setFirst($linearCongruentialGenerator->generateFirst()); + + $used = []; + + for($j = 0; $j < $GENERATE_TIMES; $j++) { + $generatedNumber = $linearCongruentialGenerator->generate($j); + $this->assertFalse(array_key_exists($generatedNumber, $used)); + $used[$generatedNumber] = true; + } + } + } +} diff --git a/server/tests/libs/validations/captchaTest.php b/server/tests/libs/validations/captchaTest.php index c85bfcf7..73ba73ea 100755 --- a/server/tests/libs/validations/captchaTest.php +++ b/server/tests/libs/validations/captchaTest.php @@ -8,11 +8,12 @@ include_once 'tests/__mocks__/ReCaptchaMock.php'; include_once 'libs/validations/captcha.php'; -class CaptchaValidationTest extends PHPUnit_Framework_TestCase { +use PHPUnit\Framework\TestCase; + +class CaptchaValidationTest extends TestCase { protected function setUp() { Setting::initStubs(); - Controller::initStubs(); APIKey::initStubs(); \ReCaptcha\ReCaptcha::initVerify(); @@ -28,7 +29,7 @@ class CaptchaValidationTest extends PHPUnit_Framework_TestCase { $response = $captchaValidation->validate('MOCK_RESPONSE'); $this->assertFalse($response); } - + public function testShouldPassCorrectValuesToCaptcha() { $captchaValidation = new \CustomValidations\Captcha(); $captchaValidation->validate('MOCK_RESPONSE'); @@ -36,4 +37,4 @@ class CaptchaValidationTest extends PHPUnit_Framework_TestCase { $this->assertTrue(Setting::get('getSetting')->hasBeenCalledWithArgs('recaptcha-private')); $this->assertTrue(\ReCaptcha\ReCaptcha::$staticVerify->hasBeenCalledWithArgs('MOCK_RESPONSE', 'MOCK_REMOTE')); } -} \ No newline at end of file +} diff --git a/server/tests/models/DataStoreTest.php b/server/tests/models/DataStoreTest.php index a4aa7e63..3c852f46 100755 --- a/server/tests/models/DataStoreTest.php +++ b/server/tests/models/DataStoreTest.php @@ -6,6 +6,7 @@ include_once 'tests/__mocks__/RedBeanMock.php'; include_once 'models/DataStore.php'; use RedBeanPHP\Facade as RedBean; +use PHPUnit\Framework\TestCase; class DataStoreMock extends DataStore { const TABLE = 'MOCK_TABLE'; @@ -26,7 +27,7 @@ class DataStoreMock extends DataStore { } } -class DataStoreTest extends PHPUnit_Framework_TestCase { +class DataStoreTest extends TestCase { protected function setUp() { RedBean::initStubs(); diff --git a/server/tests/models/MailTemplateTest.php b/server/tests/models/MailTemplateTest.php index db4ffbfe..fe69c378 100755 --- a/server/tests/models/MailTemplateTest.php +++ b/server/tests/models/MailTemplateTest.php @@ -6,8 +6,9 @@ include_once 'tests/__mocks__/RedBeanMock.php'; include_once 'models/MailTemplate.php'; use RedBeanPHP\Facade as RedBean; +use PHPUnit\Framework\TestCase; -class MailTemplateTest extends PHPUnit_Framework_TestCase { +class MailTemplateTest extends TestCase { protected function setUp() { RedBean::initStubs(); @@ -20,7 +21,7 @@ class MailTemplateTest extends PHPUnit_Framework_TestCase { public function testGetTemplateShouldReturnSpecifiedTemplate() { $mailTemplate = MailTemplate::getTemplate(MailTemplate::USER_SIGNUP); - + $this->assertEquals('TEST_TYPE', $mailTemplate->type); $this->assertTrue(Redbean::get('findOne')->hasBeenCalledWithArgs('mailtemplate', 'type = :type AND language = :language', array( ':type' => 'USER_SIGNUP', diff --git a/server/tests/models/ResponseTest.php b/server/tests/models/ResponseTest.php index 510030d7..567766fe 100755 --- a/server/tests/models/ResponseTest.php +++ b/server/tests/models/ResponseTest.php @@ -3,7 +3,9 @@ include_once 'tests/__lib__/Mock.php'; include_once 'tests/__mocks__/SlimMock.php'; include_once 'models/Response.php'; -class ResponseTest extends PHPUnit_Framework_TestCase { +use PHPUnit\Framework\TestCase; + +class ResponseTest extends TestCase { public function testErrorResponseFormat() { //Mock data $mockErrorValue = 'MOCK_ERROR_VALUE'; @@ -13,7 +15,8 @@ class ResponseTest extends PHPUnit_Framework_TestCase { 'message' => $mockErrorValue, 'data' => $mockData )); - $responseInstance = \Slim\Slim::getInstance()->response(); + $responseInstance = \Slim\Slim::getInstance(); + $responseInstance = $responseInstance->response; //Execute Response Response::respondError($mockErrorValue, $mockData); @@ -31,7 +34,7 @@ class ResponseTest extends PHPUnit_Framework_TestCase { 'message' => $mockErrorValue, 'data' => null )); - $responseInstance = \Slim\Slim::getInstance()->response(); + $responseInstance = \Slim\Slim::getInstance()->response; //Execute Response Response::respondError($mockErrorValue); @@ -48,7 +51,7 @@ class ResponseTest extends PHPUnit_Framework_TestCase { 'status' => 'success', 'data' => $mockData )); - $responseInstance = \Slim\Slim::getInstance()->response(); + $responseInstance = \Slim\Slim::getInstance()->response; //Execute Response Response::respondSuccess($mockData); @@ -64,7 +67,7 @@ class ResponseTest extends PHPUnit_Framework_TestCase { 'status' => 'success', 'data' => null )); - $responseInstance = \Slim\Slim::getInstance()->response(); + $responseInstance = \Slim\Slim::getInstance()->response; //Execute Response Response::respondSuccess();