RoutingMiddlewareTest.php 15 KB

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