Я пытаюсь проверить аутентификацию с помощью Laravel's Passport, и нет возможности ... всегда получал 401, что этот клиент недействителен, я оставлю вам то, что я пробовал:
Моя конфигурация phpunit - это та, которая исходит из базы с laravel
abstract class TestCase extends BaseTestCase
{
use CreatesApplication, DatabaseTransactions;
protected $client, $user, $token;
public function setUp()
{
parent::setUp();
$clientRepository = new ClientRepository();
$this->client = $clientRepository->createPersonalAccessClient(
null, 'Test Personal Access Client', '/'
);
DB::table('oauth_personal_access_clients')->insert([
'client_id' => $this->client->id,
'created_at' => date('Y-m-d'),
'updated_at' => date('Y-m-d'),
]);
$this->user = User::create([
'id' => 1,
'name' => 'test',
'lastname' => 'er',
'email' => '[email protected]',
'password' => bcrypt('secret')
]);
$this->token = $this->user->createToken('TestToken', [])->accessToken;
}
}
class AuthTest extends TestCase
{
use DatabaseMigrations;
public function testShouldSignIn()
{
// Arrange
$body = [
'client_id' => (string) $this->client->id,
'client_secret' => $this->client->secret,
'email' => '[email protected]',
'password' => 'secret',
];
// Act
$this->json('POST', '/api/signin', $body, ['Accept' => 'application/json'])
// Assert
->assertStatus(200)
->assertJsonStructure([
'data' => [
'jwt' => [
'access_token',
'expires_in',
'token_type',
]
],
'errors'
]);
}
}
Моя удобная аутентификация с паспортом для тестирования
Route::post('/signin', function () {
$args = request()->only(['email', 'password', 'client_id', 'client_secret']);
request()->request->add([
'grant_type' => 'password',
'client_id' => $args['client_id'] ?? env('PASSPORT_CLIENT_ID', ''),
'client_secret' => $args['client_secret'] ?? env('PASSPORT_CLIENT_SECRET', ''),
'username' => $args['email'],
'password' => $args['password'],
'scope' => '*',
]);
$res = Route::dispatch(Request::create('oauth/token', 'POST'));
$data = json_decode($res->getContent());
$isOk = $res->getStatusCode() === 200;
return response()->json([
'data' => $isOk ? [ 'jwt' => $data ] : null,
'errors' => $isOk ? null : [ $data ]
], 200);
});






Laravel Passport на самом деле поставляется с некоторыми помощниками по тестированию, который вы можете использовать для тестирования ваших аутентифицированных конечных точек API.
Passport::actingAs(
factory(User::class)->create(),
);
Спасибо тебе за это! По какой-то странной причине использование $ this-> json () и установка пользовательских заголовков с токенами-носителями каждый раз не работает. Только в первый раз. Он просто привязан к одному пользователю, как если бы он был кеширован внутри или что-то в этом роде. Выдергивание волос - неприятная штука. Вне тестов вел себя нормально. Но это заставило его работать! Другой тоже должен быть, но эй - другая проблема решена ;-)
Есть идеи, как получить доступ к этому пользователю? Мне нужен их ID для утверждений. ОБНОВЛЕНО - Вы можете получить его с помощью Auth :: user ()
@PlushyObject Вы можете сначала сохранить пользователя в переменной, а затем передать ее в метод actingAs.
Я также получил доступ к нему с помощью Auth :: user ()
Я не был знаком с инструментом Passport, о котором говорил Дуайт, когда писал это, так что, возможно, это более простое решение. Но вот кое-что, что может помочь. Он создает для вас токен, который затем можно применить к вызову фиктивного API.
/**
* @param Authenticatable $model
* @param array $scope
* @param bool $personalAccessToken
* @return mixed
*/
public function makeOauthLoginToken(Authenticatable $model = null, array $scope = ['*'], $personalAccessToken = true)
{
$tokenName = $clientName = 'testing';
Artisan::call('passport:client', ['--personal' => true, '--name' => $clientName]);
if (!$personalAccessToken) {
$clientId = app(Client::class)->where('name', $clientName)->first(['id'])->id;
Passport::$personalAccessClient = $clientId;
}
$userId = $model->getKey();
return app(PersonalAccessTokenFactory::class)->make($userId, $tokenName, $scope)->accessToken;
}
Затем вы просто примените его к заголовкам:
$user = app(User::class)->first($testUserId);
$token = $this->makeOauthLoginToken($user);
$headers = ['authorization' => "Bearer $token"];
$server = $this->transformHeadersToServerVars($headers);
$body = $cookies = $files = [];
$response = $this->call($method, $uri, $body, $cookies, $files, $server);
$content = $response->getContent();
$code = $response->getStatusCode();
Если вам нужно проанализировать токен, попробуйте следующее:
/**
* @param string $token
* @param Authenticatable $model
* @return Authenticatable|null
*/
public function parsePassportToken($token, Authenticatable $model = null)
{
if (!$model) {
$provider = config('auth.guards.passport.provider');
$model = config("auth.providers.$provider.model");
$model = app($model);
}
//Passport's token parsing is looking to a bearer token using a protected method. So a dummy-request is needed.
$request = app(Request::class);
$request->headers->add(['authorization' => "Bearer $token"]);
//Laravel\Passport\Guards\TokenGuard::authenticateViaBearerToken() expects the user table to leverage the
//HasApiTokens trait. If that's missing, a query macro can satisfy its expectation for this method.
if (!method_exists($model, 'withAccessToken')) {
Builder::macro('withAccessToken', function ($accessToken) use ($model) {
$model->accessToken = $accessToken;
return $this;
});
/** @var TokenGuard $guard */
$guard = Auth::guard('passport');
return $guard->user($request)->getModel();
}
/** @var TokenGuard $guard */
$guard = Auth::guard('passport');
return $guard->user($request);
}
Вы уверены, что система, создавшая токен, использует тот же APP_KEY, что и система, анализирующая его?
Для тестирования паспорта вам не нужно было вводить реальный логин и пароль, вы можете создать тестовый.
Вы можете использовать Passport::actingAs или setup().
Для actingAs можно сделать как
public function testServerCreation()
{
Passport::actingAs(
factory(User::class)->create(),
['create-servers']
);
$response = $this->post('/api/create-server');
$response->assertStatus(200);
}
и с setUp() вы можете добиться этого,
public function setUp()
{
parent::setUp();
$clientRepository = new ClientRepository();
$client = $clientRepository->createPersonalAccessClient(
null, 'Test Personal Access Client', $this->baseUrl
);
DB::table('oauth_personal_access_clients')->insert([
'client_id' => $client->id,
'created_at' => new DateTime,
'updated_at' => new DateTime,
]);
$this->user = factory(User::class)->create();
$token = $this->user->createToken('TestToken', $this->scopes)->accessToken;
$this->headers['Accept'] = 'application/json';
$this->headers['Authorization'] = 'Bearer '.$token;
}
Вы можете получить более подробную информацию Здесь и https://laravel.com/docs/5.6/passport#testing.
вы также можете проверить laracasts.com/discuss/channels/testing/….
Вот как вы можете реализовать это, чтобы оно действительно работало.
Прежде всего, следует правильно реализовать db: семена и Установка паспорта.
Во-вторых, вам не нужно создавать свой собственный маршрут для проверки, работает ли он (для этого достаточно базовых ответов Заграничный пасспорт).
Итак, вот описание того, как это работало в моей установке (Laravel 5.5) ...
В моем случае мне нужен только один клиент Заграничный пасспорт, поэтому я создал другой маршрут для авторизации api (api/v1/login), чтобы указать только имя пользователя и пароль. Подробнее об этом можно прочитать здесь.
К счастью, этот пример также охватывает базовый тест Авторизация паспорта.
Итак, чтобы успешно запустить ваши тесты, основная идея:
.env с PASSPORT_CLIENT_ID (необязательно - Заграничный пасспорт всегда создает password grant token с id = 2 на пустой базе данных).Примеры кода ...
ApiLoginTest.php
/**
* @group apilogintests
*/
public function testApiLogin() {
$body = [
'username' => '[email protected]',
'password' => 'admin'
];
$this->json('POST','/api/v1/login',$body,['Accept' => 'application/json'])
->assertStatus(200)
->assertJsonStructure(['token_type','expires_in','access_token','refresh_token']);
}
/**
* @group apilogintests
*/
public function testOauthLogin() {
$oauth_client_id = env('PASSPORT_CLIENT_ID');
$oauth_client = OauthClients::findOrFail($oauth_client_id);
$body = [
'username' => '[email protected]',
'password' => 'admin',
'client_id' => $oauth_client_id,
'client_secret' => $oauth_client->secret,
'grant_type' => 'password',
'scope' => '*'
];
$this->json('POST','/oauth/token',$body,['Accept' => 'application/json'])
->assertStatus(200)
->assertJsonStructure(['token_type','expires_in','access_token','refresh_token']);
}
Заметки:
Конечно, необходимо изменить учетные данные.
PASSPORT_CLIENT_ID должен быть 2, как объяснялось ранее.
Проверка JsonStructure избыточна, так как мы получаем ответ 200, только если авторизация прошла успешно. Однако, если вам нужна дополнительная проверка, она также проходит ...
TestCase.php
public function setUp() {
parent::setUp();
\Artisan::call('migrate',['-vvv' => true]);
\Artisan::call('passport:install',['-vvv' => true]);
\Artisan::call('db:seed',['-vvv' => true]);
}
Заметки:
Здесь мы создаем соответствующие записи в db, которые необходимы в наших тестах. Так что помните, чтобы здесь были засеяны пользователи с ролями и т. д.
Заключительные примечания ...
Этого должно быть достаточно, чтобы ваш код заработал. В моей системе все это проходит зеленый цвет, а также работает на моем gitlab CI runner.
Наконец, проверьте также свое промежуточное ПО на маршрутах. Особенно, если вы экспериментировали с пакетом динго (или jwt по тимону).
Единственное промежуточное ПО, которое вы можете рассмотреть, применяя к маршруту авторизации Заграничный пасспорт, - это throttle, чтобы иметь некоторую защиту от атака грубой силой.
Примечание...
Заграничный пасспорт и динго имеют совершенно разные реализации jwt.
В моих тестах только Заграничный пасспорт ведет себя правильно, и я предполагаю, что это причина, по которой динго больше не поддерживается.
Надеюсь, это решит вашу проблему ...
Привет, хороший и полезный ответ. У меня есть небольшое дополнение - нет необходимости устанавливать заголовок ['Accept' => 'application / json'], когда мы используем метод $ this-> json, потому что эти заголовки объединены по умолчанию в этом методе.
Я думаю, что выбранный ответ, вероятно, является наиболее надежным и лучшим на данный момент, но я хотел предоставить альтернативу, которая сработает для меня, если вам просто нужно быстро пройти тесты по паспорту без большой настройки.
Важное примечание: я думаю, что если вы собираетесь делать много этого, это неправильный способ, и другие ответы лучше. Но по моей оценке это действительно кажется просто работать
Вот полный тестовый пример, в котором мне нужно предположить, что пользователь выполняет POST для конечной точки и использует его токен авторизации для выполнения запроса.
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Models\User;
use Laravel\Passport\Passport;
class MyTest extends TestCase
{
use WithFaker, RefreshDatabase;
public function my_test()
{
/**
*
* Without Artisan call you will get a passport
* "please create a personal access client" error
*/
\Artisan::call('passport:install');
$user = factory(User::class)->create();
Passport::actingAs($user);
//See Below
$token = $user->generateToken();
$headers = [ 'Authorization' => 'Bearer $token'];
$payload = [
//...
];
$response = $this->json('POST', '/api/resource', $payload, $headers);
$response->assertStatus(200)
->assertJson([
//...
]);
}
}
И для ясности, вот метод generateToken() в модели User, который использует свойство HasApiTokens.
public function generateToken() {
return $this->createToken('my-oauth-client-name')->accessToken;
}
На мой взгляд, это довольно грубо и готово. Например, если вы используете черту RefreshDatabase, вам нужно запускать команду «паспорт: установка», подобную этой, в каждом методе. Возможно, есть лучший способ сделать это через глобальную настройку, но я новичок в PHPUnit, поэтому я делаю это (пока) именно так.
Вот пример для всех, кто хочет протестировать ваш api с использованием токенов личного доступа.
Сначала настройте тестовый класс
protected function setUp(): void
{
parent::setUp();
$this->actingAs(User::first());
$this->access_token = $this->getAccessToken();
}
Что касается метода getAccessToken(), просто используйте api интерфейса Passport.
private function getAccessToken()
{
$response = $this->post('/oauth/personal-access-tokens',[
'name' => 'temp-test-token'
]);
return $response->json('accessToken');
}
А просто:
public function the_personal_access_token_allows_us_to_use_the_api()
{
$response = $this->get('/api/user', [
'Authorization' => "Bearer $this->access_token"
]);
$response->assertStatus(200);
}
Оптимизация для ненужных миграций БД
Вот пример, который гарантирует, что вы по-прежнему можете писать тесты, которые не зависят от базы данных - не выполняя миграции db.
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Schema;
use Laravel\Passport\ClientRepository;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
public function setUp(): void
{
parent::setUp();
if (Schema::hasTable('oauth_clients')) {
resolve(ClientRepository::class)->createPersonalAccessClient(
null, config('app.name') . ' Personal Access Client', config('app.url')
);
}
}
}
Тогда в вашем тесте:
...
use RefreshDatabase;
/**
* Test login
*
* @return void
*/
public function test_login()
{
$this->withExceptionHandling();
$user = factory(User::class)->create([
'password' => 'secret'
]);
$response = $this->json('POST', route('api.auth.login'), [
'email' => $user->email,
'password' => 'secret',
]);
$response->assertStatus(200);
$response->assertJsonStructure([
//...
]);
}
...
Таким образом, вы можете писать тесты без каких-либо миграций БД.
Спасибо за ответ, но можно ли получить client_id и client_secret после того, как jwt сгенерирован этим методом? Мне нужно получить их для теста, в документации это не очень ясно: github.com/laravel/passport/blob/…