Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Commit dacfab0

Browse files
committed
Ensure error middleware pipelines can be executed
This patch provides tests for the following scenarios: - Given a middleware spec that resolves to a middleware service and which specifies error middleware, the error middleware will be executed in error conditions. - Given a middleware spec that resolves to an error middleware pipeline and which specifies error middleware, the error middleware pipeline will be executed in error conditions. The latter was failing, prompting this patch. The fix was reasonably simple: when an object is used as error middleware, `Zend\Stratigility\Utils::getArity()` looks at the number of *required* arguments, and omits *optional* arguments, despite the fact that the `ErrorMiddlewareInterface` specifies the fourth argument as optional. Making the last (`$out`) argument required allows the error middleware to execute. Additionally, I noticed during step-debugging that `pipeErrorMiddleware()` was calling `$this->pipe()` — which contains much of the same logic for preparing lazy middleware. As such, I updated it to call `parent::pipe()` as a minor optimization.
1 parent a656721 commit dacfab0

File tree

3 files changed

+229
-2
lines changed

3 files changed

+229
-2
lines changed

src/Application.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ public function pipeErrorHandler($path, $middleware = null)
339339
$middleware = $this->prepareMiddleware($middleware, $this->container, $forError = true);
340340
}
341341

342-
$this->pipe($path, $middleware);
342+
parent::pipe($path, $middleware);
343343

344344
return $this;
345345
}

src/ErrorMiddlewarePipe.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public function __construct(MiddlewarePipe $pipeline)
6262
* @param callable $out
6363
* @return Response
6464
*/
65-
public function __invoke($error, Request $request, Response $response, callable $out = null)
65+
public function __invoke($error, Request $request, Response $response, callable $out)
6666
{
6767
// Decorate instances with Stratigility decorators; required to work
6868
// with Next implementation.
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
<?php
2+
/**
3+
* Zend Framework (http://framework.zend.com/)
4+
*
5+
* @see https://github.com/zendframework/zend-expressive for the canonical source repository
6+
* @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
7+
* @license https://github.com/zendframework/zend-expressive/blob/master/LICENSE.md New BSD License
8+
*/
9+
10+
namespace ZendTest\Expressive\Container;
11+
12+
use PHPUnit_Framework_TestCase as TestCase;
13+
use Prophecy\Argument;
14+
use Psr\Http\Message\ServerRequestInterface;
15+
use Psr\Http\Message\ResponseInterface;
16+
use Zend\Diactoros\Response;
17+
use Zend\Diactoros\Response\EmitterInterface;
18+
use Zend\Diactoros\ServerRequest;
19+
use Zend\Expressive\Container\ApplicationFactory;
20+
use Zend\Expressive\Router\Route;
21+
use Zend\Expressive\Router\RouteResult;
22+
use Zend\Expressive\Router\RouterInterface;
23+
use ZendTest\Expressive\ContainerTrait;
24+
25+
/**
26+
* Integration test case for Application instances generated by ApplicationFactory.
27+
*
28+
* This test case is to verify that various configuration scenarios provide an
29+
* application that will behave in specific ways, including:
30+
*
31+
* - Given a middleware spec resolving to error middleware, that middleware
32+
* will be executed in error conditions.
33+
* - Given a middleware spec resolving to an error middleware pipeline, that
34+
* pipeline will be executed in error conditions.
35+
*/
36+
class ApplicationFactoryIntegrationTest extends TestCase
37+
{
38+
use ContainerTrait;
39+
40+
protected $container;
41+
42+
public function setUp()
43+
{
44+
$this->container = $this->mockContainerInterface();
45+
$this->factory = new ApplicationFactory();
46+
47+
$this->router = $this->prophesize(RouterInterface::class);
48+
$this->emitter = $this->prophesize(EmitterInterface::class);
49+
50+
$this->injectServiceInContainer($this->container, RouterInterface::class, $this->router->reveal());
51+
$this->injectServiceInContainer($this->container, EmitterInterface::class, $this->emitter->reveal());
52+
}
53+
54+
public function testConfiguredErrorMiddlewarePipeIsExecutedWhenMiddlewareCallsNextWithError()
55+
{
56+
$always = function ($request, $response, $next) {
57+
$response = $next($request, $response);
58+
return $response->withHeader('X-Always', 'true');
59+
};
60+
61+
$routeResultSpy = function ($request, $response, $next) {
62+
$result = $request->getAttribute(RouteResult::class, false);
63+
$value = $result ? $result->getMatchedRouteName() : 'not-found';
64+
return $next($request, $response->withHeader('X-Route-Result', $value));
65+
};
66+
67+
$needsAuthentication = function ($request, $response, $next) {
68+
return $next($request, $response, '401');
69+
};
70+
71+
$unauthorized = function ($error, $request, $response, callable $next = null) {
72+
$this->assertEquals('401', $error);
73+
$response->getBody()->write('Error middleware called');
74+
return $response->withStatus(401);
75+
};
76+
77+
$finalHandler = function ($request, $response, $err = null) {
78+
$this->fail('Should not hit final handler, but did');
79+
};
80+
81+
$routeResult = $this->prophesize(RouteResult::class);
82+
$routeResult->isFailure()->willReturn(false);
83+
$routeResult->getMatchedParams()->willReturn([]);
84+
$routeResult->getMatchedRouteName()->willReturn('needs-auth');
85+
$routeResult->getMatchedMiddleware()->willReturn('NeedsAuthentication');
86+
$this->router->match(Argument::type(ServerRequestInterface::class))->willReturn($routeResult->reveal());
87+
88+
$this->injectServiceInContainer($this->container, 'Always', $always);
89+
$this->injectServiceInContainer($this->container, 'RouteResultSpy', $routeResultSpy);
90+
$this->injectServiceInContainer($this->container, 'NeedsAuthentication', $needsAuthentication);
91+
$this->injectServiceInContainer($this->container, 'Unauthorized', $unauthorized);
92+
$this->injectServiceInContainer($this->container, 'Zend\Expressive\FinalHandler', $finalHandler);
93+
94+
$config = [
95+
'routes' => [
96+
'name' => 'needs-auth',
97+
'path' => '/needs/authentication',
98+
'middleware' => 'NeedsAuthentication',
99+
'allowed_methods' => ['GET'],
100+
],
101+
'middleware_pipeline' => [
102+
'always' => [
103+
'middleware' => [
104+
'Always',
105+
],
106+
'priority' => 10000,
107+
],
108+
'routing' => [
109+
'middleware' => [
110+
ApplicationFactory::ROUTING_MIDDLEWARE,
111+
'RouteResultSpy',
112+
ApplicationFactory::DISPATCH_MIDDLEWARE,
113+
],
114+
'priority' => 1,
115+
],
116+
'error' => [
117+
'middleware' => [
118+
'Unauthorized',
119+
],
120+
'priority' => -10000,
121+
'error' => true,
122+
],
123+
],
124+
];
125+
$this->injectServiceInContainer($this->container, 'config', $config);
126+
127+
$app = $this->factory->__invoke($this->container->reveal());
128+
129+
$request = new ServerRequest([], [], 'http://example.com/needs/authentication', 'GET');
130+
$response = new Response();
131+
132+
$response = $app($request, $response);
133+
$this->assertInstanceOf(ResponseInterface::class, $response);
134+
$this->assertEquals(401, $response->getStatusCode(), 'Unexpected response');
135+
$this->assertTrue($response->hasHeader('X-Always'));
136+
$this->assertEquals('true', $response->getHeaderLine('X-Always'));
137+
$this->assertTrue($response->hasHeader('X-Route-Result'));
138+
$this->assertEquals('needs-auth', $response->getHeaderLine('X-Route-Result'));
139+
$this->assertEquals('Error middleware called', (string) $response->getBody());
140+
}
141+
142+
public function testConfiguredErrorMiddlewareIsExecutedWhenMiddlewareCallsNextWithError()
143+
{
144+
$always = function ($request, $response, $next) {
145+
$response = $next($request, $response);
146+
return $response->withHeader('X-Always', 'true');
147+
};
148+
149+
$routeResultSpy = function ($request, $response, $next) {
150+
$result = $request->getAttribute(RouteResult::class, false);
151+
$value = $result ? $result->getMatchedRouteName() : 'not-found';
152+
return $next($request, $response->withHeader('X-Route-Result', $value));
153+
};
154+
155+
$needsAuthentication = function ($request, $response, $next) {
156+
return $next($request, $response, '401');
157+
};
158+
159+
$unauthorized = function ($error, $request, $response, callable $next = null) {
160+
$this->assertEquals('401', $error);
161+
$response->getBody()->write('Error middleware called');
162+
return $response->withStatus(401);
163+
};
164+
165+
$finalHandler = function ($request, $response, $err = null) {
166+
$this->fail('Should not hit final handler, but did');
167+
};
168+
169+
$routeResult = $this->prophesize(RouteResult::class);
170+
$routeResult->isFailure()->willReturn(false);
171+
$routeResult->getMatchedParams()->willReturn([]);
172+
$routeResult->getMatchedRouteName()->willReturn('needs-auth');
173+
$routeResult->getMatchedMiddleware()->willReturn('NeedsAuthentication');
174+
$this->router->match(Argument::type(ServerRequestInterface::class))->willReturn($routeResult->reveal());
175+
176+
$this->injectServiceInContainer($this->container, 'Always', $always);
177+
$this->injectServiceInContainer($this->container, 'RouteResultSpy', $routeResultSpy);
178+
$this->injectServiceInContainer($this->container, 'NeedsAuthentication', $needsAuthentication);
179+
$this->injectServiceInContainer($this->container, 'Unauthorized', $unauthorized);
180+
$this->injectServiceInContainer($this->container, 'Zend\Expressive\FinalHandler', $finalHandler);
181+
182+
$config = [
183+
'routes' => [
184+
'name' => 'needs-auth',
185+
'path' => '/needs/authentication',
186+
'middleware' => 'NeedsAuthentication',
187+
'allowed_methods' => ['GET'],
188+
],
189+
'middleware_pipeline' => [
190+
'always' => [
191+
'middleware' => [
192+
'Always',
193+
],
194+
'priority' => 10000,
195+
],
196+
'routing' => [
197+
'middleware' => [
198+
ApplicationFactory::ROUTING_MIDDLEWARE,
199+
'RouteResultSpy',
200+
ApplicationFactory::DISPATCH_MIDDLEWARE,
201+
],
202+
'priority' => 1,
203+
],
204+
'error' => [
205+
'middleware' => 'Unauthorized',
206+
'priority' => -10000,
207+
'error' => true,
208+
],
209+
],
210+
];
211+
$this->injectServiceInContainer($this->container, 'config', $config);
212+
213+
$app = $this->factory->__invoke($this->container->reveal());
214+
215+
$request = new ServerRequest([], [], 'http://example.com/needs/authentication', 'GET');
216+
$response = new Response();
217+
218+
$response = $app($request, $response);
219+
$this->assertInstanceOf(ResponseInterface::class, $response);
220+
$this->assertEquals(401, $response->getStatusCode(), 'Unexpected response');
221+
$this->assertTrue($response->hasHeader('X-Always'));
222+
$this->assertEquals('true', $response->getHeaderLine('X-Always'));
223+
$this->assertTrue($response->hasHeader('X-Route-Result'));
224+
$this->assertEquals('needs-auth', $response->getHeaderLine('X-Route-Result'));
225+
$this->assertEquals('Error middleware called', (string) $response->getBody());
226+
}
227+
}

0 commit comments

Comments
 (0)