It may be harder to do, but I find it makes more readable tests. Hope you can help me simplify what Iโm going to describe.
My idea is to drown out http requests. Considering facebook, there are two of them: 1) /oauth/access_token (to get the access token), 2) /me (to get user data).
To do this, I temporarily bound php to mitmproxy to create a vcr fixture:
Tell php use http proxy (add the following lines to the .env file):
HTTP_PROXY=http://localhost:8080 HTTPS_PROXY=http://localhost:8080
Tell php where the proxy certificate is: add openssl.cafile = /etc/php/mitmproxy-ca-cert.pem in php.ini . Or curl.cainfo , for that matter.
- Reboot
php-fpm . - Start
mitmproxy . Make your browser connected via mitmproxy .Log in to the site you are developing using facebook (there is no TDD here).
Press z in mitmproxy ( C for mitmproxy <0.18) to clear the request (stream) list before redirecting to facebook, if necessary. Or, alternatively, use the f command ( l for mitmproxy <0.18) with graph.facebook.com to filter additional requests.
Note that for twitter you will need league/oauth1-client 1.7 or later. One switched from guzzle/guzzle to guzzlehttp/guzzle . Or you wonโt be able to log in.
Copy the data from mimtproxy to tests/fixtures/facebook . I used the yaml format and here is 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":"..."}'
You can use the E command for this if you have mitmproxy > = 0.18. Alternatively, use the P command. It copies the request / response to the clipboard. If you want mitmproxy retain their right to a file, you can run it with DISPLAY= mitmproxy .
I do not see the possibility of using php-vcr , as I am not testing the entire workflow.
With this, I was able to write the following tests (and yes, they are fine with all of these values โโreplaced by dots, feel free to copy as is).
Please note , but the sizes depend on the version of laravel/socialite . I had a problem with facebook. In version 2.0.16 laravel/socialite started making post messages to get an access token. Also there is api version in facebook urls.
These lights are for 2.0.14 . One way to handle this is to have the laravel/socialite dependency in the require-dev section of the composer.json file (with strict version specifications) to ensure that socialite has the correct version in the development environment (hopefully composer will ignore the section in require-dev in a production environment.) Given that you are running composer install --no-dev in a production environment.
AuthController_HandleFacebookCallbackTest.php :
<?php use 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', ]); } function testCreatesUserWithCorrespondingName() { $this->doCallbackRequest(); $this->assertEquals('John Doe', User::first()->name); } function testCreatesUserWithCorrespondingEmail() { $this->doCallbackRequest(); $this->assertEquals(' john.doe@gmail.com ', User::first()->email); } function testCreatesUserWithCorrespondingFbId() { $this->doCallbackRequest(); $this->assertEquals(123, User::first()->fb_id); } function testCreatesUserWithFbData() { $this->doCallbackRequest(); $this->assertNotEquals('', User::first()->fb_data); } function testRedirectsToHomePage() { $this->doCallbackRequest(); $this->assertRedirectedTo('/'); } function testAuthenticatesUser() { $this->doCallbackRequest(); $this->assertEquals(User::first()->id, Auth::user()->id); } 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 :
<?php use 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', ]); } function testCreatesUserWithCorrespondingName() { $this->doCallbackRequest(); $this->assertEquals('joe', User::first()->name); } function testCreatesUserWithCorrespondingTwId() { $this->doCallbackRequest(); $this->assertEquals(123, User::first()->tw_id); } function testCreatesUserWithTwData() { $this->doCallbackRequest(); $this->assertNotEquals('', User::first()->tw_data); } function testRedirectsToHomePage() { $this->doCallbackRequest(); $this->assertRedirectedTo('/'); } function testAuthenticatesUser() { $this->doCallbackRequest(); $this->assertEquals(User::first()->id, Auth::user()->id); } 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 :
<?php use 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', ]); } function testCreatesUserWithCorrespondingName() { $this->doCallbackRequest(); $this->assertEquals('John Doe', User::first()->name); } function testCreatesUserWithCorrespondingEmail() { $this->doCallbackRequest(); $this->assertEquals(' john.doe@gmail.com ', User::first()->email); } function testCreatesUserWithCorrespondingGpId() { $this->doCallbackRequest(); $this->assertEquals(123, User::first()->gp_id); } function testCreatesUserWithGpData() { $this->doCallbackRequest(); $this->assertNotEquals('', User::first()->gp_data); } function testRedirectsToHomePage() { $this->doCallbackRequest(); $this->assertRedirectedTo('/'); } function testAuthenticatesUser() { $this->doCallbackRequest(); $this->assertEquals(User::first()->id, Auth::user()->id); } 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 , and that you have the following line in phpunit.xml :
<listeners> <listener class="PHPUnit_Util_Log_VCR" file="vendor/php-vcr/phpunit-testlistener-vcr/PHPUnit/Util/Log/VCR.php"/> </listeners>
There was also a problem with $_SERVER['HTTP_HOST'] , which was not installed during the tests. I am talking about the config/services.php file here, namely the URL redirection. I processed it like this:
<?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 could not find a better way. I was going to use config('app.url') there, but it does not work in configuration files.
UPD You can get rid of the setUpBeforeClass part by deleting this method, running tests, and updating some of the inventory requests with which vcr records. Actually, all this can be done only with vcr (no mitmproxy ).