Как протестировать Laravel Socialite

У меня есть приложение, которое использует socialite, я хочу создать тест для аутентификации Github, поэтому я использовал Socialite Facade, чтобы сымитировать вызов метода Socialite driver, но когда я запускаю свой тест, он говорит мне, что я пытаюсь получить значение на нулевой тип.

Ниже тест, который я написал

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, и, учитывая, что я не могу создать экземпляр этого значения, будет невозможно указать тип возвращаемого значения. Мне нужна помощь, чтобы успешно протестировать контроллер. Спасибо


person James Okpe George    schedule 09.02.2016    source источник
comment
Я думаю, вам может понадобиться Socialite::shouldReceive('driver->redirect').   -  person ceejayoz    schedule 09.02.2016
comment
@ceejayoz Не работает, жалуется, что не видит метод driver->redirect   -  person James Okpe George    schedule 09.02.2016


Ответы (4)


Что ж, оба ответа были отличными, но в них много ненужных кодов, и я смог вывести из них свой ответ.

Это все, что мне нужно было сделать.

Сначала издевайтесь над типом Socialite User

$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('/')
person James Okpe George    schedule 15.11.2016
comment
похоже тут многого не хватает - person Harry Bosh; 16.11.2016
comment
Не много, просто издевательство над драйвером забыл добавить. - person James Okpe George; 16.11.2016
comment
Я не понимаю эту часть Socialite::shouldReceive('driver->user')->andReturn($abstractValidUser);, чего-то не хватает? часть driver->user должна быть точно - person LTroya; 29.07.2017
comment
Это стало для меня полным фиктивным примером. Спасибо. - person Onur Demir; 19.06.2018
comment
@LTroya Эта часть driver->user является ярлыком для имитации вызова цепочки методов. Причина для driver->user в том, что это то, что вызывается при использовании Socialite. docs.mockery.io/en/latest/reference/demeter_chains.html - person Oliver Nybroe; 27.05.2019

$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. Надеюсь, это поможет.

person Olotin Temitope    schedule 13.05.2016

Это может быть сложнее сделать, но я считаю, что это делает тесты более читабельными. Надеюсь, вы поможете мне упростить то, что я собираюсь описать.

Моя идея состоит в том, чтобы заглушить http-запросы. С учетом facebook их два: 1) /oauth/access_token (для получения токена доступа), 2) /me (для получения данных о пользователе).

Для этого я временно прикрепил php к mitmproxy, чтобы создать фикстуру vcr:

  1. Скажите php использовать http-прокси (добавьте следующие строки в файл .env):

    HTTP_PROXY=http://localhost:8080
    HTTPS_PROXY=http://localhost:8080
    
  2. Сообщите php, где находится сертификат прокси: добавьте openssl.cafile = /etc/php/mitmproxy-ca-cert.pem к php.ini. Или curl.cainfo, если уж на то пошло.

  3. Перезапустите php-fpm.
  4. Старт mitmproxy.
  5. Сделайте так, чтобы ваш браузер подключался через mitmproxy.
  6. Войдите на сайт, который вы разрабатываете, используя facebook (здесь нет TDD).

    Нажмите z в mitmproxy (C для mitmproxy ‹ 0,18), чтобы очистить список запросов (потоков) перед перенаправлением на facebook, если это необходимо. Либо используйте команду f (l для mitmproxy ‹ 0,18) с graph.facebook.com, чтобы отфильтровать дополнительные запросы.

    Обратите внимание, что для твиттера вам понадобится league/oauth1-client 1.7 или новее. Один переключился с guzzle/guzzle на guzzlehttp/guzzle. Иначе вы не сможете войти в систему.

  7. Скопируйте данные из 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":"..."}'
    

    Для этого вы можете использовать команду E, если у вас есть mitmproxy >= 0,18. В качестве альтернативы используйте команду P. Копирует запрос/ответ в буфер обмена. Если вы хотите, чтобы mitmproxy сохранял их прямо в файл, вы можете запустить его с помощью DISPLAY= mitmproxy.

    Я не вижу возможности использовать возможности записи php-vcr, так как я не тестирую весь рабочий процесс.

С этим я смог написать следующие тесты (и да, они в порядке со всеми этими значениями, замененными точками, не стесняйтесь копировать как есть).

Обратите внимание, однако, фикстуры зависят от версии laravel/socialite. У меня была проблема с фейсбуком. В версии 2.0.16 laravel/socialite начал делать запросы на публикацию, чтобы получить токен доступа. Также есть версия API в facebook. URL.

Эти приспособления для 2.0.14. Один из способов справиться с этим - иметь зависимость laravel/socialite в разделе require-dev файла composer.json (со строгой спецификацией версии), чтобы гарантировать, что 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('[email protected]', 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('[email protected]', 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":"[email protected]"}],"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, а именно о URL-адресе перенаправления. Я обработал это так:

 <?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. На самом деле все это можно было бы сделать с помощью одного vcr (без mitmproxy).

person x-yuri    schedule 24.10.2016
comment
во всяком случае, это действительно интересная идея с использованием php-vcr. - person Adam Klein; 12.12.2016

На самом деле я создал поддельные классы, которые возвращают фиктивные пользовательские данные, потому что я заинтересован в проверке своей логики, а не в том, работают ли 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']
        );
    }
}

А вот как выглядит один из Fake-провайдеров:

class FacebookProvider extends SocialiteFacebookProvider
{
    protected function getUserByToken($token)
    {
        return [
            'id' => '123123123',
            'name' => 'John Doe',
            'email' => '[email protected]',
            'avatar' => 'image.jpg',
        ];
    }
}

И, конечно же, в тестовом классе я заменил исходный SocialiteManager своей версией:

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

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

Это работает очень хорошо для меня. Не надо ничего издеваться.

person Nikolay Traykov    schedule 05.06.2020