Browse Source

Merge pull request #8667 from cakephp/action-dispatcher

Action dispatcher
Mark Story 10 years ago
parent
commit
90ce428c06

+ 164 - 0
src/Http/ActionDispatcher.php

@@ -0,0 +1,164 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Http;
+
+use Cake\Controller\Controller;
+use Cake\Event\EventDispatcherTrait;
+use Cake\Event\EventListenerInterface;
+use Cake\Http\ControllerFactory;
+use Cake\Network\Request;
+use Cake\Network\Response;
+use Cake\Routing\DispatcherFactory;
+use Cake\Routing\Exception\MissingControllerException;
+use Cake\Routing\Router;
+use LogicException;
+
+/**
+ * This class provides compatibility with dispatcher filters
+ * and interacting with the controller layers.
+ *
+ * Long term this should just be the controller dispatcher, but
+ * for now it will do a bit more than that.
+ */
+class ActionDispatcher
+{
+    use EventDispatcherTrait;
+
+    /**
+     * Attached routing filters
+     *
+     * @var array
+     */
+    protected $filters = [];
+
+    /**
+     * Controller factory instance.
+     *
+     * @var \Cake\Http\ControllerFactory
+     */
+    protected $factory;
+
+    /**
+     * Constructor
+     *
+     * @param \Cake\Http\ControllerFactory $factory A controller factory instance.
+     * @param \Cake\Event\EventManager $eventManager An event manager if you want to inject one.
+     */
+    public function __construct($factory = null, $eventManager = null)
+    {
+        if ($eventManager) {
+            $this->eventManager($eventManager);
+        }
+        $this->factory = $factory ?: new ControllerFactory();
+    }
+
+    /**
+     * Dispatches a Request & Response
+     *
+     * @param \Cake\Network\Request $request The request to dispatch.
+     * @param \Cake\Network\Response $response The response to dispatch.
+     * @return \Cake\Network\Response a modified/replaced response.
+     */
+    public function dispatch(Request $request, Response $response)
+    {
+        if (Router::getRequest(true) !== $request) {
+            Router::pushRequest($request);
+        }
+        $beforeEvent = $this->dispatchEvent('Dispatcher.beforeDispatch', compact('request', 'response'));
+
+        $request = $beforeEvent->data['request'];
+        if ($beforeEvent->result instanceof Response) {
+            return $beforeEvent->result;
+        }
+
+        // Use the controller built by an beforeDispatch
+        // event handler if there is one.
+        if (isset($beforeEvent->data['controller'])) {
+            $controller = $beforeEvent->data['controller'];
+        } else {
+            $controller = $this->factory->create($request, $response);
+        }
+
+        $response = $this->_invoke($controller);
+        if (isset($request->params['return'])) {
+            return $response;
+        }
+
+        $afterEvent = $this->dispatchEvent('Dispatcher.afterDispatch', compact('request', 'response'));
+        return $afterEvent->data['response'];
+    }
+
+    /**
+     * Invoke a controller's action and wrapping methods.
+     *
+     * @param \Cake\Controller\Controller $controller The controller to invoke.
+     * @return \Cake\Network\Response The response
+     * @throws \LogicException If the controller action returns a non-response value.
+     */
+    protected function _invoke(Controller $controller)
+    {
+        $this->dispatchEvent('Dispatcher.invokeController', ['controller' => $controller]);
+
+        $result = $controller->startupProcess();
+        if ($result instanceof Response) {
+            return $result;
+        }
+
+        $response = $controller->invokeAction();
+        if ($response !== null && !($response instanceof Response)) {
+            throw new LogicException('Controller actions can only return Cake\Network\Response or null.');
+        }
+
+        if (!$response && $controller->autoRender) {
+            $response = $controller->render();
+        } elseif (!$response) {
+            $response = $controller->response;
+        }
+
+        $result = $controller->shutdownProcess();
+        if ($result instanceof Response) {
+            return $result;
+        }
+
+        return $response;
+    }
+
+    /**
+     * Add a filter to this dispatcher.
+     *
+     * The added filter will be attached to the event manager used
+     * by this dispatcher.
+     *
+     * @param \Cake\Event\EventListenerInterface $filter The filter to connect. Can be
+     *   any EventListenerInterface. Typically an instance of \Cake\Routing\DispatcherFilter.
+     * @return void
+     * @deprecated This is only available for backwards compatibility with DispatchFilters
+     */
+    public function addFilter(EventListenerInterface $filter)
+    {
+        $this->filters[] = $filter;
+        $this->eventManager()->on($filter);
+    }
+
+    /**
+     * Get the connected filters.
+     *
+     * @return array
+     */
+    public function getFilters()
+    {
+        return $this->filters;
+    }
+}

+ 6 - 60
src/Routing/Dispatcher.php

@@ -17,6 +17,7 @@ namespace Cake\Routing;
 use Cake\Controller\Controller;
 use Cake\Event\EventDispatcherTrait;
 use Cake\Event\EventListenerInterface;
+use Cake\Http\ActionDispatcher;
 use Cake\Network\Request;
 use Cake\Network\Response;
 use LogicException;
@@ -58,69 +59,15 @@ class Dispatcher
      */
     public function dispatch(Request $request, Response $response)
     {
-        $beforeEvent = $this->dispatchEvent('Dispatcher.beforeDispatch', compact('request', 'response'));
-
-        $request = $beforeEvent->data['request'];
-        if ($beforeEvent->result instanceof Response) {
-            if (isset($request->params['return'])) {
-                return $beforeEvent->result->body();
-            }
-            $beforeEvent->result->send();
-            return null;
-        }
-
-        if (!isset($beforeEvent->data['controller'])) {
-            throw new LogicException(
-                'The Dispatcher.beforeDispatch event did not create a controller. ' .
-                'Ensure you have added the ControllerFactoryFilter.'
-            );
+        $actionDispatcher = new ActionDispatcher(null, $this->eventManager());
+        foreach ($this->_filters as $filter) {
+            $actionDispatcher->addFilter($filter);
         }
-        $controller = $beforeEvent->data['controller'];
-
-        $response = $this->_invoke($controller);
+        $response = $actionDispatcher->dispatch($request, $response);
         if (isset($request->params['return'])) {
             return $response->body();
         }
-
-        $afterEvent = $this->dispatchEvent('Dispatcher.afterDispatch', compact('request', 'response'));
-        $afterEvent->data['response']->send();
-    }
-
-    /**
-     * Initializes the components and models a controller will be using.
-     * Triggers the controller action and invokes the rendering if Controller::$autoRender
-     * is true. If a response object is returned by controller action that is returned
-     * else controller's $response property is returned.
-     *
-     * @param \Cake\Controller\Controller $controller Controller to invoke
-     * @return \Cake\Network\Response The resulting response object
-     * @throws \LogicException If data returned by controller action is not an
-     *   instance of Response
-     */
-    protected function _invoke(Controller $controller)
-    {
-        $result = $controller->startupProcess();
-        if ($result instanceof Response) {
-            return $result;
-        }
-
-        $response = $controller->invokeAction();
-        if ($response !== null && !($response instanceof Response)) {
-            throw new LogicException('Controller action can only return an instance of Response');
-        }
-
-        if (!$response && $controller->autoRender) {
-            $response = $controller->render();
-        } elseif (!$response) {
-            $response = $controller->response;
-        }
-
-        $result = $controller->shutdownProcess();
-        if ($result instanceof Response) {
-            return $result;
-        }
-
-        return $response;
+        return $response->send();
     }
 
     /**
@@ -136,7 +83,6 @@ class Dispatcher
     public function addFilter(EventListenerInterface $filter)
     {
         $this->_filters[] = $filter;
-        $this->eventManager()->on($filter);
     }
 
     /**

+ 3 - 1
src/Routing/Filter/RoutingFilter.php

@@ -48,7 +48,9 @@ class RoutingFilter extends DispatcherFilter
     public function beforeDispatch(Event $event)
     {
         $request = $event->data['request'];
-        Router::setRequestInfo($request);
+        if (Router::getRequest(true) !== $request) {
+            Router::setRequestInfo($request);
+        }
 
         try {
             if (empty($request->params['controller'])) {

+ 5 - 8
src/TestSuite/IntegrationTestCase.php

@@ -125,7 +125,6 @@ abstract class IntegrationTestCase extends TestCase
 
     /**
      *
-     *
      * @var null|string
      */
     protected $_cookieEncriptionKey = null;
@@ -357,7 +356,7 @@ abstract class IntegrationTestCase extends TestCase
         $response = new Response();
         $dispatcher = DispatcherFactory::create();
         $dispatcher->eventManager()->on(
-            'Dispatcher.beforeDispatch',
+            'Dispatcher.invokeController',
             ['priority' => 999],
             [$this, 'controllerSpy']
         );
@@ -379,15 +378,13 @@ abstract class IntegrationTestCase extends TestCase
      * Adds additional event spies to the controller/view event manager.
      *
      * @param \Cake\Event\Event $event A dispatcher event.
+     * @param \Cake\Controller\Controller $controller Controller instance.
      * @return void
      */
-    public function controllerSpy($event)
+    public function controllerSpy($event, $controller)
     {
-        if (empty($event->data['controller'])) {
-            return;
-        }
-        $this->_controller = $event->data['controller'];
-        $events = $this->_controller->eventManager();
+        $this->_controller = $controller;
+        $events = $controller->eventManager();
         $events->on('View.beforeRender', function ($event, $viewFile) {
             if (!$this->_viewName) {
                 $this->_viewName = $viewFile;

+ 386 - 0
tests/TestCase/Http/ActionDispatcherTest.php

@@ -0,0 +1,386 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.3.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\Http;
+
+use Cake\Core\Configure;
+use Cake\Http\ActionDispatcher;
+use Cake\Network\Request;
+use Cake\Network\Response;
+use Cake\Network\Session;
+use Cake\Routing\Filter\ControllerFactoryFilter;
+use Cake\Routing\Router;
+use Cake\TestSuite\TestCase;
+
+/**
+ * Test case for the ActionDispatcher.
+ */
+class ActionDispatcherTest extends TestCase
+{
+    /**
+     * Setup
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        parent::setUp();
+        Router::reload();
+        Configure::write('App.namespace', 'TestApp');
+        $this->dispatcher = new ActionDispatcher();
+        $this->dispatcher->addFilter(new ControllerFactoryFilter());
+    }
+
+    /**
+     * Ensure the constructor args end up on the right protected properties.
+     *
+     * @return void
+     */
+    public function testConstructorArgs()
+    {
+        $factory = $this->getMock('Cake\Http\ControllerFactory');
+        $events = $this->getMock('Cake\Event\EventManager');
+        $dispatcher = new ActionDispatcher($factory, $events);
+
+        $this->assertAttributeSame($events, '_eventManager', $dispatcher);
+        $this->assertAttributeSame($factory, 'factory', $dispatcher);
+    }
+
+    /**
+     * Test adding routing filters
+     *
+     * @return void
+     */
+    public function testAddFilter()
+    {
+        $this->assertCount(1, $this->dispatcher->getFilters());
+        $events = $this->dispatcher->eventManager();
+        $this->assertCount(1, $events->listeners('Dispatcher.beforeDispatch'));
+        $this->assertCount(1, $events->listeners('Dispatcher.afterDispatch'));
+
+        $filter = $this->getMock(
+            'Cake\Routing\DispatcherFilter',
+            ['beforeDispatch', 'afterDispatch']
+        );
+        $this->dispatcher->addFilter($filter);
+
+        $this->assertCount(2, $this->dispatcher->getFilters());
+        $this->assertCount(2, $events->listeners('Dispatcher.beforeDispatch'));
+        $this->assertCount(2, $events->listeners('Dispatcher.afterDispatch'));
+    }
+
+    /**
+     * Ensure that aborting in the beforeDispatch doesn't invoke the controller
+     *
+     * @return void
+     */
+    public function testBeforeDispatchEventAbort()
+    {
+        $response = new Response();
+        $dispatcher = new ActionDispatcher();
+        $filter = $this->getMock(
+            'Cake\Routing\DispatcherFilter',
+            ['beforeDispatch', 'afterDispatch']
+        );
+        $filter->expects($this->once())
+            ->method('beforeDispatch')
+            ->will($this->returnValue($response));
+
+        $req = new Request();
+        $res = new Response();
+        $dispatcher->addFilter($filter);
+        $result = $dispatcher->dispatch($req, $res);
+        $this->assertSame($response, $result, 'Should be response from filter.');
+    }
+
+    /**
+     * Ensure afterDispatch can replace the response
+     *
+     * @return void
+     */
+    public function testDispatchAfterDispatchEventModifyResponse()
+    {
+        $filter = $this->getMock(
+            'Cake\Routing\DispatcherFilter',
+            ['beforeDispatch', 'afterDispatch']
+        );
+        $filter->expects($this->once())
+            ->method('afterDispatch')
+            ->will($this->returnCallback(function ($event) {
+                $event->data['response']->body('Filter body');
+            }));
+
+        $req = new Request([
+            'url' => '/cakes',
+            'params' => [
+                'plugin' => null,
+                'controller' => 'Cakes',
+                'action' => 'index',
+                'pass' => [],
+            ],
+            'session' => new Session
+        ]);
+        $res = new Response();
+        $this->dispatcher->addFilter($filter);
+        $result = $this->dispatcher->dispatch($req, $res);
+        $this->assertSame('Filter body', $result->body(), 'Should be response from filter.');
+    }
+
+    /**
+     * Test that a controller action returning a response
+     * results in no afterDispatch event.
+     *
+     * @return void
+     */
+    public function testDispatchActionReturnResponseNoAfterDispatch()
+    {
+        $filter = $this->getMock(
+            'Cake\Routing\DispatcherFilter',
+            ['beforeDispatch', 'afterDispatch']
+        );
+        $filter->expects($this->never())
+            ->method('afterDispatch');
+
+        $req = new Request([
+            'url' => '/cakes',
+            'params' => [
+                'plugin' => null,
+                'controller' => 'Cakes',
+                'action' => 'index',
+                'pass' => [],
+                'return' => true,
+            ],
+        ]);
+        $res = new Response();
+        $this->dispatcher->addFilter($filter);
+        $result = $this->dispatcher->dispatch($req, $res);
+        $this->assertSame('Hello Jane', $result->body(), 'Response from controller.');
+    }
+
+    /**
+     * Test that dispatching sets the Router request state.
+     *
+     * @return void
+     */
+    public function testDispatchSetsRequestContext()
+    {
+        $this->assertNull(Router::getRequest());
+        $req = new Request([
+            'url' => '/cakes',
+            'params' => [
+                'plugin' => null,
+                'controller' => 'Cakes',
+                'action' => 'index',
+                'pass' => [],
+                'return' => true,
+            ],
+        ]);
+        $res = new Response();
+        $this->dispatcher->dispatch($req, $res);
+        $this->assertSame($req, Router::getRequest(true));
+    }
+
+    /**
+     * test invalid response from dispatch process.
+     *
+     * @expectedException \LogicException
+     * @expectedExceptionMessage Controller actions can only return Cake\Network\Response or null
+     * @return void
+     */
+    public function testDispatchInvalidResponse()
+    {
+        $req = new Request([
+            'url' => '/cakes',
+            'params' => [
+                'plugin' => null,
+                'controller' => 'Cakes',
+                'action' => 'invalid',
+                'pass' => [],
+            ],
+        ]);
+        $res = new Response();
+        $result = $this->dispatcher->dispatch($req, $res);
+    }
+
+    /**
+     * Test dispatch with autorender
+     *
+     * @return void
+     */
+    public function testDispatchAutoRender()
+    {
+        $request = new Request([
+            'url' => 'posts',
+            'params' => [
+                'controller' => 'Posts',
+                'action' => 'index',
+                'pass' => [],
+            ]
+        ]);
+        $response = new Response();
+        $result = $this->dispatcher->dispatch($request, $response);
+        $this->assertInstanceOf('Cake\Network\Response', $result);
+        $this->assertContains('posts index', $result->body());
+    }
+
+    /**
+     * Test dispatch with autorender=false
+     *
+     * @return void
+     */
+    public function testDispatchAutoRenderFalse()
+    {
+        $request = new Request([
+            'url' => 'posts',
+            'params' => [
+                'controller' => 'Cakes',
+                'action' => 'noRender',
+                'pass' => [],
+            ]
+        ]);
+        $response = new Response();
+        $result = $this->dispatcher->dispatch($request, $response);
+        $this->assertInstanceOf('Cake\Network\Response', $result);
+        $this->assertContains('autoRender false body', $result->body());
+    }
+
+    /**
+     * testMissingController method
+     *
+     * @expectedException \Cake\Routing\Exception\MissingControllerException
+     * @expectedExceptionMessage Controller class SomeController could not be found.
+     * @return void
+     */
+    public function testMissingController()
+    {
+        $request = new Request([
+            'url' => 'some_controller/home',
+            'params' => [
+                'controller' => 'SomeController',
+                'action' => 'home',
+            ]
+        ]);
+        $response = $this->getMock('Cake\Network\Response');
+        $this->dispatcher->dispatch($request, $response);
+    }
+
+    /**
+     * testMissingControllerInterface method
+     *
+     * @expectedException \Cake\Routing\Exception\MissingControllerException
+     * @expectedExceptionMessage Controller class Interface could not be found.
+     * @return void
+     */
+    public function testMissingControllerInterface()
+    {
+        $request = new Request([
+            'url' => 'interface/index',
+            'params' => [
+                'controller' => 'Interface',
+                'action' => 'index',
+            ]
+        ]);
+        $response = $this->getMock('Cake\Network\Response');
+        $this->dispatcher->dispatch($request, $response);
+    }
+
+    /**
+     * testMissingControllerInterface method
+     *
+     * @expectedException \Cake\Routing\Exception\MissingControllerException
+     * @expectedExceptionMessage Controller class Abstract could not be found.
+     * @return void
+     */
+    public function testMissingControllerAbstract()
+    {
+        $request = new Request([
+            'url' => 'abstract/index',
+            'params' => [
+                'controller' => 'Abstract',
+                'action' => 'index',
+            ]
+        ]);
+        $response = $this->getMock('Cake\Network\Response');
+        $this->dispatcher->dispatch($request, $response);
+    }
+
+    /**
+     * Test that lowercase controller names result in missing controller errors.
+     *
+     * In case-insensitive file systems, lowercase controller names will kind of work.
+     * This causes annoying deployment issues for lots of folks.
+     *
+     * @expectedException \Cake\Routing\Exception\MissingControllerException
+     * @expectedExceptionMessage Controller class somepages could not be found.
+     * @return void
+     */
+    public function testMissingControllerLowercase()
+    {
+        $request = new Request([
+            'url' => 'pages/home',
+            'params' => [
+                'plugin' => null,
+                'controller' => 'somepages',
+                'action' => 'display',
+                'pass' => ['home'],
+            ]
+        ]);
+        $response = $this->getMock('Cake\Network\Response');
+        $this->dispatcher->dispatch($request, $response);
+    }
+
+    /**
+     * Ensure that a controller's startup event can stop the request.
+     *
+     * @return void
+     */
+    public function testStartupProcessAbort()
+    {
+        $request = new Request([
+            'url' => 'cakes/index',
+            'params' => [
+                'plugin' => null,
+                'controller' => 'Cakes',
+                'action' => 'index',
+                'stop' => 'startup',
+                'pass' => [],
+            ]
+        ]);
+        $response = new Response();
+        $result = $this->dispatcher->dispatch($request, $response);
+        $this->assertSame('startup stop', $result->body());
+    }
+
+    /**
+     * Ensure that a controllers startup process can emit a response
+     *
+     * @return void
+     */
+    public function testShutdownProcessResponse()
+    {
+        $request = new Request([
+            'url' => 'cakes/index',
+            'params' => [
+                'plugin' => null,
+                'controller' => 'Cakes',
+                'action' => 'index',
+                'stop' => 'shutdown',
+                'pass' => [],
+            ]
+        ]);
+        $response = new Response();
+        $result = $this->dispatcher->dispatch($request, $response);
+        $this->assertSame('shutdown stop', $result->body());
+    }
+}

+ 5 - 235
tests/TestCase/Routing/DispatcherTest.php

@@ -24,177 +24,6 @@ use Cake\Routing\Filter\ControllerFactoryFilter;
 use Cake\TestSuite\TestCase;
 
 /**
- * A testing stub that doesn't send headers.
- */
-class DispatcherMockResponse extends Response
-{
-
-    protected function _sendHeader($name, $value = null)
-    {
-        return $name . ' ' . $value;
-    }
-}
-
-/**
- * TestDispatcher class
- */
-class TestDispatcher extends Dispatcher
-{
-
-    /**
-     * Controller instance, made publicly available for testing
-     *
-     * @var Controller
-     */
-    public $controller;
-
-    /**
-     * invoke method
-     *
-     * @param \Cake\Controller\Controller $controller
-     * @return \Cake\Network\Response $response
-     */
-    protected function _invoke(Controller $controller)
-    {
-        $this->controller = $controller;
-        return parent::_invoke($controller);
-    }
-}
-
-/**
- * MyPluginAppController class
- *
- */
-class MyPluginAppController extends Controller
-{
-}
-
-/**
- * MyPluginController class
- *
- */
-class MyPluginController extends MyPluginAppController
-{
-
-    /**
-     * name property
-     *
-     * @var string
-     */
-    public $name = 'MyPlugin';
-
-    /**
-     * index method
-     *
-     * @return void
-     */
-    public function index()
-    {
-        return true;
-    }
-
-    /**
-     * add method
-     *
-     * @return void
-     */
-    public function add()
-    {
-        return true;
-    }
-
-    /**
-     * admin_add method
-     *
-     * @param mixed $id
-     * @return void
-     */
-    public function admin_add($id = null)
-    {
-        return $id;
-    }
-}
-
-/**
- * OtherPagesController class
- *
- */
-class OtherPagesController extends MyPluginAppController
-{
-
-    /**
-     * name property
-     *
-     * @var string
-     */
-    public $name = 'OtherPages';
-
-    /**
-     * display method
-     *
-     * @param string $page
-     * @return void
-     */
-    public function display($page = null)
-    {
-        return $page;
-    }
-
-    /**
-     * index method
-     *
-     * @return void
-     */
-    public function index()
-    {
-        return true;
-    }
-}
-
-/**
- * ArticlesTestAppController class
- *
- */
-class ArticlesTestAppController extends Controller
-{
-}
-
-/**
- * ArticlesTestController class
- *
- */
-class ArticlesTestController extends ArticlesTestAppController
-{
-
-    /**
-     * name property
-     *
-     * @var string
-     */
-    public $name = 'ArticlesTest';
-
-    /**
-     * admin_index method
-     *
-     * @return void
-     */
-    public function admin_index()
-    {
-        return true;
-    }
-
-    /**
-     * fake index method.
-     *
-     * @return void
-     */
-    public function index()
-    {
-        return true;
-    }
-}
-
-/**
  * DispatcherTest class
  *
  */
@@ -217,7 +46,7 @@ class DispatcherTest extends TestCase
         Configure::write('App.webroot', 'webroot');
         Configure::write('App.namespace', 'TestApp');
 
-        $this->dispatcher = new TestDispatcher();
+        $this->dispatcher = new Dispatcher();
         $this->dispatcher->addFilter(new ControllerFactoryFilter());
     }
 
@@ -330,14 +159,14 @@ class DispatcherTest extends TestCase
                 'controller' => 'Pages',
                 'action' => 'display',
                 'pass' => ['extract'],
-                'return' => 1
             ]
         ]);
         $response = $this->getMock('Cake\Network\Response');
+        $response->expects($this->once())
+            ->method('send');
 
-        $this->dispatcher->dispatch($url, $response);
-        $expected = 'Pages';
-        $this->assertEquals($expected, $this->dispatcher->controller->name);
+        $result = $this->dispatcher->dispatch($url, $response);
+        $this->assertNull($result);
     }
 
     /**
@@ -365,65 +194,6 @@ class DispatcherTest extends TestCase
     }
 
     /**
-     * testPrefixDispatch method
-     *
-     * @return void
-     */
-    public function testPrefixDispatch()
-    {
-        $request = new Request([
-            'url' => 'admin/posts/index',
-            'params' => [
-                'prefix' => 'Admin',
-                'controller' => 'Posts',
-                'action' => 'index',
-                'pass' => [],
-                'return' => 1
-            ]
-        ]);
-        $response = $this->getMock('Cake\Network\Response');
-
-        $this->dispatcher->dispatch($request, $response);
-
-        $this->assertInstanceOf(
-            'TestApp\Controller\Admin\PostsController',
-            $this->dispatcher->controller
-        );
-        $expected = '/admin/posts/index';
-        $this->assertSame($expected, $request->here);
-    }
-
-    /**
-     * test prefix dispatching in a plugin.
-     *
-     * @return void
-     */
-    public function testPrefixDispatchPlugin()
-    {
-        Plugin::load('TestPlugin');
-
-        $request = new Request([
-            'url' => 'admin/test_plugin/comments/index',
-            'params' => [
-                'plugin' => 'TestPlugin',
-                'prefix' => 'Admin',
-                'controller' => 'Comments',
-                'action' => 'index',
-                'pass' => [],
-                'return' => 1
-            ]
-        ]);
-        $response = $this->getMock('Cake\Network\Response');
-
-        $this->dispatcher->dispatch($request, $response);
-
-        $this->assertInstanceOf(
-            'TestPlugin\Controller\Admin\CommentsController',
-            $this->dispatcher->controller
-        );
-    }
-
-    /**
      * test forbidden controller names.
      *
      * @expectedException \Cake\Routing\Exception\MissingControllerException

+ 7 - 0
tests/TestCase/Routing/RequestActionTraitTest.php

@@ -73,29 +73,36 @@ class RequestActionTraitTest extends TestCase
 
         $result = $this->object->requestAction('');
         $this->assertFalse($result);
+        $this->assertNull(Router::getRequest(), 'requests were not popped off the stack, this will break url generation');
 
         $result = $this->object->requestAction('/request_action/test_request_action');
         $expected = 'This is a test';
         $this->assertEquals($expected, $result);
+        $this->assertNull(Router::getRequest(), 'requests were not popped off the stack, this will break url generation');
 
         $result = $this->object->requestAction(Configure::read('App.fullBaseUrl') . '/request_action/test_request_action');
         $expected = 'This is a test';
         $this->assertEquals($expected, $result);
+        $this->assertNull(Router::getRequest(), 'requests were not popped off the stack, this will break url generation');
 
         $result = $this->object->requestAction('/request_action/another_ra_test/2/5');
         $expected = 7;
         $this->assertEquals($expected, $result);
+        $this->assertNull(Router::getRequest(), 'requests were not popped off the stack, this will break url generation');
 
         $result = $this->object->requestAction('/tests_apps/index', ['return']);
         $expected = 'This is the TestsAppsController index view ';
         $this->assertEquals($expected, $result);
+        $this->assertNull(Router::getRequest(), 'requests were not popped off the stack, this will break url generation');
 
         $result = $this->object->requestAction('/tests_apps/some_method');
         $expected = 5;
         $this->assertEquals($expected, $result);
+        $this->assertNull(Router::getRequest(), 'requests were not popped off the stack, this will break url generation');
 
         $result = $this->object->requestAction('/request_action/paginate_request_action');
         $this->assertNull($result);
+        $this->assertNull(Router::getRequest(), 'requests were not popped off the stack, this will break url generation');
 
         $result = $this->object->requestAction('/request_action/normal_request_action');
         $expected = 'Hello World';

+ 2 - 0
tests/TestCase/TestSuite/IntegrationTestCaseTest.php

@@ -181,7 +181,9 @@ class IntegrationTestCaseTest extends IntegrationTestCase
     {
         $this->post('/posts/index');
         $this->assertInstanceOf('Cake\Controller\Controller', $this->_controller);
+        $this->assertNotEmpty($this->_viewName, 'View name not set');
         $this->assertContains('Template' . DS . 'Posts' . DS . 'index.ctp', $this->_viewName);
+        $this->assertNotEmpty($this->_layoutName, 'Layout name not set');
         $this->assertContains('Template' . DS . 'Layout' . DS . 'default.ctp', $this->_layoutName);
 
         $this->assertTemplate('index');

+ 35 - 0
tests/test_app/TestApp/Controller/CakesController.php

@@ -28,6 +28,17 @@ class CakesController extends Controller
     }
 
     /**
+     * No autoRender
+     *
+     * @return void
+     */
+    public function noRender()
+    {
+        $this->autoRender = false;
+        $this->response->body('autoRender false body');
+    }
+
+    /**
      * invalid method
      *
      * @return \Cake\Network\Response
@@ -36,4 +47,28 @@ class CakesController extends Controller
     {
         return 'Some string';
     }
+
+    /**
+     * startup process.
+     */
+    public function startupProcess()
+    {
+        parent::startupProcess();
+        if ($this->request->param('stop') === 'startup') {
+            $this->response->body('startup stop');
+            return $this->response;
+        }
+    }
+
+    /**
+     * shutdown process.
+     */
+    public function shutdownProcess()
+    {
+        parent::shutdownProcess();
+        if ($this->request->param('stop') === 'shutdown') {
+            $this->response->body('shutdown stop');
+            return $this->response;
+        }
+    }
 }