2

文章转发自专业的Laravel开发者社区,原始链接:https://learnku.com/laravel/t...

在本文中,我将展示一个使用 HTTP 测试中间件的实例。HTTP 级测试更能适应变化,可读性更强。

在最近与 Adam Wathan 和 Taylor Otwell 合拍的《全栈广播》(http://www.fullstackradio.com/72) 节目中,听到他们在 HTTP 测试中发现了许多实用价值,令人耳目一新。我发现 HTTP 测试更易编写和维护,但我确实觉得我在测试 Wrong™ ,或没有模拟(对象),隔离每一测试项在作弊一样。如果你还没有听过这一集的话,请听一听,里面充满了好的、实用的测试建议。

介绍

今年早些时候,我构建了一个中间件,用于在我的一个项目上验证和保护Mailgun webhook,并在Laravel News上的 用Mailgun对Laravel中的电子邮件进行入站处理 中对此进行了描述。 总之,我将演示如何在处理入站电子邮件时使用Laravel中间件验证Mailgun webhook(以确保webhook 实际上 来自Mailgun)。

在设置 Mailgun webhook 的核心部分时,作为 HTTP POST 有效负载部分的签名建议使用rquest提供的签名、时间戳和令牌来验证,从而保护您的 webhook。这是我发布的完整中间件:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Response;

class ValidateMailgunWebhook
{
    public function handle($request, Closure $next)
    {
        if (!$request->isMethod('post')) {
            abort(Response::HTTP_FORBIDDEN, 'Only POST requests are allowed.');
        }

        if ($this->verify($request)) {
            return $next($request);
        }

        abort(Response::HTTP_FORBIDDEN, 'The webhook signature was invalid.');
    }

    protected function buildSignature($request)
    {
        return hash_hmac(
            'sha256',
            sprintf('%s%s', $request->input('timestamp'), $request->input('token')),
            config('services.mailgun.secret')
        );
    }

    protected function verify($request)
    {
        if (abs(time() - $request->input('timestamp')) > 15) {
            return false;
        }

        return $this->buildSignature($request) === $request->input('signature');
    }
}

该中间件只接受 POST 请求,并将传入的签名与使用 Mailgun 密钥生成的签名进行比较。

我已经看到了各种测试中间件的方法,例如直接在单元测试中构建它,根据需要模拟对象,以及直接运行中间件。 在这篇文章中,我将向你展示如何使用更高级别的 HTTP 测试来测试此中间件。 你的整个堆栈将在测试中运行,让你更有信心 你的应用程序将会按预期工作。

将测试不直接绑定到特定的中间件实现 这是你能了解到的一个重要的福利。 我们可以完全重构中间件,而不需要更改任何测试或更新模拟来验证中间件是否正常工作。 我相信你会发现这些测试将会更加健壮。

配置

让我们使用示例Laravel 5.5项目快速构建对上述中间件的测试:

$ laravel new middleware-tests

# 切换到 middleware-tests 文件夹
$ cd $_

$ php artisan make:middleware ValidateMailgunWebhook

获取上面的中间件代码并将其粘贴到此中间件文件中。

接下来,将此中间件添加到 app/Http/Kernel.php 文件中:

protected $routeMiddleware = [
    // ...
    'mailgun.webhook' => \App\Http\Middleware\ValidateMailgunWebhook::class,
];

编写HTTP测试

我们准备针对这个中间件编写一些测试,我们甚至不必定义任何路由routes/api.php来测试它!

首先,让我们创建功能测试文件:

$ php artisan make:test SecureMailgunWebhookTest

查看Mailgun中间件,以下是我们要测试的内容,以确保中间件按预期工作:

  1. 除了之外的任何HTTP动词 POST 都应该引起 403 Forbidden 响应。
  2. 无效的签名应该创建 403 Forbidden 响应。
  3. 有效签名应该通过并命中可调用的路由。
  4. 旧的时间戳应该引起 403 Forbidden 响应。

测试无效的HTTP方法

有了这个介绍,让我们编写第一个测试并设置我们的测试。

使用以下内容更新SecureMailgunWebhookTest文件:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;

class SecureMailgunWebhookTest extends TestCase
{
    protected function setUp()
    {
        parent::setUp();

        config()->set('services.mailgun.secret', 'secret');

        \Route::middleware('mailgun.webhook')->any('/_test/webhook', function () {
            return 'OK';
        });
    }

    /** @test */
    public function it_forbids_non_post_methods()
    {
        $this->withoutExceptionHandling();

        $exceptionCount = 0;
        $httpVerbs = ['get', 'put', 'patch', 'delete'];

        foreach ($httpVerbs as $httpVerb) {
            try {
                $response = $this->$httpVerb('/_test/webhook');
            } catch (HttpException $e) {
                $exceptionCount++;
                $this->assertEquals(Response::HTTP_FORBIDDEN, $e->getStatusCode());
                $this->assertEquals('Only POST requests are allowed.', $e->getMessage());
            }
        }

        if (count($httpVerbs) === $exceptionCount) {
            return;
        }

        $this->fail('Expected a 403 forbidden');
    }
}

setUp() 方法中,我们定义一个假的 Mailgun 密钥,这样我们就可以针对这个密钥编写我们的测试,然后使用 any() 路由方法定义一个全局(catch-all)路由。 我们的路由将允许我们使用虚假的测试路由来使用中间件发出 HTTP 请求。

在 Laravel 5.5 中引入了 withoutExceptionHandling() 这个方法,这意味着我们可以在测试中自己来捕获抛出的异常,来替代使用 HTTP 响应来呈现这种异常。

try/catch 将会确保为每个 HTTP 请求捕获到 HttpException ,还会提供一个递增的异常计数器。如果捕获异常的数量与我们测试的 HTTP 请求的数量匹配,则测试通过。否则的话,如果我们的请求没有引起异常, $this->fail() 方法将会被调用。

与使用注释相比,我更喜欢捕获和断言异常的方法。它会让我感觉到更清楚,同时我还可以对异常进行断言,以确保异常是我所期望的。

您可以使用以下 PhpUnit 命令直接运行中间件特性测试::

# Run all tests in the file
$ ./vendor/bin/phpunit tests/Feature/SecureMailgunWebhookTest.php

# Filter a specific method
$ ./vendor/bin/phpunit \
  tests/Feature/SecureMailgunWebhookTest.php \
  --filter=it_forbids_non_post_methods

Testing an Invalid Signature

下一个测试验证无效签名是否会导致 403 Forbidden error。这个测试与第一个测试不同,它使用的是 POST 方法,但发送无效的请求数据:

/** @test */
public function it_aborts_with_an_invalid_signature()
{
    $this->withoutExceptionHandling();

    try {
        $this->post('/_test/webhook', [
            'timestamp' => abs(time() - 100),
            'token' => 'invalid-token',
            'signature' => 'invalid-signature',
        ]);
    } catch (HttpException $e) {
        $this->assertEquals(Response::HTTP_FORBIDDEN, $e->getStatusCode());
        $this->assertEquals('The webhook signature was invalid.', $e->getMessage());
        return;
    }

    $this->fail('Expected the webhook signature to be invalid.');
}

我们传递将导致无效签名的假数据,然后断言在 HttpException中设置了正确的响应状态和消息。

测试有效签名

当 webhook 发送有效签名时,路由将处理响应,而不会中断中间件。中间件调用verify(),然后在签名匹配时调用$next()

if ($this->verify($request)) {
    return $next($request);
}

要编写此测试,我们需要发送有效的签名、时间戳和令牌。我们将在测试类中构建 SHA-256 hash 版本,它几乎是中间件内相同方法的副本。中间件和我们的测试都将使用在setup()方法中配置的services.mailgun.secret密钥:

/** @test */
public function it_passes_with_a_valid_signature()
{
    $this->withoutExceptionHandling();

    $timestamp = time();
    $token = 'token';
    $response = $this->post('/_test/webhook', [
        'timestamp' => $timestamp,
        'token' => $token,
        'signature' => $this->buildSignature($timestamp, $token),
    ]);

    $this->assertEquals('OK', $response->getContent());
}

protected function buildSignature($timestamp, $token)
{
    return hash_hmac(
        'sha256',
        sprintf('%s%s', $timestamp, $token),
        config('services.mailgun.secret')
    );
}

我们的测试在中间件中使用相同代码构建签名,因此我们可以生成中间件期望的有效签名。 在测试结束时,我们断言在测试路径中返回的响应内容等于 "OK”。

使用旧时间戳测试失败

我们的中间件采取的另一个预防措施是,如果 timestamp 应用是旧的,则不允许请求继续进行。 测试类似于我们断言失败的其他测试,但是这次我们使一切有效(签名和令牌)除了 时间戳之外:

/** @test */
public function it_fails_with_an_old_timestamp()
{
    try {
        $this->withoutExceptionHandling();

        $timestamp = abs(time() - 16);
        $token = 'token';
        $response = $this->post('/_test/webhook', [
            'timestamp' => $timestamp,
            'token' => $token,
            'signature' => $this->buildSignature($timestamp, $token),
        ]);
    } catch (HttpException $e) {
        $this->assertEquals(Response::HTTP_FORBIDDEN, $e->getStatusCode());
        $this->assertEquals('The webhook signature was invalid.', $e->getMessage());
        return;
    }

    $this->fail('The timestamp should have failed verification.');
}

密切关注 $timestamp = abs(time() - 16); 这将使中间件时间戳比较无效。

学习更多

这是一个在 HTTP 级别测试中间件的快速呈现。我更喜欢这种级别的测试,因为在中间件上使用 mocks (假数据)可能会很乏味,而且会绑定到特定的实现。如果我选择稍后重构,很可能需要重写我的测试以匹配新的中间件。通过 HTTP 测试,我可以自由地重构中间件,并期望得到相同的结果。

在 Laravel 中编写[HTTP 测试](https://laravel.com/docs/5.5/...) 非常简便,我发现自己在这个级别上做了更多的测试。我相信我写的测试很容易理解,因为我们不 mock (模拟)任何东西。您应熟练使用Laravel 测试套件的断言(功能)来测试。这些工具使你的测试工作更容易,我敢说更有趣。

如果你不熟悉测试, 你也可以在 Laravel News中查看Test Driven Laravel 。我也经历过这个过程;如果您刚刚开始测试Web应用程序,那么这将是一个不错的资源。


summerblue
11k 声望15.4k 粉丝

刻意练习,每日精进