diff --git a/api/components/ReCaptcha/Validator.php b/api/components/ReCaptcha/Validator.php index b169d46..a30b919 100644 --- a/api/components/ReCaptcha/Validator.php +++ b/api/components/ReCaptcha/Validator.php @@ -3,13 +3,19 @@ namespace api\components\ReCaptcha; use common\helpers\Error as E; use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\ConnectException; +use GuzzleHttp\Exception\ServerException; +use Psr\Http\Message\ResponseInterface; use Yii; use yii\base\Exception; use yii\di\Instance; class Validator extends \yii\validators\Validator { - protected const SITE_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify'; + private const SITE_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify'; + + private const REPEAT_LIMIT = 3; + private const REPEAT_TIMEOUT = 1; public $skipOnEmpty = false; @@ -42,20 +48,47 @@ class Validator extends \yii\validators\Validator { return [$this->requiredMessage, []]; } - $response = $this->client->request('POST', self::SITE_VERIFY_URL, [ + $repeats = 0; + do { + $isSuccess = true; + try { + $response = $this->performRequest($value); + } catch (ConnectException | ServerException $e) { + if (++$repeats >= self::REPEAT_LIMIT) { + throw $e; + } + + $isSuccess = false; + sleep(self::REPEAT_TIMEOUT); + } + } while (!$isSuccess); + + /** @noinspection PhpUndefinedVariableInspection */ + $data = json_decode($response->getBody(), true); + if (!isset($data['success'])) { + throw new Exception('Invalid recaptcha verify response.'); + } + + if (!$data['success']) { + return [$this->message, []]; + } + + return null; + } + + /** + * @param string $value + * @throws \GuzzleHttp\Exception\GuzzleException + * @return ResponseInterface + */ + protected function performRequest(string $value): ResponseInterface { + return $this->client->request('POST', self::SITE_VERIFY_URL, [ 'form_params' => [ 'secret' => $this->component->secret, 'response' => $value, 'remoteip' => Yii::$app->getRequest()->getUserIP(), ], ]); - $data = json_decode($response->getBody(), true); - - if (!isset($data['success'])) { - throw new Exception('Invalid recaptcha verify response.'); - } - - return $data['success'] ? null : [$this->message, []]; } } diff --git a/tests/codeception/api/unit/components/ReCaptcha/ValidatorTest.php b/tests/codeception/api/unit/components/ReCaptcha/ValidatorTest.php index 93df269..1747c8d 100644 --- a/tests/codeception/api/unit/components/ReCaptcha/ValidatorTest.php +++ b/tests/codeception/api/unit/components/ReCaptcha/ValidatorTest.php @@ -3,16 +3,21 @@ namespace codeception\api\unit\components\ReCaptcha; use api\components\ReCaptcha\Validator; use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Psr7\Response; +use phpmock\mockery\PHPMockery; +use ReflectionClass; use tests\codeception\api\unit\TestCase; class ValidatorTest extends TestCase { - public function testValidateValue() { + public function testValidateEmptyValue() { $validator = new Validator(mock(ClientInterface::class)); $this->assertFalse($validator->validate('', $error)); $this->assertEquals('error.captcha_required', $error, 'Get error.captcha_required, if passed empty value'); + } + public function testValidateInvalidValue() { $mockClient = mock(ClientInterface::class); $mockClient->shouldReceive('request')->andReturn(new Response(200, [], json_encode([ 'success' => false, @@ -20,11 +25,39 @@ class ValidatorTest extends TestCase { 'invalid-input-response', // The response parameter is invalid or malformed. ], ]))); + $validator = new Validator($mockClient); $this->assertFalse($validator->validate('12341234', $error)); $this->assertEquals('error.captcha_invalid', $error, 'Get error.captcha_invalid, if passed wrong value'); - unset($error); + } + public function testValidateWithNetworkTroubles() { + $mockClient = mock(ClientInterface::class); + $mockClient->shouldReceive('request')->andThrow(mock(ConnectException::class))->once(); + $mockClient->shouldReceive('request')->andReturn(new Response(200, [], json_encode([ + 'success' => true, + 'error-codes' => [ + 'invalid-input-response', // The response parameter is invalid or malformed. + ], + ])))->once(); + PHPMockery::mock($this->getClassNamespace(Validator::class), 'sleep')->once(); + + $validator = new Validator($mockClient); + $this->assertTrue($validator->validate('12341234', $error)); + $this->assertNull($error); + } + + public function testValidateWithHugeNetworkTroubles() { + $mockClient = mock(ClientInterface::class); + $mockClient->shouldReceive('request')->andThrow(mock(ConnectException::class))->times(3); + PHPMockery::mock($this->getClassNamespace(Validator::class), 'sleep')->times(2); + + $validator = new Validator($mockClient); + $this->expectException(ConnectException::class); + $validator->validate('12341234', $error); + } + + public function testValidateValidValue() { $mockClient = mock(ClientInterface::class); $mockClient->shouldReceive('request')->andReturn(new Response(200, [], json_encode([ 'success' => true, @@ -34,4 +67,8 @@ class ValidatorTest extends TestCase { $this->assertNull($error); } + private function getClassNamespace(string $className): string { + return (new ReflectionClass($className))->getNamespaceName(); + } + }