diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3809b37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# phpstorm project files +.idea + +# netbeans project files +nbproject + +# zend studio for eclipse project files +.buildpath +.project +.settings + +# windows thumbnail cache +Thumbs.db + +# composer vendor dir +/vendor + +/composer.lock + +# composer itself is not needed +composer.phar + +# Mac DS_Store Files +.DS_Store + +# phpunit itself is not needed +phpunit.phar +# local phpunit config +/phpunit.xml + +# local tests configuration +/tests/data/config.local.php + +# runtime cache +/tests/runtime diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..07dc6e4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,31 @@ +language: php + +php: + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - 7.2 + +sudo: true + +# cache vendor dirs +cache: + directories: + - $HOME/.composer/cache + +install: + - travis_retry composer self-update && composer --version + - export PATH="$HOME/.composer/vendor/bin:$PATH" + - travis_retry composer install --prefer-dist --no-interaction + +script: + - phpunit --verbose $PHPUNIT_FLAGS + +after_script: + - | + if [ $TRAVIS_PHP_VERSION = '5.6' ]; then + cd ../../.. + travis_retry wget https://scrutinizer-ci.com/ocular.phar + php ocular.phar code-coverage:upload --format=php-clover coverage.clover + fi diff --git a/Merchant.php b/Merchant.php index 03f1f2f..0ab8523 100644 --- a/Merchant.php +++ b/Merchant.php @@ -3,9 +3,9 @@ namespace robokassa; use Yii; -use yii\base\Object; +use yii\base\BaseObject; -class Merchant extends Object +class Merchant extends BaseObject { public $sMerchantLogin; @@ -23,9 +23,11 @@ public function payment($nOutSum, $nInvId, $sInvDesc = null, $sIncCurrLabel=null $url = $this->baseUrl; $signature = "{$this->sMerchantLogin}:{$nOutSum}:{$nInvId}:{$this->sMerchantPass1}"; + if (!empty($shp)) { $signature .= ':' . $this->implodeShp($shp); } + $sSignatureValue = $this->encryptSignature($signature); $url .= '?' . http_build_query([ @@ -37,7 +39,7 @@ public function payment($nOutSum, $nInvId, $sInvDesc = null, $sIncCurrLabel=null 'IncCurrLabel' => $sIncCurrLabel, 'Email' => $sEmail, 'Culture' => $sCulture, - 'IsTest' => (int)$this->isTest, + 'IsTest' => $this->isTest ? 1 : null, ]); if (!empty($shp) && ($query = http_build_query($shp)) !== '') { @@ -55,6 +57,7 @@ public function payment($nOutSum, $nInvId, $sInvDesc = null, $sIncCurrLabel=null private function implodeShp($shp) { ksort($shp); + foreach($shp as $key => $value) { $shp[$key] = $key . '=' . $value; } @@ -62,17 +65,19 @@ private function implodeShp($shp) return implode(':', $shp); } - public function checkSignature($sSignatureValue, $nOutSum, $nInvId, $sMerchantPass, $shp) + public function checkSignature($sSignatureValue, $nOutSum, $nInvId, $sMerchantPass, $shp = []) { $signature = "{$nOutSum}:{$nInvId}:{$sMerchantPass}"; + if (!empty($shp)) { $signature .= ':' . $this->implodeShp($shp); } + return strtolower($this->encryptSignature($signature)) === strtolower($sSignatureValue); } - private function encryptSignature($signature) + protected function encryptSignature($signature) { return hash($this->hashAlgo, $signature); } diff --git a/README.md b/README.md index fac34de..0079214 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ yii2-robokassa ============== +[![Latest Stable Version](https://poser.pugx.org/yii-cms/yii2-robokassa/v/stable.png)](https://packagist.org/packages/yii-cms/yii2-robokassa) +[![Total Downloads](https://poser.pugx.org/yii-cms/yii2-robokassa/downloads.png)](https://packagist.org/packages/yii-cms/yii2-robokassa) +[![Build Status](https://travis-ci.org/yii-cms/yii2-robokassa.svg?branch=master)](https://travis-ci.org/yii-cms/yii2-robokassa) + + ## Install via Composer ~~~ diff --git a/composer.json b/composer.json index c3d4acb..1fc1dfc 100644 --- a/composer.json +++ b/composer.json @@ -12,10 +12,20 @@ ], "minimum-stability": "dev", "require": { - "php": ">=5.4.0", - "yiisoft/yii2": "*" + "yiisoft/yii2": "~2.0.13" }, + "repositories": [ + { + "type": "composer", + "url": "https://asset-packagist.org" + } + ], "autoload": { "psr-4": { "robokassa\\": "" } + }, + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..346708d --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,21 @@ + + + + + ./tests/unit + + + + + . + + vendor + + + + diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..ef5fb8e --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,17 @@ +setExpectedException($exception); + } + + /** + * @param string $message + */ + public function expectExceptionMessage($message) + { + $parentClassMethods = get_class_methods('PHPUnit_Framework_TestCase'); + if (in_array('expectExceptionMessage', $parentClassMethods)) { + parent::expectExceptionMessage($message); + return; + } + $this->setExpectedException($this->getExpectedException(), $message); + } + + /** + * @param string $messageRegExp + */ + public function expectExceptionMessageRegExp($messageRegExp) + { + $parentClassMethods = get_class_methods('PHPUnit_Framework_TestCase'); + if (in_array('expectExceptionMessageRegExp', $parentClassMethods)) { + parent::expectExceptionMessageRegExp($messageRegExp); + return; + } + $this->setExpectedExceptionRegExp($this->getExpectedException(), $messageRegExp); + } + } + } +} diff --git a/tests/unit/MerchantTest.php b/tests/unit/MerchantTest.php new file mode 100644 index 0000000..2667f4d --- /dev/null +++ b/tests/unit/MerchantTest.php @@ -0,0 +1,94 @@ + 'demo', + 'sMerchantPass1' => 'password_1', + 'hashAlgo' => 'md5', + 'isTest' => true, + ]); + + $returnUrl = $merchant->payment(100, 1, 'Description', null, null, 'en', [], true); + + $this->assertEquals("https://auth.robokassa.ru/Merchant/Index.aspx?MrchLogin=demo&OutSum=100&InvId=1&Desc=Description&SignatureValue=8a50b8d86ed28921edfc371cff6e156f&Culture=en&IsTest=1", $returnUrl); + + // disable test + $merchant->isTest = false; + + $returnUrl = $merchant->payment(100, 1, 'Description', null, null, 'en', [], true); + + $this->assertEquals("https://auth.robokassa.ru/Merchant/Index.aspx?MrchLogin=demo&OutSum=100&InvId=1&Desc=Description&SignatureValue=8a50b8d86ed28921edfc371cff6e156f&Culture=en", $returnUrl); + } + + public function testSignature() + { + $merchant = new Merchant([ + 'sMerchantLogin' => 'demo', + 'sMerchantPass1' => 'password_1', + 'hashAlgo' => 'md5', + 'isTest' => true, + ]); + + $signature = md5('100:1:pass1'); // '1e8f0be69238c13020beba0206951535' + + $check = $merchant->checkSignature($signature, 100, 1, 'pass1'); + + $this->assertInternalType('boolean', $check); + + $this->assertTrue($check); + } + + public function testSignatureUserParams() + { + $merchant = new Merchant([ + 'sMerchantLogin' => 'demo', + 'sMerchantPass1' => 'password_1', + 'hashAlgo' => 'md5', + 'isTest' => true, + ]); + + $signature = md5('100:1:pass1:shp_id=1:shp_login=user1'); // 'd2b1beae30b0c2586eb4b4a7ce23aedd' + + $this->assertTrue($merchant->checkSignature($signature, 100, 1, 'pass1', [ + 'shp_id' => 1, + 'shp_login' => 'user1', + ])); + } + + public function testSignatureInvalidSortUserParams() + { + $merchant = new Merchant([ + 'sMerchantLogin' => 'demo', + 'sMerchantPass1' => 'password_1', + 'hashAlgo' => 'md5', + 'isTest' => true, + ]); + + $signatureInvalidSort = md5('100:1:pass1:shp_login=user1:shp_id=1'); + + $this->assertFalse($merchant->checkSignature($signatureInvalidSort, 100, 1, 'pass1', [ + 'shp_id' => 1, + 'shp_login' => 'user1', + ])); + } + + public function testSignatureAlgo() + { + $merchant = new Merchant([ + 'sMerchantLogin' => 'demo', + 'sMerchantPass1' => 'password_1', + 'hashAlgo' => 'sha256', // <=== 'sha256' + 'isTest' => true, + ]); + + $signature = hash('sha256', '100:1:pass1'); + + $this->assertTrue($merchant->checkSignature($signature, 100, 1, 'pass1')); + } +} diff --git a/tests/unit/TestCase.php b/tests/unit/TestCase.php new file mode 100644 index 0000000..da30f0d --- /dev/null +++ b/tests/unit/TestCase.php @@ -0,0 +1,79 @@ +mockApplication(); + } + + protected function tearDown() + { + $this->destroyApplication(); + } + + /** + * Populates Yii::$app with a new application + * The application will be destroyed on tearDown() automatically. + * @param array $config The application configuration, if needed + * @param string $appClass name of the application class to create + */ + protected function mockApplication($config = [], $appClass = '\yii\console\Application') + { + new $appClass(ArrayHelper::merge([ + 'id' => 'testapp', + 'basePath' => __DIR__, + 'vendorPath' => $this->getVendorPath(), + ], $config)); + } + + protected function mockWebApplication($config = [], $appClass = '\yii\web\Application') + { + new $appClass(ArrayHelper::merge([ + 'id' => 'testapp', + 'basePath' => __DIR__, + 'vendorPath' => dirname(__DIR__) . '/vendor', + 'components' => [ + 'request' => [ + 'cookieValidationKey' => 'test', + 'scriptFile' => __DIR__ .'/index.php', + 'scriptUrl' => '/index.php', + ], + 'robokassa' => [ + 'class' => '\robokassa\Merchant', + 'baseUrl' => 'https://auth.robokassa.ru/Merchant/Index.aspx', + 'sMerchantLogin' => 'demo', + 'sMerchantPass1' => 'test1TEST', + 'sMerchantPass2' => 'test2TEST', + 'isTest' => true, + ], + ] + ], $config)); + } + + /** + * @return string vendor path + */ + protected function getVendorPath() + { + return dirname(dirname(__DIR__)) . '/vendor'; + } + + /** + * Destroys application in Yii::$app by setting it to null. + */ + protected function destroyApplication() + { + Yii::$app = null; + Yii::$container = new Container(); + } +}