简体   繁体   English

如何测试 Laravel Socialite

[英]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夹具:

  1. 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
  2. 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 ,就此而言。

  3. Restart php-fpm .重启php-fpm
  4. Start mitmproxy .启动mitmproxy
  5. Make your browser connect through mitmproxy as well. 让您的浏览器也通过 mitmproxy连接。
  6. 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.如果需要,请在mitmproxyz (对于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/guzzleguzzlehttp/guzzle Or else you'll be unable to log in.否则您将无法登录。

  7. 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM