diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index daffdd5..2044172 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -11,10 +11,12 @@ jobs: strategy: fail-fast: true matrix: - php: [8.0] - laravel: [8.*] + php: [7.4, 8.0] + laravel: [7.*, 8.*] dependency-version: [prefer-lowest, prefer-stable] include: + - laravel: 7.* + testbench: ^5.18 - laravel: 8.* testbench: ^6.14 @@ -22,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -32,7 +34,7 @@ jobs: coverage: xdebug - name: Cache dependencies - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ~/.composer/cache/files key: ${{ runner.os }}-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} @@ -52,5 +54,5 @@ jobs: COVERALLS_SERVICE_NAME: github run: | rm -rf composer.* vendor/ - composer require cedx/coveralls - vendor/bin/coveralls build/logs/clover.xml + composer require php-coveralls/php-coveralls + vendor/bin/php-coveralls diff --git a/README.md b/README.md index 7cace20..707d044 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![Franck V. - Unsplash (UL) #JjGXjESMxOY](https://images.unsplash.com/photo-1535378620166-273708d44e4c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1280&h=400&q=80) -[![Latest Version on Packagist](https://img.shields.io/packagist/v/darkghosthunter/captchavel.svg?style=flat-square)](https://packagist.org/packages/darkghosthunter/captchavel) [![License](https://poser.pugx.org/darkghosthunter/captchavel/license)](https://packagist.org/packages/darkghosthunter/larapoke) ![](https://img.shields.io/packagist/php-v/darkghosthunter/captchavel.svg) ![](https://github.com/DarkGhostHunter/Captchavel/workflows/PHP%20Composer/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/DarkGhostHunter/Captchavel/badge.svg?branch=master)](https://coveralls.io/github/DarkGhostHunter/Captchavel?branch=master) [![Maintainability](https://api.codeclimate.com/v1/badges/9571f57106069b5f3aac/maintainability)](https://codeclimate.com/github/DarkGhostHunter/Captchavel/maintainability) [![Laravel Octane Incompatible](https://img.shields.io/badge/Laravel%20Octane-Incompatible-red?style=flat&logo=laravel)](https://github.com/laravel/octane) +[![Latest Version on Packagist](https://img.shields.io/packagist/v/darkghosthunter/captchavel.svg?style=flat-square)](https://packagist.org/packages/darkghosthunter/captchavel) [![License](https://poser.pugx.org/darkghosthunter/captchavel/license)](https://packagist.org/packages/darkghosthunter/larapoke) ![](https://img.shields.io/packagist/php-v/darkghosthunter/captchavel.svg) ![](https://github.com/DarkGhostHunter/Captchavel/workflows/PHP%20Composer/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/DarkGhostHunter/Captchavel/badge.svg?branch=master)](https://coveralls.io/github/DarkGhostHunter/Captchavel?branch=master) [![Maintainability](https://api.codeclimate.com/v1/badges/9571f57106069b5f3aac/maintainability)](https://codeclimate.com/github/DarkGhostHunter/Captchavel/maintainability) [![Laravel Octane Compatible](https://img.shields.io/badge/Laravel%20Octane-Compatible-success?style=flat&logo=laravel)](https://github.com/laravel/octane) # Captchavel @@ -14,8 +14,6 @@ It uses your Laravel HTTP Client and **HTTP/2**, making your app **fast**. You o * [Installation](#installation) * [Set Up](#set-up) * [Usage](#usage) - - [Checkbox, Invisible and Android challenges](#checkbox-invisible-and-android-challenges) - - [Score driven interaction](#score-driven-interaction) * [Frontend integration](#frontend-integration) * [Advanced configuration](#advanced-configuration) * [Testing with Captchavel](#testing-with-captchavel) @@ -24,8 +22,8 @@ It uses your Laravel HTTP Client and **HTTP/2**, making your app **fast**. You o ## Requirements -* Laravel 8.x -* PHP 8.0 +* Laravel 7.x, 8.x, or later +* PHP 7.4, 8.0 or later > If you need support for old versions, consider sponsoring or donating. @@ -39,22 +37,22 @@ composer require darkghosthunter/captchavel ## Set up -Add the reCAPTCHA keys for your site to the environment file of your project. You can add each of them for reCAPTCHA v2 **checkbox**, **invisible**, **Android**, and **v3** (score). +Add the reCAPTCHA keys for your site to the environment file of your project. You can add each of them for reCAPTCHA v2 **checkbox**, **invisible**, **Android**, and **score**. If you don't have one, generate it in your [reCAPTCHA Admin panel](https://www.google.com/recaptcha/admin/). ```dotenv -RECAPTCHA_V2_CHECKBOX_SECRET=6t5geA1UAAAAAN... -RECAPTCHA_V2_CHECKBOX_KEY=6t5geA1UAAAAAN... +RECAPTCHA_CHECKBOX_SECRET=6t5geA1UAAAAAN... +RECAPTCHA_CHECKBOX_KEY=6t5geA1UAAAAAN... -RECAPTCHA_V2_INVISIBLE_SECRET=6t5geA2UAAAAAN... -RECAPTCHA_V2_INVISIBLE_KEY=6t5geA2UAAAAAN... +RECAPTCHA_INVISIBLE_SECRET=6t5geA2UAAAAAN... +RECAPTCHA_INVISIBLE_KEY=6t5geA2UAAAAAN... -RECAPTCHA_V2_ANDROID_SECRET=6t5geA3UAAAAAN... -RECAPTCHA_V2_ANDROID_KEY=6t5geA3UAAAAAN... +RECAPTCHA_ANDROID_SECRET=6t5geA3UAAAAAN... +RECAPTCHA_ANDROID_KEY=6t5geA3UAAAAAN... -RECAPTCHA_V3_SECRET=6t5geA4UAAAAAN... -RECAPTCHA_V3_KEY=6t5geA4UAAAAAN... +RECAPTCHA_SCORE_SECRET=6t5geA4UAAAAAN... +RECAPTCHA_SCORE_KEY=6t5geA4UAAAAAN... ``` This allows you to check different reCAPTCHA mechanisms using the same application, in different environments. @@ -63,37 +61,38 @@ This allows you to check different reCAPTCHA mechanisms using the same applicati ## Usage -After you integrate reCAPTCHA into your frontend or Android app, set the Captchavel middleware in the routes you want: +Usage differs based on if you're using checkbox, invisible, or Android challenges, or the v3 score-driven challenge. -* `recaptcha.v2` for Checkbox, Invisible and Android challenges. -* `recaptcha.v3` for Score driven interaction. +### Checkbox, invisible and Android challenges -### Checkbox, Invisible and Android challenges +After you integrate reCAPTCHA into your frontend or Android app, set the Captchavel middleware in the `POST` routes where a form with reCAPTCHA is submitted. The middleware will catch the `g-recaptcha-response` input and check if it's valid. -Add the `recaptcha.v2` middleware to your `POST` routes. The middleware will catch the `g-recaptcha-response` input and check if it's valid. - -* `recaptcha.v2:checkbox` for explicitly rendered checkbox challenges. -* `recaptcha.v2:invisible` for invisible challenges. -* `recaptcha.v2:android` for Android app challenges. +* `recaptcha:checkbox` for explicitly rendered checkbox challenges. +* `recaptcha:invisible` for invisible challenges. +* `recaptcha:android` for Android app challenges. When the validation fails, the user will be redirected back to the form route, or a JSON response will be returned with the validation errors. ```php -Route::post('login', 'LoginController@login') - ->middleware('recaptcha.v2:checkbox'); +use App\Http\Controllers\Auth\LoginController; + +Route::post('login', [LoginController::class, 'login'])->middleware('recaptcha:checkbox'); ``` -> You can change the input name from `g-recaptcha-response` to a custom using a second parameter, like `recaptcha.v2:checkbox,_recaptcha`. +> You can change the input name from `g-recaptcha-response` to a custom using a second parameter, like `recaptcha.checkbox:my_input_name`. + +### Score-driven challenge -### Score driven interaction +The reCAPTCHA v3 middleware works differently from v2. This is a score-driven response is _always_ a success, but the challenge is scored between `0.0` and `1.0`. Robots will get lower scores, while human-like interaction will be higher. -The reCAPTCHA v3 middleware works differently from v2. This is a score-driven challenge between `0.0` and `1.0` where robots will get lower scores than humans. The default threshold is `0.5`. +The default threshold is `0.5`, which will set apart robots that score less than that. -Simply add the `recaptcha.v3` middleware to your route: +Simply add the `recaptcha.score` middleware to your route: ```php -Route::post('comment', 'CommentController@store') - ->middleware('recaptcha.v3'); +use App\Http\Controllers\CommentController; + +Route::post('comment', [CommentController::class, 'create'])->middleware('recaptcha.score'); ``` Once the challenge has been received, you will have access to two methods from the Request instance: `isHuman()` and `isRobot()`, which return `true` or `false`: @@ -107,7 +106,7 @@ public function store(Request $request, Post $post) $comment = $post->comment()->make($request->only('body')); - // Flag the comment as "moderated" if it was a written by robot. + // Flag the comment as "moderated" if it was a written by robot. $comment->moderated = $request->isRobot(); $comment->save(); @@ -118,36 +117,38 @@ public function store(Request $request, Post $post) #### Threshold, action and input name -The middleware accepts three parameters in the following order: +The middleware accepts three additional parameters in the following order: 1. Threshold: Values **above or equal** are considered human. 2. Action: The action name to optionally check against. 3. Input: The name of the reCAPTCHA input to verify. ```php -Route::post('comment', 'CommentController@store') - ->middleware('recaptcha.v3:0.7,login,custom-recaptcha-input'); +use App\Http\Controllers\CommentController; + +Route::post('comment', [CommentController::class, 'create']) + ->middleware('recaptcha.score:0.7,login,custom-recaptcha-input'); ``` -> When checking the action name, ensure your Frontend action matches +> When checking the action name, ensure your Frontend action matches . -#### Faking robot and human scores +#### Faking reCAPTCHA scores -You can easily fake a reCAPTCHA v3 response in your local development by setting `CAPTCHAVEL_FAKE` to `true`. +You can easily fake a reCAPTCHA response scores in your local development by setting `CAPTCHAVEL_FAKE` to `true`. ```dotenv CAPTCHAVEL_FAKE=true ``` -Then, you can fake a low-score response by adding an `is_robot` a checkbox, respectively. +This environment variable changes the reCAPTCHA Factory for a fake one, which will fake successful responses from reCAPTCHA, instead of resolving real challenges. + +From there, you can fake a robot or human response by adding an `is_robot` and returning `true` or `false`, respectively. ```blade -
- - - - -
+ + + +
``` @@ -174,7 +175,7 @@ You can use the `captchavel()` helper to output the site key depending on the ch Captchavel is intended to work out-of-the-box, but you can publish the configuration file for fine-tuning and additional reCAPTCHA verification. ```bash -php artisan vendor:publish --provider="DarkGhostHunter\Captchavel\CaptchavelServiceProvider" +php artisan vendor:publish --provider="DarkGhostHunter\Captchavel\CaptchavelServiceProvider" --tag="config" ``` You will get a config file with this array: @@ -196,13 +197,23 @@ return [ ### Enable Switch +```php + env('CAPTCHAVEL_ENABLE', false), +]; +``` + +By default, Captchavel is disabled, so it doesn't check reCAPTCHA challenges. You can forcefully enable it with the `CAPTCHAVEL_ENABLE` environment variable. + ```dotenv CAPTCHAVEL_ENABLE=true ``` -The main switch to enable or disable Captchavel middleware. This can be handy to enable on some local environments to check real interaction using the included localhost test keys. +This can be handy to enable on some local or development environments to check real interaction using the included localhost test keys, which only work on `localhost`. -When switched off, the `g-recaptcha-response` won't be validated in the Request input, so you can safely disregard any frontend script or reCAPTCHA tokens or boxes. +> When switched off, the reCAPTCHA challenge is not validated in the Request input, so you can safely disregard any frontend script or reCAPTCHA tokens or boxes. ### Fake responses @@ -210,7 +221,7 @@ When switched off, the `g-recaptcha-response` won't be validated in the Request CAPTCHAVEL_FAKE=true ``` -Setting this to true will allow your application to [fake v3-score responses from reCAPTCHA servers](#faking-robot-and-human-scores). +If Captchavel is [enabled](#enable-switch), setting this to true will allow your application to [fake v3-score responses from reCAPTCHA servers](#faking-recaptcha-scores). > This is automatically set to `true` when [running unit tests](#testing-with-captchavel). @@ -249,13 +260,39 @@ return [ Here is the full array of [reCAPTCHA credentials](#set-up) to use depending on the version. Do not change the array unless you know what you're doing. -## Testing with Captchavel +## Testing Score with Captchavel -When unit testing your application, this package [automatically fakes reCAPTCHA responses](#fake-responses). +On testing, when Captchavel is disabled, routes set with the v2 middleware won't need to input the challenge in their body as it will be not verified. -> When mocking requests, there is no need to add any reCAPTCHA token or secrets in your tests. +On the other hand, reCAPTCHA v3 (score) responses can be [automatically faked](#fake-responses). -When using reCAPTCHA v3 (score), you can fake a response made by a human or robot by simply using the `fakeHuman()` and `fakeRobot()` methods, which will score `1.0` or `0.0` respectively. +To do that, you should [enable Captchavel](#enable-switch) on your tests, through the `.env.testing` environment file, or in [PHPUnit environment section](https://phpunit.readthedocs.io/en/9.5/configuration.html?highlight=environment#the-env-element). If you use another testing framework, refer to its documentation. + +```xml + + + + + + + +``` + +Alternatively, you can change the configuration before your unit test: + +```php +public function test_this_route() +{ + config()->set('captchavel.enable', true); + config()->set('captchavel.fake', true); + + // ... +} +``` + +> When faking challenges, there is no need to add any reCAPTCHA token or secrets in your tests. + +When using reCAPTCHA v3 (score), you can fake a response made by a human or robot by simply using the `fakeHuman()` and `fakeRobot()` methods, which will score `1.0` or `0.0` respectively for all subsequent requests. ```php post('login', [ ])->assertViewIs('login.2fa'); ``` -Alternatively, `fakeScore()` method that will fake any score you set. - > Fake responses don't come with actions, hostnames or APK package names. -### Events - -When a reCAPTCHA challenge is resolved, whatever result is received, the `ReCaptchaResponseReceived` event fires with the HTTP Request instance and the reCAPTCHA response. +### Faking Scores manually -### Using your own reCAPTCHA middleware - -You may want to create your own reCAPTCHA middleware. Instead of doing one from scratch, you can extend the `BaseReCaptchaMiddleware`. +Alternatively, `fakeScore()` method will fake responses with any score you set. ```php validateRequest($request, $input); - - $response = $this->retrieve($request, $input, 2, 'checkbox'); - - if ($response->isInvalid()) { - throw $this->validationException($input, 'Complete the reCAPTCHA challenge'); - } - - return $next($request); - } -} +$this->post('comment', [ + 'body' => 'This comment was made by a human', +])->assertSee('Your comment has been posted!'); + +// A robot should have its comment moderated. +Captchavel::fakeScore(0.4); + +$this->post('comment', [ + 'body' => 'Comment made by robot.', +])->assertSee('Your comment will be reviewed before publishing.'); ``` ## Security diff --git a/composer.json b/composer.json index 108ef63..75793eb 100644 --- a/composer.json +++ b/composer.json @@ -17,22 +17,19 @@ } ], "require": { - "php": "^8.0", + "php": ">=7.4", "ext-json": "*", - "illuminate/support": "^8.0", - "illuminate/http": "^8.0", - "illuminate/routing": "^8.0", - "illuminate/container": "^8.0", - "illuminate/events": "^8.0", - "guzzlehttp/guzzle": "^7.0" + "illuminate/support": "^7.0||^8.0", + "illuminate/http": "^7.0||^8.0", + "illuminate/routing": "^7.0||^8.0", + "illuminate/container": "^7.0||^8.0", + "illuminate/events": "^7.0||^8.0", + "guzzlehttp/guzzle": "^6.0||^7.0" }, "require-dev": { - "orchestra/testbench": "^6.14.0", + "orchestra/testbench": "^5.18||^6.14.0", "phpunit/phpunit": "^9.5.2" }, - "conflict": { - "laravel/octane": "*" - }, "autoload": { "psr-4": { "DarkGhostHunter\\Captchavel\\": "src" diff --git a/config/captchavel.php b/config/captchavel.php index d1ed7c7..3030348 100644 --- a/config/captchavel.php +++ b/config/captchavel.php @@ -69,23 +69,21 @@ */ 'credentials' => [ - 'v2' => [ - 'checkbox' => [ - 'secret' => env('RECAPTCHA_V2_CHECKBOX_SECRET', Captchavel::TEST_V2_SECRET), - 'key' => env('RECAPTCHA_V2_CHECKBOX_KEY', Captchavel::TEST_V2_KEY), - ], - 'invisible' => [ - 'secret' => env('RECAPTCHA_V2_INVISIBLE_SECRET', Captchavel::TEST_V2_SECRET), - 'key' => env('RECAPTCHA_V2_INVISIBLE_KEY', Captchavel::TEST_V2_KEY), - ], - 'android' => [ - 'secret' => env('RECAPTCHA_V2_ANDROID_SECRET', Captchavel::TEST_V2_SECRET), - 'key' => env('RECAPTCHA_V2_ANDROID_KEY', Captchavel::TEST_V2_KEY), + Captchavel::CHECKBOX => [ + 'secret' => env('RECAPTCHA_CHECKBOX_SECRET', Captchavel::TEST_V2_SECRET), + 'key' => env('RECAPTCHA_CHECKBOX_KEY', Captchavel::TEST_V2_KEY), ], + Captchavel::INVISIBLE => [ + 'secret' => env('RECAPTCHA_INVISIBLE_SECRET', Captchavel::TEST_V2_SECRET), + 'key' => env('RECAPTCHA_INVISIBLE_KEY', Captchavel::TEST_V2_KEY), + ], + Captchavel::ANDROID => [ + 'secret' => env('RECAPTCHA_ANDROID_SECRET', Captchavel::TEST_V2_SECRET), + 'key' => env('RECAPTCHA_ANDROID_KEY', Captchavel::TEST_V2_KEY), ], - 'v3' => [ - 'secret' => env('RECAPTCHA_V3_SECRET'), - 'key' => env('RECAPTCHA_V3_KEY'), + Captchavel::SCORE => [ + 'secret' => env('RECAPTCHA_SCORE_SECRET'), + 'key' => env('RECAPTCHA_SCORE_KEY'), ], ], ]; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 067cd56..abf7a10 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,21 +1,27 @@ - - - - src/ - - - - - - - - - - tests - - - - - + + + + src/ + + + + + + + + + + tests + + + + + + + + diff --git a/src/Captchavel.php b/src/Captchavel.php index 515c421..695f15f 100644 --- a/src/Captchavel.php +++ b/src/Captchavel.php @@ -2,14 +2,22 @@ namespace DarkGhostHunter\Captchavel; -use LogicException; +use DarkGhostHunter\Captchavel\Http\ReCaptchaResponse; +use Illuminate\Container\Container; +use Illuminate\Contracts\Config\Repository; use Illuminate\Http\Client\Factory; use Illuminate\Http\Client\Response; -use Illuminate\Contracts\Config\Repository as Config; -use DarkGhostHunter\Captchavel\Http\ReCaptchaResponse; +use LogicException; +use RuntimeException; class Captchavel { + // Constants to identify each reCAPTCHA service. + public const CHECKBOX = 'checkbox'; + public const INVISIBLE = 'invisible'; + public const ANDROID = 'android'; + public const SCORE = 'score'; + /** * reCAPTCHA v2 secret for testing on "localhost". * @@ -38,99 +46,70 @@ class Captchavel */ public const INPUT = 'g-recaptcha-response'; - /** - * The available reCAPTCHA v2 variants name. - * - * @var string - */ - public const V2_VARIANTS = [ - 'checkbox', 'invisible', 'android', - ]; - /** * Laravel HTTP Client factory. * - * @var \Illuminate\Http\Client\Factory|\Illuminate\Http\Client\PendingRequest + * @var \Illuminate\Http\Client\Factory */ - protected $httpFactory; + protected Factory $http; /** * Config Repository. * * @var \Illuminate\Contracts\Config\Repository */ - protected $config; - - /** - * Secret to use with given challenge. - * - * @var string - */ - protected $secret; - - /** - * The Captchavel Response created from the reCAPTCHA response. - * - * @var null|\DarkGhostHunter\Captchavel\Http\ReCaptchaResponse - */ - protected $response; + protected Repository $config; /** * Create a new Captchavel instance. * - * @param \Illuminate\Http\Client\Factory $httpFactory + * @param \Illuminate\Http\Client\Factory $http * @param \Illuminate\Contracts\Config\Repository $config */ - public function __construct(Factory $httpFactory, Config $config) + public function __construct(Factory $http, Repository $config) { - $this->httpFactory = $httpFactory; + $this->http = $http; $this->config = $config; } /** - * Returns the Captchavel Response, if any. + * Resolves a reCAPTCHA challenge. * - * @return null|\DarkGhostHunter\Captchavel\Http\ReCaptchaResponse - */ - public function getResponse() - { - return $this->response; - } - - /** - * Check if the a response was resolved from reCAPTCHA servers. + * @param string $challenge + * @param string $ip + * @param string $version * - * @return bool + * @return \DarkGhostHunter\Captchavel\Http\ReCaptchaResponse */ - public function isNotResolved() + public function getChallenge(string $challenge, string $ip, string $version): ReCaptchaResponse { - return $this->response === null; + $response = $this->send($challenge, $ip, $this->useCredentials($version)) + ->setVersion($version) + ->setAsResolved(); + + Container::getInstance()->instance(ReCaptchaResponse::class, $response); + + return $response; } /** * Sets the correct credentials to use to retrieve the challenge results. * - * @param int $version - * @param string|null $variant - * @return $this + * @param string $mode + * + * @return string */ - public function useCredentials(int $version, string $variant = null) + protected function useCredentials(string $mode): string { - if ($version === 2) { - if (! in_array($variant, static::V2_VARIANTS, true)) { - throw new LogicException("The reCAPTCHA v2 variant must be [checkbox], [invisible] or [android]."); - } - $this->secret = $this->config->get("captchavel.credentials.v2.{$variant}.secret"); - } elseif ($version === 3) { - $this->secret = $this->config->get('captchavel.credentials.v3.secret'); + if (!in_array($mode, static::getModes())) { + throw new LogicException('The reCAPTCHA mode must be: ' . implode(', ', static::getModes())); } - if (! $this->secret) { - $name = 'v' . $version . ($variant ? '-' . $variant : ''); - throw new LogicException("The reCAPTCHA secret for [{$name}] doesn't exists."); + if (! $key = $this->config->get("captchavel.credentials.{$mode}.secret")) { + throw new RuntimeException("The reCAPTCHA secret for [{$mode}] doesn't exists"); } - return $this; + return $key; } /** @@ -138,17 +117,16 @@ public function useCredentials(int $version, string $variant = null) * * @param string $challenge * @param string $ip + * @param string $secret + * * @return \DarkGhostHunter\Captchavel\Http\ReCaptchaResponse */ - public function retrieve(string $challenge, string $ip) + protected function send(string $challenge, string $ip, string $secret): ReCaptchaResponse { - $response = $this->httpFactory->asForm() + $response = $this->http + ->asForm() ->withOptions(['version' => 2.0]) - ->post(static::RECAPTCHA_ENDPOINT, [ - 'secret' => $this->secret, - 'response' => $challenge, - 'remoteip' => $ip, - ]); + ->post(static::RECAPTCHA_ENDPOINT, ['secret' => $secret, 'response' => $challenge, 'remoteip' => $ip]); return $this->parse($response); } @@ -157,10 +135,21 @@ public function retrieve(string $challenge, string $ip) * Parses the Response * * @param \Illuminate\Http\Client\Response $response + * * @return \DarkGhostHunter\Captchavel\Http\ReCaptchaResponse */ - protected function parse(Response $response) + protected function parse(Response $response): ReCaptchaResponse + { + return new ReCaptchaResponse($response->json()); + } + + /** + * Checks if the mode is a valid mode name. + * + * @return array|string[] + */ + protected static function getModes(): array { - return $this->response = new ReCaptchaResponse($response->json()); + return [static::CHECKBOX, static::INVISIBLE, static::ANDROID, static::SCORE]; } } diff --git a/src/CaptchavelFake.php b/src/CaptchavelFake.php index eae39dc..79b070e 100644 --- a/src/CaptchavelFake.php +++ b/src/CaptchavelFake.php @@ -7,76 +7,64 @@ class CaptchavelFake extends Captchavel { /** - * Sets a fake response. + * Score to fake * - * @param \DarkGhostHunter\Captchavel\Http\ReCaptchaResponse $response - * @return $this + * @var float|null */ - public function setResponse(ReCaptchaResponse $response) - { - $this->response = $response; - - return $this; - } + public ?float $score = null; /** - * Sets the correct credentials to use to retrieve the challenge results. + * Resolves a reCAPTCHA challenge. * - * @param int $version - * @param string|null $variant - * @return $this - */ - public function useCredentials(int $version, string $variant = null) - { - return $this; - } - - /** - * Retrieves the Response Challenge. - * - * @param string $challenge + * @param string|null $challenge * @param string $ip + * @param string $version + * * @return \DarkGhostHunter\Captchavel\Http\ReCaptchaResponse */ - public function retrieve(?string $challenge, string $ip) + public function getChallenge(?string $challenge = null, string $ip, string $version): ReCaptchaResponse { - return $this->response; + return (new ReCaptchaResponse( + [ + 'success' => true, + 'action' => null, + 'hostname' => null, + 'apk_package_name' => null, + 'challenge_ts' => now()->toAtomString(), + 'score' => $this->score, + ] + ))->setVersion(Captchavel::SCORE)->setAsResolved(); } /** - * Makes the fake Captchavel response with a fake score. + * Adds a fake score to return as a reCAPTCHA response. + * + * @param float $score * - * @param float $score - * @return $this + * @return void */ - public function fakeScore(float $score) + public function fakeScore(float $score): void { - return $this->setResponse(new ReCaptchaResponse([ - 'success' => true, - 'score' => $score, - 'action' => null, - 'hostname' => null, - 'apk_package_name' => null, - ])); + $this->score = $score; } /** * Makes a fake Captchavel response made by a robot with "0" score. * - * @return $this + * @return void */ - public function fakeRobot() + public function fakeRobots(): void { - return $this->fakeScore(0); + $this->score = 0; } /** * Makes a fake Captchavel response made by a human with "1.0" score. * - * @return $this + * @return void */ - public function fakeHuman() + public function fakeHumans(): void { - return $this->fakeScore(1); + $this->score = 1.0; } } diff --git a/src/CaptchavelServiceProvider.php b/src/CaptchavelServiceProvider.php index f44df30..4a81119 100644 --- a/src/CaptchavelServiceProvider.php +++ b/src/CaptchavelServiceProvider.php @@ -2,12 +2,13 @@ namespace DarkGhostHunter\Captchavel; +use DarkGhostHunter\Captchavel\Http\Middleware\VerifyReCaptchaV2; +use DarkGhostHunter\Captchavel\Http\Middleware\VerifyReCaptchaV3; +use DarkGhostHunter\Captchavel\Http\ReCaptchaResponse; +use Illuminate\Contracts\Config\Repository; use Illuminate\Http\Request; use Illuminate\Routing\Router; use Illuminate\Support\ServiceProvider; -use Illuminate\Contracts\Config\Repository; -use DarkGhostHunter\Captchavel\Http\Middleware\VerifyReCaptchaV2; -use DarkGhostHunter\Captchavel\Http\Middleware\VerifyReCaptchaV3; class CaptchavelServiceProvider extends ServiceProvider { @@ -20,7 +21,11 @@ public function register() { $this->mergeConfigFrom(__DIR__.'/../config/captchavel.php', 'captchavel'); + // This is our new factory of reCAPTCHA responses. $this->app->singleton(Captchavel::class); + + // Bind an empty response which by default is never resolved. + $this->app->singleton(ReCaptchaResponse::class); } /** @@ -33,17 +38,15 @@ public function register() public function boot(Router $router, Repository $config) { if ($this->app->runningInConsole()) { - $this->publishes([ - __DIR__.'/../config/captchavel.php' => config_path('captchavel.php'), - ], 'config'); + $this->publishes([__DIR__.'/../config/captchavel.php' => config_path('captchavel.php')], 'config'); if ($this->app->runningUnitTests()) { $config->set('captchavel.fake', true); } } - $router->aliasMiddleware('recaptcha.v2', VerifyReCaptchaV2::class); - $router->aliasMiddleware('recaptcha.v3', VerifyReCaptchaV3::class); + $router->aliasMiddleware('recaptcha', VerifyReCaptchaV2::class); + $router->aliasMiddleware('recaptcha.score', VerifyReCaptchaV3::class); Request::macro('isRobot', [RequestMacro::class, 'isRobot']); Request::macro('isHuman', [RequestMacro::class, 'isHuman']); diff --git a/src/Events/ReCaptchaResponseReceived.php b/src/Events/ReCaptchaResponseReceived.php deleted file mode 100644 index b64c440..0000000 --- a/src/Events/ReCaptchaResponseReceived.php +++ /dev/null @@ -1,35 +0,0 @@ -request = $request; - $this->response = $response; - } -} diff --git a/src/Facades/Captchavel.php b/src/Facades/Captchavel.php index 5a3d9e3..860c40f 100644 --- a/src/Facades/Captchavel.php +++ b/src/Facades/Captchavel.php @@ -2,7 +2,6 @@ namespace DarkGhostHunter\Captchavel\Facades; -use DarkGhostHunter\Captchavel\Captchavel as BaseCaptchavel; use DarkGhostHunter\Captchavel\CaptchavelFake; use Illuminate\Support\Facades\Facade; @@ -16,9 +15,9 @@ class Captchavel extends Facade * * @return string */ - protected static function getFacadeAccessor() + protected static function getFacadeAccessor(): string { - return BaseCaptchavel::class; + return \DarkGhostHunter\Captchavel\Captchavel::class; } /** @@ -26,13 +25,15 @@ protected static function getFacadeAccessor() * * @return \DarkGhostHunter\Captchavel\CaptchavelFake */ - public static function fake() + public static function fake(): CaptchavelFake { - if (static::$resolvedInstance instanceof CaptchavelFake) { - return static::$resolvedInstance; + $instance = static::getFacadeRoot(); + + if ($instance instanceof CaptchavelFake) { + return $instance; } - static::swap($fake = static::$app->make(CaptchavelFake::class)); + static::swap($fake = static::getFacadeApplication()->make(CaptchavelFake::class)); return $fake; } @@ -41,30 +42,31 @@ public static function fake() * Makes the fake Captchavel response with a fake score. * * @param float $score - * @return \DarkGhostHunter\Captchavel\CaptchavelFake + * + * @return void */ public static function fakeScore(float $score) { - return static::fake()->fakeScore($score); + static::fake()->fakeScore($score); } /** * Makes a fake Captchavel response made by a robot with "0" score. * - * @return \DarkGhostHunter\Captchavel\CaptchavelFake + * @return void */ public static function fakeRobot() { - return static::fake()->fakeRobot(); + static::fake()->fakeRobots(); } /** * Makes a fake Captchavel response made by a human with "1.0" score. * - * @return \DarkGhostHunter\Captchavel\CaptchavelFake + * @return void */ public static function fakeHuman() { - return static::fake()->fakeHuman(); + static::fake()->fakeHumans(); } } diff --git a/src/Http/Middleware/BaseReCaptchaMiddleware.php b/src/Http/Middleware/BaseReCaptchaMiddleware.php deleted file mode 100644 index d4e04d4..0000000 --- a/src/Http/Middleware/BaseReCaptchaMiddleware.php +++ /dev/null @@ -1,174 +0,0 @@ -captchavel = $captchavel; - $this->config = $config; - } - - /** - * Determines if the reCAPTCHA verification should be enabled. - * - * @return bool - */ - protected function isEnabled() - { - return $this->config->get('captchavel.enable'); - } - - /** - * Check if the reCAPTCHA response can be faked on-demand. - * - * @return bool - */ - protected function isFake() - { - return $this->config->get('captchavel.fake'); - } - - /** - * Check if the reCAPTCHA response must be real. - * - * @return bool - */ - protected function isReal() - { - return ! $this->isFake(); - } - - /** - * Validate if this Request has the reCAPTCHA challenge string. - * - * @param \Illuminate\Http\Request $request - * @param string $name - * @return void - * @throws \Illuminate\Validation\ValidationException - */ - protected function validateRequest($request, string $name) - { - if (! is_string($request->get($name))) { - throw $this->validationException($name, 'The reCAPTCHA challenge is missing or has not been completed.'); - } - } - - /** - * Retrieves the Captchavel response from reCAPTCHA servers. - * - * @param \Illuminate\Http\Request $request - * @param string $input - * @param int $version - * @param string|null $variant - * @return \DarkGhostHunter\Captchavel\Http\ReCaptchaResponse - */ - protected function retrieve(Request $request, string $input, int $version, string $variant = null) - { - return $this->captchavel - ->useCredentials($version, $variant) - ->retrieve($request->input($input), $request->ip()); - } - - /** - * Fakes a v3 reCAPTCHA response. - * - * @param \Illuminate\Http\Request $request - * @return void - */ - protected function fakeResponseScore($request) - { - // We will first check if the Captchavel instance was not already faked. If it has been, - // we will hands out the control to the developer. Otherwise, we will manually create a - // fake Captchavel instance and look for the "is_robot" parameter to set a fake score. - if ($this->captchavel instanceof Captchavel && $this->captchavel->isNotResolved()) { - $this->captchavel = $request->has('is_robot') ? - CaptchavelFacade::fakeRobot() : - CaptchavelFacade::fakeHuman(); - } - } - - /** - * Validate the Hostname and APK name from the response. - * - * @param \DarkGhostHunter\Captchavel\Http\ReCaptchaResponse $response - * @param string $input - * @param string|null $action - * @throws \Illuminate\Validation\ValidationException - */ - protected function validateResponse(ReCaptchaResponse $response, $input = Captchavel::INPUT, ?string $action = null) - { - if ($response->differentHostname($this->config->get('captchavel.hostname'))) { - throw $this->validationException('hostname', - "The hostname [{$response->hostname}] of the response is invalid."); - } - - if ($response->differentApk($this->config->get('captchavel.apk_package_name'))) { - throw $this->validationException('apk_package_name', - "The apk_package_name [{$response->apk_package_name}] of the response is invalid."); - } - - if ($response->differentAction($action)) { - throw $this->validationException('action', - "The action [{$response->action}] of the response is invalid."); - } - - if ($response->isInvalid()) { - throw $this->validationException($input, - "The reCAPTCHA challenge is invalid or was not completed."); - } - } - - /** - * Creates a new Validation Exception instance. - * - * @param string $input - * @param string $message - * @return \Illuminate\Validation\ValidationException - */ - protected function validationException($input, $message) - { - return ValidationException::withMessages([$input => trans($message)])->redirectTo(back()->getTargetUrl()); - } - - /** - * Dispatch an event with the request and the Captchavel Response - * - * @param \Illuminate\Http\Request $request - * @param \DarkGhostHunter\Captchavel\Http\ReCaptchaResponse $response - */ - protected function dispatch(Request $request, ReCaptchaResponse $response) - { - event(new ReCaptchaResponseReceived($request, $response)); - } -} diff --git a/src/Http/Middleware/ChecksCaptchavelStatus.php b/src/Http/Middleware/ChecksCaptchavelStatus.php new file mode 100644 index 0000000..1ebd5ee --- /dev/null +++ b/src/Http/Middleware/ChecksCaptchavelStatus.php @@ -0,0 +1,26 @@ +config->get('captchavel.enable'); + } + + /** + * Check if the reCAPTCHA response should be faked on-demand. + * + * @return bool + */ + protected function isFake(): bool + { + return $this->config->get('captchavel.fake'); + } +} diff --git a/src/Http/Middleware/ValidatesRequestAndResponse.php b/src/Http/Middleware/ValidatesRequestAndResponse.php new file mode 100644 index 0000000..9200afd --- /dev/null +++ b/src/Http/Middleware/ValidatesRequestAndResponse.php @@ -0,0 +1,86 @@ +input($input); + + if (!is_string($value) || blank($value)) { + throw $this->validationException($input, 'The reCAPTCHA challenge is missing or has not been completed.'); + } + } + + /** + * Creates a new Validation Exception instance. + * + * @param string $input + * @param string $message + * + * @return \Illuminate\Validation\ValidationException + */ + protected function validationException(string $input, string $message): ValidationException + { + return ValidationException::withMessages([$input => trans($message)])->redirectTo(back()->getTargetUrl()); + } + + + /** + * Validate the Hostname and APK name from the response. + * + * @param \DarkGhostHunter\Captchavel\Http\ReCaptchaResponse $response + * @param string $input + * @param string|null $action + * + * @throws \Illuminate\Validation\ValidationException + */ + protected function validateResponse( + ReCaptchaResponse $response, + $input = Captchavel::INPUT, + ?string $action = null + ): void { + if ($response->isDifferentHostname($this->config->get('captchavel.hostname'))) { + throw $this->validationException( + 'hostname', + "The hostname [{$response->hostname}] of the response is invalid." + ); + } + + if ($response->isDifferentApk($this->config->get('captchavel.apk_package_name'))) { + throw $this->validationException( + 'apk_package_name', + "The apk_package_name [{$response->apk_package_name}] of the response is invalid." + ); + } + + if ($response->isDifferentAction($action)) { + throw $this->validationException( + 'action', + "The action [{$response->action}] of the response is invalid." + ); + } + + if ($response->isInvalid()) { + throw $this->validationException( + $input, + "The reCAPTCHA challenge is invalid or was not completed." + ); + } + } +} diff --git a/src/Http/Middleware/VerifyReCaptchaV2.php b/src/Http/Middleware/VerifyReCaptchaV2.php index 6990908..93ec60d 100644 --- a/src/Http/Middleware/VerifyReCaptchaV2.php +++ b/src/Http/Middleware/VerifyReCaptchaV2.php @@ -4,41 +4,61 @@ use Closure; use DarkGhostHunter\Captchavel\Captchavel; +use Illuminate\Config\Repository; +use Illuminate\Http\Request; -class VerifyReCaptchaV2 extends BaseReCaptchaMiddleware +class VerifyReCaptchaV2 { + use ChecksCaptchavelStatus; + use ValidatesRequestAndResponse; + /** - * Handle the incoming request. + * Captchavel connector. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @param string $variant - * @param string $input - * @return mixed - * @throws \Illuminate\Validation\ValidationException + * @var \DarkGhostHunter\Captchavel\Captchavel|\DarkGhostHunter\Captchavel\CaptchavelFake */ - public function handle($request, Closure $next, $variant, $input = Captchavel::INPUT) - { - if ($this->isEnabled() && $this->isReal()) { - $this->validateRequest($request, $input); - $this->processChallenge($request, $variant, $input); - } + protected Captchavel $captchavel; - return $next($request); + /** + * Application Config repository. + * + * @var \Illuminate\Config\Repository + */ + protected Repository $config; + + /** + * BaseReCaptchaMiddleware constructor. + * + * @param \DarkGhostHunter\Captchavel\Captchavel $captchavel + * @param \Illuminate\Config\Repository $config + */ + public function __construct(Captchavel $captchavel, Repository $config) + { + $this->config = $config; + $this->captchavel = $captchavel; } /** - * Process a real challenge and response from reCAPTCHA servers. + * Handle the incoming request. * * @param \Illuminate\Http\Request $request - * @param string $variant + * @param \Closure $next + * @param string $version * @param string $input + * + * @return mixed * @throws \Illuminate\Validation\ValidationException */ - protected function processChallenge($request, $variant, $input) + public function handle(Request $request, Closure $next, string $version, string $input = Captchavel::INPUT) { - $this->dispatch($request, $response = $this->retrieve($request, $input, 2, $variant)); + if ($this->isEnabled() && !$this->isFake()) { + $this->validateRequest($request, $input); + $this->validateResponse( + $this->captchavel->getChallenge($request->input($input), $request->ip(), $version), + $input + ); + } - $this->validateResponse($response, $input); + return $next($request); } } diff --git a/src/Http/Middleware/VerifyReCaptchaV3.php b/src/Http/Middleware/VerifyReCaptchaV3.php index ad660a1..5b3f327 100644 --- a/src/Http/Middleware/VerifyReCaptchaV3.php +++ b/src/Http/Middleware/VerifyReCaptchaV3.php @@ -4,10 +4,44 @@ use Closure; use DarkGhostHunter\Captchavel\Captchavel; +use DarkGhostHunter\Captchavel\CaptchavelFake; +use DarkGhostHunter\Captchavel\Facades\Captchavel as CaptchavelFacade; use DarkGhostHunter\Captchavel\Http\ReCaptchaResponse; +use Illuminate\Config\Repository; +use Illuminate\Container\Container; +use Illuminate\Http\Request; -class VerifyReCaptchaV3 extends BaseReCaptchaMiddleware +class VerifyReCaptchaV3 { + use ChecksCaptchavelStatus; + use ValidatesRequestAndResponse; + + /** + * Captchavel connector. + * + * @var \DarkGhostHunter\Captchavel\Captchavel|\DarkGhostHunter\Captchavel\CaptchavelFake + */ + protected Captchavel $captchavel; + + /** + * Application Config repository. + * + * @var \Illuminate\Config\Repository + */ + protected Repository $config; + + /** + * BaseReCaptchaMiddleware constructor. + * + * @param \DarkGhostHunter\Captchavel\Captchavel $captchavel + * @param \Illuminate\Config\Repository $config + */ + public function __construct(Captchavel $captchavel, Repository $config) + { + $this->config = $config; + $this->captchavel = $captchavel; + } + /** * Handle the incoming request. * @@ -16,59 +50,83 @@ class VerifyReCaptchaV3 extends BaseReCaptchaMiddleware * @param string|null $threshold * @param string|null $action * @param string $input + * * @return mixed * @throws \Illuminate\Validation\ValidationException */ - public function handle($request, Closure $next, $threshold = null, $action = null, $input = Captchavel::INPUT) + public function handle(Request $request, + Closure $next, + string $threshold = null, + string $action = null, + string $input = Captchavel::INPUT + ) { if ($this->isEnabled()) { - if ($this->isReal()) { - $this->validateRequest($request, $input); - } else { + if ($this->isFake()) { $this->fakeResponseScore($request); - - // We will disable the action name since it will be verified if we don't null it. - $action = null; + } else { + $this->validateRequest($request, $input); } - $this->processChallenge($request, $threshold, $action, $input); + $this->processChallenge($request, $input, $threshold, $action); } return $next($request); } + /** + * Fakes a score reCAPTCHA response. + * + * @param \Illuminate\Http\Request $request + * + * @return void + */ + protected function fakeResponseScore(Request $request): void + { + if (! $this->captchavel instanceof CaptchavelFake) { + $this->captchavel = CaptchavelFacade::fake(); + } + + // If the Captchavel has set an score to fake, use it, otherwise go default. + if ($this->captchavel->score === null) { + $request->filled('is_robot') ? $this->captchavel->fakeRobots() : $this->captchavel->fakeHumans(); + } + } + /** * Process the response from reCAPTCHA servers. * * @param \Illuminate\Http\Request $request + * @param string $input * @param null|string $threshold * @param null|string $action - * @param string $input + * * @throws \Illuminate\Validation\ValidationException */ - protected function processChallenge($request, $threshold, $action, $input) + protected function processChallenge(Request $request, string $input, ?string $threshold, ?string $action) { - $response = $this->retrieve($request, $input, 3); - - $response->setThreshold($this->normalizeThreshold($threshold)); - - $this->dispatch($request, $response); + $response = $this->captchavel->getChallenge( + $request->input($input), + $request->ip(), + Captchavel::SCORE + )->setThreshold($this->normalizeThreshold($threshold)); $this->validateResponse($response, $input, $this->normalizeAction($action)); - // After we get the response, we will register the instance as a shared ("singleton"). - // Obviously we will set the threshold set by the developer or just use the default. - // The Response should not be available until the middleware runs, so this is ok. - app()->instance(ReCaptchaResponse::class, $response); + // After we get the response, we will register the instance as a shared + // "singleton" for the current request lifetime. Obviously we will set + // the threshold set by the developer or just use the config default. + Container::getInstance()->instance(ReCaptchaResponse::class, $response); } /** * Normalize the threshold string. * - * @param string|null $threshold - * @return array|float|mixed + * @param string|null $threshold + * + * @return float */ - protected function normalizeThreshold($threshold) + protected function normalizeThreshold(?string $threshold): float { return $threshold === 'null' ? $this->config->get('captchavel.threshold') : (float)$threshold; } @@ -77,9 +135,10 @@ protected function normalizeThreshold($threshold) * Normalizes the action name, or returns null. * * @param null|string $action + * * @return null|string */ - protected function normalizeAction($action) + protected function normalizeAction(?string $action) : ?string { return strtolower($action) === 'null' ? null : $action; } diff --git a/src/Http/ReCaptchaResponse.php b/src/Http/ReCaptchaResponse.php index 51a1f66..c645041 100644 --- a/src/Http/ReCaptchaResponse.php +++ b/src/Http/ReCaptchaResponse.php @@ -2,14 +2,11 @@ namespace DarkGhostHunter\Captchavel\Http; -use LogicException; +use DarkGhostHunter\Captchavel\Captchavel; use Illuminate\Support\Fluent; +use RuntimeException; /** - * Class ReCaptchaResponse - * - * @package DarkGhostHunter\Captchavel\Http - * * @property-read null|string $hostname * @property-read null|string $challenge_ts * @property-read null|string $apk_package_name @@ -20,12 +17,26 @@ */ class ReCaptchaResponse extends Fluent { + /** + * Default reCAPTCHA version. + * + * @var string|null + */ + public ?string $version = null; + /** * The threshold for reCAPTCHA v3. * * @var float */ - protected $threshold; + protected float $threshold = 1.0; + + /** + * Check if the response from reCAPTCHA servers has been received. + * + * @var mixed + */ + protected bool $resolved = false; /** * Sets the threshold to check the response. @@ -33,23 +44,63 @@ class ReCaptchaResponse extends Fluent * @param float $threshold * @return $this */ - public function setThreshold(float $threshold) + public function setThreshold(float $threshold): ReCaptchaResponse { $this->threshold = $threshold; return $this; } + /** + * Sets the reCAPTCHA response as resolved. + * + * @return $this + */ + public function setAsResolved(): ReCaptchaResponse + { + $this->resolved = true; + + return $this; + } + + /** + * Check if the reCAPTCHA response has been resolved. + * + * @return bool + */ + public function isResolved(): bool + { + return $this->resolved; + } + + /** + * Check if the reCAPTCHA response has not been resolved for the request. + * + * @return bool + */ + public function isNotResolved():bool + { + return ! $this->isResolved(); + } + /** * Returns if the response was made by a Human. * * @throws \LogicException * @return bool */ - public function isHuman() + public function isHuman(): bool { + if ($this->isNotResolved()) { + throw new RuntimeException('There is no reCAPTCHA v3 response resolved for this request'); + } + + if ($this->version !== Captchavel::SCORE) { + throw new RuntimeException('This is not a reCAPTCHA v3 response'); + } + if ($this->score === null) { - throw new LogicException('This is not a reCAPTCHA v3 response, or the score is absent.'); + throw new RuntimeException('This is reCAPTCHA v3 response has no score'); } return $this->score >= $this->threshold; @@ -60,7 +111,7 @@ public function isHuman() * * @return bool */ - public function isRobot() + public function isRobot(): bool { return ! $this->isHuman(); } @@ -70,7 +121,7 @@ public function isRobot() * * @return bool */ - public function isValid() + public function isValid(): bool { return $this->success && empty($this->error_codes); } @@ -80,7 +131,7 @@ public function isValid() * * @return bool */ - public function isInvalid() + public function isInvalid(): bool { return ! $this->isValid(); } @@ -91,7 +142,7 @@ public function isInvalid() * @param string|null $string * @return bool */ - public function differentHostname(?string $string) + public function isDifferentHostname(?string $string): bool { return $string && $this->hostname !== $string; } @@ -102,7 +153,7 @@ public function differentHostname(?string $string) * @param string|null $string * @return bool */ - public function differentApk(?string $string) + public function isDifferentApk(?string $string): bool { return $string && $this->apk_package_name !== $string; } @@ -113,7 +164,7 @@ public function differentApk(?string $string) * @param null|string $action * @return bool */ - public function differentAction(?string $action) + public function isDifferentAction(?string $action): bool { return $action && $this->action !== $action; } @@ -121,12 +172,67 @@ public function differentAction(?string $action) /** * Dynamically return an attribute as a property. * - * @param $name - * @return null|mixed + * @param $key + * @return mixed */ - public function __get($name) + public function __get($key) { // Minor fix for getting the error codes - return parent::__get($name === 'error_codes' ? 'error-codes' : $name); + return parent::__get($key === 'error_codes' ? 'error-codes' : $key); + } + + /** + * Sets the version for this reCAPTCHA response. + * + * @param string $version + * + * @return $this + */ + public function setVersion(string $version): ReCaptchaResponse + { + $this->version = $version; + + return $this; } + + /** + * Checks if the reCAPTCHA challenge is for a given version. + * + * @return bool + */ + public function isCheckbox(): bool + { + return $this->version === Captchavel::CHECKBOX; + } + + /** + * Checks if the reCAPTCHA challenge is for a given version. + * + * @return bool + */ + public function isInvisible(): bool + { + return $this->version === Captchavel::INVISIBLE; + } + + /** + * Checks if the reCAPTCHA challenge is for a given version. + * + * @return bool + */ + public function isAndroid(): bool + { + return $this->version === Captchavel::ANDROID; + } + + /** + * Checks if the reCAPTCHA challenge is for a given version. + * + * @return bool + */ + public function isScore(): bool + { + return $this->version === Captchavel::SCORE; + } + } diff --git a/src/RequestMacro.php b/src/RequestMacro.php index 3bdb0e2..c7d9edf 100644 --- a/src/RequestMacro.php +++ b/src/RequestMacro.php @@ -11,7 +11,7 @@ class RequestMacro * * @return bool */ - public static function isHuman() + public static function isHuman(): bool { return app(ReCaptchaResponse::class)->isHuman(); } @@ -21,8 +21,8 @@ public static function isHuman() * * @return bool */ - public static function isRobot() + public static function isRobot(): bool { return ! static::isHuman(); } -} \ No newline at end of file +} diff --git a/src/helpers.php b/src/helpers.php index 4487514..5e90acd 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -1,23 +1,20 @@ assertTrue(config('captchavel.fake')); + static::assertTrue(config('captchavel.fake')); } public function test_makes_fake_score() { Captchavel::fakeScore(0.3); - Route::post('test', function (ReCaptchaResponse $response) { + $this->app['router']->post('test', function (ReCaptchaResponse $response) { return [$response->score, $response->isRobot(), $response->isHuman()]; - })->middleware('recaptcha.v3:0.6'); + })->middleware('recaptcha.score:0.6'); $this->post('test')->assertOk()->assertExactJson([0.3, true, false]); } @@ -41,9 +40,9 @@ public function test_makes_human_score_one() { Captchavel::fakeHuman(); - Route::post('test', function (ReCaptchaResponse $response) { + $this->app['router']->post('test', function (ReCaptchaResponse $response) { return [$response->score, $response->isRobot(), $response->isHuman()]; - })->middleware('recaptcha.v3:0.6'); + })->middleware('recaptcha.score:0.6'); $this->post('test')->assertOk()->assertExactJson([1.0, false, true]); } @@ -52,9 +51,9 @@ public function test_makes_robot_score_zero() { Captchavel::fakeRobot(); - Route::post('test', function (ReCaptchaResponse $response) { + $this->app['router']->post('test', function (ReCaptchaResponse $response) { return [$response->score, $response->isRobot(), $response->isHuman()]; - })->middleware('recaptcha.v3:0.6'); + })->middleware('recaptcha.score:0.6'); $this->post('test')->assertOk()->assertExactJson([0.0, true, false]); } diff --git a/tests/CaptchavelTest.php b/tests/CaptchavelTest.php index 5aa9d31..cec4b58 100644 --- a/tests/CaptchavelTest.php +++ b/tests/CaptchavelTest.php @@ -1,14 +1,17 @@ shouldReceive('asForm')->withNoArgs()->times(3)->andReturnSelf(); $mock->shouldReceive('withOptions')->with(['version' => 2.0])->times(3)->andReturnSelf(); $mock->shouldReceive('post') - ->with(Captchavel::RECAPTCHA_ENDPOINT, [ - 'secret' => Captchavel::TEST_V2_SECRET, - 'response' => 'token', - 'remoteip' => '127.0.0.1', - ]) + ->with( + Captchavel::RECAPTCHA_ENDPOINT, + [ + 'secret' => Captchavel::TEST_V2_SECRET, + 'response' => 'token', + 'remoteip' => '127.0.0.1', + ] + ) ->times(3) - ->andReturn(new Response(new GuzzleResponse(200, ['Content-type' => 'application/json'], json_encode([ - 'success' => true, - 'score' => 0.5, - 'foo' => 'bar', - ])))); - + ->andReturn( + new Response( + new GuzzleResponse( + 200, ['Content-type' => 'application/json'], json_encode( + $array = [ + 'success' => true, + 'score' => 0.5, + 'foo' => 'bar', + ] + ) + ) + ) + ); + + /** @var \DarkGhostHunter\Captchavel\Captchavel $instance */ $instance = app(Captchavel::class); - $this->assertNull($instance->getResponse()); - - $checkbox = $instance->useCredentials(2, 'checkbox')->retrieve('token', '127.0.0.1'); - $this->assertSame($checkbox, $instance->getResponse()); - $this->assertInstanceOf(ReCaptchaResponse::class, $checkbox); - $this->assertTrue($checkbox->success); - $this->assertSame(0.5, $checkbox->score); - $this->assertSame('bar', $checkbox->foo); - - $invisible = $instance->useCredentials(2, 'invisible')->retrieve('token', '127.0.0.1'); - $this->assertSame($invisible, $instance->getResponse()); - $this->assertInstanceOf(ReCaptchaResponse::class, $invisible); - $this->assertTrue($invisible->success); - $this->assertSame(0.5, $invisible->score); - $this->assertSame('bar', $invisible->foo); - - $android = $instance->useCredentials(2, 'android')->retrieve('token', '127.0.0.1'); - $this->assertSame($android, $instance->getResponse()); - $this->assertInstanceOf(ReCaptchaResponse::class, $android); - $this->assertTrue($android->success); - $this->assertSame(0.5, $android->score); - $this->assertSame('bar', $android->foo); + $checkbox = $instance->getChallenge('token', '127.0.0.1', 'checkbox'); + + static::assertTrue($checkbox->isResolved()); + static::assertSame($checkbox->version, 'checkbox'); + static::assertTrue($checkbox->success); + static::assertSame(0.5, $checkbox->score); + static::assertSame('bar', $checkbox->foo); + + $invisible = $instance->getChallenge('token', '127.0.0.1', 'invisible'); + + static::assertTrue($invisible->isResolved()); + static::assertSame($invisible->version, 'invisible'); + static::assertTrue($invisible->success); + static::assertSame(0.5, $invisible->score); + static::assertSame('bar', $invisible->foo); + + $android = $instance->getChallenge('token', '127.0.0.1', 'android'); + + static::assertTrue($android->isResolved()); + static::assertSame($android->version, 'android'); + static::assertTrue($android->success); + static::assertSame(0.5, $android->score); + static::assertSame('bar', $android->foo); } public function test_uses_v2_custom_credentials() { - config(['captchavel.credentials.v2' => [ - 'checkbox' => ['secret' => 'secret-checkbox'], - 'invisible' => ['secret' => 'secret-invisible'], - 'android' => ['secret' => 'secret-android'], - ]]); + config( + [ + 'captchavel.credentials' => [ + 'checkbox' => ['secret' => 'secret-checkbox'], + 'invisible' => ['secret' => 'secret-invisible'], + 'android' => ['secret' => 'secret-android'], + ], + ] + ); $mock = $this->mock(Factory::class); @@ -73,93 +93,118 @@ public function test_uses_v2_custom_credentials() $mock->shouldReceive('withOptions')->with(['version' => 2.0])->times(3)->andReturnSelf(); $mock->shouldReceive('post') - ->with(Captchavel::RECAPTCHA_ENDPOINT, [ - 'secret' => 'secret-checkbox', - 'response' => 'token', - 'remoteip' => '127.0.0.1', - ]) + ->with( + Captchavel::RECAPTCHA_ENDPOINT, + [ + 'secret' => 'secret-checkbox', + 'response' => 'token', + 'remoteip' => '127.0.0.1', + ] + ) ->once() - ->andReturn(new Response(new GuzzleResponse(200, ['Content-type' => 'application/json'], json_encode([ - 'success' => true, - 'score' => 0.5, - 'foo' => 'bar', - ])))); + ->andReturn( + new Response( + new GuzzleResponse( + 200, ['Content-type' => 'application/json'], json_encode( + [ + 'success' => true, + 'score' => 0.5, + 'foo' => 'bar', + ] + ) + ) + ) + ); $mock->shouldReceive('post') - ->with(Captchavel::RECAPTCHA_ENDPOINT, [ - 'secret' => 'secret-invisible', - 'response' => 'token', - 'remoteip' => '127.0.0.1', - ]) + ->with( + Captchavel::RECAPTCHA_ENDPOINT, + [ + 'secret' => 'secret-invisible', + 'response' => 'token', + 'remoteip' => '127.0.0.1', + ] + ) ->once() - ->andReturn(new Response(new GuzzleResponse(200, ['Content-type' => 'application/json'], json_encode([ - 'success' => true, - 'score' => 0.5, - 'foo' => 'bar', - ])))); + ->andReturn( + new Response( + new GuzzleResponse( + 200, ['Content-type' => 'application/json'], json_encode( + [ + 'success' => true, + 'score' => 0.5, + 'foo' => 'bar', + ] + ) + ) + ) + ); $mock->shouldReceive('post') - ->with(Captchavel::RECAPTCHA_ENDPOINT, [ - 'secret' => 'secret-android', - 'response' => 'token', - 'remoteip' => '127.0.0.1', - ]) + ->with( + Captchavel::RECAPTCHA_ENDPOINT, + [ + 'secret' => 'secret-android', + 'response' => 'token', + 'remoteip' => '127.0.0.1', + ] + ) ->once() - ->andReturn(new Response(new GuzzleResponse(200, ['Content-type' => 'application/json'], json_encode([ - 'success' => true, - 'score' => 0.5, - 'foo' => 'bar', - ])))); + ->andReturn( + new Response( + new GuzzleResponse( + 200, ['Content-type' => 'application/json'], json_encode( + [ + 'success' => true, + 'score' => 0.5, + 'foo' => 'bar', + ] + ) + ) + ) + ); $instance = app(Captchavel::class); - $this->assertNull($instance->getResponse()); - - $checkbox = $instance->useCredentials(2, 'checkbox')->retrieve('token', '127.0.0.1'); - $this->assertSame($checkbox, $instance->getResponse()); - $this->assertInstanceOf(ReCaptchaResponse::class, $checkbox); - $this->assertTrue($checkbox->success); - $this->assertSame(0.5, $checkbox->score); - $this->assertSame('bar', $checkbox->foo); - - $invisible = $instance->useCredentials(2, 'invisible')->retrieve('token', '127.0.0.1'); - $this->assertSame($invisible, $instance->getResponse()); - $this->assertInstanceOf(ReCaptchaResponse::class, $invisible); - $this->assertTrue($invisible->success); - $this->assertSame(0.5, $invisible->score); - $this->assertSame('bar', $invisible->foo); - - $android = $instance->useCredentials(2, 'android')->retrieve('token', '127.0.0.1'); - $this->assertSame($android, $instance->getResponse()); - $this->assertInstanceOf(ReCaptchaResponse::class, $android); - $this->assertTrue($android->success); - $this->assertSame(0.5, $android->score); - $this->assertSame('bar', $android->foo); + static::assertEquals( + Captchavel::CHECKBOX, + $instance->getChallenge('token', '127.0.0.1', 'checkbox')->version + ); + static::assertEquals( + Captchavel::INVISIBLE, + $instance->getChallenge('token', '127.0.0.1', 'invisible')->version + ); + static::assertEquals( + Captchavel::ANDROID, + $instance->getChallenge('token', '127.0.0.1', 'android')->version + ); } - public function test_exception_if_no_v3_secret_issued() + public function test_default_response_singleton_never_resolved() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('The reCAPTCHA secret for [v3] doesn\'t exists.'); + static::assertFalse(app(ReCaptchaResponse::class)->isResolved()); + static::assertNull(app(ReCaptchaResponse::class)->version); + } - $instance = app(Captchavel::class); + public function test_exception_if_no_v3_secret_issued() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The reCAPTCHA secret for [score] doesn\'t exists'); - $instance->useCredentials(3)->retrieve('token', '127.0.0.1'); + app(Captchavel::class)->getChallenge('token', '127.0.0.1', 'score'); } public function test_exception_when_invalid_credentials_issued() { $this->expectException(LogicException::class); - $this->expectExceptionMessage('The reCAPTCHA v2 variant must be [checkbox], [invisible] or [android].'); - - $instance = app(Captchavel::class); + $this->expectExceptionMessage('The reCAPTCHA mode must be: checkbox, invisible, android, score'); - $instance->useCredentials(2, 'invalid')->retrieve('token', '127.0.0.1'); + app(Captchavel::class)->getChallenge('token', '127.0.0.1', 'invalid'); } public function test_receives_v3_secret() { - config(['captchavel.credentials.v3.secret' => 'secret']); + config(['captchavel.credentials.score.secret' => 'secret']); $mock = $this->mock(Factory::class); @@ -180,14 +225,19 @@ public function test_receives_v3_secret() $instance = app(Captchavel::class); - $this->assertNull($instance->getResponse()); + $score = $instance->getChallenge('token', '127.0.0.1', 'score'); - $score = $instance->useCredentials(3)->retrieve('token', '127.0.0.1'); + static::assertEquals('score', $score->version); + static::assertTrue($score->isResolved()); + static::assertTrue($score->success); + static::assertSame(0.5, $score->score); + static::assertSame('bar', $score->foo); + } + + protected function tearDown(): void + { + Mockery::close(); - $this->assertSame($score, $instance->getResponse()); - $this->assertInstanceOf(ReCaptchaResponse::class, $score); - $this->assertTrue($score->success); - $this->assertSame(0.5, $score->score); - $this->assertSame('bar', $score->foo); + parent::tearDown(); } } diff --git a/tests/HelperTest.php b/tests/HelperTest.php index 63481a6..66ab6b6 100644 --- a/tests/HelperTest.php +++ b/tests/HelperTest.php @@ -2,9 +2,9 @@ namespace Tests; -use LogicException; -use Orchestra\Testbench\TestCase; use DarkGhostHunter\Captchavel\Captchavel; +use Orchestra\Testbench\TestCase; +use RuntimeException; class HelperTest extends TestCase { @@ -12,7 +12,7 @@ class HelperTest extends TestCase public function test_exception_when_no_v3_key_loaded() { - $this->expectException(LogicException::class); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('The reCAPTCHA site key for [3] doesn\'t exist.'); captchavel(3); @@ -20,29 +20,23 @@ public function test_exception_when_no_v3_key_loaded() public function test_retrieves_test_keys_by_default() { - $this->assertSame(Captchavel::TEST_V2_KEY, captchavel('checkbox')); - $this->assertSame(Captchavel::TEST_V2_KEY, captchavel('invisible')); - $this->assertSame(Captchavel::TEST_V2_KEY, captchavel('android')); + static::assertSame(Captchavel::TEST_V2_KEY, captchavel('checkbox')); + static::assertSame(Captchavel::TEST_V2_KEY, captchavel('invisible')); + static::assertSame(Captchavel::TEST_V2_KEY, captchavel('android')); } public function test_retrieves_secrets() { - config(['captchavel.credentials.v2' => [ + config(['captchavel.credentials' => [ 'checkbox' => ['key' => 'key-checkbox'], 'invisible' => ['key' => 'key-invisible'], 'android' => ['key' => 'key-android'], + 'score' => ['key' => 'key-score'], ]]); - config(['captchavel.credentials.v3' => [ - 'key' => 'key-score' - ]]); - - $this->assertSame('key-checkbox', captchavel('checkbox')); - $this->assertSame('key-invisible', captchavel('invisible')); - $this->assertSame('key-android', captchavel('android')); - $this->assertSame('key-score', captchavel('score')); - $this->assertSame('key-score', captchavel('v3')); - $this->assertSame('key-score', captchavel('3')); - $this->assertSame('key-score', captchavel(3)); + static::assertSame('key-checkbox', captchavel('checkbox')); + static::assertSame('key-invisible', captchavel('invisible')); + static::assertSame('key-android', captchavel('android')); + static::assertSame('key-score', captchavel('score')); } } diff --git a/tests/Http/Middleware/ChallengeMiddlewareTest.php b/tests/Http/Middleware/ChallengeMiddlewareTest.php index 4b6d62b..f23e34b 100644 --- a/tests/Http/Middleware/ChallengeMiddlewareTest.php +++ b/tests/Http/Middleware/ChallengeMiddlewareTest.php @@ -2,34 +2,39 @@ namespace Tests\Http\Middleware; -use Tests\RegistersPackage; -use Orchestra\Testbench\TestCase; -use Illuminate\Support\Facades\Event; -use Illuminate\Support\Facades\Route; use DarkGhostHunter\Captchavel\Captchavel; -use DarkGhostHunter\Captchavel\Http\ReCaptchaResponse; use DarkGhostHunter\Captchavel\Events\ReCaptchaResponseReceived; +use DarkGhostHunter\Captchavel\Http\ReCaptchaResponse; +use Orchestra\Testbench\TestCase; +use Tests\RegistersPackage; class ChallengeMiddlewareTest extends TestCase { use RegistersPackage; use UsesRoutesWithMiddleware; - protected function setUp() : void + protected function setUp(): void { - $this->afterApplicationCreated(function () { - $this->createsRoutes(); - config(['captchavel.fake' => false]); - }); + $this->afterApplicationCreated( + function () { + $this->createsRoutes(); + config(['captchavel.fake' => false]); + } + ); parent::setUp(); } public function test_exception_if_no_challenge_specified() { - Route::post('test', function () { - return 'ok'; - })->middleware('recaptcha.v2'); + config()->set('app.debug', false); + + $this->app['router']->post( + 'test', + function () { + return 'ok'; + } + )->middleware('recaptcha'); $this->post('test')->assertStatus(500); @@ -40,20 +45,28 @@ public function test_bypass_if_not_enabled() { config(['captchavel.enable' => false]); - $event = Event::fake(); - - $this->mock(Captchavel::class)->shouldNotReceive('useCredentials', 'retrieve'); + $this->mock(Captchavel::class)->shouldNotReceive('resolve'); $this->post('v2/checkbox')->assertOk(); $this->post('v2/invisible')->assertOk(); $this->post('v2/android')->assertOk(); + } + public function test_success_when_enabled_and_fake() + { + config(['captchavel.enable' => true]); + config(['captchavel.fake' => true]); - $event->assertNotDispatched(ReCaptchaResponseReceived::class); + $this->post('v2/checkbox')->assertOk(); + $this->post('v2/checkbox/input_bar')->assertOk(); + $this->post('v2/invisible')->assertOk(); + $this->post('v2/invisible/input_bar')->assertOk(); + $this->post('v2/android')->assertOk(); + $this->post('v2/android/input_bar')->assertOk(); } - public function test_fakes_success() + public function test_success_when_disabled() { - config(['captchavel.fake' => true]); + config(['captchavel.enable' => false]); $this->post('v2/checkbox')->assertOk(); $this->post('v2/checkbox/input_bar')->assertOk(); @@ -65,75 +78,51 @@ public function test_fakes_success() public function test_validates_if_real() { - $event = Event::fake(); - $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials')->once()->with(2, 'checkbox')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->once()->with(2, 'invisible')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->once()->with(2, 'android')->andReturnSelf(); - - $mock->shouldReceive('retrieve') - ->times(3) - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ + $response = new ReCaptchaResponse( + [ 'success' => true, 'score' => 0.5, - 'foo' => 'bar' - ])); - - $this->post('v2/checkbox', [ - Captchavel::INPUT => 'token' - ])->assertOk(); - $this->post('v2/invisible', [ - Captchavel::INPUT => 'token' - ])->assertOk(); - $this->post('v2/android', [ - Captchavel::INPUT => 'token' - ])->assertOk(); - - $event->assertDispatchedTimes(ReCaptchaResponseReceived::class, 3); + 'foo' => 'bar', + ] + ); + + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'checkbox')->andReturn($response); + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'invisible')->andReturn($response); + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'android')->andReturn($response); + + $this->post('v2/checkbox', [Captchavel::INPUT => 'token'])->assertOk(); + $this->post('v2/invisible', [Captchavel::INPUT => 'token'])->assertOk(); + $this->post('v2/android', [Captchavel::INPUT => 'token'])->assertOk(); } public function test_uses_custom_input() { - $event = Event::fake(); - $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials')->once()->with(2, 'checkbox')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->once()->with(2, 'invisible')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->once()->with(2, 'android')->andReturnSelf(); - - $mock->shouldReceive('retrieve') - ->times(3) - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ + $response = new ReCaptchaResponse( + [ 'success' => true, 'score' => 0.5, - 'foo' => 'bar' - ])); - - $this->post('v2/checkbox/input_bar', [ - 'bar' => 'token' - ])->assertOk(); - $this->post('v2/invisible/input_bar', [ - 'bar' => 'token' - ])->assertOk(); - $this->post('v2/android/input_bar', [ - 'bar' => 'token' - ])->assertOk(); - - $event->assertDispatchedTimes(ReCaptchaResponseReceived::class, 3); + 'foo' => 'bar', + ] + ); + + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'checkbox')->andReturn($response); + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'invisible')->andReturn($response); + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'android')->andReturn($response); + + $this->post('v2/checkbox/input_bar',['bar' => 'token'])->assertOk(); + $this->post('v2/invisible/input_bar',['bar' => 'token'])->assertOk(); + $this->post('v2/android/input_bar',['bar' => 'token'])->assertOk(); } public function test_exception_when_token_absent() { - $event = Event::fake(); - $mock = $this->mock(Captchavel::class); - $mock->shouldNotReceive('useCredentials', 'retrieve'); + $mock->shouldNotReceive('getChallenge'); $this->post('v2/checkbox')->assertRedirect('/'); $this->postJson('v2/checkbox')->assertJsonValidationErrors(Captchavel::INPUT); @@ -148,26 +137,23 @@ public function test_exception_when_token_absent() $this->postJson('v2/invisible/input_bar')->assertJsonValidationErrors('bar'); $this->post('v2/android/input_bar')->assertRedirect('/'); $this->postJson('v2/android/input_bar')->assertJsonValidationErrors('bar'); - - $event->assertNotDispatched(ReCaptchaResponseReceived::class); } public function test_exception_when_response_invalid() { - $event = Event::fake(); - $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials')->twice()->with(2, 'checkbox')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->twice()->with(2, 'invisible')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->twice()->with(2, 'android')->andReturnSelf(); - - $mock->shouldReceive('retrieve') - ->times(6) - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ + $response = new ReCaptchaResponse( + [ 'success' => false, - ])); + 'score' => 0.5, + 'foo' => 'bar', + ] + ); + + $mock->shouldReceive('getChallenge')->twice()->with('token', '127.0.0.1', 'checkbox')->andReturn($response); + $mock->shouldReceive('getChallenge')->twice()->with('token', '127.0.0.1', 'invisible')->andReturn($response); + $mock->shouldReceive('getChallenge')->twice()->with('token', '127.0.0.1', 'android')->andReturn($response); $this->post('v2/checkbox', [Captchavel::INPUT => 'token'])->assertRedirect('/'); $this->postJson('v2/checkbox', [Captchavel::INPUT => 'token'])->assertJsonValidationErrors(Captchavel::INPUT); @@ -175,83 +161,73 @@ public function test_exception_when_response_invalid() $this->postJson('v2/invisible', [Captchavel::INPUT => 'token'])->assertJsonValidationErrors(Captchavel::INPUT); $this->post('v2/android', [Captchavel::INPUT => 'token'])->assertRedirect('/'); $this->postJson('v2/android', [Captchavel::INPUT => 'token'])->assertJsonValidationErrors(Captchavel::INPUT); - - $event->assertDispatchedTimes(ReCaptchaResponseReceived::class, 6); } public function test_no_error_if_not_hostname_issued() { config(['captchavel.hostname' => null]); - $event = Event::fake(); - $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials')->once()->with(2, 'checkbox')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->once()->with(2, 'invisible')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->once()->with(2, 'android')->andReturnSelf(); - - $mock->shouldReceive('retrieve') - ->times(3) - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ + $response = new ReCaptchaResponse( + [ 'success' => true, - 'hostname' => 'foo' - ])); + 'score' => 0.5, + 'foo' => 'bar', + ] + ); + + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'checkbox')->andReturn($response); + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'invisible')->andReturn($response); + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'android')->andReturn($response); $this->post('v2/checkbox', [Captchavel::INPUT => 'token'])->assertOk(); $this->post('v2/invisible', [Captchavel::INPUT => 'token'])->assertOk(); $this->post('v2/android', [Captchavel::INPUT => 'token'])->assertOk(); - - $event->assertDispatchedTimes(ReCaptchaResponseReceived::class, 3); } public function test_no_error_if_not_hostname_same() { config(['captchavel.hostname' => 'foo']); - $event = Event::fake(); - $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials')->once()->with(2, 'checkbox')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->once()->with(2, 'invisible')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->once()->with(2, 'android')->andReturnSelf(); - - $mock->shouldReceive('retrieve') - ->times(3) - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ + $response = new ReCaptchaResponse( + [ 'success' => true, - 'hostname' => 'foo' - ])); + 'score' => 0.5, + 'foo' => 'bar', + 'hostname' => 'foo', + ] + ); + + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'checkbox')->andReturn($response); + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'invisible')->andReturn($response); + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'android')->andReturn($response); $this->post('v2/checkbox', [Captchavel::INPUT => 'token'])->assertOk(); $this->post('v2/invisible', [Captchavel::INPUT => 'token'])->assertOk(); $this->post('v2/android', [Captchavel::INPUT => 'token'])->assertOk(); - - $event->assertDispatchedTimes(ReCaptchaResponseReceived::class, 3); } public function test_exception_if_hostname_not_equal() { config(['captchavel.hostname' => 'bar']); - $event = Event::fake(); - $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials')->twice()->with(2, 'checkbox')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->twice()->with(2, 'invisible')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->twice()->with(2, 'android')->andReturnSelf(); - - $mock->shouldReceive('retrieve') - ->times(6) - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ + $response = new ReCaptchaResponse( + [ 'success' => true, - 'hostname' => 'foo' - ])); + 'score' => 0.5, + 'foo' => 'bar', + 'hostname' => 'foo', + ] + ); + + $mock->shouldReceive('getChallenge')->twice()->with('token', '127.0.0.1', 'checkbox')->andReturn($response); + $mock->shouldReceive('getChallenge')->twice()->with('token', '127.0.0.1', 'invisible')->andReturn($response); + $mock->shouldReceive('getChallenge')->twice()->with('token', '127.0.0.1', 'android')->andReturn($response); $this->post('v2/checkbox', [Captchavel::INPUT => 'token'])->assertRedirect('/'); $this->postJson('v2/checkbox', [Captchavel::INPUT => 'token'])->assertJsonValidationErrors('hostname'); @@ -259,83 +235,74 @@ public function test_exception_if_hostname_not_equal() $this->postJson('v2/invisible', [Captchavel::INPUT => 'token'])->assertJsonValidationErrors('hostname'); $this->post('v2/android', [Captchavel::INPUT => 'token'])->assertRedirect('/'); $this->postJson('v2/android', [Captchavel::INPUT => 'token'])->assertJsonValidationErrors('hostname'); - - $event->assertDispatchedTimes(ReCaptchaResponseReceived::class, 6); } public function test_no_error_if_no_apk_issued() { config(['captchavel.apk_package_name' => null]); - $event = Event::fake(); - $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials')->once()->with(2, 'checkbox')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->once()->with(2, 'invisible')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->once()->with(2, 'android')->andReturnSelf(); - - $mock->shouldReceive('retrieve') - ->times(3) - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ + $response = new ReCaptchaResponse( + [ 'success' => true, - 'apk_package_name' => 'foo' - ])); + 'score' => 0.5, + 'foo' => 'bar', + 'apk_package_name' => 'foo', + ] + ); + + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'checkbox')->andReturn($response); + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'invisible')->andReturn($response); + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'android')->andReturn($response); $this->post('v2/checkbox', [Captchavel::INPUT => 'token'])->assertOk(); $this->post('v2/invisible', [Captchavel::INPUT => 'token'])->assertOk(); $this->post('v2/android', [Captchavel::INPUT => 'token'])->assertOk(); - - $event->assertDispatchedTimes(ReCaptchaResponseReceived::class, 3); } public function test_no_error_if_no_apk_same() { config(['captchavel.apk_package_name' => 'foo']); - $event = Event::fake(); - $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials')->once()->with(2, 'checkbox')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->once()->with(2, 'invisible')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->once()->with(2, 'android')->andReturnSelf(); - - $mock->shouldReceive('retrieve') - ->times(3) - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ + $response = new ReCaptchaResponse( + [ 'success' => true, - 'apk_package_name' => 'foo' - ])); + 'score' => 0.5, + 'foo' => 'bar', + 'apk_package_name' => 'foo', + ] + ); + + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'checkbox')->andReturn($response); + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'invisible')->andReturn($response); + $mock->shouldReceive('getChallenge')->once()->with('token', '127.0.0.1', 'android')->andReturn($response); $this->post('v2/checkbox', [Captchavel::INPUT => 'token'])->assertOk(); $this->post('v2/invisible', [Captchavel::INPUT => 'token'])->assertOk(); $this->post('v2/android', [Captchavel::INPUT => 'token'])->assertOk(); - - $event->assertDispatchedTimes(ReCaptchaResponseReceived::class, 3); } public function test_exception_if_apk_not_equal() { config(['captchavel.apk_package_name' => 'bar']); - $event = Event::fake(); - $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials')->twice()->with(2, 'checkbox')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->twice()->with(2, 'invisible')->andReturnSelf(); - $mock->shouldReceive('useCredentials')->twice()->with(2, 'android')->andReturnSelf(); - - $mock->shouldReceive('retrieve') - ->times(6) - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ + $response = new ReCaptchaResponse( + [ 'success' => true, - 'apk_package_name' => 'foo' - ])); + 'score' => 0.5, + 'foo' => 'bar', + 'apk_package_name' => 'foo', + ] + ); + + $mock->shouldReceive('getChallenge')->twice()->with('token', '127.0.0.1', 'checkbox')->andReturn($response); + $mock->shouldReceive('getChallenge')->twice()->with('token', '127.0.0.1', 'invisible')->andReturn($response); + $mock->shouldReceive('getChallenge')->twice()->with('token', '127.0.0.1', 'android')->andReturn($response); $this->post('v2/checkbox', [Captchavel::INPUT => 'token'])->assertRedirect('/'); $this->postJson('v2/checkbox', [Captchavel::INPUT => 'token'])->assertJsonValidationErrors('apk_package_name'); @@ -343,7 +310,5 @@ public function test_exception_if_apk_not_equal() $this->postJson('v2/invisible', [Captchavel::INPUT => 'token'])->assertJsonValidationErrors('apk_package_name'); $this->post('v2/android', [Captchavel::INPUT => 'token'])->assertRedirect('/'); $this->postJson('v2/android', [Captchavel::INPUT => 'token'])->assertJsonValidationErrors('apk_package_name'); - - $event->assertDispatchedTimes(ReCaptchaResponseReceived::class, 6); } } diff --git a/tests/Http/Middleware/ScoreMiddlewareTest.php b/tests/Http/Middleware/ScoreMiddlewareTest.php index 4021732..fa488fb 100644 --- a/tests/Http/Middleware/ScoreMiddlewareTest.php +++ b/tests/Http/Middleware/ScoreMiddlewareTest.php @@ -1,30 +1,30 @@ -afterApplicationCreated(function () { - $this->createsRoutes(); - config(['captchavel.fake' => false]); - }); + $this->afterApplicationCreated( + function () { + $this->createsRoutes(); + config(['captchavel.fake' => false]); + } + ); parent::setUp(); } @@ -33,194 +33,151 @@ public function test_bypass_if_not_enabled() { config(['captchavel.enable' => false]); - $event = Event::fake(); - - $this->mock(Captchavel::class)->shouldNotReceive('useCredentials', 'retrieve'); + $this->mock(Captchavel::class)->shouldNotReceive('getChallenge'); $this->post('v3/default')->assertOk(); - - $event->assertNotDispatched(ReCaptchaResponseReceived::class); } public function test_validates_if_real() { $mock = $this->mock(Captchavel::class); - $event = Event::fake(); - - $mock->shouldReceive('useCredentials') - ->with(3, null) - ->andReturnSelf(); - $mock->shouldReceive('retrieve') - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ - 'success' => true, - 'score' => 0.5, - 'foo' => 'bar' - ])); - - $this->post('v3/default', [ - Captchavel::INPUT => 'token' - ]) + $mock->shouldReceive('getChallenge') + ->with('token', '127.0.0.1', 'score') + ->andReturn( + new ReCaptchaResponse( + [ + 'success' => true, + 'score' => 0.5, + 'foo' => 'bar', + ] + ) + ); + + $this->post( + 'v3/default', + [ + Captchavel::INPUT => 'token', + ] + ) ->assertOk() - ->assertExactJson([ - 'success' => true, - 'score' => 0.5, - 'foo' => 'bar' - ]); - - $event->assertDispatched(ReCaptchaResponseReceived::class, function ($event) { - return $event->response->foo === 'bar' && $event->request instanceof Request; - }); + ->assertExactJson( + [ + 'success' => true, + 'score' => 0.5, + 'foo' => 'bar', + ] + ); } public function test_fakes_human_response_automatically() { config(['captchavel.fake' => true]); - $event = Event::fake(); + Carbon::setTestNow(Carbon::now()); $this->post('v3/default') ->assertOk() - ->assertExactJson([ - 'success' => true, - 'score' => 1, - 'action' => null, - 'hostname' => null, - 'apk_package_name' => null, - ]); - - $event->assertDispatched(ReCaptchaResponseReceived::class, function ($event) { - return $event->response->success === true && $event->request instanceof Request; - }); + ->assertExactJson( + [ + 'success' => true, + 'score' => 1, + 'action' => null, + 'hostname' => null, + 'apk_package_name' => null, + 'challenge_ts' => Carbon::now()->toAtomString(), + ] + ); } public function test_fakes_robot_response_if_input_is_robot_present() { config(['captchavel.fake' => true]); - $event = Event::fake(); + Carbon::setTestNow(Carbon::now()); $this->post('v3/default', ['is_robot' => 'on']) ->assertOk() - ->assertExactJson([ - 'success' => true, - 'score' => 0, - 'action' => null, - 'hostname' => null, - 'apk_package_name' => null, - ]); - - $event->assertDispatched(ReCaptchaResponseReceived::class, function ($event) { - return $event->response->success === true && $event->request instanceof Request; - }); + ->assertExactJson( + [ + 'success' => true, + 'score' => 0, + 'action' => null, + 'hostname' => null, + 'apk_package_name' => null, + 'challenge_ts' => Carbon::now()->toAtomString(), + ] + ); } public function test_uses_custom_threshold() { $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials')->andReturnSelf(); - $mock->shouldReceive('retrieve')->andReturn(new ReCaptchaResponse([ - 'success' => true, - 'score' => 0.7, - 'foo' => 'bar' - ])); + $mock->shouldReceive('getChallenge') + ->with('token', '127.0.0.1', 'score') + ->andReturn( + (new ReCaptchaResponse(['success' => true, 'score' => 0.7, 'foo' => 'bar'])) + ->setVersion(Captchavel::SCORE) + ->setAsResolved() + ); - $event = Event::fake(); - - Route::post('test', function (ReCaptchaResponse $response) { + $this->app['router']->post('test', function (ReCaptchaResponse $response) { return [$response->isHuman(), $response->isRobot(), $response->score]; - })->middleware('recaptcha.v3:0.7'); + })->middleware('recaptcha.score:0.7'); - $this->post('test', [ - Captchavel::INPUT => 'token' - ]) + $this->post('test', [Captchavel::INPUT => 'token']) ->assertOk() - ->assertExactJson([ - true, false, 0.7 - ]); - - $event->assertDispatched(ReCaptchaResponseReceived::class, function ($event) { - return $event->response->success === true && $event->request instanceof Request; - }); + ->assertExactJson([true, false, 0.7]); } public function test_uses_custom_input() { $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials') - ->andReturnSelf(); - $mock->shouldReceive('retrieve') - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ - 'success' => true, - 'score' => 0.7, - 'foo' => 'bar' - ])); + $mock->shouldReceive('getChallenge') + ->with('token', '127.0.0.1', 'score') + ->andReturn( + (new ReCaptchaResponse(['success' => true, 'score' => 0.7, 'foo' => 'bar'])) + ->setVersion(Captchavel::SCORE) + ->setAsResolved() + ); - $event = Event::fake(); - - Route::post('test', function (ReCaptchaResponse $response) { + $this->app['router']->post('test', function (ReCaptchaResponse $response) { return $response; - })->middleware('recaptcha.v3:null,null,foo'); + })->middleware('recaptcha.score:null,null,foo'); - $this->post('test', [ - 'foo' => 'token' - ]) + $this->post('test', ['foo' => 'token']) ->assertOk() - ->assertExactJson([ - 'success' => true, - 'score' => 0.7, - 'foo' => 'bar', - ]); - - $event->assertDispatched(ReCaptchaResponseReceived::class, function ($event) { - return $event->response->success === true && $event->request instanceof Request; - }); + ->assertExactJson(['success' => true, 'score' => 0.7, 'foo' => 'bar']); } public function test_exception_when_token_absent() { - $event = Event::fake(); - - $this->post('v3/default', [ - 'foo' => 'bar' - ])->assertRedirect('/'); - - $this->postJson('v3/default', [ - 'foo' => 'bar' - ])->assertJsonValidationErrors(Captchavel::INPUT); + $this->post('v3/default', ['foo' => 'bar']) + ->assertRedirect('/'); - $event->assertNotDispatched(ReCaptchaResponseReceived::class); + $this->postJson('v3/default', ['foo' => 'bar']) + ->assertJsonValidationErrors(Captchavel::INPUT); } public function test_exception_when_response_invalid() { - $event = Event::fake(); - $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials') - ->andReturnSelf(); - $mock->shouldReceive('retrieve') - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ - 'success' => false, - ])); - - $this->post('v3/default', [ - Captchavel::INPUT => 'token' - ])->assertRedirect('/'); - - $event->assertDispatched(ReCaptchaResponseReceived::class, function ($event) { - return $event->response->success === false && $event->request instanceof Request; - }); - - $this->postJson('v3/default', [ - 'foo' => 'bar' - ])->assertJsonValidationErrors(Captchavel::INPUT); + $mock->shouldReceive('getChallenge') + ->with('token', '127.0.0.1', 'score') + ->andReturn( + (new ReCaptchaResponse(['success' => false, 'score' => 0.7, 'foo' => 'bar'])) + ->setVersion(Captchavel::SCORE) + ->setAsResolved() + ); + + $this->post('v3/default', [Captchavel::INPUT => 'token']) + ->assertRedirect('/'); + + $this->postJson('v3/default', ['foo' => 'bar']) + ->assertJsonValidationErrors(Captchavel::INPUT); } public function test_no_error_if_not_hostname_issued() @@ -229,28 +186,19 @@ public function test_no_error_if_not_hostname_issued() $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials') - ->andReturnSelf(); - $mock->shouldReceive('retrieve') - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ - 'success' => true, - 'hostname' => 'foo', - ])); - - $event = Event::fake(); - - $this->post('v3/default', [ - Captchavel::INPUT => 'token' - ])->assertOk(); + $mock->shouldReceive('getChallenge') + ->with('token', '127.0.0.1', 'score') + ->andReturn( + (new ReCaptchaResponse(['success' => true, 'score' => 0.7, 'hostname' => 'foo'])) + ->setVersion(Captchavel::SCORE) + ->setAsResolved() + ); - $event->assertDispatched(ReCaptchaResponseReceived::class, function ($event) { - return $event->response->success === true && $event->request instanceof Request; - }); + $this->post('v3/default', [Captchavel::INPUT => 'token']) + ->assertOk(); - $this->postJson('v3/default', [ - Captchavel::INPUT => 'token' - ])->assertOk(); + $this->postJson('v3/default', [Captchavel::INPUT => 'token']) + ->assertOk(); } public function test_no_error_if_hostname_same() @@ -259,28 +207,19 @@ public function test_no_error_if_hostname_same() $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials') - ->andReturnSelf(); - $mock->shouldReceive('retrieve') - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ - 'success' => true, - 'hostname' => 'bar', - ])); - - $event = Event::fake(); - - $this->post('v3/default', [ - Captchavel::INPUT => 'token' - ])->assertOk(); + $mock->shouldReceive('getChallenge') + ->with('token', '127.0.0.1', 'score') + ->andReturn( + (new ReCaptchaResponse(['success' => true, 'score' => 0.7, 'hostname' => 'bar'])) + ->setVersion(Captchavel::SCORE) + ->setAsResolved() + ); - $event->assertDispatched(ReCaptchaResponseReceived::class, function ($event) { - return $event->response->success === true && $event->request instanceof Request; - }); + $this->post('v3/default', [Captchavel::INPUT => 'token']) + ->assertOk(); - $this->postJson('v3/default', [ - Captchavel::INPUT => 'token' - ])->assertOk(); + $this->postJson('v3/default', [Captchavel::INPUT => 'token']) + ->assertOk(); } public function test_exception_if_hostname_not_equal() @@ -289,28 +228,19 @@ public function test_exception_if_hostname_not_equal() $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials') - ->andReturnSelf(); - $mock->shouldReceive('retrieve') - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ - 'success' => true, - 'hostname' => 'foo', - ])); + $mock->shouldReceive('getChallenge') + ->with('token', '127.0.0.1', 'score') + ->andReturn( + (new ReCaptchaResponse(['success' => true, 'score' => 0.7, 'hostname' => 'foo'])) + ->setVersion(Captchavel::SCORE) + ->setAsResolved() + ); - $event = Event::fake(); + $this->post('v3/default', [Captchavel::INPUT => 'token']) + ->assertRedirect('/'); - $this->post('v3/default', [ - Captchavel::INPUT => 'token' - ])->assertRedirect('/'); - - $event->assertDispatched(ReCaptchaResponseReceived::class, function ($event) { - return $event->response->hostname === 'foo' && $event->request instanceof Request; - }); - - $this->postJson('v3/default', [ - Captchavel::INPUT => 'token' - ])->assertJsonValidationErrors('hostname'); + $this->postJson('v3/default', [Captchavel::INPUT => 'token']) + ->assertJsonValidationErrors('hostname'); } public function test_no_error_if_no_apk_issued() @@ -319,28 +249,19 @@ public function test_no_error_if_no_apk_issued() $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials') - ->andReturnSelf(); - $mock->shouldReceive('retrieve') - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ - 'success' => true, - 'apk_package_name' => 'foo', - ])); - - $event = Event::fake(); + $mock->shouldReceive('getChallenge') + ->with('token', '127.0.0.1', 'score') + ->andReturn( + (new ReCaptchaResponse(['success' => true, 'score' => 0.7, 'apk_package_name' => 'foo'])) + ->setVersion(Captchavel::SCORE) + ->setAsResolved() + ); - $this->post('v3/default', [ - Captchavel::INPUT => 'token' - ])->assertOk(); + $this->post('v3/default', [Captchavel::INPUT => 'token']) + ->assertOk(); - $event->assertDispatched(ReCaptchaResponseReceived::class, function ($event) { - return $event->response->apk_package_name === 'foo' && $event->request instanceof Request; - }); - - $this->postJson('v3/default', [ - Captchavel::INPUT => 'token' - ])->assertOk(); + $this->postJson('v3/default', [Captchavel::INPUT => 'token']) + ->assertOk(); } public function test_no_error_if_apk_same() @@ -349,28 +270,19 @@ public function test_no_error_if_apk_same() $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials') - ->andReturnSelf(); - $mock->shouldReceive('retrieve') - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ - 'success' => true, - 'apk_package_name' => 'foo', - ])); - - $event = Event::fake(); + $mock->shouldReceive('getChallenge') + ->with('token', '127.0.0.1', 'score') + ->andReturn( + (new ReCaptchaResponse(['success' => true, 'score' => 0.7, 'apk_package_name' => 'foo'])) + ->setVersion(Captchavel::SCORE) + ->setAsResolved() + ); - $this->post('v3/default', [ - Captchavel::INPUT => 'token' - ])->assertOk(); + $this->post('v3/default', [Captchavel::INPUT => 'token']) + ->assertOk(); - $event->assertDispatched(ReCaptchaResponseReceived::class, function ($event) { - return $event->response->apk_package_name === 'foo' && $event->request instanceof Request; - }); - - $this->postJson('v3/default', [ - Captchavel::INPUT => 'token' - ])->assertOk(); + $this->postJson('v3/default', [Captchavel::INPUT => 'token']) + ->assertOk(); } public function test_exception_if_apk_not_equal() @@ -379,118 +291,85 @@ public function test_exception_if_apk_not_equal() $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials') - ->andReturnSelf(); - $mock->shouldReceive('retrieve') - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ - 'success' => true, - 'apk_package_name' => null, - ])); - - $event = Event::fake(); - - $this->post('v3/default', [ - Captchavel::INPUT => 'token' - ])->assertRedirect('/'); + $mock->shouldReceive('getChallenge') + ->with('token', '127.0.0.1', 'score') + ->andReturn( + (new ReCaptchaResponse(['success' => true, 'score' => 0.7, 'apk_package_name' => null])) + ->setVersion(Captchavel::SCORE) + ->setAsResolved() + ); - $event->assertDispatched(ReCaptchaResponseReceived::class, function ($event) { - return $event->response->apk_package_name === null && $event->request instanceof Request; - }); + $this->post('v3/default', [Captchavel::INPUT => 'token']) + ->assertRedirect('/'); - $this->postJson('v3/default', [ - Captchavel::INPUT => 'token' - ])->assertJsonValidationErrors('apk_package_name'); + $this->postJson('v3/default', [Captchavel::INPUT => 'token']) + ->assertJsonValidationErrors('apk_package_name'); } public function test_no_error_if_no_action() { $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials') - ->andReturnSelf(); - $mock->shouldReceive('retrieve') - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ - 'success' => true, - 'apk_package_name' => null, - 'action' => 'foo', - ])); + $mock->shouldReceive('getChallenge') + ->with('token', '127.0.0.1', 'score') + ->andReturn( + (new ReCaptchaResponse(['success' => true, 'action' => 'foo', 'apk_package_name' => null])) + ->setVersion(Captchavel::SCORE) + ->setAsResolved() + ); - $event = Event::fake(); - - Route::post('test', function (ReCaptchaResponse $response) { + $this->app['router']->post('test', function (ReCaptchaResponse $response) { return $response; - })->middleware('recaptcha.v3:null,null'); - - $this->post('test', [ - Captchavel::INPUT => 'token' - ])->assertOk(); + })->middleware('recaptcha.score:null,null'); - $event->assertDispatched(ReCaptchaResponseReceived::class, function ($event) { - return $event->response->action === 'foo' && $event->request instanceof Request; - }); + $this->post('test', [Captchavel::INPUT => 'token'])->assertOk(); } public function test_no_error_if_action_same() { $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials') - ->andReturnSelf(); - $mock->shouldReceive('retrieve') - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ - 'success' => true, - 'apk_package_name' => null, - 'action' => 'foo', - ])); + $mock->shouldReceive('getChallenge') + ->with('token', '127.0.0.1', 'score') + ->andReturn( + (new ReCaptchaResponse(['success' => true, 'action' => 'foo', 'apk_package_name' => null])) + ->setVersion(Captchavel::SCORE) + ->setAsResolved() + ); - $event = Event::fake(); - - Route::post('test', function (ReCaptchaResponse $response) { + $this->app['router']->post('test', function (ReCaptchaResponse $response) { return $response; - })->middleware('recaptcha.v3:null,foo'); - - $this->post('test', [ - Captchavel::INPUT => 'token' - ])->assertOk(); + })->middleware('recaptcha.score:null,foo'); - $event->assertDispatched(ReCaptchaResponseReceived::class, function ($event) { - return $event->response->action === 'foo' && $event->request instanceof Request; - }); + $this->post('test', [Captchavel::INPUT => 'token'])->assertOk(); } public function test_exception_if_action_not_equal() { $mock = $this->mock(Captchavel::class); - $mock->shouldReceive('useCredentials') - ->andReturnSelf(); - $mock->shouldReceive('retrieve') - ->with('token', '127.0.0.1') - ->andReturn(new ReCaptchaResponse([ - 'success' => true, - 'apk_package_name' => null, - 'action' => 'foo', - ])); - - Route::post('test', function (ReCaptchaResponse $response) { - return $response; - })->middleware('recaptcha.v3:null,bar'); - - $this->post('test', [ - Captchavel::INPUT => 'token' - ])->assertRedirect('/'); - - $this->postJson('test', [ - Captchavel::INPUT => 'token' - ])->assertJsonValidationErrors('action'); + $mock->shouldReceive('getChallenge') + ->with('token', '127.0.0.1', 'score') + ->andReturn( + (new ReCaptchaResponse(['success' => true, 'action' => 'foo', 'apk_package_name' => null])) + ->setVersion(Captchavel::SCORE) + ->setAsResolved() + ); + + $this->app['router']->post( + 'test', + function (ReCaptchaResponse $response) { + return $response; + } + )->middleware('recaptcha.score:null,bar'); + + $this->post('test', [Captchavel::INPUT => 'token'])->assertRedirect('/'); + $this->postJson('test', [Captchavel::INPUT => 'token'])->assertJsonValidationErrors('action'); } public function test_checks_for_human_score() { - config(['captchavel.credentials.v3.secret' => 'secret']); + config(['captchavel.credentials.score.secret' => 'secret']); config(['captchavel.fake' => false]); $mock = $this->mock(Factory::class); @@ -498,32 +377,55 @@ public function test_checks_for_human_score() $mock->shouldReceive('asForm')->withNoArgs()->times(4)->andReturnSelf(); $mock->shouldReceive('withOptions')->with(['version' => 2.0])->times(4)->andReturnSelf(); $mock->shouldReceive('post') - ->with(Captchavel::RECAPTCHA_ENDPOINT, [ - 'secret' => 'secret', - 'response' => 'token', - 'remoteip' => '127.0.0.1', - ]) + ->with( + Captchavel::RECAPTCHA_ENDPOINT, + [ + 'secret' => 'secret', + 'response' => 'token', + 'remoteip' => '127.0.0.1', + ] + ) ->times(4) - ->andReturn(new Response(new GuzzleResponse(200, ['Content-type' => 'application/json'], json_encode([ - 'success' => true, - 'score' => 0.5, - ])))); - - Route::post('human_human', function (Request $request) { - return $request->isHuman() ? 'true' : 'false'; - })->middleware('recaptcha.v3:0.7'); - - Route::post('human_robot', function (Request $request) { - return $request->isRobot() ? 'true' : 'false'; - })->middleware('recaptcha.v3:0.7'); - - Route::post('robot_human', function (Request $request) { - return $request->isHuman() ? 'true' : 'false'; - })->middleware('recaptcha.v3:0.3'); - - Route::post('robot_robot', function (Request $request) { - return $request->isRobot() ? 'true' : 'false'; - })->middleware('recaptcha.v3:0.3'); + ->andReturn( + new Response( + new GuzzleResponse( + 200, ['Content-type' => 'application/json'], json_encode( + [ + 'success' => true, + 'score' => 0.5, + ] + ) + ) + ) + ); + + $this->app['router']->post( + 'human_human', + function (Request $request) { + return $request->isHuman() ? 'true' : 'false'; + } + )->middleware('recaptcha.score:0.7'); + + $this->app['router']->post( + 'human_robot', + function (Request $request) { + return $request->isRobot() ? 'true' : 'false'; + } + )->middleware('recaptcha.score:0.7'); + + $this->app['router']->post( + 'robot_human', + function (Request $request) { + return $request->isHuman() ? 'true' : 'false'; + } + )->middleware('recaptcha.score:0.3'); + + $this->app['router']->post( + 'robot_robot', + function (Request $request) { + return $request->isRobot() ? 'true' : 'false'; + } + )->middleware('recaptcha.score:0.3'); $this->post('human_human', [Captchavel::INPUT => 'token'])->assertSee('false'); $this->post('human_robot', [Captchavel::INPUT => 'token'])->assertSee('true'); diff --git a/tests/Http/Middleware/UsesRoutesWithMiddleware.php b/tests/Http/Middleware/UsesRoutesWithMiddleware.php index befffe3..630385c 100644 --- a/tests/Http/Middleware/UsesRoutesWithMiddleware.php +++ b/tests/Http/Middleware/UsesRoutesWithMiddleware.php @@ -2,7 +2,6 @@ namespace Tests\Http\Middleware; -use Illuminate\Support\Facades\Route; use DarkGhostHunter\Captchavel\Http\ReCaptchaResponse; trait UsesRoutesWithMiddleware @@ -11,48 +10,48 @@ protected function createsRoutes() { config(['captchavel.enable' => true]); - Route::post('v3/default', function (ReCaptchaResponse $response) { + $this->app['router']->post('v3/default', function (ReCaptchaResponse $response) { return $response; - })->middleware('recaptcha.v3'); + })->middleware('recaptcha.score'); - Route::post('v3/threshold_1', function (ReCaptchaResponse $response) { + $this->app['router']->post('v3/threshold_1', function (ReCaptchaResponse $response) { return $response; - })->middleware('recaptcha.v3:1.0'); + })->middleware('recaptcha.score:1.0'); - Route::post('v3/threshold_0', function (ReCaptchaResponse $response) { + $this->app['router']->post('v3/threshold_0', function (ReCaptchaResponse $response) { return $response; - })->middleware('recaptcha.v3:0'); + })->middleware('recaptcha.score:0'); - Route::post('v3/action_foo', function (ReCaptchaResponse $response) { + $this->app['router']->post('v3/action_foo', function (ReCaptchaResponse $response) { return $response; - })->middleware('recaptcha.v3:null,foo'); + })->middleware('recaptcha.score:null,foo'); - Route::post('v3/input_bar', function (ReCaptchaResponse $response) { + $this->app['router']->post('v3/input_bar', function (ReCaptchaResponse $response) { return $response; - })->middleware('recaptcha.v3:null,null,bar'); + })->middleware('recaptcha.score:null,null,bar'); - Route::post('v2/checkbox', function (ReCaptchaResponse $response) { + $this->app['router']->post('v2/checkbox', function (ReCaptchaResponse $response) { return $response; - })->middleware('recaptcha.v2:checkbox'); + })->middleware('recaptcha:checkbox'); - Route::post('v2/checkbox/input_bar', function (ReCaptchaResponse $response) { + $this->app['router']->post('v2/checkbox/input_bar', function (ReCaptchaResponse $response) { return $response; - })->middleware('recaptcha.v2:checkbox,bar'); + })->middleware('recaptcha:checkbox,bar'); - Route::post('v2/invisible', function (ReCaptchaResponse $response) { + $this->app['router']->post('v2/invisible', function (ReCaptchaResponse $response) { return $response; - })->middleware('recaptcha.v2:invisible'); + })->middleware('recaptcha:invisible'); - Route::post('v2/invisible/input_bar', function (ReCaptchaResponse $response) { + $this->app['router']->post('v2/invisible/input_bar', function (ReCaptchaResponse $response) { return $response; - })->middleware('recaptcha.v2:invisible,bar'); + })->middleware('recaptcha:invisible,bar'); - Route::post('v2/android', function (ReCaptchaResponse $response) { + $this->app['router']->post('v2/android', function (ReCaptchaResponse $response) { return $response; - })->middleware('recaptcha.v2:android'); + })->middleware('recaptcha:android'); - Route::post('v2/android/input_bar', function (ReCaptchaResponse $response) { + $this->app['router']->post('v2/android/input_bar', function (ReCaptchaResponse $response) { return $response; - })->middleware('recaptcha.v2:android,bar'); + })->middleware('recaptcha:android,bar'); } } diff --git a/tests/Http/ReCaptchaResponseTest.php b/tests/Http/ReCaptchaResponseTest.php index 5b99c23..d6ef251 100644 --- a/tests/Http/ReCaptchaResponseTest.php +++ b/tests/Http/ReCaptchaResponseTest.php @@ -2,10 +2,11 @@ namespace Tests\Http; -use LogicException; -use Tests\RegistersPackage; -use Orchestra\Testbench\TestCase; +use DarkGhostHunter\Captchavel\Captchavel; use DarkGhostHunter\Captchavel\Http\ReCaptchaResponse; +use Orchestra\Testbench\TestCase; +use RuntimeException; +use Tests\RegistersPackage; class ReCaptchaResponseTest extends TestCase { @@ -13,11 +14,49 @@ class ReCaptchaResponseTest extends TestCase public function test_exception_when_checking_non_score_response() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('This is not a reCAPTCHA v3 response, or the score is absent.'); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('This is not a reCAPTCHA v3 response'); (new ReCaptchaResponse([ 'success' => true, - ]))->isHuman(); + ]))->setAsResolved()->isHuman(); + } + + public function test_exception_when_score_response_has_no_score() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('This is reCAPTCHA v3 response has no score'); + + (new ReCaptchaResponse([ + 'success' => true, + ]))->setVersion(Captchavel::SCORE)->setAsResolved()->isHuman(); + } + + public function test_is_checkbox_response() + { + static::assertTrue( + (new ReCaptchaResponse())->setVersion(Captchavel::CHECKBOX)->isCheckbox() + ); + } + + public function test_is_invisible_response() + { + static::assertTrue( + (new ReCaptchaResponse())->setVersion(Captchavel::INVISIBLE)->isInvisible() + ); + } + + public function test_is_android_response() + { + static::assertTrue( + (new ReCaptchaResponse())->setVersion(Captchavel::ANDROID)->isAndroid() + ); + } + + public function test_is_score_response() + { + static::assertTrue( + (new ReCaptchaResponse())->setVersion(Captchavel::SCORE)->isScore() + ); } }