RoutingMiddlewareTest.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.3.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Routing\Middleware;
  17. use Cake\Core\Configure;
  18. use Cake\Http\ServerRequestFactory;
  19. use Cake\Routing\Exception\MissingRouteException;
  20. use Cake\Routing\Middleware\RoutingMiddleware;
  21. use Cake\Routing\Route\Route;
  22. use Cake\Routing\RouteBuilder;
  23. use Cake\Routing\Router;
  24. use Cake\Routing\RoutingApplicationInterface;
  25. use Cake\TestSuite\TestCase;
  26. use Closure;
  27. use Laminas\Diactoros\Response;
  28. use PHPUnit\Framework\Attributes\DataProvider;
  29. use Psr\Http\Message\ResponseInterface;
  30. use Psr\Http\Message\ServerRequestInterface;
  31. use TestApp\Application;
  32. use TestApp\Http\TestRequestHandler;
  33. use TestApp\Middleware\DumbMiddleware;
  34. use TestApp\Routing\Route\HeaderRedirectRoute;
  35. /**
  36. * Test for RoutingMiddleware
  37. */
  38. class RoutingMiddlewareTest extends TestCase
  39. {
  40. protected $log = [];
  41. /**
  42. * @var \Cake\Routing\RouteBuilder
  43. */
  44. protected $builder;
  45. /**
  46. * Setup method
  47. */
  48. public function setUp(): void
  49. {
  50. parent::setUp();
  51. Router::reload();
  52. $this->builder = Router::createRouteBuilder('/');
  53. $this->builder->connect('/articles', ['controller' => 'Articles', 'action' => 'index']);
  54. $this->log = [];
  55. Configure::write('App.base', '');
  56. }
  57. /**
  58. * Test redirect responses from redirect routes
  59. */
  60. public function testRedirectResponse(): void
  61. {
  62. $this->builder->redirect('/testpath', '/pages');
  63. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/testpath']);
  64. $request = $request->withAttribute('base', '/subdir');
  65. $handler = new TestRequestHandler();
  66. $middleware = new RoutingMiddleware($this->app());
  67. $response = $middleware->process($request, $handler);
  68. $this->assertSame(301, $response->getStatusCode());
  69. $this->assertSame('http://localhost/subdir/pages', $response->getHeaderLine('Location'));
  70. }
  71. /**
  72. * Test redirects with additional headers
  73. */
  74. public function testRedirectResponseWithHeaders(): void
  75. {
  76. $this->builder->connect('/testpath', ['controller' => 'Articles', 'action' => 'index'], ['routeClass' => HeaderRedirectRoute::class]);
  77. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/testpath']);
  78. $handler = new TestRequestHandler(function ($request) {
  79. return new Response();
  80. });
  81. $middleware = new RoutingMiddleware($this->app());
  82. $response = $middleware->process($request, $handler);
  83. $this->assertSame(301, $response->getStatusCode());
  84. $this->assertSame('http://localhost/pages', $response->getHeaderLine('Location'));
  85. $this->assertSame('yes', $response->getHeaderLine('Redirect-Exception'));
  86. }
  87. /**
  88. * Test that Router sets parameters
  89. */
  90. public function testRouterSetParams(): void
  91. {
  92. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  93. $handler = new TestRequestHandler(function ($req) {
  94. $expected = [
  95. 'controller' => 'Articles',
  96. 'action' => 'index',
  97. 'plugin' => null,
  98. 'pass' => [],
  99. '_ext' => null,
  100. '_matchedRoute' => '/articles',
  101. ];
  102. $this->assertEquals($expected, $req->getAttribute('params'));
  103. return new Response();
  104. });
  105. $middleware = new RoutingMiddleware($this->app());
  106. $middleware->process($request, $handler);
  107. }
  108. /**
  109. * Test that Router sets matched routes instance.
  110. */
  111. public function testRouterSetRoute(): void
  112. {
  113. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  114. $handler = new TestRequestHandler(function ($req) {
  115. $this->assertInstanceOf(Route::class, $req->getAttribute('route'));
  116. $this->assertSame('/articles', $req->getAttribute('route')->staticPath());
  117. return new Response();
  118. });
  119. $middleware = new RoutingMiddleware($this->app());
  120. $middleware->process($request, $handler);
  121. }
  122. /**
  123. * Test routing middleware does wipe off existing params keys.
  124. */
  125. public function testPreservingExistingParams(): void
  126. {
  127. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  128. $request = $request->withAttribute('params', ['_csrfToken' => 'i-am-groot']);
  129. $handler = new TestRequestHandler(function ($req) {
  130. $expected = [
  131. 'controller' => 'Articles',
  132. 'action' => 'index',
  133. 'plugin' => null,
  134. 'pass' => [],
  135. '_matchedRoute' => '/articles',
  136. '_csrfToken' => 'i-am-groot',
  137. ];
  138. $this->assertEquals($expected, $req->getAttribute('params'));
  139. return new Response();
  140. });
  141. $middleware = new RoutingMiddleware($this->app());
  142. $middleware->process($request, $handler);
  143. }
  144. /**
  145. * Test middleware invoking hook method
  146. */
  147. public function testRoutesHookInvokedOnApp(): void
  148. {
  149. Router::reload();
  150. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/app/articles']);
  151. $handler = new TestRequestHandler(function ($req) {
  152. $expected = [
  153. 'controller' => 'Articles',
  154. 'action' => 'index',
  155. 'plugin' => null,
  156. 'pass' => [],
  157. '_ext' => null,
  158. '_matchedRoute' => '/app/articles',
  159. ];
  160. $this->assertEquals($expected, $req->getAttribute('params'));
  161. $this->assertNotEmpty(Router::routes());
  162. $this->assertSame('/app/articles', Router::routes()[5]->template);
  163. return new Response();
  164. });
  165. $app = new Application(CONFIG);
  166. $middleware = new RoutingMiddleware($app);
  167. $middleware->process($request, $handler);
  168. }
  169. /**
  170. * Test that pluginRoutes hook is called
  171. */
  172. public function testRoutesHookCallsPluginHook(): void
  173. {
  174. Router::reload();
  175. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/app/articles']);
  176. $app = new class (CONFIG) extends Application {
  177. public function pluginRoutes(RouteBuilder $routes): RouteBuilder
  178. {
  179. $routes->connect('/app/articles', ['controller' => 'Articles', 'action' => 'index']);
  180. return $routes;
  181. }
  182. };
  183. $middleware = new RoutingMiddleware($app);
  184. $middleware->process($request, new TestRequestHandler(function ($req) {
  185. $expected = [
  186. 'controller' => 'Articles',
  187. 'action' => 'index',
  188. 'plugin' => null,
  189. 'pass' => [],
  190. '_ext' => null,
  191. '_matchedRoute' => '/app/articles',
  192. ];
  193. $this->assertEquals($expected, $req->getAttribute('params'));
  194. return new Response();
  195. }));
  196. }
  197. /**
  198. * Test that routing is not applied if a controller exists already
  199. */
  200. public function testRouterNoopOnController(): void
  201. {
  202. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  203. $request = $request->withAttribute('params', ['controller' => 'Articles']);
  204. $handler = new TestRequestHandler(function ($req) {
  205. $this->assertEquals(['controller' => 'Articles'], $req->getAttribute('params'));
  206. return new Response();
  207. });
  208. $middleware = new RoutingMiddleware($this->app());
  209. $middleware->process($request, $handler);
  210. }
  211. /**
  212. * Test missing routes not being caught.
  213. */
  214. public function testMissingRouteNotCaught(): void
  215. {
  216. $this->expectException(MissingRouteException::class);
  217. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/missing']);
  218. $middleware = new RoutingMiddleware($this->app());
  219. $middleware->process($request, new TestRequestHandler());
  220. }
  221. /**
  222. * Test route with _method being parsed correctly.
  223. */
  224. public function testFakedRequestMethodParsed(): void
  225. {
  226. $this->builder->connect('/articles-patch', [
  227. 'controller' => 'Articles',
  228. 'action' => 'index',
  229. '_method' => 'PATCH',
  230. ]);
  231. $request = ServerRequestFactory::fromGlobals(
  232. [
  233. 'REQUEST_METHOD' => 'POST',
  234. 'REQUEST_URI' => '/articles-patch',
  235. ],
  236. null,
  237. ['_method' => 'PATCH']
  238. );
  239. $handler = new TestRequestHandler(function ($req) {
  240. $expected = [
  241. 'controller' => 'Articles',
  242. 'action' => 'index',
  243. '_method' => 'PATCH',
  244. 'plugin' => null,
  245. 'pass' => [],
  246. '_matchedRoute' => '/articles-patch',
  247. '_ext' => null,
  248. ];
  249. $this->assertEquals($expected, $req->getAttribute('params'));
  250. $this->assertSame('PATCH', $req->getMethod());
  251. return new Response();
  252. });
  253. $middleware = new RoutingMiddleware($this->app());
  254. $middleware->process($request, $handler);
  255. }
  256. /**
  257. * Test invoking simple scoped middleware
  258. */
  259. public function testInvokeScopedMiddleware(): void
  260. {
  261. $this->builder->scope('/api', function (RouteBuilder $routes): void {
  262. $routes->registerMiddleware('first', function ($request, $handler) {
  263. $this->log[] = 'first';
  264. return $handler->handle($request);
  265. });
  266. $routes->registerMiddleware('second', function ($request, $handler) {
  267. $this->log[] = 'second';
  268. return $handler->handle($request);
  269. });
  270. $routes->registerMiddleware('dumb', DumbMiddleware::class);
  271. // Connect middleware in reverse to test ordering.
  272. $routes->applyMiddleware('second', 'first', 'dumb');
  273. $routes->connect('/ping', ['controller' => 'Pings']);
  274. });
  275. $request = ServerRequestFactory::fromGlobals([
  276. 'REQUEST_METHOD' => 'GET',
  277. 'REQUEST_URI' => '/api/ping',
  278. ]);
  279. $app = $this->app(function ($req) {
  280. $this->log[] = 'last';
  281. return new Response();
  282. });
  283. $middleware = new RoutingMiddleware($app);
  284. $middleware->process($request, $app);
  285. $this->assertSame(['second', 'first', 'last'], $this->log);
  286. }
  287. /**
  288. * Test control flow in scoped middleware.
  289. *
  290. * Scoped middleware should be able to generate a response
  291. * and abort lower layers.
  292. */
  293. public function testInvokeScopedMiddlewareReturnResponse(): void
  294. {
  295. $this->builder->registerMiddleware('first', function ($request, $handler) {
  296. $this->log[] = 'first';
  297. return $handler->handle($request);
  298. });
  299. $this->builder->registerMiddleware('second', function ($request, $handler) {
  300. $this->log[] = 'second';
  301. return new Response();
  302. });
  303. $this->builder->applyMiddleware('first');
  304. $this->builder->connect('/', ['controller' => 'Home']);
  305. $this->builder->scope('/api', function (RouteBuilder $routes): void {
  306. $routes->applyMiddleware('second');
  307. $routes->connect('/articles', ['controller' => 'Articles']);
  308. });
  309. $request = ServerRequestFactory::fromGlobals([
  310. 'REQUEST_METHOD' => 'GET',
  311. 'REQUEST_URI' => '/api/articles',
  312. ]);
  313. $handler = new TestRequestHandler(function ($req): void {
  314. $this->fail('Should not be invoked as first should be ignored.');
  315. });
  316. $middleware = new RoutingMiddleware($this->app());
  317. $middleware->process($request, $handler);
  318. $this->assertSame(['first', 'second'], $this->log);
  319. }
  320. /**
  321. * Test control flow in scoped middleware.
  322. */
  323. public function testInvokeScopedMiddlewareReturnResponseMainScope(): void
  324. {
  325. $this->builder->registerMiddleware('first', function ($request, $handler) {
  326. $this->log[] = 'first';
  327. return new Response();
  328. });
  329. $this->builder->registerMiddleware('second', function ($request, $handler) {
  330. $this->log[] = 'second';
  331. return $handler->handle($request);
  332. });
  333. $this->builder->applyMiddleware('first');
  334. $this->builder->connect('/', ['controller' => 'Home']);
  335. $this->builder->scope('/api', function (RouteBuilder $routes): void {
  336. $routes->applyMiddleware('second');
  337. $routes->connect('/articles', ['controller' => 'Articles']);
  338. });
  339. $request = ServerRequestFactory::fromGlobals([
  340. 'REQUEST_METHOD' => 'GET',
  341. 'REQUEST_URI' => '/',
  342. ]);
  343. $handler = new TestRequestHandler(function ($req): void {
  344. $this->fail('Should not be invoked as first should be ignored.');
  345. });
  346. $middleware = new RoutingMiddleware($this->app());
  347. $middleware->process($request, $handler);
  348. $this->assertSame(['first'], $this->log);
  349. }
  350. /**
  351. * Test invoking middleware & scope separation
  352. *
  353. * Re-opening a scope should not inherit middleware declared
  354. * in the first context.
  355. */
  356. #[DataProvider('scopedMiddlewareUrlProvider')]
  357. public function testInvokeScopedMiddlewareIsolatedScopes(string $url, array $expected): void
  358. {
  359. $this->builder->registerMiddleware('first', function ($request, $handler) {
  360. $this->log[] = 'first';
  361. return $handler->handle($request);
  362. });
  363. $this->builder->registerMiddleware('second', function ($request, $handler) {
  364. $this->log[] = 'second';
  365. return $handler->handle($request);
  366. });
  367. $this->builder->scope('/api', function (RouteBuilder $routes): void {
  368. $routes->applyMiddleware('first');
  369. $routes->connect('/ping', ['controller' => 'Pings']);
  370. });
  371. $this->builder->scope('/api', function (RouteBuilder $routes): void {
  372. $routes->applyMiddleware('second');
  373. $routes->connect('/version', ['controller' => 'Version']);
  374. });
  375. $request = ServerRequestFactory::fromGlobals([
  376. 'REQUEST_METHOD' => 'GET',
  377. 'REQUEST_URI' => $url,
  378. ]);
  379. $app = $this->app(function ($req) {
  380. $this->log[] = 'last';
  381. return new Response();
  382. });
  383. $middleware = new RoutingMiddleware($app);
  384. $middleware->process($request, $app);
  385. $this->assertSame($expected, $this->log);
  386. }
  387. /**
  388. * Provider for scope isolation test.
  389. *
  390. * @return array
  391. */
  392. public static function scopedMiddlewareUrlProvider(): array
  393. {
  394. return [
  395. ['/api/ping', ['first', 'last']],
  396. ['/api/version', ['second', 'last']],
  397. ];
  398. }
  399. /**
  400. * Test middleware works without an application implementing ContainerApplicationInterface
  401. */
  402. public function testAppWithoutContainerApplicationInterface(): void
  403. {
  404. $app = new class implements RoutingApplicationInterface {
  405. public function routes(RouteBuilder $routes): void
  406. {
  407. }
  408. };
  409. $this->builder->scope('/', function (RouteBuilder $routes): void {
  410. $routes->connect('/testpath', ['controller' => 'Articles', 'action' => 'index']);
  411. });
  412. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/testpath']);
  413. $handler = new TestRequestHandler(function ($request) {
  414. return new Response('php://memory', 200);
  415. });
  416. $middleware = new RoutingMiddleware($app);
  417. $response = $middleware->process($request, $handler);
  418. $this->assertSame(200, $response->getStatusCode());
  419. }
  420. /**
  421. * Test middleware works with an application implementing ContainerApplicationInterface
  422. */
  423. public function testAppWithContainerApplicationInterface(): void
  424. {
  425. $app = $this->app();
  426. $this->builder->scope('/', function (RouteBuilder $routes): void {
  427. $routes->connect('/testpath', ['controller' => 'Articles', 'action' => 'index']);
  428. });
  429. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/testpath']);
  430. $handler = new TestRequestHandler(function ($request) {
  431. return new Response('php://memory', 200);
  432. });
  433. $middleware = new RoutingMiddleware($app);
  434. $response = $middleware->process($request, $handler);
  435. $this->assertSame(200, $response->getStatusCode());
  436. }
  437. /**
  438. * Create a stub application for testing.
  439. *
  440. * @param callable|null $handleCallback Callback for "handle" method.
  441. */
  442. protected function app(?callable $handleCallback = null): Application
  443. {
  444. $app = new class (CONFIG) extends Application {
  445. public ?Closure $handleCallback;
  446. public function routes(RouteBuilder $routes): void
  447. {
  448. }
  449. public function handle(ServerRequestInterface $request): ResponseInterface
  450. {
  451. if ($this->handleCallback) {
  452. return ($this->handleCallback)($request);
  453. }
  454. return parent::handle($request); // TODO: Change the autogenerated stub
  455. }
  456. };
  457. $app->handleCallback = $handleCallback;
  458. return $app;
  459. }
  460. }