RoutingMiddlewareTest.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  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\InvalidArgumentException as CacheInvalidArgumentException;
  19. use Cake\Core\Configure;
  20. use Cake\Http\ServerRequestFactory;
  21. use Cake\Routing\Middleware\RoutingMiddleware;
  22. use Cake\Routing\RouteBuilder;
  23. use Cake\Routing\RouteCollection;
  24. use Cake\Routing\Router;
  25. use Cake\TestSuite\TestCase;
  26. use Laminas\Diactoros\Response;
  27. use TestApp\Application;
  28. use TestApp\Http\TestRequestHandler;
  29. use TestApp\Middleware\DumbMiddleware;
  30. /**
  31. * Test for RoutingMiddleware
  32. */
  33. class RoutingMiddlewareTest extends TestCase
  34. {
  35. protected $log = [];
  36. /**
  37. * Setup method
  38. *
  39. * @return void
  40. */
  41. public function setUp(): void
  42. {
  43. parent::setUp();
  44. Router::reload();
  45. Router::connect('/articles', ['controller' => 'Articles', 'action' => 'index']);
  46. $this->log = [];
  47. Configure::write('App.base', '');
  48. }
  49. /**
  50. * Test redirect responses from redirect routes
  51. *
  52. * @return void
  53. */
  54. public function testRedirectResponse()
  55. {
  56. Router::scope('/', function (RouteBuilder $routes) {
  57. $routes->redirect('/testpath', '/pages');
  58. });
  59. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/testpath']);
  60. $request = $request->withAttribute('base', '/subdir');
  61. $handler = new TestRequestHandler();
  62. $middleware = new RoutingMiddleware($this->app());
  63. $response = $middleware->process($request, $handler);
  64. $this->assertSame(301, $response->getStatusCode());
  65. $this->assertSame('http://localhost/subdir/pages', $response->getHeaderLine('Location'));
  66. }
  67. /**
  68. * Test redirects with additional headers
  69. *
  70. * @return void
  71. */
  72. public function testRedirectResponseWithHeaders()
  73. {
  74. Router::scope('/', function (RouteBuilder $routes) {
  75. $routes->redirect('/testpath', '/pages');
  76. });
  77. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/testpath']);
  78. $handler = new TestRequestHandler(function ($request) {
  79. return new Response('php://memory', 200, ['X-testing' => 'Yes']);
  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. }
  86. /**
  87. * Test that Router sets parameters
  88. *
  89. * @return void
  90. */
  91. public function testRouterSetParams()
  92. {
  93. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  94. $handler = new TestRequestHandler(function ($req) {
  95. $expected = [
  96. 'controller' => 'Articles',
  97. 'action' => 'index',
  98. 'plugin' => null,
  99. 'pass' => [],
  100. '_ext' => null,
  101. '_matchedRoute' => '/articles',
  102. ];
  103. $this->assertEquals($expected, $req->getAttribute('params'));
  104. return new Response();
  105. });
  106. $middleware = new RoutingMiddleware($this->app());
  107. $middleware->process($request, $handler);
  108. }
  109. /**
  110. * Test routing middleware does wipe off existing params keys.
  111. *
  112. * @return void
  113. */
  114. public function testPreservingExistingParams()
  115. {
  116. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  117. $request = $request->withAttribute('params', ['_csrfToken' => 'i-am-groot']);
  118. $handler = new TestRequestHandler(function ($req) {
  119. $expected = [
  120. 'controller' => 'Articles',
  121. 'action' => 'index',
  122. 'plugin' => null,
  123. 'pass' => [],
  124. '_matchedRoute' => '/articles',
  125. '_csrfToken' => 'i-am-groot',
  126. ];
  127. $this->assertEquals($expected, $req->getAttribute('params'));
  128. return new Response();
  129. });
  130. $middleware = new RoutingMiddleware($this->app());
  131. $middleware->process($request, $handler);
  132. }
  133. /**
  134. * Test middleware invoking hook method
  135. *
  136. * @return void
  137. */
  138. public function testRoutesHookInvokedOnApp()
  139. {
  140. Router::reload();
  141. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/app/articles']);
  142. $handler = new TestRequestHandler(function ($req) {
  143. $expected = [
  144. 'controller' => 'Articles',
  145. 'action' => 'index',
  146. 'plugin' => null,
  147. 'pass' => [],
  148. '_ext' => null,
  149. '_matchedRoute' => '/app/articles',
  150. ];
  151. $this->assertEquals($expected, $req->getAttribute('params'));
  152. $this->assertNotEmpty(Router::routes());
  153. $this->assertSame('/app/articles', Router::routes()[5]->template);
  154. return new Response();
  155. });
  156. $app = new Application(CONFIG);
  157. $middleware = new RoutingMiddleware($app);
  158. $middleware->process($request, $handler);
  159. }
  160. /**
  161. * Test that pluginRoutes hook is called
  162. *
  163. * @return void
  164. */
  165. public function testRoutesHookCallsPluginHook()
  166. {
  167. Router::reload();
  168. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/app/articles']);
  169. $app = $this->getMockBuilder(Application::class)
  170. ->onlyMethods(['pluginRoutes'])
  171. ->setConstructorArgs([CONFIG])
  172. ->getMock();
  173. $app->expects($this->once())
  174. ->method('pluginRoutes')
  175. ->with($this->isInstanceOf(RouteBuilder::class));
  176. $middleware = new RoutingMiddleware($app);
  177. $middleware->process($request, new TestRequestHandler());
  178. }
  179. /**
  180. * Test that routing is not applied if a controller exists already
  181. *
  182. * @return void
  183. */
  184. public function testRouterNoopOnController()
  185. {
  186. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  187. $request = $request->withAttribute('params', ['controller' => 'Articles']);
  188. $handler = new TestRequestHandler(function ($req) {
  189. $this->assertEquals(['controller' => 'Articles'], $req->getAttribute('params'));
  190. return new Response();
  191. });
  192. $middleware = new RoutingMiddleware($this->app());
  193. $middleware->process($request, $handler);
  194. }
  195. /**
  196. * Test missing routes not being caught.
  197. */
  198. public function testMissingRouteNotCaught()
  199. {
  200. $this->expectException(\Cake\Routing\Exception\MissingRouteException::class);
  201. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/missing']);
  202. $middleware = new RoutingMiddleware($this->app());
  203. $middleware->process($request, new TestRequestHandler());
  204. }
  205. /**
  206. * Test route with _method being parsed correctly.
  207. *
  208. * @return void
  209. */
  210. public function testFakedRequestMethodParsed()
  211. {
  212. Router::connect('/articles-patch', [
  213. 'controller' => 'Articles',
  214. 'action' => 'index',
  215. '_method' => 'PATCH',
  216. ]);
  217. $request = ServerRequestFactory::fromGlobals(
  218. [
  219. 'REQUEST_METHOD' => 'POST',
  220. 'REQUEST_URI' => '/articles-patch',
  221. ],
  222. null,
  223. ['_method' => 'PATCH']
  224. );
  225. $handler = new TestRequestHandler(function ($req) {
  226. $expected = [
  227. 'controller' => 'Articles',
  228. 'action' => 'index',
  229. '_method' => 'PATCH',
  230. 'plugin' => null,
  231. 'pass' => [],
  232. '_matchedRoute' => '/articles-patch',
  233. '_ext' => null,
  234. ];
  235. $this->assertEquals($expected, $req->getAttribute('params'));
  236. $this->assertSame('PATCH', $req->getMethod());
  237. return new Response();
  238. });
  239. $middleware = new RoutingMiddleware($this->app());
  240. $middleware->process($request, $handler);
  241. }
  242. /**
  243. * Test invoking simple scoped middleware
  244. *
  245. * @return void
  246. */
  247. public function testInvokeScopedMiddleware()
  248. {
  249. Router::scope('/api', function (RouteBuilder $routes) {
  250. $routes->registerMiddleware('first', function ($request, $handler) {
  251. $this->log[] = 'first';
  252. return $handler->handle($request);
  253. });
  254. $routes->registerMiddleware('second', function ($request, $handler) {
  255. $this->log[] = 'second';
  256. return $handler->handle($request);
  257. });
  258. $routes->registerMiddleware('dumb', DumbMiddleware::class);
  259. // Connect middleware in reverse to test ordering.
  260. $routes->applyMiddleware('second', 'first', 'dumb');
  261. $routes->connect('/ping', ['controller' => 'Pings']);
  262. });
  263. $request = ServerRequestFactory::fromGlobals([
  264. 'REQUEST_METHOD' => 'GET',
  265. 'REQUEST_URI' => '/api/ping',
  266. ]);
  267. $app = $this->app(function ($req) {
  268. $this->log[] = 'last';
  269. return new Response();
  270. });
  271. $middleware = new RoutingMiddleware($app);
  272. $result = $middleware->process($request, $app);
  273. $this->assertSame(['second', 'first', 'last'], $this->log);
  274. }
  275. /**
  276. * Test control flow in scoped middleware.
  277. *
  278. * Scoped middleware should be able to generate a response
  279. * and abort lower layers.
  280. *
  281. * @return void
  282. */
  283. public function testInvokeScopedMiddlewareReturnResponse()
  284. {
  285. Router::scope('/', function (RouteBuilder $routes) {
  286. $routes->registerMiddleware('first', function ($request, $handler) {
  287. $this->log[] = 'first';
  288. return $handler->handle($request);
  289. });
  290. $routes->registerMiddleware('second', function ($request, $handler) {
  291. $this->log[] = 'second';
  292. return new Response();
  293. });
  294. $routes->applyMiddleware('first');
  295. $routes->connect('/', ['controller' => 'Home']);
  296. $routes->scope('/api', function (RouteBuilder $routes) {
  297. $routes->applyMiddleware('second');
  298. $routes->connect('/articles', ['controller' => 'Articles']);
  299. });
  300. });
  301. $request = ServerRequestFactory::fromGlobals([
  302. 'REQUEST_METHOD' => 'GET',
  303. 'REQUEST_URI' => '/api/articles',
  304. ]);
  305. $handler = new TestRequestHandler(function ($req) {
  306. $this->fail('Should not be invoked as first should be ignored.');
  307. });
  308. $middleware = new RoutingMiddleware($this->app());
  309. $result = $middleware->process($request, $handler);
  310. $this->assertSame(['first', 'second'], $this->log);
  311. }
  312. /**
  313. * Test control flow in scoped middleware.
  314. *
  315. * @return void
  316. */
  317. public function testInvokeScopedMiddlewareReturnResponseMainScope()
  318. {
  319. Router::scope('/', function (RouteBuilder $routes) {
  320. $routes->registerMiddleware('first', function ($request, $handler) {
  321. $this->log[] = 'first';
  322. return new Response();
  323. });
  324. $routes->registerMiddleware('second', function ($request, $handler) {
  325. $this->log[] = 'second';
  326. return $handler->handle($request);
  327. });
  328. $routes->applyMiddleware('first');
  329. $routes->connect('/', ['controller' => 'Home']);
  330. $routes->scope('/api', function (RouteBuilder $routes) {
  331. $routes->applyMiddleware('second');
  332. $routes->connect('/articles', ['controller' => 'Articles']);
  333. });
  334. });
  335. $request = ServerRequestFactory::fromGlobals([
  336. 'REQUEST_METHOD' => 'GET',
  337. 'REQUEST_URI' => '/',
  338. ]);
  339. $handler = new TestRequestHandler(function ($req) {
  340. $this->fail('Should not be invoked as first should be ignored.');
  341. });
  342. $middleware = new RoutingMiddleware($this->app());
  343. $result = $middleware->process($request, $handler);
  344. $this->assertSame(['first'], $this->log);
  345. }
  346. /**
  347. * Test invoking middleware & scope separation
  348. *
  349. * Re-opening a scope should not inherit middleware declared
  350. * in the first context.
  351. *
  352. * @dataProvider scopedMiddlewareUrlProvider
  353. * @return void
  354. */
  355. public function testInvokeScopedMiddlewareIsolatedScopes(string $url, array $expected)
  356. {
  357. Router::scope('/', function (RouteBuilder $routes) {
  358. $routes->registerMiddleware('first', function ($request, $handler) {
  359. $this->log[] = 'first';
  360. return $handler->handle($request);
  361. });
  362. $routes->registerMiddleware('second', function ($request, $handler) {
  363. $this->log[] = 'second';
  364. return $handler->handle($request);
  365. });
  366. $routes->scope('/api', function (RouteBuilder $routes) {
  367. $routes->applyMiddleware('first');
  368. $routes->connect('/ping', ['controller' => 'Pings']);
  369. });
  370. $routes->scope('/api', function (RouteBuilder $routes) {
  371. $routes->applyMiddleware('second');
  372. $routes->connect('/version', ['controller' => 'Version']);
  373. });
  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. $result = $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 function scopedMiddlewareUrlProvider()
  393. {
  394. return [
  395. ['/api/ping', ['first', 'last']],
  396. ['/api/version', ['second', 'last']],
  397. ];
  398. }
  399. /**
  400. * Test we store route collection in cache.
  401. *
  402. * @return void
  403. */
  404. public function testCacheRoutes()
  405. {
  406. $cacheConfigName = '_cake_router_';
  407. Cache::setConfig($cacheConfigName, [
  408. 'engine' => 'File',
  409. 'path' => CACHE,
  410. ]);
  411. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  412. $handler = new TestRequestHandler(function ($req) use ($cacheConfigName) {
  413. $routeCollection = Cache::read('routeCollection', $cacheConfigName);
  414. $this->assertInstanceOf(RouteCollection::class, $routeCollection);
  415. return new Response();
  416. });
  417. $app = new Application(CONFIG);
  418. $middleware = new RoutingMiddleware($app, $cacheConfigName);
  419. $middleware->process($request, $handler);
  420. Cache::clear($cacheConfigName);
  421. Cache::drop($cacheConfigName);
  422. }
  423. /**
  424. * Test we don't cache routes if cache is disabled.
  425. *
  426. * @return void
  427. */
  428. public function testCacheNotUsedIfCacheDisabled()
  429. {
  430. $cacheConfigName = '_cake_router_';
  431. Cache::drop($cacheConfigName);
  432. Cache::disable();
  433. Cache::setConfig($cacheConfigName, [
  434. 'engine' => 'File',
  435. 'path' => CACHE,
  436. ]);
  437. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  438. $handler = new TestRequestHandler(function ($req) use ($cacheConfigName) {
  439. $routeCollection = Cache::read('routeCollection', $cacheConfigName);
  440. $this->assertNull($routeCollection);
  441. return new Response();
  442. });
  443. $app = new Application(CONFIG);
  444. $middleware = new RoutingMiddleware($app, $cacheConfigName);
  445. $middleware->process($request, $handler);
  446. Cache::clear($cacheConfigName);
  447. Cache::drop($cacheConfigName);
  448. Cache::enable();
  449. }
  450. /**
  451. * Test cache name is used
  452. *
  453. * @return void
  454. */
  455. public function testCacheConfigNotFound()
  456. {
  457. $this->expectException(CacheInvalidArgumentException::class);
  458. $this->expectExceptionMessage('The "notfound" cache configuration does not exist.');
  459. Cache::setConfig('_cake_router_', [
  460. 'engine' => 'File',
  461. 'path' => CACHE,
  462. ]);
  463. $request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/articles']);
  464. $app = new Application(CONFIG);
  465. $middleware = new RoutingMiddleware($app, 'notfound');
  466. $middleware->process($request, new TestRequestHandler());
  467. Cache::drop('_cake_router_');
  468. }
  469. /**
  470. * Create a stub application for testing.
  471. *
  472. * @param callable|null $handleCallback Callback for "handle" method.
  473. * @return \Cake\Core\HttpApplicationInterface
  474. */
  475. protected function app($handleCallback = null)
  476. {
  477. $mock = $this->createMock(Application::class);
  478. $mock->method('routes')
  479. ->will($this->returnCallback(function (RouteBuilder $routes) {
  480. return $routes;
  481. }));
  482. if ($handleCallback) {
  483. $mock->method('handle')
  484. ->will($this->returnCallback($handleCallback));
  485. }
  486. return $mock;
  487. }
  488. }