如何测试 Laravel 社交名流

Posted

技术标签:

【中文标题】如何测试 Laravel 社交名流【英文标题】:How to Test Laravel Socialite 【发布时间】:2016-05-19 13:38:39 【问题描述】:

我有一个使用社交名流的应用程序,我想为 Github 身份验证创建测试,所以我使用 Socialite Facade 来模拟对 Socialite driver 方法的调用,但是当我运行测试时它告诉我我是试图获取 null 类型的值。

下面是我写的测试

public function testGithubLogin()

    Socialite::shouldReceive('driver')
        ->with('github')
        ->once();
    $this->call('GET', '/github/authorize')->isRedirection();

下面是测试的实现

public function authorizeProvider($provider)

    return Socialite::driver($provider)->redirect();

我理解它为什么会返回这样的结果,因为Sociallite::driver($provider) 返回了Laravel\Socialite\Two\GithubProvider 的一个实例,并且考虑到我无法实例化该值,因此无法指定返回类型。我需要帮助才能成功测试控制器。谢谢

【问题讨论】:

我想你可能想要Socialite::shouldReceive('driver->redirect') @ceejayoz 那不行,它抱怨它没有看到方法driver->redirect 【参考方案1】:

嗯,两个答案都很棒,但是它们有很多不需要的代码,我能够从它们中推断出我的答案。

这就是我需要做的。

首先模拟社交名流用户类型

$abstractUser = Mockery::mock('Laravel\Socialite\Two\User')

其次,为其方法调用设置期望值

$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');

第三,你需要模拟提供者/用户调用

Socialite::shouldReceive('driver->user')->andReturn($abstractUser);

最后你写你的断言

$this->visit('/auth/google/callback')
     ->seePageIs('/')

【讨论】:

这里好像少了很多东西 不是很多,只是我忘记添加的驱动程序模拟。 这部分没看懂Socialite::shouldReceive('driver->user')->andReturn($abstractValidUser);,是不是少了什么? driver->user 部分是准确的 这对我来说是一个完整的模拟示例。谢谢。 @Ltroya driver->user 这部分是模拟对方法链的调用的快捷方式。 driver->user 的原因是因为这是使用社交名流时所调用的。 docs.mockery.io/en/latest/reference/demeter_chains.html【参考方案2】:
$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('/');

注意:use 你班级顶部的社交名流包

使用 Laravel\Socialite\Facades\Socialite;

我遇到了同样的问题,但我能够使用上述技术解决它; @ceejayoz。我希望这会有所帮助。

【讨论】:

【参考方案3】:

这可能更难做到,但我相信它会使测试更具可读性。希望您能帮助我简化我将要描述的内容。

我的想法是存根 http 请求。考虑到 facebook,有两个:1)/oauth/access_token(获取访问令牌),2)/me(获取有关用户的数据)。

为此,我暂时将php 附加到mitmproxy 以创建vcr 夹具:

    告诉php 使用http 代理(将以下行添加到.env 文件中):

    HTTP_PROXY=http://localhost:8080
    HTTPS_PROXY=http://localhost:8080
    

    告诉php代理的证书在哪里:将openssl.cafile = /etc/php/mitmproxy-ca-cert.pem添加到php.ini。或者curl.cainfo,就此而言。

    重启php-fpm。 开始mitmproxy让您的浏览器也通过mitmproxy 连接。

    使用 facebook 登录您正在开发的网站(此处没有 TDD)。

    z in mitmproxyC for mitmproxy f 命令(l for mitmproxy graph.facebook.com 过滤掉额外的请求。

    请注意,对于 twitter,您需要 league/oauth1-client 1.7 或更高版本。一个从guzzle/guzzle 切换到guzzlehttp/guzzle。否则您将无法登录。

    将数据从mimtproxy 复制到tests/fixtures/facebook。我使用了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":"..."'
    

    如果你有mitmproxy >= 0.18,你可以使用命令E。或者,使用命令P。它将请求/响应复制到剪贴板。如果您希望mitmproxy 将它们保存到文件中,您可以使用DISPLAY= mitmproxy 运行它。

    我认为无法使用 php-vcr 的记录工具,因为我没有测试整个工作流程。

这样我就可以编写以下测试(是的,它们可以用点替换所有这些值,请随意复制)。

请注意,固定装置取决于 laravel/socialite 的版本。我遇到了 Facebook 的问题。在版本2.0.16laravel/socialite 开始做post requests 以获取访问令牌。脸书网址中还有api version。

这些装置适用于2.0.14。处理它的一种方法是在composer.json 文件的require-dev 部分中也有laravel/socialite 依赖项(具有严格的版本规范),以确保socialite 在开发环境中具有正确的版本(希望composer将忽略生产环境中require-dev 部分中的那个。)考虑到您在生产环境中执行composer install --no-dev

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:

-
    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',
            ]);
    

    /**
     * @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:

<?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:

-
    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"'

注意。确保您有 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>

还有一个问题是在运行测试时没有设置$_SERVER['HTTP_HOST']。我在这里谈论config/services.php 文件,即关于重定向网址。我是这样处理的:

 <?php

$app = include dirname(__FILE__) . '/app.php';

return [
    ...
    'facebook' => [
        ...
        'redirect' => (isset($_SERVER['HTTP_HOST']) ? 'http://' . $_SERVER['HTTP_HOST'] : $app['url']) . '/auth/facebook/callback',
    ],
];

不是特别漂亮,但我没找到更好的方法。我打算在那里使用config('app.url'),但它在配置文件中不起作用。

UPD 你可以去掉setUpBeforeClass 部分,方法是删除这个方法,运行测试,用vcr 记录的更新fixture 的request 部分。实际上,整个事情可能只用vcr 就可以完成(不是mitmproxy)。

【讨论】:

如果有的话,这是一个使用 php-vcr 的真正有趣的想法。【参考方案4】:

我实际上创建了返回虚拟用户数据的 Fake 类,因为我有兴趣测试我的逻辑,而不是社交名流和供应商是否正常工作。

// 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']
        );
    

下面是其中一个 Fake 提供者的样子:

class FacebookProvider extends SocialiteFacebookProvider

    protected function getUserByToken($token)
    
        return [
            'id' => '123123123',
            'name' => 'John Doe',
            'email' => 'test@test.com',
            'avatar' => 'image.jpg',
        ];
    

当然,在测试课中,我将原来的 SocialiteManager 替换为我的版本:

public function setUp(): void
    
        parent::setUp();

        $this->app->singleton(Factory::class, function ($app) 
            return new SocialiteManager($app);
        );
    

这对我来说很好用。无需嘲笑任何东西。

【讨论】:

以上是关于如何测试 Laravel 社交名流的主要内容,如果未能解决你的问题,请参考以下文章

如何在 laravel 中使用社交名流和护照创建 REST API

Laravel JWT 和社交名流插件

Laravel 社交名流推特

Laravel 社交名流:InvalidStateException(有时)

在 laravel 中使用社交名流登录 Facebook

Laravel 5.2.31 社交名流证书 ssl 错误