diff --git a/CHANGELOG.md b/CHANGELOG.md index 8feb3be..d63e980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Development (not released yet) + +* New feature: followRedirects option +* New feature: maxRedirects option +* BC break: `Client` and `Request` take different constructor parameters. + ## 0.4.0 (2014-02-02) * BC break: Drop unused `Response::getBody()` diff --git a/README.md b/README.md index e4f0827..311a412 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,10 @@ $dnsResolver = $dnsResolverFactory->createCached('8.8.8.8', $loop); $factory = new React\HttpClient\Factory(); $client = $factory->create($loop, $dnsResolver); -$request = $client->request('GET', 'https://github.com/'); +$request = $client->request('GET', 'https://github.com/', [], [ + 'followRedirects' => true, + 'maxRedirects' => 5 +]); $request->on('response', function ($response) { $response->on('data', function ($data) { // ... @@ -49,6 +52,8 @@ $request->on('response', function ($response) { }); $request->end(); $loop->run(); + +?> ``` ## TODO @@ -56,4 +61,3 @@ $loop->run(); * gzip content encoding * chunked transfer encoding * keep-alive connections -* following redirections diff --git a/src/Client.php b/src/Client.php index 456564c..a500826 100644 --- a/src/Client.php +++ b/src/Client.php @@ -2,29 +2,20 @@ namespace React\HttpClient; -use React\SocketClient\ConnectorInterface; - class Client { - private $connector; - private $secureConnector; + private $connectorPair; - public function __construct(ConnectorInterface $connector, ConnectorInterface $secureConnector) + public function __construct(ConnectorPair $connectorPair) { - $this->connector = $connector; - $this->secureConnector = $secureConnector; + $this->connectorPair = $connectorPair; } - public function request($method, $url, array $headers = []) + public function request($method, $url, array $headers = [], array $options = null) { $requestData = new RequestData($method, $url, $headers); - $connector = $this->getConnectorForScheme($requestData->getScheme()); - - return new Request($connector, $requestData); - } + $requestOptions = new RequestOptions($options); - private function getConnectorForScheme($scheme) - { - return ('https' === $scheme) ? $this->secureConnector : $this->connector; + return new Request($this->connectorPair, $requestData, $requestOptions); } } diff --git a/src/ConnectorPair.php b/src/ConnectorPair.php new file mode 100644 index 0000000..6e7c717 --- /dev/null +++ b/src/ConnectorPair.php @@ -0,0 +1,22 @@ +connector = $connector; + $this->secureConnector = $secureConnector; + } + + public function getConnectorForScheme($scheme) + { + return ('https' === $scheme) ? $this->secureConnector : $this->connector; + } +} diff --git a/src/Factory.php b/src/Factory.php index b9785f0..40b798d 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -13,7 +13,8 @@ public function create(LoopInterface $loop, Resolver $resolver) { $connector = new Connector($loop, $resolver); $secureConnector = new SecureConnector($connector, $loop); + $connectorPair = new ConnectorPair($connector, $secureConnector); - return new Client($connector, $secureConnector); + return new Client($connectorPair); } } diff --git a/src/Request.php b/src/Request.php index 8dbd150..4073970 100644 --- a/src/Request.php +++ b/src/Request.php @@ -4,7 +4,6 @@ use Evenement\EventEmitterTrait; use Guzzle\Parser\Message\MessageParser; -use React\SocketClient\ConnectorInterface; use React\Stream\WritableStreamInterface; /** @@ -24,7 +23,9 @@ class Request implements WritableStreamInterface const STATE_END = 3; private $connector; + private $connectorPair; private $requestData; + private $requestOptions; private $stream; private $buffer; @@ -32,10 +33,18 @@ class Request implements WritableStreamInterface private $response; private $state = self::STATE_INIT; - public function __construct(ConnectorInterface $connector, RequestData $requestData) + private $redirectCount; + private $redirectLocations; + + public function __construct(ConnectorPair $connectorPair, RequestData $requestData, RequestOptions $requestOptions) { - $this->connector = $connector; + $this->connectorPair = $connectorPair; $this->requestData = $requestData; + $this->requestOptions = $requestOptions; + $this->connector = $connectorPair->getConnectorForScheme($requestData->getScheme()); + $this->redirectCount = 0; + $this->redirectLocations = []; + $this->trackLocation($requestData); } public function isWritable() @@ -50,12 +59,11 @@ public function writeHead() } $this->state = self::STATE_WRITING_HEAD; - $requestData = $this->requestData; $streamRef = &$this->stream; $stateRef = &$this->state; - $this + return $this ->connect() ->then( function ($stream) use ($requestData, &$streamRef, &$stateRef) { @@ -89,12 +97,11 @@ public function write($data) return $this->stream->write($data); } - $this->on('headers-written', function ($this) use ($data) { - $this->write($data); - }); - if (self::STATE_WRITING_HEAD > $this->state) { - $this->writeHead(); + $this->writeHead() + ->then(function () use ($data) { + $this->stream->write($data); + }); } return false; @@ -108,7 +115,7 @@ public function end($data = null) if (null !== $data) { $this->write($data); - } else if (self::STATE_WRITING_HEAD > $this->state) { + } elseif (self::STATE_WRITING_HEAD > $this->state) { $this->writeHead(); } } @@ -132,6 +139,49 @@ public function handleData($data) $this->stream->removeListener('end', array($this, 'handleEnd')); $this->stream->removeListener('error', array($this, 'handleError')); + //Should we respond to any redirects? + if ($this->isRedirectCode($response->getCode()) + && $this->requestOptions->shouldFollowRedirects()) { + + //Have we reached our maximum redirects? + if ($this->requestOptions->getMaxRedirects() >= 0 + && $this->redirectCount >= $this->requestOptions->getMaxRedirects()) { + $this->closeError(new \RuntimeException( + sprintf("Too many redirects: %u", $this->redirectCount) + )); + + return; + } + + //Recalibrate to this new location. + $headers = $response->getHeaders(); + $this->requestData->redirect($response->getCode(), $headers['Location']); + $this->connector = $this->connectorPair->getConnectorForScheme($this->requestData->getScheme()); + + //Is the location a cyclic redirect? + if ($this->isKnownLocation($this->requestData->getMethod(), $headers['Location'])) { + $this->closeError(new \RuntimeException( + "Cyclic redirect detected" + )); + + return; + } + + //Store the next location to prevent cyclic redirects. + $this->trackLocation($this->requestData); + $this->redirectCount++; + + //Clean up and rewind. + $this->stream->close(); + $this->responseFactory = null; + $this->state = self::STATE_INIT; + + //Perform the same tricks. + $this->end(); + + return; + } + $this->response = $response; $response->on('end', function () { @@ -218,6 +268,22 @@ protected function connect() ->create($host, $port); } + protected function isRedirectCode($code) + { + //Note: 303, 307, 308 status is not supported in HTTP/1.0. + return in_array($code, [301, 302]); + } + + protected function trackLocation(RequestData $requestData) + { + $this->redirectLocations[] = $requestData->getMethod().' '.$requestData->getUrl(); + } + + protected function isKnownLocation($method, $url) + { + return in_array($method.' '.$url, $this->redirectLocations); + } + public function setResponseFactory($factory) { $this->responseFactory = $factory; diff --git a/src/RequestData.php b/src/RequestData.php index 7073ce5..04d30e2 100644 --- a/src/RequestData.php +++ b/src/RequestData.php @@ -2,6 +2,8 @@ namespace React\HttpClient; +use InvalidArgumentException; + class RequestData { private $method; @@ -32,6 +34,16 @@ private function mergeDefaultheaders(array $headers) ); } + public function getMethod() + { + return strtoupper($this->method); + } + + public function getUrl() + { + return $this->url; + } + public function getScheme() { return parse_url($this->url, PHP_URL_SCHEME); @@ -78,4 +90,34 @@ public function __toString() return $data; } + + /** + * Processes a redirect request, updating internal values to represent the new request data. + * + * @param integer $code HTTP status code for the redirect. + * @param string $location The Location header received. + */ + public function redirect($code, $location) + { + switch ($code) { + + //These cases require that we switch to the GET method. + //@see https://github.com/bagder/curl/blob/cc28bc472ec421cec2ba26d653e53892998a248d/lib/transfer.c#L1736 + case 301: + case 302: + $this->method = 'GET'; + + //Note: 303, 307, 308 status is not supported in HTTP/1.0. + // case 303: + // case 307: + // case 308: + + //Of course switch the location. + $this->url = $location; + break; + + default: + throw new InvalidArgumentException(sprintf("Redirect code %u is not supported", $code)); + } + } } diff --git a/src/RequestOptions.php b/src/RequestOptions.php new file mode 100644 index 0000000..0a1a346 --- /dev/null +++ b/src/RequestOptions.php @@ -0,0 +1,89 @@ +parseOptions($options); + } + } + + /** + * This will enforce only known options are used and in the right format. + * + * @param array $options The provided options. + */ + protected function parseOptions(array $options) + { + foreach ($options as $option => $value) { + switch ($option) { + case 'followRedirects': + if (!is_bool($value)) { + throw new InvalidArgumentException('Option "followRedirects" should be a boolean'); + } + $this->followRedirects = $value; + break; + + case 'maxRedirects': + if (!is_int($value)) { + throw new InvalidArgumentException('Option "maxRedirects" should be an integer'); + } + if ($value < -1) { + throw new InvalidArgumentException('Option "maxRedirects" should be -1 or greater'); + } + $this->maxRedirects = $value; + break; + + default: + throw new InvalidArgumentException(sprintf('Unknown option "%s"', $option)); + break; + } + } + } + + /** + * Whether or not to follow redirects for this requests. + * + * @return boolean + */ + public function shouldFollowRedirects() + { + return $this->followRedirects; + } + + /** + * Maximum amount of redirects. + * Note: -1 is unlimited, 0 means no redirects will be accepted. + * + * @return integer + */ + public function getMaxRedirects() + { + return $this->maxRedirects; + } +} diff --git a/tests/RequestOptionsTest.php b/tests/RequestOptionsTest.php new file mode 100644 index 0000000..bcf89ff --- /dev/null +++ b/tests/RequestOptionsTest.php @@ -0,0 +1,84 @@ + true, + )); + } + + /** @test */ + public function requestOptionsShouldSupportFollowRedirectsOption() + { + //Default value should be FALSE. + $requestOptions = new RequestOptions(); + $this->assertFalse($requestOptions->shouldFollowRedirects()); + + //Providing a different value should be respected. + $requestOptions = new RequestOptions(array( + 'followRedirects' => true, + )); + $this->assertTrue($requestOptions->shouldFollowRedirects()); + + //Should check for boolean type. + $this->setExpectedException( + 'InvalidArgumentException', + 'Option "followRedirects" should be a boolean' + ); + new RequestOptions(array( + 'followRedirects' => 7, + )); + } + + /** @test */ + public function requestOptionsShouldSupportMaxRedirectsOption() + { + //Default value should be 5. + $requestOptions = new RequestOptions(); + $this->assertSame(5, $requestOptions->getMaxRedirects()); + + //Providing a different value should be respected. + $requestOptions = new RequestOptions(array( + 'maxRedirects' => 42, + )); + $this->assertSame(42, $requestOptions->getMaxRedirects()); + + //Should check for integer type. + $this->setExpectedException( + 'InvalidArgumentException', + 'Option "maxRedirects" should be an integer' + ); + new RequestOptions(array( + 'maxRedirects' => 7.5, + )); + } + + /** + * @test + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Option "maxRedirects" should be -1 or greater + */ + public function requestOptionsShouldCheckValidMaxRedirectsRange() + { + new RequestOptions(array( + 'maxRedirects' => -3, + )); + } +} diff --git a/tests/RequestTest.php b/tests/RequestTest.php index 5d2d595..8a10c5e 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -2,8 +2,10 @@ namespace React\Tests\HttpClient; +use React\HttpClient\ConnectorPair; use React\HttpClient\Request; use React\HttpClient\RequestData; +use React\HttpClient\RequestOptions; use React\Stream\Stream; use React\Promise\FulfilledPromise; use React\Promise\RejectedPromise; @@ -30,7 +32,9 @@ public function setUp() public function requestShouldBindToStreamEventsAndUseconnector() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestOptions = new RequestOptions(); + $connectorPair = new ConnectorPair($this->connector, $this->connector); + $request = new Request($connectorPair, $requestData, $requestOptions); $this->successfulConnectionMock(); @@ -74,6 +78,10 @@ public function requestShouldBindToStreamEventsAndUseconnector() ->with('data', array('body')); $response->expects($this->at(0)) + ->method('getCode') + ->will($this->returnValue(200)); + + $response->expects($this->at(1)) ->method('on') ->with('end', $this->anything()) ->will($this->returnCallback(function ($event, $cb) use (&$endCallback) { @@ -120,7 +128,9 @@ public function requestShouldBindToStreamEventsAndUseconnector() public function requestShouldEmitErrorIfConnectionFails() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestOptions = new RequestOptions(); + $connectorPair = new ConnectorPair($this->connector, $this->connector); + $request = new Request($connectorPair, $requestData, $requestOptions); $this->rejectedConnectionMock(); @@ -153,7 +163,9 @@ public function requestShouldEmitErrorIfConnectionFails() public function requestShouldEmitErrorIfConnectionEndsBeforeResponseIsParsed() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestOptions = new RequestOptions(); + $connectorPair = new ConnectorPair($this->connector, $this->connector); + $request = new Request($connectorPair, $requestData, $requestOptions); $this->successfulConnectionMock(); @@ -187,7 +199,9 @@ public function requestShouldEmitErrorIfConnectionEndsBeforeResponseIsParsed() public function requestShouldEmitErrorIfConnectionEmitsError() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestOptions = new RequestOptions(); + $connectorPair = new ConnectorPair($this->connector, $this->connector); + $request = new Request($connectorPair, $requestData, $requestOptions); $this->successfulConnectionMock(); @@ -221,7 +235,9 @@ public function requestShouldEmitErrorIfConnectionEmitsError() public function postRequestShouldSendAPostRequest() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestOptions = new RequestOptions(); + $connectorPair = new ConnectorPair($this->connector, $this->connector); + $request = new Request($connectorPair, $requestData, $requestOptions); $this->successfulConnectionMock(); @@ -247,11 +263,289 @@ public function postRequestShouldSendAPostRequest() $request->handleData("\r\nbody"); } + /** @test */ + public function redirectingPostRequestShouldSwitchToGet() + { + $requestData = new RequestData('POST', 'http://www.example.com'); + $requestOptions = new RequestOptions([ + 'followRedirects' => true, + 'maxRedirects' => 1, + ]); + $connectorPair = new ConnectorPair($this->connector, $this->connector); + $request = new Request($connectorPair, $requestData, $requestOptions); + + $this->connector + ->expects($this->at(0)) + ->method('create') + ->with('www.example.com', 80) + ->will($this->returnValue(new FulfilledPromise($this->stream))); + + $secondStream = $this->getMockBuilder('React\Stream\Stream') + ->disableOriginalConstructor() + ->getMock(); + + $secondStream + ->expects($this->at(4)) + ->method('write') + ->with($this->matchesRegularExpression("#^GET / HTTP/1\.0\r\nHost: www.foo.bar\r\nUser-Agent:.*\r\n\r\n$#")); + $secondStream + ->expects($this->at(5)) + ->method('close'); + + $this->connector + ->expects($this->at(1)) + ->method('create') + ->with('www.foo.bar', 80) + ->will($this->returnValue(new FulfilledPromise($secondStream))); + + $this->stream + ->expects($this->at(4)) + ->method('write') + ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\n$#")); + $this->stream + ->expects($this->at(5)) + ->method('write') + ->with($this->identicalTo("some post data")); + $this->stream + ->expects($this->at(6)) + ->method('close'); + + $factory = $this->createCallableMock(); + $factory->expects($this->at(0)) + ->method('__invoke') + ->will($this->returnValue($this->response)); + + $this->response + ->expects($this->exactly(2)) + ->method('getCode') + ->will($this->returnValue(302)); + + $this->response + ->expects($this->exactly(1)) + ->method('getHeaders') + ->will($this->returnValue([ + 'Location' => 'http://www.foo.bar', + 'Content-Type' => 'text/plain', + ])); + + $request->setResponseFactory($factory); + $request->end('some post data'); + + $request->handleData("HTTP/1.0 302 Found\r\n"); + $request->handleData("Location: http://www.foo.bar\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("\r\nbody"); + + $request->handleData("HTTP/1.0 200 OK\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("\r\nbody"); + } + + /** @test */ + public function shouldDetectCyclicRedirect() + { + $requestData = new RequestData('GET', 'http://www.example.com'); + $requestOptions = new RequestOptions([ + 'followRedirects' => true, + 'maxRedirects' => 5, + ]); + $connectorPair = new ConnectorPair($this->connector, $this->connector); + $request = new Request($connectorPair, $requestData, $requestOptions); + + $this->connector + ->expects($this->at(0)) + ->method('create') + ->with('www.example.com', 80) + ->will($this->returnValue(new FulfilledPromise($this->stream))); + + $this->stream + ->expects($this->at(4)) + ->method('write') + ->with($this->matchesRegularExpression("#^GET / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\n$#")); + $this->stream + ->expects($this->at(5)) + ->method('close'); + + $factory = $this->createCallableMock(); + $factory->expects($this->at(0)) + ->method('__invoke') + ->will($this->returnValue($this->response)); + + $this->response + ->expects($this->exactly(2)) + ->method('getCode') + ->will($this->returnValue(302)); + + $this->response + ->expects($this->exactly(1)) + ->method('getHeaders') + ->will($this->returnValue([ + 'Location' => 'http://www.example.com', + 'Content-Type' => 'text/plain', + ])); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with( + $this->isInstanceOf('RuntimeException'), + null, + $this->isInstanceOf('React\HttpClient\Request') + ); + + $request->on('end', $handler); + $request->on('close', $this->expectCallableNever()); + + $request->setResponseFactory($factory); + $request->end(); + + $request->handleData("HTTP/1.0 302 Found\r\n"); + $request->handleData("Location: http://www.example.com\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("\r\nbody"); + } + + /** @test */ + public function shouldAllowRedirectFromPostToGet() + { + $requestData = new RequestData('POST', 'http://www.example.com'); + $requestOptions = new RequestOptions([ + 'followRedirects' => true, + 'maxRedirects' => 1, + ]); + $connectorPair = new ConnectorPair($this->connector, $this->connector); + $request = new Request($connectorPair, $requestData, $requestOptions); + + $this->connector + ->expects($this->at(0)) + ->method('create') + ->with('www.example.com', 80) + ->will($this->returnValue(new FulfilledPromise($this->stream))); + + $secondStream = $this->getMockBuilder('React\Stream\Stream') + ->disableOriginalConstructor() + ->getMock(); + + $secondStream + ->expects($this->at(4)) + ->method('write') + ->with($this->matchesRegularExpression("#^GET / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\n$#")); + $secondStream + ->expects($this->at(5)) + ->method('close'); + + $this->connector + ->expects($this->at(1)) + ->method('create') + ->with('www.example.com', 80) + ->will($this->returnValue(new FulfilledPromise($secondStream))); + + $this->stream + ->expects($this->at(4)) + ->method('write') + ->with($this->matchesRegularExpression("#^POST / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\n$#")); + $this->stream + ->expects($this->at(5)) + ->method('write') + ->with($this->identicalTo("some post data")); + $this->stream + ->expects($this->at(6)) + ->method('close'); + + $factory = $this->createCallableMock(); + $factory->expects($this->exactly(1)) + ->method('__invoke') + ->will($this->returnValue($this->response)); + + $this->response + ->expects($this->exactly(2)) + ->method('getCode') + ->will($this->returnValue(302)); + + $this->response + ->expects($this->exactly(1)) + ->method('getHeaders') + ->will($this->returnValue([ + 'Location' => 'http://www.example.com', + 'Content-Type' => 'text/plain', + ])); + + $request->setResponseFactory($factory); + $request->end('some post data'); + + $request->handleData("HTTP/1.0 302 Found\r\n"); + $request->handleData("Location: http://www.example.com\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("\r\nbody"); + + $request->handleData("HTTP/1.0 200 OK\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("\r\nbody"); + } + + /** @test */ + public function shouldDetectTooManyRedirects() + { + $requestData = new RequestData('GET', 'http://www.example.com'); + $requestOptions = new RequestOptions([ + 'followRedirects' => true, + 'maxRedirects' => 0, + ]); + $connectorPair = new ConnectorPair($this->connector, $this->connector); + $request = new Request($connectorPair, $requestData, $requestOptions); + + $this->connector + ->expects($this->at(0)) + ->method('create') + ->with('www.example.com', 80) + ->will($this->returnValue(new FulfilledPromise($this->stream))); + + $this->stream + ->expects($this->at(4)) + ->method('write') + ->with($this->matchesRegularExpression("#^GET / HTTP/1\.0\r\nHost: www.example.com\r\nUser-Agent:.*\r\n\r\n$#")); + $this->stream + ->expects($this->at(5)) + ->method('close'); + + $factory = $this->createCallableMock(); + $factory->expects($this->at(0)) + ->method('__invoke') + ->will($this->returnValue($this->response)); + + $this->response + ->expects($this->exactly(1)) + ->method('getCode') + ->will($this->returnValue(302)); + + $handler = $this->createCallableMock(); + $handler->expects($this->once()) + ->method('__invoke') + ->with( + $this->isInstanceOf('RuntimeException'), + null, + $this->isInstanceOf('React\HttpClient\Request') + ); + + $request->on('end', $handler); + $request->on('close', $this->expectCallableNever()); + + $request->setResponseFactory($factory); + $request->end(); + + $request->handleData("HTTP/1.0 302 Found\r\n"); + $request->handleData("Location: http://www.foo.bar\r\n"); + $request->handleData("Content-Type: text/plain\r\n"); + $request->handleData("\r\nbody"); + } + /** @test */ public function writeWithAPostRequestShouldSendToTheStream() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestOptions = new RequestOptions(); + $connectorPair = new ConnectorPair($this->connector, $this->connector); + $request = new Request($connectorPair, $requestData, $requestOptions); $this->successfulConnectionMock(); @@ -292,7 +586,9 @@ public function writeWithAPostRequestShouldSendToTheStream() public function pipeShouldPipeDataIntoTheRequestBody() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestOptions = new RequestOptions(); + $connectorPair = new ConnectorPair($this->connector, $this->connector); + $request = new Request($connectorPair, $requestData, $requestOptions); $this->successfulConnectionMock(); @@ -343,7 +639,9 @@ public function pipeShouldPipeDataIntoTheRequestBody() public function endShouldOnlyAcceptScalars() { $requestData = new RequestData('POST', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestOptions = new RequestOptions(); + $connectorPair = new ConnectorPair($this->connector, $this->connector); + $request = new Request($connectorPair, $requestData, $requestOptions); $request->end(array()); } @@ -352,16 +650,23 @@ public function endShouldOnlyAcceptScalars() public function requestShouldRelayErrorEventsFromResponse() { $requestData = new RequestData('GET', 'http://www.example.com'); - $request = new Request($this->connector, $requestData); + $requestOptions = new RequestOptions(); + $connectorPair = new ConnectorPair($this->connector, $this->connector); + $request = new Request($connectorPair, $requestData, $requestOptions); $this->successfulConnectionMock(); $response = $this->response; $response->expects($this->at(0)) + ->method('getCode') + ->will($this->returnValue(200)); + + $response->expects($this->at(1)) ->method('on') ->with('end', $this->anything()); - $response->expects($this->at(1)) + + $response->expects($this->at(2)) ->method('on') ->with('error', $this->anything()) ->will($this->returnCallback(function ($event, $cb) use (&$errorCallback) { @@ -403,4 +708,3 @@ private function rejectedConnectionMock() ->will($this->returnValue(new RejectedPromise(new \RuntimeException()))); } } - diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 5b86e19..ae508d9 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -73,5 +73,44 @@ public function closedResponseShouldNotBeResumedOrPaused() $response->resume(); $response->pause(); } -} + /** @test */ + public function responseShouldOfferHeaders() + { + $headers = array('content-type' => 'text/plain'); + $response = new Response($this->stream, 'http', '1.0', '200', 'ok', $headers); + $this->assertSame($headers, $response->getHeaders()); + } + + /** @test */ + public function responseShouldOfferProtocol() + { + $protocol = 'http'; + $response = new Response($this->stream, $protocol, '1.0', '200', 'ok', array('content-type' => 'text/plain')); + $this->assertSame($protocol, $response->getProtocol()); + } + + /** @test */ + public function responseShouldOfferVersion() + { + $version = '1.0'; + $response = new Response($this->stream, 'http', $version, '200', 'ok', array('content-type' => 'text/plain')); + $this->assertSame($version, $response->getVersion()); + } + + /** @test */ + public function responseShouldOfferReasonPhrase() + { + $reasonphrase = 'OK'; + $response = new Response($this->stream, 'http', '1.0', '200', $reasonphrase, array('content-type' => 'text/plain')); + $this->assertSame($reasonphrase, $response->getReasonPhrase()); + } + + /** @test */ + public function responseShouldOfferCode() + { + $code = 'OK'; + $response = new Response($this->stream, 'http', '1.0', $code, 'ok', array('content-type' => 'text/plain')); + $this->assertSame($code, $response->getCode()); + } +}