Search code examples
laravellaravel-socialitemockerylaravel-dusk

InvalidStateException when testing mocked Socialite login via Laravel Dusk


I'm using Socialite in a Laravel application to allow users to connect via Github.

My login controller contains the following two methods:

    /**
     * GET /login/github
     * Redirect the user to the GitHub authentication page.
     */
    public function redirectToProvider()
    {
        return Socialite::driver('github')->redirect();
    }

    /**
     * GET /login/github/callback
     * Obtain the user information from GitHub.
     */
    public function handleProviderCallback(Request $request)
    {
        $githubUser = Socialite::driver('github')->user();

        // Actual login procedures go here; redacted for brevity

        return redirect('/'); 
    }

When I manually test these methods in the browser, they work as expected. I visit /login/github where I'm redirected to Github to authenticate, then I'm sent back to /login/github/callback?state=somelongrandomkey which then redirects me home (/).

I'm also attempting to test these methods via Laravel Dusk, mocking Socialite.

My Dusk test method looks like this:

    public function testReceivesGithubRequestAndCreatesNewUser()
    {
        $this->browse(function (Browser $browser) {

            $user = factory('App\Models\User')->create([
                'github_token' => 'foobar',
                'github_username' => 'foobar'
            ]);

            # Mock 1 - A Socialite user
            $abstractUser = Mockery::mock('Laravel\Socialite\Two\User');

            # Mock 2 - Socialite's Github provider
            $provider = Mockery::mock('Laravel\Socialite\Contracts\Provider');
            $provider->shouldReceive('user')
                ->andReturn($abstractUser);

            # Mock 3 - Socialite
            Socialite::shouldReceive('driver')
                ->with('github')
                ->andReturn($provider);

            $browser->visit('/login/github/callback')->assertPathIs('/');
        });

When I run this test, the visit to /login/github/callback fails with an InvalidStateException.

From the log:

dusk.local.ERROR: {"exception":"[object] (Laravel\\Socialite\\Two\\InvalidStateException(code: 0):  at /redacted/vendor/laravel/socialite/src/Two/AbstractProvider.php:210)
[stacktrace]
#0 /redacted/app/Http/Controllers/Auth/LoginController.php(84): Laravel\\Socialite\\Two\\AbstractProvider->user()
[...etc...]

When I trace where the error is coming from in AbstractProvider I see it's attempting to compare state from the session with state from the query string:

protected function hasInvalidState()
    {
        if ($this->isStateless()) {
            return false;
        }
        $state = $this->request->session()->pull('state');
        return ! (strlen($state) > 0 && $this->request->input('state') === $state);
    }

In my Dusk test, when /login/github/callback is visited, there is no state on the query string, so it's logical that it's failing.

I feel I'm missing some key component in setting up the mocks that provides that state, but I'm not sure what.

My test is built using these two examples for reference:


Solution

  • There's a fundamental difference between Dusk tests and the examples you're mentioning: Dusk opens the website in an actual browser and so the test and your application run in separate processes. That's why mocking doesn't work in Dusk tests.

    The idea behind such an integration test is that you simulate a real user, without mocking or any other "shortcuts". In your case, that would mean logging in with a real GitHub account.