RoutingMiddlewareTest.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  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\Cache\Cache;
  18. use Cake\Cache\Exception\InvalidArgumentException as CacheInvalidArgumentException;
  19. use Cake\Core\Configure;
  20. use Cake\Core\HttpApplicationInterface;
  21. use Cake\Http\ServerRequestFactory;
  22. use Cake\Routing\Exception\FailedRouteCacheException;
  23. use Cake\Routing\Exception\MissingRouteException;
  24. use Cake\Routing\Middleware\RoutingMiddleware;
  25. use Cake\Routing\Route\Route;
  26. use Cake\Routing\RouteBuilder;
  27. use Cake\Routing\RouteCollection;
  28. use Cake\Routing\Router;
  29. use Cake\Routing\RoutingApplicationInterface;
  30. use Cake\TestSuite\TestCase;
  31. use Laminas\Diactoros\Response;
  32. use TestApp\Application;
  33. use TestApp\Http\TestRequestHandler;
  34. use TestApp\Middleware\DumbMiddleware;
  35. use TestApp\Middleware\UnserializableMiddleware;
  36. /**
  37. * Test for RoutingMiddleware
  38. */
  39. class RoutingMiddlewareTest extends TestCase
  40. {
  41. protected $log = [];
  42. /**
  43. * @var \Cake\Routing\RouteBuilder
  44. */
  45. protected $builder;
  46. /**
  47. * Setup method
  48. */
  49. public function setUp(): void
  50. {
  51. parent::setUp();
  52. Router::reload();
  53. $this->builder = Router::createRouteBuilder('/');
  54. $this->builder->connect('/articles', ['controller' => 'Articles', 'action' => 'index']);
  55. $this->log = [];
  56. Configure::write('App.base', '');
  57. }
  58. public function tearDown(): void
  59. {
  60. parent::tearDown();
  61. Cache::enable();
  62. if (in_array('_cake_router_', Cache::configured(), true)) {
  63. Cache::clear('_cake_router_');
  64. }
  65. Cache::drop('_cake_router_');
  66. }
  67. /**
  68. * Test redirect responses from redirect routes
  69. */
  70. public function testRedirectResponse(): void
  71. {
  72. $this->builder->redirect('/testpath', '/pages');
  73. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/testpath']);
  74. $request = $request->withAttribute('base', '/subdir');
  75. $handler = new TestRequestHandler();
  76. $middleware = new RoutingMiddleware($this->app());
  77. $response = $middleware->process($request, $handler);
  78. $this->assertSame(301, $response->getStatusCode());
  79. $this->assertSame('http://localhost/subdir/pages', $response->getHeaderLine('Location'));
  80. }
  81. /**
  82. * Test redirects with additional headers
  83. */
  84. public function testRedirectResponseWithHeaders(): void
  85. {
  86. $this->builder->scope('/', function (RouteBuilder $routes): void {
  87. $routes->redirect('/testpath', '/pages');
  88. });
  89. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/testpath']);
  90. $handler = new TestRequestHandler(function ($request) {
  91. return new Response('php://memory', 200, ['X-testing' => 'Yes']);
  92. });
  93. $middleware = new RoutingMiddleware($this->app());
  94. $response = $middleware->process($request, $handler);
  95. $this->assertSame(301, $response->getStatusCode());
  96. $this->assertSame('http://localhost/pages', $response->getHeaderLine('Location'));
  97. }
  98. /**
  99. * Test that Router sets parameters
  100. */
  101. public function testRouterSetParams(): void
  102. {
  103. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  104. $handler = new TestRequestHandler(function ($req) {
  105. $expected = [
  106. 'controller' => 'Articles',
  107. 'action' => 'index',
  108. 'plugin' => null,
  109. 'pass' => [],
  110. '_ext' => null,
  111. '_matchedRoute' => '/articles',
  112. ];
  113. $this->assertEquals($expected, $req->getAttribute('params'));
  114. return new Response();
  115. });
  116. $middleware = new RoutingMiddleware($this->app());
  117. $middleware->process($request, $handler);
  118. }
  119. /**
  120. * Test that Router sets matched routes instance.
  121. */
  122. public function testRouterSetRoute(): void
  123. {
  124. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  125. $handler = new TestRequestHandler(function ($req) {
  126. $this->assertInstanceOf(Route::class, $req->getAttribute('route'));
  127. $this->assertSame('/articles', $req->getAttribute('route')->staticPath());
  128. return new Response();
  129. });
  130. $middleware = new RoutingMiddleware($this->app());
  131. $middleware->process($request, $handler);
  132. }
  133. /**
  134. * Test routing middleware does wipe off existing params keys.
  135. */
  136. public function testPreservingExistingParams(): void
  137. {
  138. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  139. $request = $request->withAttribute('params', ['_csrfToken' => 'i-am-groot']);
  140. $handler = new TestRequestHandler(function ($req) {
  141. $expected = [
  142. 'controller' => 'Articles',
  143. 'action' => 'index',
  144. 'plugin' => null,
  145. 'pass' => [],
  146. '_matchedRoute' => '/articles',
  147. '_csrfToken' => 'i-am-groot',
  148. ];
  149. $this->assertEquals($expected, $req->getAttribute('params'));
  150. return new Response();
  151. });
  152. $middleware = new RoutingMiddleware($this->app());
  153. $middleware->process($request, $handler);
  154. }
  155. /**
  156. * Test middleware invoking hook method
  157. */
  158. public function testRoutesHookInvokedOnApp(): void
  159. {
  160. Router::reload();
  161. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/app/articles']);
  162. $handler = new TestRequestHandler(function ($req) {
  163. $expected = [
  164. 'controller' => 'Articles',
  165. 'action' => 'index',
  166. 'plugin' => null,
  167. 'pass' => [],
  168. '_ext' => null,
  169. '_matchedRoute' => '/app/articles',
  170. ];
  171. $this->assertEquals($expected, $req->getAttribute('params'));
  172. $this->assertNotEmpty(Router::routes());
  173. $this->assertSame('/app/articles', Router::routes()[5]->template);
  174. return new Response();
  175. });
  176. $app = new Application(CONFIG);
  177. $middleware = new RoutingMiddleware($app);
  178. $middleware->process($request, $handler);
  179. }
  180. /**
  181. * Test that pluginRoutes hook is called
  182. */
  183. public function testRoutesHookCallsPluginHook(): void
  184. {
  185. Router::reload();
  186. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/app/articles']);
  187. $app = $this->getMockBuilder(Application::class)
  188. ->onlyMethods(['pluginRoutes'])
  189. ->setConstructorArgs([CONFIG])
  190. ->getMock();
  191. $app->expects($this->once())
  192. ->method('pluginRoutes')
  193. ->with($this->isInstanceOf(RouteBuilder::class));
  194. $middleware = new RoutingMiddleware($app);
  195. $middleware->process($request, new TestRequestHandler());
  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. $result = $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. $result = $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. $result = $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. */
  358. public function testInvokeScopedMiddlewareIsolatedScopes(string $url, array $expected): void
  359. {
  360. $this->builder->registerMiddleware('first', function ($request, $handler) {
  361. $this->log[] = 'first';
  362. return $handler->handle($request);
  363. });
  364. $this->builder->registerMiddleware('second', function ($request, $handler) {
  365. $this->log[] = 'second';
  366. return $handler->handle($request);
  367. });
  368. $this->builder->scope('/api', function (RouteBuilder $routes): void {
  369. $routes->applyMiddleware('first');
  370. $routes->connect('/ping', ['controller' => 'Pings']);
  371. });
  372. $this->builder->scope('/api', function (RouteBuilder $routes): void {
  373. $routes->applyMiddleware('second');
  374. $routes->connect('/version', ['controller' => 'Version']);
  375. });
  376. $request = ServerRequestFactory::fromGlobals([
  377. 'REQUEST_METHOD' => 'GET',
  378. 'REQUEST_URI' => $url,
  379. ]);
  380. $app = $this->app(function ($req) {
  381. $this->log[] = 'last';
  382. return new Response();
  383. });
  384. $middleware = new RoutingMiddleware($app);
  385. $result = $middleware->process($request, $app);
  386. $this->assertSame($expected, $this->log);
  387. }
  388. /**
  389. * Provider for scope isolation test.
  390. *
  391. * @return array
  392. */
  393. public function scopedMiddlewareUrlProvider(): array
  394. {
  395. return [
  396. ['/api/ping', ['first', 'last']],
  397. ['/api/version', ['second', 'last']],
  398. ];
  399. }
  400. /**
  401. * Test we store route collection in cache.
  402. */
  403. public function testCacheRoutes(): void
  404. {
  405. Configure::write('Error.ignoredDeprecationPaths', [
  406. 'tests/TestCase/Routing/Middleware/RoutingMiddlewareTest.php',
  407. ]);
  408. $cacheConfigName = '_cake_router_';
  409. Cache::setConfig($cacheConfigName, [
  410. 'engine' => 'File',
  411. 'path' => CACHE,
  412. ]);
  413. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  414. $handler = new TestRequestHandler(function ($req) use ($cacheConfigName) {
  415. $routeCollection = Cache::read('routeCollection', $cacheConfigName);
  416. $this->assertInstanceOf(RouteCollection::class, $routeCollection);
  417. return new Response();
  418. });
  419. $app = new Application(CONFIG);
  420. $middleware = new RoutingMiddleware($app, $cacheConfigName);
  421. $middleware->process($request, $handler);
  422. Configure::delete('Error.ignoredDeprecationPaths');
  423. }
  424. /**
  425. * Test we don't cache routes if cache is disabled.
  426. */
  427. public function testCacheNotUsedIfCacheDisabled(): void
  428. {
  429. Configure::write('Error.ignoredDeprecationPaths', [
  430. 'tests/TestCase/Routing/Middleware/RoutingMiddlewareTest.php',
  431. ]);
  432. $cacheConfigName = '_cake_router_';
  433. Cache::drop($cacheConfigName);
  434. Cache::disable();
  435. Cache::setConfig($cacheConfigName, [
  436. 'engine' => 'File',
  437. 'path' => CACHE,
  438. ]);
  439. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  440. $handler = new TestRequestHandler(function ($req) use ($cacheConfigName) {
  441. $routeCollection = Cache::read('routeCollection', $cacheConfigName);
  442. $this->assertNull($routeCollection);
  443. return new Response();
  444. });
  445. $app = new Application(CONFIG);
  446. $middleware = new RoutingMiddleware($app, $cacheConfigName);
  447. $middleware->process($request, $handler);
  448. Configure::delete('Error.ignoredDeprecationPaths');
  449. }
  450. /**
  451. * Test cache name is used
  452. */
  453. public function testCacheConfigNotFound(): void
  454. {
  455. Configure::write('Error.ignoredDeprecationPaths', [
  456. 'tests/TestCase/Routing/Middleware/RoutingMiddlewareTest.php',
  457. ]);
  458. $this->expectException(CacheInvalidArgumentException::class);
  459. $this->expectExceptionMessage('The "notfound" cache configuration does not exist.');
  460. Cache::setConfig('_cake_router_', [
  461. 'engine' => 'File',
  462. 'path' => CACHE,
  463. ]);
  464. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  465. $app = new Application(CONFIG);
  466. $middleware = new RoutingMiddleware($app, 'notfound');
  467. $middleware->process($request, new TestRequestHandler());
  468. Configure::delete('Error.ignoredDeprecationPaths');
  469. }
  470. public function testFailedRouteCache(): void
  471. {
  472. Cache::setConfig('_cake_router_', [
  473. 'engine' => 'File',
  474. 'path' => CACHE,
  475. ]);
  476. $app = $this->createMock(Application::class);
  477. $this->expectDeprecation();
  478. $this->expectDeprecationMessage(
  479. 'Use of routing cache is deprecated and will be removed in 5.0. ' .
  480. 'Upgrade to the new `CakeDC/CachedRouting` plugin. ' .
  481. 'See https://github.com/CakeDC/cakephp-cached-routing'
  482. );
  483. new RoutingMiddleware($app, '_cake_router_');
  484. }
  485. public function testDeprecatedRouteCache(): void
  486. {
  487. Configure::write('Error.ignoredDeprecationPaths', [
  488. 'tests/TestCase/Routing/Middleware/RoutingMiddlewareTest.php',
  489. ]);
  490. Cache::setConfig('_cake_router_', [
  491. 'engine' => 'File',
  492. 'path' => CACHE,
  493. ]);
  494. $app = $this->createMock(Application::class);
  495. $app
  496. ->method('routes')
  497. ->will($this->returnCallback(function (RouteBuilder $routes) use ($app) {
  498. return $routes->registerMiddleware('should fail', new UnserializableMiddleware($app));
  499. }));
  500. $middleware = new RoutingMiddleware($app, '_cake_router_');
  501. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  502. $this->expectException(FailedRouteCacheException::class);
  503. $this->expectExceptionMessage('Unable to cache route collection.');
  504. $middleware->process($request, new TestRequestHandler());
  505. Configure::delete('Error.ignoredDeprecationPaths');
  506. }
  507. /**
  508. * Test middleware works without an application implementing ContainerApplicationInterface
  509. */
  510. public function testAppWithoutContainerApplicationInterface(): void
  511. {
  512. /** @var \Cake\Core\HttpApplicationInterface|\PHPUnit\Framework\MockObject\MockObject $app */
  513. $app = $this->createMock(RoutingApplicationInterface::class);
  514. $this->builder->scope('/', function (RouteBuilder $routes): void {
  515. $routes->connect('/testpath', ['controller' => 'Articles', 'action' => 'index']);
  516. });
  517. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/testpath']);
  518. $handler = new TestRequestHandler(function ($request) {
  519. return new Response('php://memory', 200);
  520. });
  521. $middleware = new RoutingMiddleware($app);
  522. $response = $middleware->process($request, $handler);
  523. $this->assertSame(200, $response->getStatusCode());
  524. }
  525. /**
  526. * Test middleware works with an application implementing ContainerApplicationInterface
  527. */
  528. public function testAppWithContainerApplicationInterface(): void
  529. {
  530. $app = $this->app();
  531. $this->builder->scope('/', function (RouteBuilder $routes): void {
  532. $routes->connect('/testpath', ['controller' => 'Articles', 'action' => 'index']);
  533. });
  534. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/testpath']);
  535. $handler = new TestRequestHandler(function ($request) {
  536. return new Response('php://memory', 200);
  537. });
  538. $middleware = new RoutingMiddleware($app);
  539. $response = $middleware->process($request, $handler);
  540. $this->assertSame(200, $response->getStatusCode());
  541. }
  542. /**
  543. * Create a stub application for testing.
  544. *
  545. * @param callable|null $handleCallback Callback for "handle" method.
  546. */
  547. protected function app($handleCallback = null): HttpApplicationInterface
  548. {
  549. $mock = $this->createMock(Application::class);
  550. $mock->method('routes')
  551. ->will($this->returnCallback(function (RouteBuilder $routes) {
  552. return $routes;
  553. }));
  554. if ($handleCallback) {
  555. $mock->method('handle')
  556. ->will($this->returnCallback($handleCallback));
  557. }
  558. return $mock;
  559. }
  560. }