RoutingMiddlewareTest.php 17 KB

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