共计 6035 个字符,预计需要花费 16 分钟才能阅读完成。
文章转发自专业的 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 中间件,以下是我们要测试的内容,以确保中间件按预期工作:
- 除了之外的任何 HTTP 动词
POST
都应该引起403 Forbidden
响应。 - 无效的签名应该创建
403 Forbidden
响应。 - 有效签名应该通过并命中可调用的路由。
- 旧的时间戳应该引起
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 应用程序,那么这将是一个不错的资源。