Browse Source

Merge branch '2.1-http' into 2.1

Jose Lorenzo Rodriguez 14 years ago
parent
commit
a1daaf5960

+ 19 - 1
lib/Cake/Controller/Component/RequestHandlerComponent.php

@@ -95,7 +95,8 @@ class RequestHandlerComponent extends Component {
  * @param array $settings Array of settings.
  */
 	public function __construct(ComponentCollection $collection, $settings = array()) {
-		parent::__construct($collection, $settings);
+		$default = array('checkHttpCache' => true);
+		parent::__construct($collection, $settings + $default);
 		$this->addInputType('xml', array(array($this, 'convertXml')));
 
 		$Controller = $collection->getController();
@@ -241,6 +242,22 @@ class RequestHandlerComponent extends Component {
 	}
 
 /**
+ * Checks if the response can be considered different according to the request
+ * headers, and the caching response headers. If it was not modified, then the
+ * render process is skipped. And the client will get a blank response with a
+ * "304 Not Modified" header.
+ *
+ * @params Controller $controller
+ * @return boolean false if the render process should be aborted
+ **/
+	public function beforeRender($controller) {
+		$shouldCheck = $this->settings['checkHttpCache'];
+		if ($shouldCheck && $this->response->checkNotModified($this->request)) {
+			return false;
+		}
+	}
+
+/**
  * Returns true if the current HTTP request is Ajax, false otherwise
  *
  * @return boolean True if call is Ajax
@@ -704,4 +721,5 @@ class RequestHandlerComponent extends Component {
 		}
 		$this->_inputTypeMap[$type] = $handler;
 	}
+
 }

+ 6 - 1
lib/Cake/Controller/Controller.php

@@ -896,7 +896,12 @@ class Controller extends Object implements CakeEventListener {
  * @link http://book.cakephp.org/2.0/en/controllers.html#Controller::render
  */
 	public function render($view = null, $layout = null) {
-		$this->getEventManager()->dispatch(new CakeEvent('Controller.beforeRender', $this));
+		$event = new CakeEvent('Controller.beforeRender', $this);
+		$this->getEventManager()->dispatch($event);
+		if ($event->isStopped()) {
+			$this->autoRender = false;
+			return $this->response;
+		}
 
 		$viewClass = $this->viewClass;
 		if ($this->viewClass != 'View') {

+ 373 - 11
lib/Cake/Network/CakeResponse.php

@@ -312,6 +312,14 @@ class CakeResponse {
 	protected $_charset = 'UTF-8';
 
 /**
+ * Holds all the cache directives that will be converted
+ * into headers when sending the request
+ *
+ * @var string
+ */
+	protected $_cacheDirectives = array();
+
+/**
  * Class constructor
  *
  * @param array $options list of parameters to setup the response. Possible values are:
@@ -348,8 +356,9 @@ class CakeResponse {
 
 		$codeMessage = $this->_statusCodes[$this->_status];
 		$this->_sendHeader("{$this->_protocol} {$this->_status} {$codeMessage}");
-		$this->_sendHeader('Content-Type', "{$this->_contentType}; charset={$this->_charset}");
+		$this->_setContent();
 		$this->_setContentLength();
+		$this->_setContentType();
 		foreach ($this->_headers as $header => $value) {
 			$this->_sendHeader($header, $value);
 		}
@@ -357,19 +366,51 @@ class CakeResponse {
 	}
 
 /**
+ * Formats the Content-Type header based on the configured contentType and charset
+ * the charset will only be set in the header if the response is of type text/*
+ *
+ * @return void
+ */
+	protected function _setContentType() {
+		if (in_array($this->_status, array(304, 204))) {
+			return;
+		}
+		if (strpos($this->_contentType, 'text/') === 0) {
+			$this->header('Content-Type', "{$this->_contentType}; charset={$this->_charset}");
+		} else {
+			$this->header('Content-Type', "{$this->_contentType}");
+		}
+	}
+
+/**
+ * Sets the response body to an empty text if the status code is 204 or 304
+ *
+ * @return void
+ */
+	protected function _setContent() {
+		if (in_array($this->_status, array(304, 204))) {
+			$this->body('');
+		}
+	}
+
+/**
  * Calculates the correct Content-Length and sets it as a header in the response
  * Will not set the value if already set or if the output is compressed.
  *
  * @return void
  */
 	protected function _setContentLength() {
-		$shouldSetLength = empty($this->_headers['Content-Length']) && !in_array($this->_status, range(301, 307));
+		$shouldSetLength = !isset($this->_headers['Content-Length']) && !in_array($this->_status, range(301, 307));
+		if (isset($this->_headers['Content-Length']) && $this->_headers['Content-Length'] === false) {
+			unset($this->_headers['Content-Length']);
+			return;
+		}
 		if ($shouldSetLength && !$this->outputCompressed()) {
 			$offset = ob_get_level() ? ob_get_length() : 0;
 			if (ini_get('mbstring.func_overload') & 2 && function_exists('mb_strlen')) {
-				$this->_headers['Content-Length'] = $offset + mb_strlen($this->_body, '8bit');
+				$this->length($offset + mb_strlen($this->_body, '8bit'));
 			} else {
-				$this->_headers['Content-Length'] = $offset + strlen($this->_body);
+				$this->length($this->_headers['Content-Length'] = $offset + strlen($this->_body));
 			}
 		}
 	}
@@ -625,8 +666,7 @@ class CakeResponse {
 		$this->header(array(
 			'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
 			'Last-Modified' => gmdate("D, d M Y H:i:s") . " GMT",
-			'Cache-Control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0',
-			'Pragma' => 'no-cache'
+			'Cache-Control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'
 		));
 	}
 
@@ -642,12 +682,272 @@ class CakeResponse {
 			$time = strtotime($time);
 		}
 		$this->header(array(
-			'Date' => gmdate("D, j M Y G:i:s ", time()) . 'GMT',
-			'Last-Modified' => gmdate("D, j M Y G:i:s ", $since) . 'GMT',
-			'Expires' => gmdate("D, j M Y H:i:s", $time) . " GMT",
-			'Cache-Control' => 'public, max-age=' . ($time - time()),
-			'Pragma' => 'cache'
+			'Date' => gmdate("D, j M Y G:i:s ", time()) . 'GMT'
 		));
+		$this->modified($since);
+		$this->expires($time);
+		$this->sharable(true);
+		$this->maxAge($time - time());
+	}
+
+/**
+ * Sets whether a response is eligible to be cached by intermediate proxies
+ * This method controls the `public` or `private` directive in the Cache-Control
+ * header
+ *
+ * @param boolean $public  if set to true, the Cache-Control header will be set as public
+ * if set to false, the response will be set to private
+ * if no value is provided, it will return whether the response is sharable or not
+ * @param int $time time in seconds after which the response should no longer be considered fresh
+ * @return boolean
+ */
+	public function sharable($public = null, $time = null) {
+		if ($public === null) {
+			$public = array_key_exists('public', $this->_cacheDirectives);
+			$private = array_key_exists('private', $this->_cacheDirectives);
+			$noCache = array_key_exists('no-cache', $this->_cacheDirectives);
+			if (!$public && !$private && !$noCache) {
+				return null;
+			}
+			$sharable = $public || ! ($private || $noCache);
+			return $sharable;
+		}
+		if ($public) {
+			$this->_cacheDirectives['public'] = true;
+			unset($this->_cacheDirectives['private']);
+			$this->sharedMaxAge($time);
+		} else {
+			$this->_cacheDirectives['private'] = true;
+			unset($this->_cacheDirectives['public']);
+			$this->maxAge($time);
+		}
+		if ($time == null) {
+			$this->_setCacheControl();
+		}
+		return (bool) $public;
+	}
+
+/**
+ * Sets the Cache-Control s-maxage directive.
+ * The max-age is the number of seconds after which the response should no longer be considered
+ * a good candidate to be fetched from a shared cache (like in a proxy server).
+ * If called with no parameters, this function will return the current max-age value if any
+ *
+ * @param int $seconds if null, the method will return the current s-maxage value
+ * @return int
+ */
+	public function sharedMaxAge($seconds = null) {
+		if ($seconds !== null) {
+			$this->_cacheDirectives['s-maxage'] = $seconds;
+			$this->_setCacheControl();
+		}
+		if (isset($this->_cacheDirectives['s-maxage'])) {
+			return $this->_cacheDirectives['s-maxage'];
+		}
+		return null;
+	}
+
+/**
+ * Sets the Cache-Control max-age directive.
+ * The max-age is the number of seconds after which the response should no longer be considered
+ * a good candidate to be fetched from the local (client) cache.
+ * If called with no parameters, this function will return the current max-age value if any
+ *
+ * @param int $seconds if null, the method will return the current max-age value
+ * @return int
+ */
+	public function maxAge($seconds = null) {
+		if ($seconds !== null) {
+			$this->_cacheDirectives['max-age'] = $seconds;
+			$this->_setCacheControl();
+		}
+		if (isset($this->_cacheDirectives['max-age'])) {
+			return $this->_cacheDirectives['max-age'];
+		}
+		return null;
+	}
+
+/**
+ * Sets the Cache-Control must-revalidate directive.
+ * must-revalidate indicates that the response should not be served 
+ * stale by a cache under any cirumstance without first revalidating 
+ * with the origin.
+ * If called with no parameters, this function will return wheter must-revalidate is present.
+ *
+ * @param int $seconds if null, the method will return the current 
+ * must-revalidate value
+ * @return boolean
+ */
+	public function mustRevalidate($enable = null) {
+		if ($enable !== null) {
+			if ($enable) {
+				$this->_cacheDirectives['must-revalidate'] = true;
+			} else {
+				unset($this->_cacheDirectives['must-revalidate']);
+			}
+			$this->_setCacheControl();
+		}
+		return array_key_exists('must-revalidate', $this->_cacheDirectives);
+	}
+
+/**
+ * Helper method to generate a valid Cache-Control header from the options set
+ * in other methods
+ *
+ * @return void
+ */
+	protected function _setCacheControl() {
+		$control = '';
+		foreach ($this->_cacheDirectives as $key => $val) {
+			$control .= $val === true ? $key : sprintf('%s=%s', $key, $val);
+			$control .= ', ';
+		}
+		$control = rtrim($control, ', ');
+		$this->header('Cache-Control', $control);
+	}
+
+/**
+ * Sets the Expires header for the response by taking an expiration time
+ * If called with no parameters it will return the current Expires value
+ *
+ * ## Examples:
+ *
+ * `$response->expires('now')` Will Expire the response cache now
+ * `$response->expires(new DateTime('+1 day'))` Will set the expiration in next 24 hours
+ * `$response->expires()` Will return the current expiration header value
+ *
+ * @param string|DateTime $time
+ * @return string
+ */
+	public function expires($time = null) {
+		if ($time !== null) {
+			$date = $this->_getUTCDate($time);
+			$this->_headers['Expires'] = $date->format('D, j M Y H:i:s') . ' GMT';
+		}
+		if (isset($this->_headers['Expires'])) {
+			return $this->_headers['Expires'];
+		}
+		return null;
+	}
+
+/**
+ * Sets the Last-Modified header for the response by taking an modification time
+ * If called with no parameters it will return the current Last-Modified value
+ *
+ * ## Examples:
+ *
+ * `$response->modified('now')` Will set the Last-Modified to the current time
+ * `$response->modified(new DateTime('+1 day'))` Will set the modification date in the past 24 hours
+ * `$response->modified()` Will return the current Last-Modified header value
+ *
+ * @param string|DateTime $time
+ * @return string
+ */
+	public function modified($time = null) {
+		if ($time !== null) {
+			$date = $this->_getUTCDate($time);
+			$this->_headers['Last-Modified'] = $date->format('D, j M Y H:i:s') . ' GMT';
+		}
+		if (isset($this->_headers['Last-Modified'])) {
+			return $this->_headers['Last-Modified'];
+		}
+		return null;
+	}
+
+/**
+ * Sets the response as Not Modified by removing any body contents 
+ * setting the status code to "304 Not Modified" and removing all 
+ * conflicting headers
+ *
+ * @return void
+ **/
+	public function notModified() {
+		$this->statusCode(304);
+		$this->body('');
+		$remove = array(
+			'Allow',
+			'Content-Encoding',
+			'Content-Language',
+			'Content-Length',
+			'Content-MD5',
+			'Content-Type',
+			'Last-Modified'
+		);
+		foreach ($remove as $header) {
+			unset($this->_headers[$header]);
+		}
+	}
+
+/**
+ * Sets the Vary header for the response, if an array is passed,
+ * values will be imploded into a comma separated string. If no 
+ * parameters are passed, then an array with the current Vary header 
+ * value is returned
+ *
+ * @param string|array $cacheVariances a single Vary string or a array 
+ * containig the list for variances.
+ * @return array
+ **/
+	public function vary($cacheVariances = null) {
+		if ($cacheVariances !== null) {
+			$cacheVariances = (array) $cacheVariances;
+			$this->_headers['Vary'] = implode(', ', $cacheVariances);
+		}
+		if (isset($this->_headers['Vary'])) {
+			return explode(', ', $this->_headers['Vary']);
+		}
+		return null;
+	}
+
+/**
+ * Sets the response Etag, Etags are a strong indicative that a response
+ * can be cached by a HTTP client. A bad way of generaing Etags is 
+ * creating a hash of the response output, instead generate a unique 
+ * hash of the unique components that identifies a request, such as a 
+ * modification time, a resource Id, and anything else you consider it 
+ * makes it unique.
+ *
+ * Second parameter is used to instuct clients that the content has 
+ * changed, but sematicallly, it can be used as the same thing. Think 
+ * for instance of a page with a hit counter, two different page views 
+ * are equivalent, but they differ by a few bytes. This leaves off to 
+ * the Client the decision of using or not the cached page.
+ *
+ * If no parameters are passed, current Etag header is returned.
+ *
+ * @param string $hash the unique has that identifies this resposnse
+ * @param boolean $weak whether the response is semantically the same as 
+ * other with th same hash or not
+ * @return string
+ **/
+	public function etag($tag = null, $weak = false) {
+		if ($tag !== null) {
+			$this->_headers['Etag'] = sprintf('%s"%s"', ($weak) ? 'W/' : null, $tag);
+		}
+		if (isset($this->_headers['Etag'])) {
+			return $this->_headers['Etag'];
+		}
+		return null;
+	}
+
+
+/**
+ * Returns a DateTime object initialized at the $time param and using UTC
+ * as timezone
+ *
+ * @param string|int|DateTime $time 
+ * @return DateTime
+ */
+	protected function _getUTCDate($time = null) {
+		if ($time instanceof DateTime) {
+			$result = clone $time;
+		} else if (is_integer($time)) {
+			$result = new DateTime(date('Y-m-d H:i:s', $time));
+		} else {
+			$result = new DateTime($time);
+		}
+		$result->setTimeZone(new DateTimeZone('UTC'));
+		return $result;
 	}
 
 /**
@@ -684,6 +984,68 @@ class CakeResponse {
 	}
 
 /**
+ * Sets the protocol to be used when sending the response. Defaults to HTTP/1.1
+ * If called with no arguments, it will return the current configured protocol
+ *
+ * @return string protocol to be used for sending response
+ */
+	public function protocol($protocol = null) {
+		if ($protocol !== null) {
+			$this->_protocol = $protocol;
+		}
+		return $this->_protocol;
+	}
+
+/**
+ * Sets the Content-Length header for the response
+ * If called with no arguments returns the last Content-Length set
+ *
+ * @return int
+ */
+	public function length($bytes = null) {
+		if ($bytes !== null ) {
+			$this->_headers['Content-Length'] = $bytes;
+		}
+		if (isset($this->_headers['Content-Length'])) {
+			return $this->_headers['Content-Length'];
+		}
+		return null;
+	}
+
+/**
+ * Checks whether a response has not been modified according to the 'If-None-Match' 
+ * (Etags) and 'If-Modified-Since' (last modification date) request 
+ * headers headers. If the response is detected to be not modified, it 
+ * is marked as so accordingly so the client can be informed of that.
+ *
+ * In order to mark a response as not modified, you need to set at least 
+ * the Last-Modified response header or a response etag to be compared 
+ * with the request itself
+ *
+ * @return boolean whether the response was marked as not modified or 
+ * not
+ **/
+	public function checkNotModified(CakeRequest $request) {
+		$etags = preg_split('/\s*,\s*/', $request->header('If-None-Match'), null, PREG_SPLIT_NO_EMPTY);
+		$modifiedSince = $request->header('If-Modified-Since');
+		if ($responseTag = $this->etag()) {
+			$etagMatches = in_array('*', $etags) || in_array($responseTag, $etags);
+		}
+		if ($modifiedSince) {
+			$timeMatches = strtotime($this->modified()) == strtotime($modifiedSince);
+		}
+		$checks = compact('etagMatches', 'timeMatches');
+		if (empty($checks)) {
+			return false;
+		}
+		$notModified = !in_array(false, $checks, true);
+		if ($notModified) {
+			$this->notModified();
+		}
+		return $notModified;
+	}
+
+/**
  * String conversion.  Fetches the response body as a string.
  * Does *not* send headers.
  *

+ 56 - 0
lib/Cake/Test/Case/Controller/Component/RequestHandlerComponentTest.php

@@ -823,4 +823,60 @@ class RequestHandlerComponentTest extends CakeTestCase {
 	public function testAddInputTypeException() {
 		$this->RequestHandler->addInputType('csv', array('I am not callable'));
 	}
+
+/**
+ * Test checkNotModified method
+ *
+ * @return void
+ **/
+	public function testCheckNotModifiedByEtagStar() {
+		$_SERVER['HTTP_IF_NONE_MATCH'] = '*';
+		$RequestHandler = $this->getMock('RequestHandlerComponent', array('_stop'), array(&$this->Controller->Components));
+		$RequestHandler->response =  $this->getMock('CakeResponse', array('notModified'));
+		$RequestHandler->response->etag('something');
+		$RequestHandler->response->expects($this->once())->method('notModified');
+		$this->assertFalse($RequestHandler->beforeRender($this->Controller));
+	}
+
+/**
+ * Test checkNotModified method
+ *
+ * @return void
+ **/
+	public function testCheckNotModifiedByEtagExact() {
+		$_SERVER['HTTP_IF_NONE_MATCH'] = 'W/"something", "other"';
+		$RequestHandler = $this->getMock('RequestHandlerComponent', array('_stop'), array(&$this->Controller->Components));
+		$RequestHandler->response =  $this->getMock('CakeResponse', array('notModified'));
+		$RequestHandler->response->etag('something', true);
+		$RequestHandler->response->expects($this->once())->method('notModified');
+		$this->assertFalse($RequestHandler->beforeRender($this->Controller));
+	}
+
+/**
+ * Test checkNotModified method
+ *
+ * @return void
+ **/
+	public function testCheckNotModifiedByEtagAndTime() {
+		$_SERVER['HTTP_IF_NONE_MATCH'] = 'W/"something", "other"';
+		$_SERVER['HTTP_IF_MODIFIED_SINCE'] = '2012-01-01 00:00:00';
+		$RequestHandler = $this->getMock('RequestHandlerComponent', array('_stop'), array(&$this->Controller->Components));
+		$RequestHandler->response =  $this->getMock('CakeResponse', array('notModified'));
+		$RequestHandler->response->etag('something', true);
+		$RequestHandler->response->modified('2012-01-01 00:00:00');
+		$RequestHandler->response->expects($this->once())->method('notModified');
+		$this->assertFalse($RequestHandler->beforeRender($this->Controller));
+	}
+
+/**
+ * Test checkNotModified method
+ *
+ * @return void
+ **/
+	public function testCheckNotModifiedNoInfo() {
+		$RequestHandler = $this->getMock('RequestHandlerComponent', array('_stop'), array(&$this->Controller->Components));
+		$RequestHandler->response =  $this->getMock('CakeResponse', array('notModified'));
+		$RequestHandler->response->expects($this->never())->method('notModified');
+		$this->assertNull($RequestHandler->beforeRender($this->Controller));
+	}
 }

+ 23 - 0
lib/Cake/Test/Case/Controller/ControllerTest.php

@@ -359,6 +359,14 @@ class TestComponent extends Object {
 	}
 }
 
+class Test2Component extends TestComponent {
+
+
+	public function beforeRender($controller) {
+		return false;
+	}
+}
+
 /**
  * AnotherTestController class
  *
@@ -686,6 +694,21 @@ class ControllerTest extends CakeTestCase {
 	}
 
 /**
+ * test that a component beforeRender can change the controller view class.
+ *
+ * @return void
+ */
+	public function testComponentCancelRender() {
+		$Controller = new Controller($this->getMock('CakeRequest'), new CakeResponse());
+		$Controller->uses = array();
+		$Controller->components = array('Test2');
+		$Controller->constructClasses();
+		$result = $Controller->render('index');
+		$this->assertInstanceOf('CakeResponse', $result);
+	}
+
+
+/**
  * testToBeInheritedGuardmethods method
  *
  * @return void

+ 448 - 27
lib/Cake/Test/Case/Network/CakeResponseTest.php

@@ -17,6 +17,7 @@
  * @license       MIT License (http://www.opensource.org/licenses/mit-license.php)
  */
 App::uses('CakeResponse', 'Network');
+App::uses('CakeRequest', 'Network');
 
 class CakeResponseTest extends CakeTestCase {
 
@@ -182,11 +183,13 @@ class CakeResponseTest extends CakeTestCase {
 		$response->expects($this->at(0))
 			->method('_sendHeader')->with('HTTP/1.1 200 OK');
 		$response->expects($this->at(1))
-			->method('_sendHeader')->with('Content-Type', 'text/html; charset=UTF-8');
-		$response->expects($this->at(2))
 			->method('_sendHeader')->with('Content-Language', 'es');
-		$response->expects($this->at(3))
+		$response->expects($this->at(2))
 			->method('_sendHeader')->with('WWW-Authenticate', 'Negotiate');
+		$response->expects($this->at(3))
+			->method('_sendHeader')->with('Content-Length', 17);
+		$response->expects($this->at(4))
+			->method('_sendHeader')->with('Content-Type', 'text/html; charset=UTF-8');
 		$response->send();
 	}
 
@@ -202,7 +205,9 @@ class CakeResponseTest extends CakeTestCase {
 		$response->expects($this->at(0))
 			->method('_sendHeader')->with('HTTP/1.1 200 OK');
 		$response->expects($this->at(1))
-			->method('_sendHeader')->with('Content-Type', 'audio/mpeg; charset=UTF-8');
+			->method('_sendHeader')->with('Content-Length', 17);
+		$response->expects($this->at(2))
+			->method('_sendHeader')->with('Content-Type', 'audio/mpeg');
 		$response->send();
 	}
 
@@ -218,7 +223,9 @@ class CakeResponseTest extends CakeTestCase {
 		$response->expects($this->at(0))
 			->method('_sendHeader')->with('HTTP/1.1 200 OK');
 		$response->expects($this->at(1))
-			->method('_sendHeader')->with('Content-Type', 'audio/mpeg; charset=UTF-8');
+			->method('_sendHeader')->with('Content-Length', 17);
+		$response->expects($this->at(2))
+			->method('_sendHeader')->with('Content-Type', 'audio/mpeg');
 		$response->send();
 	}
 
@@ -232,9 +239,9 @@ class CakeResponseTest extends CakeTestCase {
 		$response->expects($this->at(0))
 			->method('_sendHeader')->with('HTTP/1.1 302 Found');
 		$response->expects($this->at(1))
-			->method('_sendHeader')->with('Content-Type', 'text/html; charset=UTF-8');
-		$response->expects($this->at(2))
 			->method('_sendHeader')->with('Location', 'http://www.example.com');
+		$response->expects($this->at(2))
+			->method('_sendHeader')->with('Content-Type', 'text/html; charset=UTF-8');		
 		$response->send();
 	}
 
@@ -247,8 +254,7 @@ class CakeResponseTest extends CakeTestCase {
 		$expected = array(
 			'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
 			'Last-Modified' => gmdate("D, d M Y H:i:s") . " GMT",
-			'Cache-Control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0',
-			'Pragma' => 'no-cache'
+			'Cache-Control' => 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'
 		);
 		$response->disableCache();
 		$this->assertEquals($response->header(), $expected);
@@ -261,13 +267,13 @@ class CakeResponseTest extends CakeTestCase {
 	public function testCache() {
 		$response = new CakeResponse();
 		$since = time();
-		$time = '+1 day';
+		$time = new DateTime('+1 day', new DateTimeZone('UTC'));
+		$response->expires('+1 day');
 		$expected = array(
 			'Date' => gmdate("D, j M Y G:i:s ", $since) . 'GMT',
-			'Last-Modified' => gmdate("D, j M Y G:i:s ", $since) . 'GMT',
-			'Expires' => gmdate("D, j M Y H:i:s", strtotime($time)) . " GMT",
-			'Cache-Control' => 'public, max-age=' . (strtotime($time) - time()),
-			'Pragma' => 'cache'
+			'Last-Modified' => gmdate("D, j M Y H:i:s ", $since) . 'GMT',
+			'Expires' => $time->format('D, j M Y H:i:s') . ' GMT',
+			'Cache-Control' => 'public, max-age=' . ($time->format('U') - time())
 		);
 		$response->cache($since);
 		$this->assertEquals($response->header(), $expected);
@@ -277,10 +283,9 @@ class CakeResponseTest extends CakeTestCase {
 		$time = '+5 day';
 		$expected = array(
 			'Date' => gmdate("D, j M Y G:i:s ", $since) . 'GMT',
-			'Last-Modified' => gmdate("D, j M Y G:i:s ", $since) . 'GMT',
+			'Last-Modified' => gmdate("D, j M Y H:i:s ", $since) . 'GMT',
 			'Expires' => gmdate("D, j M Y H:i:s", strtotime($time)) . " GMT",
-			'Cache-Control' => 'public, max-age=' . (strtotime($time) - time()),
-			'Pragma' => 'cache'
+			'Cache-Control' => 'public, max-age=' . (strtotime($time) - time())
 		);
 		$response->cache($since, $time);
 		$this->assertEquals($response->header(), $expected);
@@ -290,10 +295,9 @@ class CakeResponseTest extends CakeTestCase {
 		$time = time();
 		$expected = array(
 			'Date' => gmdate("D, j M Y G:i:s ", $since) . 'GMT',
-			'Last-Modified' => gmdate("D, j M Y G:i:s ", $since) . 'GMT',
+			'Last-Modified' => gmdate("D, j M Y H:i:s ", $since) . 'GMT',
 			'Expires' => gmdate("D, j M Y H:i:s", $time) . " GMT",
-			'Cache-Control' => 'public, max-age=0',
-			'Pragma' => 'cache'
+			'Cache-Control' => 'public, max-age=0'
 		);
 		$response->cache($since, $time);
 		$this->assertEquals($response->header(), $expected);
@@ -439,9 +443,9 @@ class CakeResponseTest extends CakeTestCase {
 		$response->expects($this->once())->method('_sendContent')->with('the response body');
 		$response->expects($this->at(0))
 			->method('_sendHeader')->with('HTTP/1.1 200 OK');
-		$response->expects($this->at(1))
-			->method('_sendHeader')->with('Content-Type', 'text/html; charset=UTF-8');
 		$response->expects($this->at(2))
+			->method('_sendHeader')->with('Content-Type', 'text/html; charset=UTF-8');
+		$response->expects($this->at(1))
 			->method('_sendHeader')->with('Content-Length', strlen('the response body'));
 		$response->send();
 
@@ -451,9 +455,9 @@ class CakeResponseTest extends CakeTestCase {
 		$response->expects($this->once())->method('_sendContent')->with($body);
 		$response->expects($this->at(0))
 			->method('_sendHeader')->with('HTTP/1.1 200 OK');
-		$response->expects($this->at(1))
-			->method('_sendHeader')->with('Content-Type', 'text/html; charset=UTF-8');
 		$response->expects($this->at(2))
+			->method('_sendHeader')->with('Content-Type', 'text/html; charset=UTF-8');
+		$response->expects($this->at(1))
 			->method('_sendHeader')->with('Content-Length', 116);
 		$response->send();
 
@@ -471,7 +475,7 @@ class CakeResponseTest extends CakeTestCase {
 		$response->header('Content-Length', 1);
 		$response->expects($this->never())->method('outputCompressed');
 		$response->expects($this->once())->method('_sendContent')->with($body);
-			$response->expects($this->at(2))
+			$response->expects($this->at(1))
 				->method('_sendHeader')->with('Content-Length', 1);
 		$response->send();
 
@@ -494,10 +498,427 @@ class CakeResponseTest extends CakeTestCase {
 		$response->expects($this->at(0))
 			->method('_sendHeader')->with('HTTP/1.1 200 OK');
 		$response->expects($this->at(1))
-			->method('_sendHeader')->with('Content-Type', 'text/html; charset=UTF-8');
-		$response->expects($this->at(2))
 			->method('_sendHeader')->with('Content-Length', strlen($goofyOutput) + 116);
+		$response->expects($this->at(2))
+			->method('_sendHeader')->with('Content-Type', 'text/html; charset=UTF-8');
 		$response->send();
 		ob_end_clean();
 	}
+
+/**
+ * Tests getting/setting the protocol
+ *
+ * @return void
+ */
+	public function testProtocol() {
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->protocol('HTTP/1.0');
+		$this->assertEquals('HTTP/1.0', $response->protocol());
+		$response->expects($this->at(0))
+			->method('_sendHeader')->with('HTTP/1.0 200 OK');
+		$response->send();
+	}
+
+/**
+ * Tests getting/setting the Content-Length
+ *
+ * @return void
+ */
+	public function testLength() {
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->length(100);
+		$this->assertEquals(100, $response->length());
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Content-Length', 100);
+		$response->send();
+
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->length(false);
+		$this->assertFalse($response->length());
+		$response->expects($this->exactly(2))
+			->method('_sendHeader');
+		$response->send();
+	}
+
+/**
+ * Tests that the response body is unset if the status code is 304 or 204
+ *
+ * @return void
+ */
+	public function testUnmodifiedContent() {
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->body('This is a body');
+		$response->statusCode(204);
+		$response->expects($this->once())
+			->method('_sendContent')->with('');
+		$response->send();
+		$this->assertFalse(array_key_exists('Content-Type', $response->header()));
+
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->body('This is a body');
+		$response->statusCode(304);
+		$response->expects($this->once())
+			->method('_sendContent')->with('');
+		$response->send();
+
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->body('This is a body');
+		$response->statusCode(200);
+		$response->expects($this->once())
+			->method('_sendContent')->with('This is a body');
+		$response->send();
+	}
+
+/**
+ * Tests setting the expiration date
+ *
+ * @return void
+ */
+	public function testExpires() {
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$now = new DateTime('now', new DateTimeZone('America/Los_Angeles'));
+		$response->expires($now);
+		$now->setTimeZone(new DateTimeZone('UTC'));
+		$this->assertEquals($now->format('D, j M Y H:i:s') . ' GMT', $response->expires());
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Expires', $now->format('D, j M Y H:i:s') . ' GMT');
+		$response->send();
+
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$now = time();
+		$response->expires($now);
+		$this->assertEquals(gmdate('D, j M Y H:i:s', $now) . ' GMT', $response->expires());
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Expires', gmdate('D, j M Y H:i:s', $now) . ' GMT');
+		$response->send();
+
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$time = new DateTime('+1 day', new DateTimeZone('UTC'));
+		$response->expires('+1 day');
+		$this->assertEquals($time->format('D, j M Y H:i:s') . ' GMT', $response->expires());
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Expires', $time->format('D, j M Y H:i:s') . ' GMT');
+		$response->send();
+	}
+
+/**
+ * Tests setting the modification date
+ *
+ * @return void
+ */
+	public function testModified() {
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$now = new DateTime('now', new DateTimeZone('America/Los_Angeles'));
+		$response->modified($now);
+		$now->setTimeZone(new DateTimeZone('UTC'));
+		$this->assertEquals($now->format('D, j M Y H:i:s') . ' GMT', $response->modified());
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Last-Modified', $now->format('D, j M Y H:i:s') . ' GMT');
+		$response->send();
+
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$now = time();
+		$response->modified($now);
+		$this->assertEquals(gmdate('D, j M Y H:i:s', $now) . ' GMT', $response->modified());
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Last-Modified', gmdate('D, j M Y H:i:s', $now) . ' GMT');
+		$response->send();
+
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$time = new DateTime('+1 day', new DateTimeZone('UTC'));
+		$response->modified('+1 day');
+		$this->assertEquals($time->format('D, j M Y H:i:s') . ' GMT', $response->modified());
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Last-Modified', $time->format('D, j M Y H:i:s') . ' GMT');
+		$response->send();
+	}
+
+/**
+ * Tests setting of public/private Cache-Control directives
+ *
+ * @return void
+ */
+	public function testSharable() {
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$this->assertNull($response->sharable());
+		$response->sharable(true);
+		$headers = $response->header();
+		$this->assertEquals('public', $headers['Cache-Control']);
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Cache-Control', 'public');
+		$response->send();
+
+
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->sharable(false);
+		$headers = $response->header();
+		$this->assertEquals('private', $headers['Cache-Control']);
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Cache-Control', 'private');
+		$response->send();
+
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->sharable(true);
+		$headers = $response->header();
+		$this->assertEquals('public', $headers['Cache-Control']);
+		$response->sharable(false);
+		$headers = $response->header();
+		$this->assertEquals('private', $headers['Cache-Control']);
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Cache-Control', 'private');
+		$response->send();
+		$this->assertFalse($response->sharable());
+		$response->sharable(true);
+		$this->assertTrue($response->sharable());
+
+		$response = new CakeResponse;
+		$response->sharable(true, 3600);
+		$headers = $response->header();
+		$this->assertEquals('public, s-maxage=3600', $headers['Cache-Control']);
+
+		$response = new CakeResponse;
+		$response->sharable(false, 3600);
+		$headers = $response->header();
+		$this->assertEquals('private, max-age=3600', $headers['Cache-Control']);
+		$response->send();
+	}
+
+/**
+ * Tests setting of max-age Cache-Control directive
+ *
+ * @return void
+ */
+	public function testMaxAge() {
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$this->assertNull($response->maxAge());
+		$response->maxAge(3600);
+		$this->assertEquals(3600, $response->maxAge());
+		$headers = $response->header();
+		$this->assertEquals('max-age=3600', $headers['Cache-Control']);
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Cache-Control', 'max-age=3600');
+		$response->send();
+
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->maxAge(3600);
+		$response->sharable(false);
+		$headers = $response->header();
+		$this->assertEquals('max-age=3600, private', $headers['Cache-Control']);
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Cache-Control', 'max-age=3600, private');
+		$response->send();
+	}
+
+/**
+ * Tests setting of s-maxage Cache-Control directive
+ *
+ * @return void
+ */
+	public function testSharedMaxAge() {
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$this->assertNull($response->maxAge());
+		$response->sharedMaxAge(3600);
+		$this->assertEquals(3600, $response->sharedMaxAge());
+		$headers = $response->header();
+		$this->assertEquals('s-maxage=3600', $headers['Cache-Control']);
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Cache-Control', 's-maxage=3600');
+		$response->send();
+
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->sharedMaxAge(3600);
+		$response->sharable(true);
+		$headers = $response->header();
+		$this->assertEquals('s-maxage=3600, public', $headers['Cache-Control']);
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Cache-Control', 's-maxage=3600, public');
+		$response->send();
+	}
+
+/**
+ * Tests setting of must-revalidate Cache-Control directive
+ *
+ * @return void
+ */
+	public function testMustRevalidate() {
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$this->assertFalse($response->mustRevalidate());
+		$response->mustRevalidate(true);
+		$this->assertTrue($response->mustRevalidate());
+		$headers = $response->header();
+		$this->assertEquals('must-revalidate', $headers['Cache-Control']);
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Cache-Control', 'must-revalidate');
+		$response->send();
+		$response->mustRevalidate(false);
+		$this->assertFalse($response->mustRevalidate());
+
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->sharedMaxAge(3600);
+		$response->mustRevalidate(true);
+		$headers = $response->header();
+		$this->assertEquals('s-maxage=3600, must-revalidate', $headers['Cache-Control']);
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Cache-Control', 's-maxage=3600, must-revalidate');
+		$response->send();
+
+	}
+
+/**
+ * Tests getting/setting the Vary header
+ *
+ * @return void
+ */
+	public function testVary() {
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->vary('Accept-encoding');
+		$this->assertEquals(array('Accept-encoding'), $response->vary());
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Vary', 'Accept-encoding');
+		$response->send();
+
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->vary(array('Accept-language', 'Accept-encoding'));
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Vary', 'Accept-language, Accept-encoding');
+		$response->send();
+		$this->assertEquals(array('Accept-language', 'Accept-encoding'), $response->vary());
+	}
+
+/**
+ * Tests getting/setting the Etag header
+ *
+ * @return void
+ */
+	public function testEtag() {
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->etag('something');
+		$this->assertEquals('"something"', $response->etag());
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Etag', '"something"');
+		$response->send();
+
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->etag('something', true);
+		$this->assertEquals('W/"something"', $response->etag());
+		$response->expects($this->at(1))
+			->method('_sendHeader')->with('Etag', 'W/"something"');
+		$response->send();
+	}
+
+/**
+ * Tests that the response is able to be marked as not modified
+ *
+ * @return void
+ */
+	public function testNotModified() {
+		$response = $this->getMock('CakeResponse', array('_sendHeader', '_sendContent'));
+		$response->body('something');
+		$response->statusCode(200);
+		$response->length(100);
+		$response->modified('now');
+		$response->notModified();
+
+		$this->assertEmpty($response->header());
+		$this->assertEmpty($response->body());
+		$this->assertEquals(304, $response->statusCode());
+	}
+
+/**
+ * Test checkNotModified method
+ *
+ * @return void
+ **/
+	public function testCheckNotModifiedByEtagStar() {
+		$_SERVER['HTTP_IF_NONE_MATCH'] = '*';
+		$response =  $this->getMock('CakeResponse', array('notModified'));
+		$response->etag('something');
+		$response->expects($this->once())->method('notModified');
+		$response->checkNotModified(new CakeRequest);
+	}
+
+/**
+ * Test checkNotModified method
+ *
+ * @return void
+ **/
+	public function testCheckNotModifiedByEtagExact() {
+		$_SERVER['HTTP_IF_NONE_MATCH'] = 'W/"something", "other"';
+		$response =  $this->getMock('CakeResponse', array('notModified'));
+		$response->etag('something', true);
+		$response->expects($this->once())->method('notModified');
+		$this->assertTrue($response->checkNotModified(new CakeRequest));
+	}
+
+/**
+ * Test checkNotModified method
+ *
+ * @return void
+ **/
+	public function testCheckNotModifiedByEtagAndTime() {
+		$_SERVER['HTTP_IF_NONE_MATCH'] = 'W/"something", "other"';
+		$_SERVER['HTTP_IF_MODIFIED_SINCE'] = '2012-01-01 00:00:00';
+		$response =  $this->getMock('CakeResponse', array('notModified'));
+		$response->etag('something', true);
+		$response->modified('2012-01-01 00:00:00');
+		$response->expects($this->once())->method('notModified');
+		$this->assertTrue($response->checkNotModified(new CakeRequest));
+	}
+
+/**
+ * Test checkNotModified method
+ *
+ * @return void
+ **/
+	public function testCheckNotModifiedByEtagAndTimeMismatch() {
+		$_SERVER['HTTP_IF_NONE_MATCH'] = 'W/"something", "other"';
+		$_SERVER['HTTP_IF_MODIFIED_SINCE'] = '2012-01-01 00:00:00';
+		$response =  $this->getMock('CakeResponse', array('notModified'));
+		$response->etag('something', true);
+		$response->modified('2012-01-01 00:00:01');
+		$response->expects($this->never())->method('notModified');
+		$this->assertFalse($response->checkNotModified(new CakeRequest));
+	}
+
+/**
+ * Test checkNotModified method
+ *
+ * @return void
+ **/
+	public function testCheckNotModifiedByEtagMismatch() {
+		$_SERVER['HTTP_IF_NONE_MATCH'] = 'W/"something-else", "other"';
+		$_SERVER['HTTP_IF_MODIFIED_SINCE'] = '2012-01-01 00:00:00';
+		$response =  $this->getMock('CakeResponse', array('notModified'));
+		$response->etag('something', true);
+		$response->modified('2012-01-01 00:00:00');
+		$response->expects($this->never())->method('notModified');
+		$this->assertFalse($response->checkNotModified(new CakeRequest));
+	}
+
+
+/**
+ * Test checkNotModified method
+ *
+ * @return void
+ **/
+	public function testCheckNotModifiedByTime() {
+		$_SERVER['HTTP_IF_MODIFIED_SINCE'] = '2012-01-01 00:00:00';
+		$response =  $this->getMock('CakeResponse', array('notModified'));
+		$response->modified('2012-01-01 00:00:00');
+		$response->expects($this->once())->method('notModified');
+		$this->assertTrue($response->checkNotModified(new CakeRequest));
+	}
+
+/**
+ * Test checkNotModified method
+ *
+ * @return void
+ **/
+	public function testCheckNotModifiedNoHints() {
+		$_SERVER['HTTP_IF_NONE_MATCH'] = 'W/"something", "other"';
+		$_SERVER['HTTP_IF_MODIFIED_SINCE'] = '2012-01-01 00:00:00';
+		$response =  $this->getMock('CakeResponse', array('notModified'));
+		$response->expects($this->never())->method('notModified');
+		$this->assertFalse($response->checkNotModified(new CakeRequest));
+	}
 }

+ 16 - 8
lib/Cake/Test/Case/Routing/Route/RedirectRouteTest.php

@@ -48,51 +48,59 @@ class RedirectRouteTestCase extends  CakeTestCase {
 		$route->stop = false;
 		$route->response = $this->getMock('CakeResponse', array('_sendHeader'));
 		$result = $route->parse('/home');
-		$this->assertEquals($route->response->header(), array('Location' => Router::url('/posts', true)));
+		$header = $route->response->header();
+		$this->assertEquals($header['Location'], Router::url('/posts', true));
 
 		$route = new RedirectRoute('/home', array('controller' => 'posts', 'action' => 'index'));
 		$route->stop = false;
 		$route->response = $this->getMock('CakeResponse', array('_sendHeader'));
 		$result = $route->parse('/home');
-		$this->assertEquals($route->response->header(), array('Location' => Router::url('/posts', true)));
+		$header = $route->response->header();
+		$this->assertEquals($header['Location'], Router::url('/posts', true));
 		$this->assertEquals($route->response->statusCode(), 301);
 
 		$route = new RedirectRoute('/google', 'http://google.com');
 		$route->stop = false;
 		$route->response = $this->getMock('CakeResponse', array('_sendHeader'));
 		$result = $route->parse('/google');
-		$this->assertEquals($route->response->header(), array('Location' => 'http://google.com'));
+		$header = $route->response->header();
+		$this->assertEquals($header['Location'], 'http://google.com');
 
 		$route = new RedirectRoute('/posts/*', array('controller' => 'posts', 'action' => 'view'), array('status' => 302));
 		$route->stop = false;
 		$route->response = $this->getMock('CakeResponse', array('_sendHeader'));
 		$result = $route->parse('/posts/2');
-		$this->assertEquals($route->response->header(), array('Location' => Router::url('/posts/view', true)));
+		$header = $route->response->header();
+		$this->assertEquals($header['Location'], Router::url('/posts/view', true));
 		$this->assertEquals($route->response->statusCode(), 302);
 
 		$route = new RedirectRoute('/posts/*', array('controller' => 'posts', 'action' => 'view'), array('persist' => true));
 		$route->stop = false;
 		$route->response = $this->getMock('CakeResponse', array('_sendHeader'));
 		$result = $route->parse('/posts/2');
-		$this->assertEquals($route->response->header(), array('Location' => Router::url('/posts/view/2', true)));
+		$header = $route->response->header();
+		$this->assertEquals($header['Location'], Router::url('/posts/view/2', true));
 
 		$route = new RedirectRoute('/posts/*', '/test', array('persist' => true));
 		$route->stop = false;
 		$route->response = $this->getMock('CakeResponse', array('_sendHeader'));
 		$result = $route->parse('/posts/2');
-		$this->assertEquals($route->response->header(), array('Location' => Router::url('/test', true)));
+		$header = $route->response->header();
+		$this->assertEquals($header['Location'], Router::url('/test', true));
 
 		$route = new RedirectRoute('/my_controllers/:action/*', array('controller' => 'tags', 'action' => 'add'), array('persist' => true));
 		$route->stop = false;
 		$route->response = $this->getMock('CakeResponse', array('_sendHeader'));
 		$result = $route->parse('/my_controllers/do_something/passme/named:param');
-		$this->assertEquals($route->response->header(), array('Location' => Router::url('/tags/add/passme/named:param', true)));
+		$header = $route->response->header();
+		$this->assertEquals($header['Location'], Router::url('/tags/add/passme/named:param', true));
 
 		$route = new RedirectRoute('/my_controllers/:action/*', array('controller' => 'tags', 'action' => 'add'));
 		$route->stop = false;
 		$route->response = $this->getMock('CakeResponse', array('_sendHeader'));
 		$result = $route->parse('/my_controllers/do_something/passme/named:param');
-		$this->assertEquals($route->response->header(), array('Location' => Router::url('/tags/add', true)));
+		$header = $route->response->header();
+		$this->assertEquals($header['Location'], Router::url('/tags/add', true));
 	}
 
 }

+ 2 - 1
lib/Cake/Test/Case/Routing/RouterTest.php

@@ -2519,7 +2519,8 @@ class RouterTest extends CakeTestCase {
 		$this->assertEquals(Router::$routes[0]->options['status'], 302);
 
 		Router::parse('/blog');
-		$this->assertEquals(Router::$routes[0]->response->header(), array('Location' => Router::url('/posts', true)));
+		$header = Router::$routes[0]->response->header();
+		$this->assertEquals($header['Location'], Router::url('/posts', true));
 		$this->assertEquals(Router::$routes[0]->response->statusCode(), 302);
 
 		Router::$routes[0]->response = $this->getMock('CakeResponse', array('_sendHeader'));