ARCANEDEV / noCAPTCHA

:passport_control: Helper for Google's new noCAPTCHA (reCAPTCHA v2 & v3)
MIT License
358 stars 56 forks source link

Don't validate captcha in testing environment #39

Closed ivanvermeyen closed 8 years ago

ivanvermeyen commented 8 years ago

Hi,

This isn't really an issue, just a suggestion in case anyone cares :)

I'm trying to test my code with Laravel's handy helpers and MailThief for intercepting e-mails... But getting past the captcha with an automated script is obviously not so easy...

To get around this, I read that some people just disable the captcha for testing. So to do this, I copied your validator and changed it like this:

    Validator::extend('nocaptcha', function($attribute, $value, $parameters, $validator) {
        unset($attribute);

        if (app()->environment('testing')) {
            return true;
        }

        return app('arcanedev.no-captcha')->verify($value, request()->getClientIp());
    });

So basically, I just check if the environment is set to testing, which is the default for Laravel, and if so I always let it pass.

I don't know if this is something you would want to include in your package, perhaps with configurable environments that should be skipped or something...

arcanedev-maroc commented 8 years ago

Yeah, i'm going to find an "eloquent" way to skip the verification.

There are multiple options:

ivanvermeyen commented 8 years ago

Awesome :) 👍

ivanvermeyen commented 8 years ago

Woops! At some point I removed the required validation rule and forgot to put it back... This complicates things... My tests worked because the captcha wasn't required... doh!

So to make it work (for real) I had to create a custom sometimes rule... I added this to the Request class that holds my validation rules:

/**
 * Validate conditional rules.
 *
 * @return \Illuminate\Contracts\Validation\Validator
 */
protected function getValidatorInstance()
{
    $validator = parent::getValidatorInstance();

    $validator->sometimes('g-recaptcha-response', 'required|captcha', function ($input) {
        return ! app()->environment('testing');
    });

    return $validator;
}

So now it applies the required and captcha rules only if the environment isn't testing... Of course now I need to add this to every Request class that needs a captcha.

Maybe you know a better approach ;)

arcanedev-maroc commented 8 years ago

I think the best way to do it is by using the Mockery.

https://laravel.com/docs/master/testing#mocking-facades

use Arcanedev\NoCaptcha\Facades\NoCaptcha;

// This is my dummy test
public function testContactForm()
{
    $this->ignoreCaptcha();

    $this->visit('/contact')
        ->type('John DOE', 'name')
        ->type('user@example.com', 'email')
        ->check('g-recaptcha-response')
        ->press('Send')
        ->seePageIs('/contact')
        ->see('Your message was sent successfully!');
}

// You can place this method to your base TestCase
protected function ignoreCaptcha($name = 'g-recaptcha-response')
{
    NoCaptcha::shouldReceive('display')
        ->andReturn('<input type="checkbox" value="yes" name="' . $name . '">');
    NoCaptcha::shouldReceive('script')
        ->andReturn('<script src="captcha.js"></script>');
    NoCaptcha::shouldReceive('verify')
        ->andReturn(true);
}

Routes:

Route::group(['prefix' => 'contact'], function () {
    Route::get('/', function () {
        return view('contact');
    });

    Route::post('/', function (Illuminate\Http\Request $request) {
        $validator = Validator::make($request->all(), [
            'name'                 => 'required',
            'email'                => 'required|email',
            'g-recaptcha-response' => 'required|captcha',
        ]);

        if ($validator->fails()) {
            return back()->withErrors($validator)->withInput();
        }

        return back()->with('success', 'Your message was sent successfully!');
    });
});

The view:

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <span class="label label-success">{{ Session::get('success') }}</span>
    <form method="POST">
        {{ csrf_field() }}
        <label for="name">Name :</label>
        <input id="name" name="name" type="text">
        {{ $errors->first('name') }}
        <br>

        <label for="email">Email :</label>
        <input id="email" name="email" type="email">
        {{ $errors->first('email') }}
        <br>

        {!! NoCaptcha::display() !!}
        {{ $errors->first('g-recaptcha-response') }}

        <input type="submit" value="Send">
    </form>

    {!! NoCaptcha::script() !!}
</body>
</html>

And of course you can customize the ignoreCaptcha method for your specific tests.

ivanvermeyen commented 8 years ago

Brilliant! That's a much cleaner solution! Thanks 👍

ivanvermeyen commented 8 years ago

This doesn't seem to work for me:

\NoCaptcha::shouldReceive('verify')->andReturn(true);

In your validator, you reference:

return $app['arcanedev.no-captcha']->verify($value, $ip);

I tried mocking the real NoCaptcha class or the contract, but that didn't work either.

The display() method that I use in the view gets mocked as expected.

ivanvermeyen commented 8 years ago

Ah... the validator requires a field with the name g-recaptcha-response but in the test we used captcha. If I use g-recaptcha-response it all works....

Something like this:

public function testContactForm()
{
    $this->ignoreCaptcha();

    $this->visit('/contact')
        ->type('John DOE', 'name')
        ->type('user@example.com', 'email')
        ->check('g-recaptcha-response')
        ->press('Send')
        ->seePageIs('/contact')
        ->see('Your message was sent successfully!');
}

protected function ignoreCaptcha($name = 'g-recaptcha-response')
{
    Captcha::shouldReceive('display')->andReturn('<input type="checkbox" value="yes" name="' . $name . '">');
    Captcha::shouldReceive('script')->andReturn('<script src="captcha.js"></script>');
    Captcha::shouldReceive('verify')->andReturn(true);
}
arcanedev-maroc commented 8 years ago

You're right, i've updated my response! Tnx :+1:

flap152 commented 5 years ago

Hi, I cannot use this method anymore, since Facades were removed and the class returned by the helper (as referenced in https://github.com/ARCANEDEV/noCAPTCHA/issues/81) does not implement shouldReceive. How can we mock it now?

arcanedev-maroc commented 5 years ago

Hi @flap152, you can mock it by using the contract.

use Arcanedev\NoCaptcha\Contracts\NoCaptcha;

// ...

$this->mock(NoCaptcha::class, function ($mock) {
    $mock->shouldReceive(...)->andReturn(...);
});

Or by using the Real-Time Facades:

use Facades\Arcanedev\NoCaptcha\Contracts\NoCaptcha as NoCaptchaFacade;

//...

NoCaptchaFacade::shouldReceive(...)->andReturn(...);

Check the official documantation for more details:

flap152 commented 5 years ago

Hi, successfully mocked using real-time Facades, but had to modify the usage, with v2 (have not tried v3 yet):

because of CaptchaRule rule implementation:

return no_captcha($this->version)
        ->verify($value, $ip)
        ->isSuccess();

I had to change ignoreCaptcha to this (notice the textareavs checkbox, and different and added shouldReceive):

    // You can place this method to your base TestCase
    protected function ignoreCaptcha($name = 'g-recaptcha-response')
    {
      NoCaptchaFacade::shouldReceive('display')
        ->andReturn('<input type="textarea"  value="anything, really" name="' . $name . '">');
      NoCaptchaFacade::shouldReceive('script')
        ->andReturn('<script src="captcha.js"></script>');
      NoCaptchaFacade::shouldReceive('verify')
//        ->andReturn(true);
        ->andReturn(NoCaptchaFacade::getFacadeRoot());
      NoCaptchaFacade::shouldReceive('isSuccess')
        ->andReturn(true);
    }

Maybe there is a better way, or this will help someone...

MadRac commented 5 years ago

Hi, I get

Facebook\WebDriver\Exception\NoSuchElementException: no such element: Unable to locate element: {"method":"css selector","selector":"body input[type=checkbox][name='g-recaptcha-response']"}
  (Session info: headless chrome=65.0.3325.146)
use Facades\Arcanedev\NoCaptcha\Contracts\NoCaptcha as NoCaptchaFacade;

...

public function testBasicExample()
    {
        $this->ignoreCaptcha();
        $this->browse(function (Browser $browser) {
            $browser->visit('/login')
                    ->assertSee('Login')
                    ->type('email', 'test@test.ru')
                    ->type('password', 'secret')
                    ->check('g-recaptcha-response')
                    ->press('Login')
                    ->assertTitleContains('Main page')
                    ->screenshot('ffs');
        });
    }

    protected function ignoreCaptcha($name = 'g-recaptcha-response')
    {
        // NoCaptchaFacade::shouldReceive('display')
        //     ->andReturn('<input type="checkbox" value="yes" name="' . $name . '">');
        // NoCaptchaFacade::shouldReceive('script')
        //     ->andReturn('<script src="captcha.js"></script>');
        // NoCaptchaFacade::shouldReceive('verify')
        //     ->andReturn(true);

        NoCaptchaFacade::shouldReceive('display')
            ->andReturn('<input type="textarea"  value="yes" name="' . $name . '">');
        NoCaptchaFacade::shouldReceive('script')
            ->andReturn('<script src="captcha.js"></script>');
        NoCaptchaFacade::shouldReceive('verify')
            ->andReturn(NoCaptchaFacade::getFacadeRoot());
        NoCaptchaFacade::shouldReceive('isSuccess')
            ->andReturn(true);
    }

what am I doing wrong?

arcanedev-maroc commented 5 years ago

Hi @MadRac, i never tested NoCaptcha with Laravel Dusk, so i have no idea on how to solve your issue.

But there is another trick (Cleaner way ?) to skip the NoCaptcha validation.

// This is where you specify your validation rules

use Arcanedev\NoCaptcha\Rules\CaptchaRule;

$ips = app()->runningUnitTests() ? ['your-local-ip', 'your-testing-ip'] : [];

$rule = (new CaptchaRule)->skipIps($ips); 

Or in your config file

flap152 commented 5 years ago

@MadRac , The captcha field is now a textarea, not a checkbox. You would need to change ->check('g-recaptcha-response') to ->type() or something compatible with a textarea field to avoid the error.

But also, the facade returns html for a textarea with text already in it (value="yes"), so you don't need to "input" anything during the test, so remove the ->check() altogether.

MadRac commented 5 years ago

@flap152 , If i just remove ->check('g-recaptcha-response') I get

Please verify that you are not a robot

If I uncomment the code with the checkbox, the same error

use Facades\Arcanedev\NoCaptcha\Contracts\NoCaptcha as NoCaptchaFacade;

...

public function testBasicExample()
    {
        $this->ignoreCaptcha();
        $this->browse(function (Browser $browser) {
            $browser->visit('/login')
                    ->assertSee('Login')
                    ->type('email', 'test@test.ru')
                    ->type('password', 'secret')
                    ->check('g-recaptcha-response')
                    ->press('Login')
                    ->assertTitleContains('Main page')
                    ->screenshot('ffs');
        });
    }

    protected function ignoreCaptcha($name = 'g-recaptcha-response')
    {
         NoCaptchaFacade::shouldReceive('display')
             ->andReturn('<input type="checkbox" value="yes" name="' . $name . '">');
         NoCaptchaFacade::shouldReceive('script')
             ->andReturn('<script src="captcha.js"></script>');
         NoCaptchaFacade::shouldReceive('verify')
             ->andReturn(true);
    }

I get

There was 1 error:

1) Tests\Browser\ExampleTest::testBasicExample
Facebook\WebDriver\Exception\NoSuchElementException: no such element: Unable to locate element: {"method":"css selector","selector":"body input[type=checkbox][name='g-recaptcha-response']"}
  (Session info: headless chrome=65.0.3325.146)
  (Driver info: chromedriver=2.36.540471 (9c759b81a907e70363c6312294d30b6ccccc2752),platform=Linux 4.14.146-119.123.amzn2.x86_64 x86_64)

Can you show the code dusk test and login.blade.php

flap152 commented 5 years ago

@MadRac ,

blade excerpt - in form:

    <div class="row">
          <div class="col">
                {!! Captcha::display() !!}
          </div><!--col-->
     </div><!--row-->

and for script:

@push('after-scripts')
    {!! Captcha::script() !!}
@endpush

I must admit we use both Dusk and Browserkit for our tests, and Browserkit for that part of the tests...

Morinohtar commented 2 years ago

Using Laravel 9 with Vue for the form submission.

Cant seem to make the test work with Dusk, though the Feature Test works fine. Dont think the mock is working in Dusk...

If i log the response from the rule, it spits this:

`{"Arcanedev\\NoCaptcha\\Utilities\\ResponseV2":{"success":false,"hostname":null,"challenge_ts":null,"apk_package_name":null,"error-codes":["invalid-input-response"]}}`

So, the rule response comes as false.

Code:


    protected function ignoreCaptcha(string $name = 'g-recaptcha-response'): void
    {
        NoCaptchaFacade::shouldReceive('display')
            ->andReturn('<input type="textarea"  value="anything, really" name="' . $name . '">');

        NoCaptchaFacade::shouldReceive('script')
            ->andReturn('<script src="captcha.js"></script>');

        NoCaptchaFacade::shouldReceive('verify')
            ->andReturn(NoCaptchaFacade::getFacadeRoot());

        NoCaptchaFacade::shouldReceive('isSuccess')
            ->andReturn(true);
    }

    /** @test */
    public function it_submits_contact_form()
    {
        $this->ignoreCaptcha();

        $this->browse(function (Browser $browser) {
            $browser->visitRoute('contact')
                ->type('name', $this->faker->name)
                ->type('email', $this->faker->safeEmail)
                ->type('message', $this->faker->paragraphs(3, true))
                ->fillHidden('g-recaptcha-response', '1')
                ->pressAndWaitFor('Submit', 3)
                ->assertSee('Message sent!');
        });
    }