[英]How to Test Laravel Socialite
I have an application that makes use of socialite, I want to create test for Github authentication, So I used Socialite Facade to mock call to the Socialite driver
method, but when I run my test it tells me that I am trying to get value on null type.我有一个使用 socialite 的应用程序,我想为 Github 身份验证创建测试,所以我使用 Socialite Facade 来模拟对 Socialite
driver
方法的调用,但是当我运行我的测试时,它告诉我我正在尝试获取价值空类型。
Below is the test I have written下面是我写的测试
public function testGithubLogin()
{
Socialite::shouldReceive('driver')
->with('github')
->once();
$this->call('GET', '/github/authorize')->isRedirection();
}
Below is the implementation of the test下面是测试的实现
public function authorizeProvider($provider)
{
return Socialite::driver($provider)->redirect();
}
I understand why it might return such result because Sociallite::driver($provider)
returns an instance of Laravel\\Socialite\\Two\\GithubProvider
, and considering that I am unable to instantiate this value it will be impossible to specify a return type.我理解为什么它可能会返回这样的结果,因为
Sociallite::driver($provider)
返回Laravel\\Socialite\\Two\\GithubProvider
一个实例,并且考虑到我无法实例化这个值,因此无法指定返回类型。 I need help to successfully test the controller.我需要帮助才能成功测试控制器。 Thanks
谢谢
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首先模拟 Socialite 用户类型
$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
班级顶部的 socialite 包
use Laravel\\Socialite\\Facades\\Socialite;
使用 Laravel\\Socialite\\Facades\\Socialite;
I had the same problem, but I was able to solve it using the technique above;我遇到了同样的问题,但我能够使用上述技术解决它; @ceejayoz.
@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.我的想法是存根 http 请求。 Considering facebook, there are two of them: 1)
/oauth/access_token
(to get access token), 2) /me
(to get data about the user).考虑到facebook,有两个:1)
/oauth/access_token
(获取访问令牌),2) /me
(获取有关用户的数据)。
For that I temporarily attached php
to mitmproxy
to create vcr
fixture:为此,我暂时将
php
附加到mitmproxy
以创建vcr
夹具:
Tell php
to use http proxy (add the following lines to the .env
file):告诉
php
使用 http 代理(在.env
文件中添加以下几行):
HTTP_PROXY=http://localhost:8080 HTTPS_PROXY=http://localhost:8080
Tell php
where proxy's certificate is: add openssl.cafile = /etc/php/mitmproxy-ca-cert.pem
to php.ini
.告诉
php
代理的证书在哪里:将openssl.cafile = /etc/php/mitmproxy-ca-cert.pem
添加到php.ini
。 Or curl.cainfo
, for that matter.或者
curl.cainfo
,就此而言。
php-fpm
.php-fpm
。mitmproxy
.mitmproxy
。mitmproxy
as well.
mitmproxy
连接。
Log in to the site you're developing using facebook (no TDD here).使用 facebook 登录到您正在开发的站点(此处没有 TDD)。
Press z
in mitmproxy
( C
for mitmproxy
< 0.18) to clear request (flow) list before redirecting to facebook if need be.如果需要,请在
mitmproxy
按z
(对于mitmproxy
< 0.18 为C
)以清除请求(流)列表,然后再重定向到 facebook。 Or alternatively, use f
command ( l
for mitmproxy
< 0.18) with graph.facebook.com
to filter out extra requests.或者,将
f
命令(对于mitmproxy
< 0.18 使用l
)和graph.facebook.com
来过滤掉额外的请求。
Do note, that for twitter you'll need league/oauth1-client
1.7 or newer.请注意,对于 twitter,您需要
league/oauth1-client
1.7 或更高版本。 The one switched from guzzle/guzzle
to guzzlehttp/guzzle
.从一个切换
guzzle/guzzle
到guzzlehttp/guzzle
。 Or else you'll be unable to log in.否则您将无法登录。
Copy data from mimtproxy
to tests/fixtures/facebook
.将数据从
mimtproxy
复制到tests/fixtures/facebook
。 I used yaml
format and here's what it looks like:我使用了
yaml
格式,这是它的样子:
- 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 got mitmproxy
>= 0.18.为此,如果您的
mitmproxy
>= 0.18,则可以使用命令E
Alternatively, use command P
.或者,使用命令
P
。 It copies request/response to clipboard.它将请求/响应复制到剪贴板。 If you want
mitmproxy
to save them right to file, you can run it with DISPLAY= mitmproxy
.如果您希望
mitmproxy
将它们直接保存到文件中,您可以使用DISPLAY= mitmproxy
运行它。
I see no way to use php-vcr
's recording facilities, since I'm not testing the whole workflow.我认为没有办法使用
php-vcr
的录音工具,因为我没有测试整个工作流程。
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.请注意,装置取决于
laravel/socialite
的版本。 I had an issue with facebook.我在 facebook 上遇到了问题。 In version
2.0.16
laravel/socialite
started doing post requests to get access token.在
2.0.16
版本中, 2.0.16
laravel/socialite
开始执行post 请求以获取访问令牌。 Also there's api version in facebook urls. facebook 网址中还有api 版本。
These fixtures are for 2.0.14
.这些装置适用于
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.处理它的一种方法是在
composer.json
文件的require-dev
部分也有laravel/socialite
依赖项(具有严格的版本规范)以确保socialite
在开发环境中是正确的版本(希望composer
会忽略那个在生产环境中的require-dev
部分。)考虑到您在生产环境中执行composer install --no-dev
。
AuthController_HandleFacebookCallbackTest.php
: 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',
]);
}
/**
* @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
: 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
: 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',
]);
}
/**
* @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
: 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
: 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',
]);
}
/**
* @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
: 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
:确保您需要
php-vcr/phpunit-testlistener-vcr
,并且您的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.在运行测试时,还存在未设置
$_SERVER['HTTP_HOST']
的问题。 I'm talking about config/services.php
file here, namely about redirect url.我在这里谈论的是
config/services.php
文件,即重定向 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.我打算在那里使用
config('app.url')
,但它在配置文件中不起作用。
UPD You can get rid of setUpBeforeClass
part by removing this method, running tests, and updating request part of fixtures with what vcr records. UPD您可以通过删除此方法、运行测试以及使用 vcr 记录更新装置的请求部分来摆脱
setUpBeforeClass
部分。 Actually, the whole thing might be done with vcr
alone (no mitmproxy
).实际上,整个事情可以单独用
vcr
完成(没有mitmproxy
)。
I've actually created Fake classes that return a dummy user data because I'm interested in testing my logic, not whether Socialite, nor the vendor work properly.我实际上创建了返回虚拟用户数据的 Fake 类,因为我有兴趣测试我的逻辑,而不是 Socialite 或供应商是否正常工作。
// This is the fake class that extends the original SocialiteManager
class SocialiteManager extends SocialiteSocialiteManager
{
protected function createFacebookDriver()
{
return $this->buildProvider(
FacebookProvider::class, // This class is a fake that returns dummy user in facebook's format
$this->app->make('config')['services.facebook']
);
}
protected function createGoogleDriver()
{
return $this->buildProvider(
GoogleProvider::class, // This is a fake class that ereturns dummy user in google's format
$this->app->make('config')['services.google']
);
}
}
And here is how one of the Fake providers look like:以下是其中一个 Fake 提供商的样子:
class FacebookProvider extends SocialiteFacebookProvider
{
protected function getUserByToken($token)
{
return [
'id' => '123123123',
'name' => 'John Doe',
'email' => 'test@test.com',
'avatar' => 'image.jpg',
];
}
}
And of course in the test class, I replace the original SocialiteManager with my version:当然,在测试类中,我用我的版本替换了原来的 SocialiteManager:
public function setUp(): void
{
parent::setUp();
$this->app->singleton(Factory::class, function ($app) {
return new SocialiteManager($app);
});
}
This works pretty fine to me.这对我来说很好用。 No need to mock anything.
没有必要嘲笑任何东西。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.