The article was forwarded from the professional Laravel developer community with the original link: https://learnku.com/laravel/t...
In this article, I'll show you an example of testing middleware using HTTP.HTTP-level testing is more adaptable and readable.
Full Stack Broadcast, recently co-filmed with Adam Wathan and Taylor Otwell http://www.fullstackradio.com/72 ) It is refreshing to hear that they have found a lot of practical value in HTTP testing.I find HTTP tests easier to write and maintain, but I do feel like I'm testing Wrong', or that I'm not impersonating (objects), isolating each test item is the same as cheating.If you haven't heard this episode yet, listen to it, and it's full of good, practical test suggestions.
introduce
Earlier this year, I built a middleware for validating and protecting Mailgun webhook s on one of my projects and for Laravel News Inbound processing of e-mail in Laravel with Mailgun This is described.In summary, I will demonstrate how to use the Laravel middleware to validate the Mailgun webhook when processing inbound e-mail (to ensure that the webhook actually comes from Mailgun).
When setting the core part of the Mailgun webhook, signatures that are part of the HTTP POST payload recommend using the signatures, timestamps, and tokens provided by rquest to verify and protect your webhook.This is the complete middleware I published:
<?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'); } }
The middleware accepts only POST requests and compares the incoming signatures with those generated using the Mailgun key.
I've seen various ways to test middleware, such as building it directly in a unit test, simulating objects as needed, and running the middleware directly.In this article, I will show you how to use higher-level HTTP testing to test this middleware.Your entire stack will run in tests, giving you more confidence that your application will work as expected.
Not binding tests directly to specific middleware implementations is an important benefit you can learn.We can completely refactor the middleware without changing any tests or updating simulations to verify that it is working properly.I'm sure you'll find these tests more robust.
To configure
Let's quickly build a test of the above middleware using the sample Laravel 5.5 project:
$ laravel new middleware-tests # Switch to the middleware-tests folder $ cd $_ $ php artisan make:middleware ValidateMailgunWebhook
Get the middleware code above and paste it into this middleware file.
Next, add this middleware to the app/Http/Kernel.php file:
protected $routeMiddleware = [ // ... 'mailgun.webhook' => \App\Http\Middleware\ValidateMailgunWebhook::class, ];
Write HTTP tests
We're going to write some tests for this middleware, and we don't even have to define any routes/api.php to test it!
First, let's create a functional test file:
$ php artisan make:test SecureMailgunWebhookTest
Looking at the Mailgun middleware, here's what we're testing to make sure it works as expected:
- Any HTTP verb POST except should cause a 403 Forbidden response.
- An invalid signature should create a 403 Forbidden response.
- A valid signature should pass and hit the callable route.
- The old timestamp should cause a 403 Forbidden response.
Testing invalid HTTP methods
With this introduction, let's write the first test and set up our test.
Update the SecureMailgunWebhookTest file with the following:
<?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'); } }
In the setUp() method, we define a fake Mailgun key so that we can write our tests against it and then use any() routing method to define a catch-all route.Our routes will allow us to use fake test routes to make HTTP requests using middleware.
The withoutExceptionHandling() method was introduced in Laravel 5.5, which means that we can catch exceptions thrown by ourselves in our tests instead of using HTTP responses to render them.
try/catch will ensure that HttpException is captured for each HTTP request and will also provide an incremental exception counter.If the number of exceptions caught matches the number of HTTP requests we tested, the test passes.Otherwise, if our request does not cause an exception, the $this->fail() method will be called.
I prefer the method of catching and asserting exceptions to using comments.It will make me feel clearer, and I can assert anomalies to make sure they are what I expected.
You can run the middleware feature test directly using the following PhpUnit command:
# 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
The next test verifies that an invalid signature will result in a 403 Forbidden error.This test differs from the first test in that it uses the POST method but sends invalid request data:
/** @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.'); }
We pass in false data that will result in an invalid signature and then assert that the correct response state and message are set in HttpException.
Test valid signature
When a webhook sends a valid signature, the route processes the response without interrupting the middleware.The middleware calls verify(), then $next() when the signature matches:
if ($this->verify($request)) { return $next($request); }
To write this test, we need to send valid signatures, timestamps, and tokens.We will build the SHA-256 hash version in the test class, which is almost a copy of the same method in the middleware.Both the middleware and our tests will use the services.mailgun.secret key configured in the setup() method:
/** @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') ); }
Our tests use the same code to build signatures in the middleware, so we can generate the valid signatures expected by the middleware.At the end of the test, we assert that the response content returned in the test path is equal to OK.
Failed to test with old timestamp
Another precaution taken by our middleware is that if the timestamp application is old, requests are not allowed to proceed.The test is like any other test we assert failed, but this time we make everything valid (signatures and tokens) except the time stamp:
/** @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.'); }
Keep an eye on $timestamp = abs(time() - 16); this will invalidate middleware timestamps.
Learn more
This is a quick rendering of the test middleware at the HTTP level.I prefer this level of testing because using mocks (fake data) in the middleware can be tedious and bind to specific implementations.If I choose to refactor later, I will most likely need to rewrite my tests to match the new middleware.Through HTTP testing, I am free to refactor the middleware and expect the same results.
Write [HTTP test] in Laravel ( https://laravel.com/docs/5.5/... ) Very easy, I find myself doing more tests at this level.I believe the tests I wrote are easy to understand because we don't mock anything.You should be familiar with using the assertions (capabilities) of the Laravel test suite to test.These tools make your testing easier and, I dare say, more interesting.
If you are unfamiliar with testing, you can also view it in Laravel News Test Driven Laravel .I've also experienced this process; if you're just starting to test your Web application, this will be a good resource.