How to Test Laravel Socialite
Well, both answers were great, but they have lots of codes that are not required, and I was able to infer my answer from them.
This is all I needed to do.
Firstly mock the Socialite User type
$abstractUser = Mockery::mock('Laravel\Socialite\Two\User')
Second, set the expected values for its method calls
$abstractUser ->shouldReceive('getId') ->andReturn(rand()) ->shouldReceive('getName') ->andReturn(str_random(10)) ->shouldReceive('getEmail') ->andReturn(str_random(10) . '@gmail.com') ->shouldReceive('getAvatar') ->andReturn('https://en.gravatar.com/userimage');
Thirdly, you need to mock the provider/user call
Socialite::shouldReceive('driver->user')->andReturn($abstractUser);
Then lastly you write your assertions
$this->visit('/auth/google/callback') ->seePageIs('/')
$provider = Mockery::mock('Laravel\Socialite\Contracts\Provider');$provider->shouldReceive('redirect')->andReturn('Redirected');$providerName = class_basename($provider);//Call your model factory here$socialAccount = factory('LearnCast\User')->create(['provider' => $providerName]);$abstractUser = Mockery::mock('Laravel\Socialite\Two\User');// Get the api user object here$abstractUser->shouldReceive('getId') ->andReturn($socialAccount->provider_user_id) ->shouldReceive('getEmail') ->andReturn(str_random(10).'@noemail.app') ->shouldReceive('getNickname') ->andReturn('Laztopaz') ->shouldReceive('getAvatar') ->andReturn('https://en.gravatar.com/userimage');$provider = Mockery::mock('Laravel\Socialite\Contracts\Provider');$provider->shouldReceive('user')->andReturn($abstractUser);Socialite::shouldReceive('driver')->with('facebook')->andReturn($provider);// After Oauth redirect back to the route$this->visit('/auth/facebook/callback')// See the page that the user login into->seePageIs('/');
Note: use
the socialite package at the top of your class
use Laravel\Socialite\Facades\Socialite;
I had the same problem, but I was able to solve it using the technique above; @ceejayoz. I hope this helps.
This may be harder to do, but I believe it makes for more readable tests. Hopefully you'll help me simplify what I'm about to describe.
My idea is to stub http requests. Considering facebook, there are two of them: 1) /oauth/access_token
(to get access token), 2) /me
(to get data about the user).
For that I temporarily attached php
to mitmproxy
to create vcr
fixture:
Tell
php
to use http proxy (add the following lines to the.env
file):HTTP_PROXY=http://localhost:8080HTTPS_PROXY=http://localhost:8080
Tell
php
where proxy's certificate is: addopenssl.cafile = /etc/php/mitmproxy-ca-cert.pem
tophp.ini
. Orcurl.cainfo
, for that matter.- Restart
php-fpm
. - Start
mitmproxy
. Make your browser connect throughmitmproxy
as well.Log in to the site you're developing using facebook (no TDD here).
Press
z
inmitmproxy
(C
formitmproxy
< 0.18) to clear request (flow) list before redirecting to facebook if need be. Or alternatively, usef
command (l
formitmproxy
< 0.18) withgraph.facebook.com
to filter out extra requests.Do note, that for twitter you'll need
league/oauth1-client
1.7 or newer. The one switched fromguzzle/guzzle
toguzzlehttp/guzzle
. Or else you'll be unable to log in.Copy data from
mimtproxy
totests/fixtures/facebook
. I usedyaml
format and here's what it looks like:- request: method: GET url: https://graph.facebook.com/oauth/access_token?client_id=...&client_secret=...&code=...&redirect_uri=... response: status: http_version: '1.1' code: 200 message: OK body: access_token=...&expires=...- request: method: GET url: https://graph.facebook.com/v2.5/me?access_token=...&appsecret_proof=...&fields=first_name,last_name,email,gender,verified response: status: http_version: '1.1' code: 200 message: OK body: '{"first_name":"...","last_name":"...","email":"...","gender":"...","verified":true,"id":"..."}'
For that you can use command
E
if you've gotmitmproxy
>= 0.18. Alternatively, use commandP
. It copies request/response to clipboard. If you wantmitmproxy
to save them right to file, you can run it withDISPLAY= mitmproxy
.I see no way to use
php-vcr
's recording facilities, since I'm not testing the whole workflow.
With that I was able to write the following tests (and yes, they are fine with all those values replaced by dots, feel free to copy as is).
Do note though, fixtures depend on laravel/socialite
's version. I had an issue with facebook. In version 2.0.16
laravel/socialite
started doing post requests to get access token. Also there's api version in facebook urls.
These fixtures are for 2.0.14
. One way to deal with it is to have laravel/socialite
dependency in require-dev
section of composer.json
file as well (with strict version specification) to ensure that socialite
is of proper version in development environment (Hopefully, composer
will ignore the one in require-dev
section in production environment.) Considering you do composer install --no-dev
in production environment.
AuthController_HandleFacebookCallbackTest.php
:
<?phpuse Illuminate\Foundation\Testing\DatabaseTransactions;use Illuminate\Support\Facades\Auth;use VCR\VCR;use App\User;class AuthController_HandleFacebookCallbackTest extends TestCase{ use DatabaseTransactions; static function setUpBeforeClass() { VCR::configure()->enableLibraryHooks(['stream_wrapper', 'curl']) ->enableRequestMatchers([ 'method', 'url', ]); } /** * @vcr facebook */ function testCreatesUserWithCorrespondingName() { $this->doCallbackRequest(); $this->assertEquals('John Doe', User::first()->name); } /** * @vcr facebook */ function testCreatesUserWithCorrespondingEmail() { $this->doCallbackRequest(); $this->assertEquals('john.doe@gmail.com', User::first()->email); } /** * @vcr facebook */ function testCreatesUserWithCorrespondingFbId() { $this->doCallbackRequest(); $this->assertEquals(123, User::first()->fb_id); } /** * @vcr facebook */ function testCreatesUserWithFbData() { $this->doCallbackRequest(); $this->assertNotEquals('', User::first()->fb_data); } /** * @vcr facebook */ function testRedirectsToHomePage() { $this->doCallbackRequest(); $this->assertRedirectedTo('/'); } /** * @vcr facebook */ function testAuthenticatesUser() { $this->doCallbackRequest(); $this->assertEquals(User::first()->id, Auth::user()->id); } /** * @vcr facebook */ function testDoesntCreateUserIfAlreadyExists() { $user = factory(User::class)->create([ 'fb_id' => 123, ]); $this->doCallbackRequest(); $this->assertEquals(1, User::count()); } function doCallbackRequest() { return $this->withSession([ 'state' => '...', ])->get('/auth/facebook/callback?' . http_build_query([ 'state' => '...', ])); }}
tests/fixtures/facebook
:
- request: method: GET url: https://graph.facebook.com/oauth/access_token response: status: http_version: '1.1' code: 200 message: OK body: access_token=...- request: method: GET url: https://graph.facebook.com/v2.5/me response: status: http_version: '1.1' code: 200 message: OK body: '{"first_name":"John","last_name":"Doe","email":"john.doe\u0040gmail.com","id":"123"}'
AuthController_HandleTwitterCallbackTest.php
:
<?phpuse Illuminate\Foundation\Testing\DatabaseTransactions;use Illuminate\Support\Facades\Auth;use VCR\VCR;use League\OAuth1\Client\Credentials\TemporaryCredentials;use App\User;class AuthController_HandleTwitterCallbackTest extends TestCase{ use DatabaseTransactions; static function setUpBeforeClass() { VCR::configure()->enableLibraryHooks(['stream_wrapper', 'curl']) ->enableRequestMatchers([ 'method', 'url', ]); } /** * @vcr twitter */ function testCreatesUserWithCorrespondingName() { $this->doCallbackRequest(); $this->assertEquals('joe', User::first()->name); } /** * @vcr twitter */ function testCreatesUserWithCorrespondingTwId() { $this->doCallbackRequest(); $this->assertEquals(123, User::first()->tw_id); } /** * @vcr twitter */ function testCreatesUserWithTwData() { $this->doCallbackRequest(); $this->assertNotEquals('', User::first()->tw_data); } /** * @vcr twitter */ function testRedirectsToHomePage() { $this->doCallbackRequest(); $this->assertRedirectedTo('/'); } /** * @vcr twitter */ function testAuthenticatesUser() { $this->doCallbackRequest(); $this->assertEquals(User::first()->id, Auth::user()->id); } /** * @vcr twitter */ function testDoesntCreateUserIfAlreadyExists() { $user = factory(User::class)->create([ 'tw_id' => 123, ]); $this->doCallbackRequest(); $this->assertEquals(1, User::count()); } function doCallbackRequest() { $temporaryCredentials = new TemporaryCredentials(); $temporaryCredentials->setIdentifier('...'); $temporaryCredentials->setSecret('...'); return $this->withSession([ 'oauth.temp' => $temporaryCredentials, ])->get('/auth/twitter/callback?' . http_build_query([ 'oauth_token' => '...', 'oauth_verifier' => '...', ])); }}
tests/fixtures/twitter
:
- request: method: POST url: https://api.twitter.com/oauth/access_token response: status: http_version: '1.1' code: 200 message: OK body: oauth_token=...&oauth_token_secret=...- request: method: GET url: https://api.twitter.com/1.1/account/verify_credentials.json response: status: http_version: '1.1' code: 200 message: OK body: '{"id_str":"123","name":"joe","screen_name":"joe","location":"","description":"","profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/456\/userpic.png"}'
AuthController_HandleGoogleCallbackTest.php
:
<?phpuse Illuminate\Foundation\Testing\DatabaseTransactions;use Illuminate\Support\Facades\Auth;use VCR\VCR;use App\User;class AuthController_HandleGoogleCallbackTest extends TestCase{ use DatabaseTransactions; static function setUpBeforeClass() { VCR::configure()->enableLibraryHooks(['stream_wrapper', 'curl']) ->enableRequestMatchers([ 'method', 'url', ]); } /** * @vcr google */ function testCreatesUserWithCorrespondingName() { $this->doCallbackRequest(); $this->assertEquals('John Doe', User::first()->name); } /** * @vcr google */ function testCreatesUserWithCorrespondingEmail() { $this->doCallbackRequest(); $this->assertEquals('john.doe@gmail.com', User::first()->email); } /** * @vcr google */ function testCreatesUserWithCorrespondingGpId() { $this->doCallbackRequest(); $this->assertEquals(123, User::first()->gp_id); } /** * @vcr google */ function testCreatesUserWithGpData() { $this->doCallbackRequest(); $this->assertNotEquals('', User::first()->gp_data); } /** * @vcr google */ function testRedirectsToHomePage() { $this->doCallbackRequest(); $this->assertRedirectedTo('/'); } /** * @vcr google */ function testAuthenticatesUser() { $this->doCallbackRequest(); $this->assertEquals(User::first()->id, Auth::user()->id); } /** * @vcr google */ function testDoesntCreateUserIfAlreadyExists() { $user = factory(User::class)->create([ 'gp_id' => 123, ]); $this->doCallbackRequest(); $this->assertEquals(1, User::count()); } function doCallbackRequest() { return $this->withSession([ 'state' => '...', ])->get('/auth/google/callback?' . http_build_query([ 'state' => '...', ])); }}
tests/fixtures/google
:
- request: method: POST url: https://accounts.google.com/o/oauth2/token response: status: http_version: '1.1' code: 200 message: OK body: access_token=...- request: method: GET url: https://www.googleapis.com/plus/v1/people/me response: status: http_version: '1.1' code: 200 message: OK body: '{"emails":[{"value":"john.doe@gmail.com"}],"id":"123","displayName":"John Doe","image":{"url":"https://googleusercontent.com/photo.jpg"}}'
Note. Make sure you have php-vcr/phpunit-testlistener-vcr
required, and that you have the following line in your phpunit.xml
:
<listeners> <listener class="PHPUnit_Util_Log_VCR" file="vendor/php-vcr/phpunit-testlistener-vcr/PHPUnit/Util/Log/VCR.php"/></listeners>
There also was an issue with $_SERVER['HTTP_HOST']
not being set, when running tests. I'm talking about config/services.php
file here, namely about redirect url. I handled it like so:
<?php$app = include dirname(__FILE__) . '/app.php';return [ ... 'facebook' => [ ... 'redirect' => (isset($_SERVER['HTTP_HOST']) ? 'http://' . $_SERVER['HTTP_HOST'] : $app['url']) . '/auth/facebook/callback', ],];
Not particularly beautiful, but I failed to find a better way. I was going to use config('app.url')
there, but it doesn't work in config files.
UPD You can get rid of setUpBeforeClass
part by removing this method, running tests, and updating request part of fixtures with what vcr records. Actually, the whole thing might be done with vcr
alone (no mitmproxy
).