<?php
namespace React\Tests\Socket;
use React\Promise;
use React\Promise\Deferred;
use React\Socket\DnsConnector;
class DnsConnectorTest extends TestCase
{
private $tcp;
private $resolver;
private $connector;
public function setUp()
{
$this->tcp = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock();
$this->resolver = $this->getMockBuilder('React\Dns\Resolver\Resolver')->disableOriginalConstructor()->getMock();
$this->connector = new DnsConnector($this->tcp, $this->resolver);
}
public function testPassByResolverIfGivenIp()
{
$this->resolver->expects($this->never())->method('resolve');
$this->tcp->expects($this->once())->method('connect')->with($this->equalTo('127.0.0.1:80'))->will($this->returnValue(Promise\reject()));
$this->connector->connect('127.0.0.1:80');
}
public function testPassThroughResolverIfGivenHost()
{
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('1.2.3.4')));
$this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80?hostname=google.com'))->will($this->returnValue(Promise\reject()));
$this->connector->connect('google.com:80');
}
public function testPassThroughResolverIfGivenHostWhichResolvesToIpv6()
{
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('::1')));
$this->tcp->expects($this->once())->method('connect')->with($this->equalTo('[::1]:80?hostname=google.com'))->will($this->returnValue(Promise\reject()));
$this->connector->connect('google.com:80');
}
public function testPassByResolverIfGivenCompleteUri()
{
$this->resolver->expects($this->never())->method('resolve');
$this->tcp->expects($this->once())->method('connect')->with($this->equalTo('scheme://127.0.0.1:80/path?query#fragment'))->will($this->returnValue(Promise\reject()));
$this->connector->connect('scheme://127.0.0.1:80/path?query#fragment');
}
public function testPassThroughResolverIfGivenCompleteUri()
{
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('1.2.3.4')));
$this->tcp->expects($this->once())->method('connect')->with($this->equalTo('scheme://1.2.3.4:80/path?query&hostname=google.com#fragment'))->will($this->returnValue(Promise\reject()));
$this->connector->connect('scheme://google.com:80/path?query#fragment');
}
public function testPassThroughResolverIfGivenExplicitHost()
{
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('google.com'))->will($this->returnValue(Promise\resolve('1.2.3.4')));
$this->tcp->expects($this->once())->method('connect')->with($this->equalTo('scheme://1.2.3.4:80/?hostname=google.de'))->will($this->returnValue(Promise\reject()));
$this->connector->connect('scheme://google.com:80/?hostname=google.de');
}
public function testRejectsImmediatelyIfUriIsInvalid()
{
$this->resolver->expects($this->never())->method('resolve');
$this->tcp->expects($this->never())->method('connect');
$promise = $this->connector->connect('////');
$promise->then($this->expectCallableNever(), $this->expectCallableOnce());
}
/**
* @expectedException RuntimeException
* @expectedExceptionMessage Connection failed
*/
public function testRejectsWithTcpConnectorRejectionIfGivenIp()
{
$promise = Promise\reject(new \RuntimeException('Connection failed'));
$this->resolver->expects($this->never())->method('resolve');
$this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80'))->willReturn($promise);
$promise = $this->connector->connect('1.2.3.4:80');
$promise->cancel();
$this->throwRejection($promise);
}
/**
* @expectedException RuntimeException
* @expectedExceptionMessage Connection failed
*/
public function testRejectsWithTcpConnectorRejectionAfterDnsIsResolved()
{
$promise = Promise\reject(new \RuntimeException('Connection failed'));
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->willReturn(Promise\resolve('1.2.3.4'));
$this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80?hostname=example.com'))->willReturn($promise);
$promise = $this->connector->connect('example.com:80');
$promise->cancel();
$this->throwRejection($promise);
}
/**
* @expectedException RuntimeException
* @expectedExceptionMessage Connection to example.invalid:80 failed during DNS lookup: DNS error
*/
public function testSkipConnectionIfDnsFails()
{
$promise = Promise\reject(new \RuntimeException('DNS error'));
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.invalid'))->willReturn($promise);
$this->tcp->expects($this->never())->method('connect');
$promise = $this->connector->connect('example.invalid:80');
$this->throwRejection($promise);
}
public function testRejectionExceptionUsesPreviousExceptionIfDnsFails()
{
$exception = new \RuntimeException();
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.invalid'))->willReturn(Promise\reject($exception));
$promise = $this->connector->connect('example.invalid:80');
$promise->then(null, function ($e) {
throw $e->getPrevious();
})->then(null, $this->expectCallableOnceWith($this->identicalTo($exception)));
}
/**
* @expectedException RuntimeException
* @expectedExceptionMessage Connection to example.com:80 cancelled during DNS lookup
*/
public function testCancelDuringDnsCancelsDnsAndDoesNotStartTcpConnection()
{
$pending = new Promise\Promise(function () { }, $this->expectCallableOnce());
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->will($this->returnValue($pending));
$this->tcp->expects($this->never())->method('connect');
$promise = $this->connector->connect('example.com:80');
$promise->cancel();
$this->throwRejection($promise);
}
public function testCancelDuringTcpConnectionCancelsTcpConnectionIfGivenIp()
{
$pending = new Promise\Promise(function () { }, $this->expectCallableOnce());
$this->resolver->expects($this->never())->method('resolve');
$this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80'))->willReturn($pending);
$promise = $this->connector->connect('1.2.3.4:80');
$promise->cancel();
}
public function testCancelDuringTcpConnectionCancelsTcpConnectionAfterDnsIsResolved()
{
$pending = new Promise\Promise(function () { }, $this->expectCallableOnce());
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->willReturn(Promise\resolve('1.2.3.4'));
$this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80?hostname=example.com'))->willReturn($pending);
$promise = $this->connector->connect('example.com:80');
$promise->cancel();
}
/**
* @expectedException RuntimeException
* @expectedExceptionMessage Connection cancelled
*/
public function testCancelDuringTcpConnectionCancelsTcpConnectionWithTcpRejectionAfterDnsIsResolved()
{
$first = new Deferred();
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->willReturn($first->promise());
$pending = new Promise\Promise(function () { }, function () {
throw new \RuntimeException('Connection cancelled');
});
$this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80?hostname=example.com'))->willReturn($pending);
$promise = $this->connector->connect('example.com:80');
$first->resolve('1.2.3.4');
$promise->cancel();
$this->throwRejection($promise);
}
public function testRejectionDuringDnsLookupShouldNotCreateAnyGarbageReferences()
{
if (class_exists('React\Promise\When')) {
$this->markTestSkipped('Not supported on legacy Promise v1 API');
}
gc_collect_cycles();
$dns = new Deferred();
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->willReturn($dns->promise());
$this->tcp->expects($this->never())->method('connect');
$promise = $this->connector->connect('example.com:80');
$dns->reject(new \RuntimeException('DNS failed'));
unset($promise, $dns);
$this->assertEquals(0, gc_collect_cycles());
}
public function testRejectionAfterDnsLookupShouldNotCreateAnyGarbageReferences()
{
if (class_exists('React\Promise\When')) {
$this->markTestSkipped('Not supported on legacy Promise v1 API');
}
gc_collect_cycles();
$dns = new Deferred();
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->willReturn($dns->promise());
$tcp = new Deferred();
$this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80?hostname=example.com'))->willReturn($tcp->promise());
$promise = $this->connector->connect('example.com:80');
$dns->resolve('1.2.3.4');
$tcp->reject(new \RuntimeException('Connection failed'));
unset($promise, $dns, $tcp);
$this->assertEquals(0, gc_collect_cycles());
}
public function testRejectionAfterDnsLookupShouldNotCreateAnyGarbageReferencesAgain()
{
if (class_exists('React\Promise\When')) {
$this->markTestSkipped('Not supported on legacy Promise v1 API');
}
gc_collect_cycles();
$dns = new Deferred();
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->willReturn($dns->promise());
$tcp = new Deferred();
$dns->promise()->then(function () use ($tcp) {
$tcp->reject(new \RuntimeException('Connection failed'));
});
$this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80?hostname=example.com'))->willReturn($tcp->promise());
$promise = $this->connector->connect('example.com:80');
$dns->resolve('1.2.3.4');
unset($promise, $dns, $tcp);
$this->assertEquals(0, gc_collect_cycles());
}
public function testCancelDuringDnsLookupShouldNotCreateAnyGarbageReferences()
{
if (class_exists('React\Promise\When')) {
$this->markTestSkipped('Not supported on legacy Promise v1 API');
}
gc_collect_cycles();
$dns = new Deferred(function () {
throw new \RuntimeException();
});
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->willReturn($dns->promise());
$this->tcp->expects($this->never())->method('connect');
$promise = $this->connector->connect('example.com:80');
$promise->cancel();
unset($promise, $dns);
$this->assertEquals(0, gc_collect_cycles());
}
public function testCancelDuringTcpConnectionShouldNotCreateAnyGarbageReferences()
{
if (class_exists('React\Promise\When')) {
$this->markTestSkipped('Not supported on legacy Promise v1 API');
}
gc_collect_cycles();
$dns = new Deferred();
$this->resolver->expects($this->once())->method('resolve')->with($this->equalTo('example.com'))->willReturn($dns->promise());
$tcp = new Promise\Promise(function () { }, function () {
throw new \RuntimeException('Connection cancelled');
});
$this->tcp->expects($this->once())->method('connect')->with($this->equalTo('1.2.3.4:80?hostname=example.com'))->willReturn($tcp);
$promise = $this->connector->connect('example.com:80');
$dns->resolve('1.2.3.4');
$promise->cancel();
unset($promise, $dns, $tcp);
$this->assertEquals(0, gc_collect_cycles());
}
private function throwRejection($promise)
{
$ex = null;
$promise->then(null, function ($e) use (&$ex) {
$ex = $e;
});
throw $ex;
}
}