Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Add a flexible http client providing both contract, and only emulating what's necessary
- HTTP Client Router: route requests to underlying clients
- Plugin client and core plugins moved here from `php-http/plugins`

### Deprecated

Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"php": ">=5.4",
"php-http/httplug": "^1.0",
"php-http/message-factory": "^1.0",
"php-http/message": "^1.2"
"php-http/message": "^1.2",
"symfony/options-resolver": "^2.6|^3.0"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one of my concerns. Adding this dependency here. Maybe we should rethink option handling in the plugin client?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the big benefit of options-resolver is that we are much more flexible to add things without a BC break. adding new constructor parameters can easily lead to BC break, and for these options things, its tricky to decide on the order of them - with separate parameters, the order can not be changed to make logical sense. the array is so much easier.

hm. on the other hand we have only one option for now. maybe we could instead do that manually instead of using OptionsResolver - we can add a requirement for the resolver when we add more options. for just one, its indeed overkill if its not a free include. wdyt?

  • remove the resolver dependency
  • keep $options an array
  • resolve manually for now
  • add a comment next to the options that if we add more options, we should introduce the OptionsResolver

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly what I had in my mind.

},
"require-dev": {
"phpspec/phpspec": "^2.4",
Expand Down
1 change: 1 addition & 0 deletions spec/FlexibleHttpClientSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ function it_does_not_emulate_a_client($client, RequestInterface $syncRequest, Re
{
$client->implement('Http\Client\HttpClient');
$client->implement('Http\Client\HttpAsyncClient');

$client->sendRequest($syncRequest)->shouldBeCalled();
$client->sendRequest($asyncRequest)->shouldNotBeCalled();
$client->sendAsyncRequest($asyncRequest)->shouldBeCalled();
Expand Down
84 changes: 84 additions & 0 deletions spec/Plugin/AddHostPluginSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace spec\Http\Client\Common\Plugin;

use Http\Message\StreamFactory;
use Http\Message\UriFactory;
use Http\Promise\FulfilledPromise;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use PhpSpec\ObjectBehavior;

class AddHostPluginSpec extends ObjectBehavior
{
function let(UriInterface $uri)
{
$this->beConstructedWith($uri);
}

function it_is_initializable(UriInterface $uri)
{
$uri->getHost()->shouldBeCalled()->willReturn('example.com');

$this->shouldHaveType('Http\Client\Common\Plugin\AddHostPlugin');
}

function it_is_a_plugin(UriInterface $uri)
{
$uri->getHost()->shouldBeCalled()->willReturn('example.com');

$this->shouldImplement('Http\Client\Common\Plugin');
}

function it_adds_domain(
RequestInterface $request,
UriInterface $host,
UriInterface $uri
) {
$host->getScheme()->shouldBeCalled()->willReturn('http://');
$host->getHost()->shouldBeCalled()->willReturn('example.com');

$request->getUri()->shouldBeCalled()->willReturn($uri);
$request->withUri($uri)->shouldBeCalled()->willReturn($request);

$uri->withScheme('http://')->shouldBeCalled()->willReturn($uri);
$uri->withHost('example.com')->shouldBeCalled()->willReturn($uri);
$uri->getHost()->shouldBeCalled()->willReturn('');

$this->beConstructedWith($host);
$this->handleRequest($request, function () {}, function () {});
}

function it_replaces_domain(
RequestInterface $request,
UriInterface $host,
UriInterface $uri
) {
$host->getScheme()->shouldBeCalled()->willReturn('http://');
$host->getHost()->shouldBeCalled()->willReturn('example.com');

$request->getUri()->shouldBeCalled()->willReturn($uri);
$request->withUri($uri)->shouldBeCalled()->willReturn($request);

$uri->withScheme('http://')->shouldBeCalled()->willReturn($uri);
$uri->withHost('example.com')->shouldBeCalled()->willReturn($uri);


$this->beConstructedWith($host, ['replace' => true]);
$this->handleRequest($request, function () {}, function () {});
}

function it_does_nothing_when_domain_exists(
RequestInterface $request,
UriInterface $host,
UriInterface $uri
) {
$request->getUri()->shouldBeCalled()->willReturn($uri);
$uri->getHost()->shouldBeCalled()->willReturn('default.com');

$this->beConstructedWith($host);
$this->handleRequest($request, function () {}, function () {});
}
}
40 changes: 40 additions & 0 deletions spec/Plugin/AuthenticationPluginSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace spec\Http\Client\Common\Plugin;

use Http\Message\Authentication;
use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class AuthenticationPluginSpec extends ObjectBehavior
{
function let(Authentication $authentication)
{
$this->beConstructedWith($authentication);
}

function it_is_initializable(Authentication $authentication)
{
$this->shouldHaveType('Http\Client\Common\Plugin\AuthenticationPlugin');
}

function it_is_a_plugin()
{
$this->shouldImplement('Http\Client\Common\Plugin');
}

function it_sends_an_authenticated_request(Authentication $authentication, RequestInterface $notAuthedRequest, RequestInterface $authedRequest, Promise $promise)
{
$authentication->authenticate($notAuthedRequest)->willReturn($authedRequest);

$next = function (RequestInterface $request) use($authedRequest, $promise) {
if (Argument::is($authedRequest->getWrappedObject())->scoreArgument($request)) {
return $promise->getWrappedObject();
}
};

$this->handleRequest($notAuthedRequest, $next, function () {})->shouldReturn($promise);
}
}
48 changes: 48 additions & 0 deletions spec/Plugin/ContentLengthPluginSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace spec\Http\Client\Common\Plugin;

use PhpSpec\Exception\Example\SkippingException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class ContentLengthPluginSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType('Http\Client\Common\Plugin\ContentLengthPlugin');
}

function it_is_a_plugin()
{
$this->shouldImplement('Http\Client\Common\Plugin');
}

function it_adds_content_length_header(RequestInterface $request, StreamInterface $stream)
{
$request->hasHeader('Content-Length')->shouldBeCalled()->willReturn(false);
$request->getBody()->shouldBeCalled()->willReturn($stream);
$stream->getSize()->shouldBeCalled()->willReturn(100);
$request->withHeader('Content-Length', 100)->shouldBeCalled()->willReturn($request);

$this->handleRequest($request, function () {}, function () {});
}

function it_streams_chunked_if_no_size(RequestInterface $request, StreamInterface $stream)
{
if(defined('HHVM_VERSION')) {
throw new SkippingException('Skipping test on hhvm, as there is no chunk encoding on hhvm');
}

$request->hasHeader('Content-Length')->shouldBeCalled()->willReturn(false);
$request->getBody()->shouldBeCalled()->willReturn($stream);

$stream->getSize()->shouldBeCalled()->willReturn(null);
$request->withBody(Argument::type('Http\Message\Encoding\ChunkStream'))->shouldBeCalled()->willReturn($request);
$request->withAddedHeader('Transfer-Encoding', 'chunked')->shouldBeCalled()->willReturn($request);

$this->handleRequest($request, function () {}, function () {});
}
}
183 changes: 183 additions & 0 deletions spec/Plugin/CookiePluginSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php

namespace spec\Http\Client\Common\Plugin;

use Http\Promise\FulfilledPromise;
use Http\Message\Cookie;
use Http\Message\CookieJar;
use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class CookiePluginSpec extends ObjectBehavior
{
private $cookieJar;

function let()
{
$this->cookieJar = new CookieJar();

$this->beConstructedWith($this->cookieJar);
}

function it_is_initializable()
{
$this->shouldHaveType('Http\Client\Common\Plugin\CookiePlugin');
}

function it_is_a_plugin()
{
$this->shouldImplement('Http\Client\Common\Plugin');
}

function it_loads_cookie(RequestInterface $request, UriInterface $uri, Promise $promise)
{
$cookie = new Cookie('name', 'value', 86400, 'test.com');
$this->cookieJar->addCookie($cookie);

$request->getUri()->willReturn($uri);
$uri->getHost()->willReturn('test.com');
$uri->getPath()->willReturn('/');

$request->withAddedHeader('Cookie', 'name=value')->willReturn($request);

$this->handleRequest($request, function (RequestInterface $requestReceived) use ($request, $promise) {
if (Argument::is($requestReceived)->scoreArgument($request->getWrappedObject())) {
return $promise->getWrappedObject();
}
}, function () {});
}

function it_does_not_load_cookie_if_expired(RequestInterface $request, UriInterface $uri, Promise $promise)
{
$cookie = new Cookie('name', 'value', null, 'test.com', false, false, null, (new \DateTime())->modify('-1 day'));
$this->cookieJar->addCookie($cookie);

$request->withAddedHeader('Cookie', 'name=value')->shouldNotBeCalled();

$this->handleRequest($request, function (RequestInterface $requestReceived) use ($request, $promise) {
if (Argument::is($requestReceived)->scoreArgument($request->getWrappedObject())) {
return $promise->getWrappedObject();
}
}, function () {});
}

function it_does_not_load_cookie_if_domain_does_not_match(RequestInterface $request, UriInterface $uri, Promise $promise)
{
$cookie = new Cookie('name', 'value', 86400, 'test2.com');
$this->cookieJar->addCookie($cookie);

$request->getUri()->willReturn($uri);
$uri->getHost()->willReturn('test.com');

$request->withAddedHeader('Cookie', 'name=value')->shouldNotBeCalled();

$this->handleRequest($request, function (RequestInterface $requestReceived) use ($request, $promise) {
if (Argument::is($requestReceived)->scoreArgument($request->getWrappedObject())) {
return $promise->getWrappedObject();
}
}, function () {});
}

function it_does_not_load_cookie_if_path_does_not_match(RequestInterface $request, UriInterface $uri, Promise $promise)
{
$cookie = new Cookie('name', 'value', 86400, 'test.com', '/sub');
$this->cookieJar->addCookie($cookie);

$request->getUri()->willReturn($uri);
$uri->getHost()->willReturn('test.com');
$uri->getPath()->willReturn('/');

$request->withAddedHeader('Cookie', 'name=value')->shouldNotBeCalled();

$this->handleRequest($request, function (RequestInterface $requestReceived) use ($request, $promise) {
if (Argument::is($requestReceived)->scoreArgument($request->getWrappedObject())) {
return $promise->getWrappedObject();
}
}, function () {});
}

function it_does_not_load_cookie_when_cookie_is_secure(RequestInterface $request, UriInterface $uri, Promise $promise)
{
$cookie = new Cookie('name', 'value', 86400, 'test.com', null, true);
$this->cookieJar->addCookie($cookie);

$request->getUri()->willReturn($uri);
$uri->getHost()->willReturn('test.com');
$uri->getPath()->willReturn('/');
$uri->getScheme()->willReturn('http');

$request->withAddedHeader('Cookie', 'name=value')->shouldNotBeCalled();

$this->handleRequest($request, function (RequestInterface $requestReceived) use ($request, $promise) {
if (Argument::is($requestReceived)->scoreArgument($request->getWrappedObject())) {
return $promise->getWrappedObject();
}
}, function () {});
}

function it_loads_cookie_when_cookie_is_secure(RequestInterface $request, UriInterface $uri, Promise $promise)
{
$cookie = new Cookie('name', 'value', 86400, 'test.com', null, true);
$this->cookieJar->addCookie($cookie);

$request->getUri()->willReturn($uri);
$uri->getHost()->willReturn('test.com');
$uri->getPath()->willReturn('/');
$uri->getScheme()->willReturn('https');

$request->withAddedHeader('Cookie', 'name=value')->willReturn($request);

$this->handleRequest($request, function (RequestInterface $requestReceived) use ($request, $promise) {
if (Argument::is($requestReceived)->scoreArgument($request->getWrappedObject())) {
return $promise->getWrappedObject();
}
}, function () {});
}

function it_saves_cookie(RequestInterface $request, ResponseInterface $response, UriInterface $uri)
{
$next = function () use ($response) {
return new FulfilledPromise($response->getWrappedObject());
};

$response->hasHeader('Set-Cookie')->willReturn(true);
$response->getHeader('Set-Cookie')->willReturn([
'cookie=value; expires=Tuesday, 31-Mar-99 07:42:12 GMT; Max-Age=60; path=/; domain=test.com; secure; HttpOnly'
]);

$request->getUri()->willReturn($uri);
$uri->getHost()->willReturn('test.com');
$uri->getPath()->willReturn('/');

$promise = $this->handleRequest($request, $next, function () {});
$promise->shouldHaveType('Http\Promise\Promise');
$promise->wait()->shouldReturnAnInstanceOf('Psr\Http\Message\ResponseInterface');
}

function it_throws_exception_on_invalid_expires_date(
RequestInterface $request,
ResponseInterface $response,
UriInterface $uri
) {
$next = function () use ($response) {
return new FulfilledPromise($response->getWrappedObject());
};

$response->hasHeader('Set-Cookie')->willReturn(true);
$response->getHeader('Set-Cookie')->willReturn([
'cookie=value; expires=i-am-an-invalid-date;'
]);

$request->getUri()->willReturn($uri);
$uri->getHost()->willReturn('test.com');
$uri->getPath()->willReturn('/');

$promise = $this->handleRequest($request, $next, function () {});
$promise->shouldReturnAnInstanceOf('Http\Promise\RejectedPromise');
$promise->shouldThrow('Http\Client\Exception\TransferException')->duringWait();
}
}
Loading