将 Laravel 测试 7 和 Laravel Passport 9.3 与个人访问客户端一起使用会出现异常 "Trying to get property 'id' of non-object"

Using Laravel Test 7 and Laravel Passport 9.3 with Personal Access Client gives exception "Trying to get property 'id' of non-object"

我正在设计自定义身份验证方案(基于 public 密钥)以及无状态 API,并决定 Passport 将满足 post 身份验证请求的需要。

假设身份验证成功,并且用户通过了身份验证,他们将收到一个个人访问令牌,并将该令牌用于所有进一步的请求。我遇到的麻烦(仍然在通过各种论坛和 Stack Overflow 进行大量搜索之后)是,当使用 Laravel 的内置测试套件时,在 createToken() 方法上,它会生成一个(公认的常见)异常:

“ErrorException:尝试获取非对象的 属性 'id'”。

我可以通过 Tinker 手动创建用户,并通过 Tinker 创建令牌。但是,我在 验证 后尝试自动执行此过程时遇到问题。

这里是相关的代码片段post-authentication:

            Auth::login($user);
            $user = Auth::user();
            $tokenResult = $user->createToken('Personal Access Token');
            $token = $tokenResult->token;

            $token->expires_at = Carbon::now()->addWeeks(1);
            $token->save();            

            return response()->json([
                "access_token" => $tokenResult->accessToken,
                "token_type" => "Bearer",
                "expires_at" => Carbon::parse(
                    $tokenResult->token->expires_at)->toDateTimeString()
            ],
            200);

我手动调用了用户 Auth::login 以确保用户已登录,并且 Auth::user() returns 用户(非空)。执行第三行代码后,将抛出异常并显示以下迷你堆栈跟踪(如果需要,我可以提供完整的堆栈跟踪)。

laravel\passport\src\PersonalAccessTokenFactory.php:100
laravel\passport\src\PersonalAccessTokenFactory.php:71
laravel\passport\src\HasApiTokens.php:67
app\Http\Controllers\Auth\LoginController.php:97
laravel\framework\src\Illuminate\Routing\Controller.php:54
laravel\framework\src\Illuminate\Routing\ControllerDispatcher.php:45

从 运行 通过调试几次 - 即使 class 被调用和加载,并且看起来客户端是通过 ControllerDispatcher -> Client::find(id)并在ClientRepository中找到,当它到达PersonalAccessTokenFactory时,传入的$client为null(这就解释了为什么找不到$client->id,虽然我不知道为什么此时$client为null)。

protected function createRequest($client, $userId, array $scopes)
{
    $secret = Passport::$hashesClientSecrets ? Passport::$personalAccessClientSecret : $client->secret;

    return (new ServerRequest)->withParsedBody([
        'grant_type' => 'personal_access',
        'client_id' => $client->id,
        ...
}

根据文档和其他 post 的一些指导,我有 done/tried 的东西:

目前我不确定 look/what 其他地方可以尝试,因此非常感谢任何帮助或指导!

我最终找到了解决方案。问题是 multi-layered,部分与有关测试和 Passport 个人访问客户端的过时 Laravel 文档有关。

问题的第一部分与在我的单元测试中使用特征 RefreshDatabase 有关。由于这会创建一个包含空数据集的模拟数据库,尽管客户端本身存在于真实数据库和 .env 文件中,但当测试为 运行 时,测试不会将这些客户端视为存在于模拟数据库中。要解决这个问题,必须在测试前create a client in the setup function 运行.

public function setUp() : void
{
    parent::setUp();
    $this->createClient(); //Private method->Full code below
}

这解决了关于在测试期间有一个空客户端的问题,但是从 Laravel 7 开始,Laravel 添加了对 Personal Access Clients that the id and the client secret has to be kept inside the .env file. When running the test, the test will see the actual client id and secret in the .env, and fail to validate these with the client that was created and stored in the mock database, returning another exception: "Client Authentication Failed".

的要求

此问题的解决方案是在您的主项目目录中创建一个 .env.testing 文件,将您的 .env 文件内容复制到其中,并确保下面的键与您创建的主项目的值存在访问客户端,或从仅为测试而生成的客户端复制机密(我建议后者)。

 PASSPORT_PERSONAL_ACCESS_CLIENT_ID=1

 PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET=unhashed-client-secret-value

然后使用下面的代码,确保 $clientSecret 值与您的 .env.testing 文件中的键值相同。

private function createClient() : void
{
    $clientRepository = new ClientRepository();
    $client = $clientRepository->createPersonalAccessClient(
        null, 'Test Personal Access Client', 'http://localhost'
    );

    DB::table('oauth_personal_access_clients')->insert([
        'client_id'  => $client->id,
        'created_at' => new DateTime,
        'updated_at' => new DateTime,
    ]);

    $clientSecret = 'unhashed-client-secret-value';
    $client->setSecretAttribute($clientSecret);
    $client->save();
}

这将创建一个新客户端,将属性 secret 设置为变量中的值,并更新模拟数据库 secret 以包含相同的值。希望这对遇到同样问题的人有所帮助。

另一种防止 copy/paste 源代码的方法是在设置方法中调用 artisan 命令。

public function setUp() {
        parent::setUp();
        $this->artisan('passport:install');
}

original here

就用门面

 public function setUp() {
    parent::setUp();
    Artisan::call('passport:install');}