diff --git a/examples/92-query-any.php b/examples/92-query-any.php index 3b98fe2a..17383cff 100644 --- a/examples/92-query-any.php +++ b/examples/92-query-any.php @@ -73,7 +73,7 @@ break; default: // unknown type uses HEX format - $type = 'Type ' . $answer->type; + $type = 'TYPE' . $answer->type; $data = wordwrap(strtoupper(bin2hex($data)), 2, ' ', true); } diff --git a/src/Query/CachingExecutor.php b/src/Query/CachingExecutor.php index 9e0bec05..e530b24c 100644 --- a/src/Query/CachingExecutor.php +++ b/src/Query/CachingExecutor.php @@ -57,7 +57,7 @@ function (Message $message) use ($cache, $id, $that) { $pending = null; }); }, function ($_, $reject) use (&$pending, $query) { - $reject(new \RuntimeException('DNS query for ' . $query->name . ' has been cancelled')); + $reject(new \RuntimeException('DNS query for ' . $query->describe() . ' has been cancelled')); $pending->cancel(); $pending = null; }); diff --git a/src/Query/CoopExecutor.php b/src/Query/CoopExecutor.php index 93c97d48..2c6653c3 100644 --- a/src/Query/CoopExecutor.php +++ b/src/Query/CoopExecutor.php @@ -81,7 +81,7 @@ public function query(Query $query) $promise->cancel(); $promise = null; } - throw new \RuntimeException('DNS query for ' . $query->name . ' has been cancelled'); + throw new \RuntimeException('DNS query for ' . $query->describe() . ' has been cancelled'); }); } diff --git a/src/Query/Query.php b/src/Query/Query.php index 7885023d..a3dcfb58 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -2,6 +2,8 @@ namespace React\Dns\Query; +use React\Dns\Model\Message; + /** * This class represents a single question in a query/response message * @@ -39,4 +41,29 @@ public function __construct($name, $type, $class) $this->type = $type; $this->class = $class; } + + /** + * Describes the hostname and query type/class for this query + * + * The output format is supposed to be human readable and is subject to change. + * The format is inspired by RFC 3597 when handling unkown types/classes. + * + * @return string "example.com (A)" or "example.com (CLASS0 TYPE1234)" + * @link https://tools.ietf.org/html/rfc3597 + */ + public function describe() + { + $class = $this->class !== Message::CLASS_IN ? 'CLASS' . $this->class . ' ' : ''; + + $type = 'TYPE' . $this->type; + $ref = new \ReflectionClass('React\Dns\Model\Message'); + foreach ($ref->getConstants() as $name => $value) { + if ($value === $this->type && \strpos($name, 'TYPE_') === 0) { + $type = \substr($name, 5); + break; + } + } + + return $this->name . ' (' . $class . $type . ')'; + } } diff --git a/src/Query/RetryExecutor.php b/src/Query/RetryExecutor.php index d9cbfe56..7efcacc8 100644 --- a/src/Query/RetryExecutor.php +++ b/src/Query/RetryExecutor.php @@ -43,7 +43,7 @@ public function tryQuery(Query $query, $retries) } elseif ($retries <= 0) { $errorback = null; $deferred->reject($e = new \RuntimeException( - 'DNS query for ' . $query->name . ' failed: too many retries', + 'DNS query for ' . $query->describe() . ' failed: too many retries', 0, $e )); diff --git a/src/Query/TcpTransportExecutor.php b/src/Query/TcpTransportExecutor.php index 05dab587..959cf742 100644 --- a/src/Query/TcpTransportExecutor.php +++ b/src/Query/TcpTransportExecutor.php @@ -147,7 +147,7 @@ public function __construct($nameserver, LoopInterface $loop) throw new \InvalidArgumentException('Invalid nameserver address given'); } - $this->nameserver = $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53); + $this->nameserver = 'tcp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53); $this->loop = $loop; $this->parser = new Parser(); $this->dumper = new BinaryDumper(); @@ -166,7 +166,7 @@ public function query(Query $query) $length = \strlen($queryData); if ($length > 0xffff) { return \React\Promise\reject(new \RuntimeException( - 'DNS query for ' . $query->name . ' failed: Query too large for TCP transport' + 'DNS query for ' . $query->describe() . ' failed: Query too large for TCP transport' )); } @@ -177,7 +177,7 @@ public function query(Query $query) $socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0, \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT); if ($socket === false) { return \React\Promise\reject(new \RuntimeException( - 'DNS query for ' . $query->name . ' failed: Unable to connect to DNS server (' . $errstr . ')', + 'DNS query for ' . $query->describe() . ' failed: Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')', $errno )); } @@ -214,7 +214,7 @@ public function query(Query $query) }); $this->pending[$request->id] = $deferred; - $this->names[$request->id] = $query->name; + $this->names[$request->id] = $query->describe(); return $deferred->promise(); } @@ -227,7 +227,19 @@ public function handleWritable() if ($this->readPending === false) { $name = @\stream_socket_get_name($this->socket, true); if ($name === false) { - $this->closeError('Connection to DNS server rejected'); + // Connection failed? Check socket error if available for underlying errno/errstr. + // @codeCoverageIgnoreStart + if (\function_exists('socket_import_stream')) { + $socket = \socket_import_stream($this->socket); + $errno = \socket_get_option($socket, \SOL_SOCKET, \SO_ERROR); + $errstr = \socket_strerror($errno); + } else { + $errno = \defined('SOCKET_ECONNREFUSED') ? \SOCKET_ECONNREFUSED : 111; + $errstr = 'Connection refused'; + } + // @codeCoverageIgnoreEnd + + $this->closeError('Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')', $errno); return; } @@ -240,7 +252,7 @@ public function handleWritable() $error = \error_get_last(); \preg_match('/errno=(\d+) (.+)/', $error['message'], $m); $this->closeError( - 'Unable to send query to DNS server (' . (isset($m[2]) ? $m[2] : $error['message']) . ')', + 'Unable to send query to DNS server ' . $this->nameserver . ' (' . (isset($m[2]) ? $m[2] : $error['message']) . ')', isset($m[1]) ? (int) $m[1] : 0 ); return; @@ -264,7 +276,7 @@ public function handleRead() // any error is fatal, this is a stream of TCP/IP data $chunk = @\fread($this->socket, 65536); if ($chunk === false || $chunk === '') { - $this->closeError('Connection to DNS server lost'); + $this->closeError('Connection to DNS server ' . $this->nameserver . ' lost'); return; } @@ -286,13 +298,13 @@ public function handleRead() $response = $this->parser->parseMessage($data); } catch (\Exception $e) { // reject all pending queries if we received an invalid message from remote server - $this->closeError('Invalid message received from DNS server'); + $this->closeError('Invalid message received from DNS server ' . $this->nameserver); return; } // reject all pending queries if we received an unexpected response ID or truncated response if (!isset($this->pending[$response->id]) || $response->tc) { - $this->closeError('Invalid response message received from DNS server'); + $this->closeError('Invalid response message received from DNS server ' . $this->nameserver); return; } diff --git a/src/Query/TimeoutExecutor.php b/src/Query/TimeoutExecutor.php index c6fd2387..e20ff596 100644 --- a/src/Query/TimeoutExecutor.php +++ b/src/Query/TimeoutExecutor.php @@ -22,7 +22,7 @@ public function query(Query $query) { return Timer\timeout($this->executor->query($query), $this->timeout, $this->loop)->then(null, function ($e) use ($query) { if ($e instanceof Timer\TimeoutException) { - $e = new TimeoutException(sprintf("DNS query for %s timed out", $query->name), 0, $e); + $e = new TimeoutException(sprintf("DNS query for %s timed out", $query->describe()), 0, $e); } throw $e; }); diff --git a/src/Query/UdpTransportExecutor.php b/src/Query/UdpTransportExecutor.php index ff3169db..0b7e7669 100644 --- a/src/Query/UdpTransportExecutor.php +++ b/src/Query/UdpTransportExecutor.php @@ -128,7 +128,7 @@ public function query(Query $query) $queryData = $this->dumper->toBinary($request); if (isset($queryData[$this->maxPacketSize])) { return \React\Promise\reject(new \RuntimeException( - 'DNS query for ' . $query->name . ' failed: Query too large for UDP transport', + 'DNS query for ' . $query->describe() . ' failed: Query too large for UDP transport', \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 )); } @@ -137,7 +137,7 @@ public function query(Query $query) $socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0); if ($socket === false) { return \React\Promise\reject(new \RuntimeException( - 'DNS query for ' . $query->name . ' failed: Unable to connect to DNS server (' . $errstr . ')', + 'DNS query for ' . $query->describe() . ' failed: Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')', $errno )); } @@ -154,7 +154,7 @@ public function query(Query $query) $error = \error_get_last(); \preg_match('/errno=(\d+) (.+)/', $error['message'], $m); return \React\Promise\reject(new \RuntimeException( - 'DNS query for ' . $query->name . ' failed: Unable to send query to DNS server (' . (isset($m[2]) ? $m[2] : $error['message']) . ')', + 'DNS query for ' . $query->describe() . ' failed: Unable to send query to DNS server ' . $this->nameserver . ' (' . (isset($m[2]) ? $m[2] : $error['message']) . ')', isset($m[1]) ? (int) $m[1] : 0 )); } @@ -165,12 +165,13 @@ public function query(Query $query) $loop->removeReadStream($socket); \fclose($socket); - throw new CancellationException('DNS query for ' . $query->name . ' has been cancelled'); + throw new CancellationException('DNS query for ' . $query->describe() . ' has been cancelled'); }); $max = $this->maxPacketSize; $parser = $this->parser; - $loop->addReadStream($socket, function ($socket) use ($loop, $deferred, $query, $parser, $request, $max) { + $nameserver = $this->nameserver; + $loop->addReadStream($socket, function ($socket) use ($loop, $deferred, $query, $parser, $request, $max, $nameserver) { // try to read a single data packet from the DNS server // ignoring any errors, this is uses UDP packets and not a stream of data $data = @\fread($socket, $max); @@ -198,7 +199,7 @@ public function query(Query $query) if ($response->tc) { $deferred->reject(new \RuntimeException( - 'DNS query for ' . $query->name . ' failed: The server returned a truncated result for a UDP query', + 'DNS query for ' . $query->describe() . ' failed: The DNS server ' . $nameserver . ' returned a truncated result for a UDP query', \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 )); return; diff --git a/src/Resolver/Resolver.php b/src/Resolver/Resolver.php index 71a9b93a..92926f3f 100644 --- a/src/Resolver/Resolver.php +++ b/src/Resolver/Resolver.php @@ -72,7 +72,7 @@ public function extractValues(Query $query, Message $response) $message = 'Unknown error response code ' . $code; } throw new RecordNotFoundException( - 'DNS query for ' . $query->name . ' returned an error response (' . $message . ')', + 'DNS query for ' . $query->describe() . ' returned an error response (' . $message . ')', $code ); } @@ -83,7 +83,7 @@ public function extractValues(Query $query, Message $response) // reject if we did not receive a valid answer (domain is valid, but no record for this type could be found) if (0 === count($addresses)) { throw new RecordNotFoundException( - 'DNS query for ' . $query->name . ' did not return a valid answer (NOERROR / NODATA)' + 'DNS query for ' . $query->describe() . ' did not return a valid answer (NOERROR / NODATA)' ); } diff --git a/tests/FunctionalResolverTest.php b/tests/FunctionalResolverTest.php index ca037fee..9cb05615 100644 --- a/tests/FunctionalResolverTest.php +++ b/tests/FunctionalResolverTest.php @@ -107,14 +107,19 @@ public function testResolveAllGoogleCaaResolvesWithCache() */ public function testResolveInvalidRejects() { - $ex = $this->callback(function ($param) { - return ($param instanceof RecordNotFoundException && $param->getCode() === Message::RCODE_NAME_ERROR); - }); - $promise = $this->resolver->resolve('example.invalid'); - $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($ex)); $this->loop->run(); + + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + /** @var \React\Dns\RecordNotFoundException $exception */ + $this->assertInstanceOf('React\Dns\RecordNotFoundException', $exception); + $this->assertEquals('DNS query for example.invalid (A) returned an error response (Non-Existent Domain / NXDOMAIN)', $exception->getMessage()); + $this->assertEquals(Message::RCODE_NAME_ERROR, $exception->getCode()); } public function testResolveCancelledRejectsImmediately() @@ -122,12 +127,7 @@ public function testResolveCancelledRejectsImmediately() // max_nesting_level was set to 100 for PHP Versions < 5.4 which resulted in failing test for legacy PHP ini_set('xdebug.max_nesting_level', 256); - $ex = $this->callback(function ($param) { - return ($param instanceof \RuntimeException && $param->getMessage() === 'DNS query for google.com has been cancelled'); - }); - $promise = $this->resolver->resolve('google.com'); - $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($ex)); $promise->cancel(); $time = microtime(true); @@ -135,6 +135,35 @@ public function testResolveCancelledRejectsImmediately() $time = microtime(true) - $time; $this->assertLessThan(0.1, $time); + + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + /** @var \React\Dns\Query\CancellationException $exception */ + $this->assertInstanceOf('React\Dns\Query\CancellationException', $exception); + $this->assertEquals('DNS query for google.com (A) has been cancelled', $exception->getMessage()); + } + + /** + * @group internet + */ + public function testResolveAllInvalidTypeRejects() + { + $promise = $this->resolver->resolveAll('google.com', Message::TYPE_PTR); + + $this->loop->run(); + + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + /** @var \React\Dns\RecordNotFoundException $exception */ + $this->assertInstanceOf('React\Dns\RecordNotFoundException', $exception); + $this->assertEquals('DNS query for google.com (PTR) did not return a valid answer (NOERROR / NODATA)', $exception->getMessage()); + $this->assertEquals(0, $exception->getCode()); } public function testInvalidResolverDoesNotResolveGoogle() diff --git a/tests/Query/CachingExecutorTest.php b/tests/Query/CachingExecutorTest.php index b0aa4c2f..2a4250fc 100644 --- a/tests/Query/CachingExecutorTest.php +++ b/tests/Query/CachingExecutorTest.php @@ -129,7 +129,7 @@ public function testQueryWillReturnRejectedPromiseWhenCacheReturnsMissAndFallbac $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); - $fallback->expects($this->once())->method('query')->willReturn(\React\Promise\reject(new \RuntimeException())); + $fallback->expects($this->once())->method('query')->willReturn(\React\Promise\reject($exception = new \RuntimeException())); $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); $cache->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null)); @@ -138,7 +138,7 @@ public function testQueryWillReturnRejectedPromiseWhenCacheReturnsMissAndFallbac $promise = $executor->query($query); - $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($exception)); } public function testCancelQueryWillReturnRejectedPromiseAndCancelPendingPromiseFromCache() @@ -157,7 +157,14 @@ public function testCancelQueryWillReturnRejectedPromiseAndCancelPendingPromiseF $promise = $executor->query($query); $promise->cancel(); - $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('DNS query for reactphp.org (A) has been cancelled', $exception->getMessage()); } public function testCancelQueryWillReturnRejectedPromiseAndCancelPendingPromiseFromFallbackExecutorWhenCacheReturnsMiss() @@ -178,6 +185,13 @@ public function testCancelQueryWillReturnRejectedPromiseAndCancelPendingPromiseF $deferred->resolve(null); $promise->cancel(); - $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('DNS query for reactphp.org (A) has been cancelled', $exception->getMessage()); } } diff --git a/tests/Query/CoopExecutorTest.php b/tests/Query/CoopExecutorTest.php index 423f5c59..44ea9676 100644 --- a/tests/Query/CoopExecutorTest.php +++ b/tests/Query/CoopExecutorTest.php @@ -127,7 +127,14 @@ public function testCancelQueryWillCancelPromiseFromBaseExecutorAndReject() $promise->cancel(); - $promise->then(null, $this->expectCallableOnce()); + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('DNS query for reactphp.org (A) has been cancelled', $exception->getMessage()); } public function testCancelOneQueryWhenOtherQueryIsStillPendingWillNotCancelPromiseFromBaseExecutorAndRejectCancelled() diff --git a/tests/Query/QueryTest.php b/tests/Query/QueryTest.php new file mode 100644 index 00000000..79935ba2 --- /dev/null +++ b/tests/Query/QueryTest.php @@ -0,0 +1,24 @@ +assertEquals('example.com (A)', $query->describe()); + } + + public function testDescribeUnknownType() + { + $query = new Query('example.com', 0, 0); + + $this->assertEquals('example.com (CLASS0 TYPE0)', $query->describe()); + } +} diff --git a/tests/Query/RetryExecutorTest.php b/tests/Query/RetryExecutorTest.php index 71792160..754846f9 100644 --- a/tests/Query/RetryExecutorTest.php +++ b/tests/Query/RetryExecutorTest.php @@ -84,18 +84,19 @@ public function queryShouldStopRetryingAfterSomeAttempts() return Promise\reject(new TimeoutException("timeout")); })); - $callback = $this->expectCallableNever(); - - $errorback = $this->createCallableMock(); - $errorback - ->expects($this->once()) - ->method('__invoke') - ->with($this->isInstanceOf('RuntimeException')); - $retryExecutor = new RetryExecutor($executor, 2); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); - $retryExecutor->query($query)->then($callback, $errorback); + $promise = $retryExecutor->query($query); + + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('DNS query for igor.io (A) failed: too many retries', $exception->getMessage()); } /** diff --git a/tests/Query/TcpTransportExecutorTest.php b/tests/Query/TcpTransportExecutorTest.php index 0d6d988f..322815a8 100644 --- a/tests/Query/TcpTransportExecutorTest.php +++ b/tests/Query/TcpTransportExecutorTest.php @@ -35,27 +35,27 @@ public static function provideDefaultPortProvider() return array( array( '8.8.8.8', - '8.8.8.8:53' + 'tcp://8.8.8.8:53' ), array( '1.2.3.4:5', - '1.2.3.4:5' + 'tcp://1.2.3.4:5' ), array( 'tcp://1.2.3.4', - '1.2.3.4:53' + 'tcp://1.2.3.4:53' ), array( 'tcp://1.2.3.4:53', - '1.2.3.4:53' + 'tcp://1.2.3.4:53' ), array( '::1', - '[::1]:53' + 'tcp://[::1]:53' ), array( '[::1]:53', - '[::1]:53' + 'tcp://[::1]:53' ) ); } @@ -94,12 +94,22 @@ public function testQueryRejectsIfMessageExceedsMaximumMessageSize() $query = new Query('google.' . str_repeat('.com', 60000), Message::TYPE_A, Message::CLASS_IN); $promise = $executor->query($query); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); - $promise->then(null, $this->expectCallableOnce()); + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('DNS query for '. $query->name . ' (A) failed: Query too large for TCP transport', $exception->getMessage()); } public function testQueryRejectsIfServerConnectionFails() { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('HHVM reports different error message for invalid addresses'); + } + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addWriteStream'); @@ -112,8 +122,14 @@ public function testQueryRejectsIfServerConnectionFails() $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); $promise = $executor->query($query); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); - $promise->then(null, $this->expectCallableOnce()); + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('DNS query for google.com (A) failed: Unable to connect to DNS server /// (Failed to parse address "///")', $exception->getMessage()); } public function testQueryRejectsOnCancellationWithoutClosingSocketButStartsIdleTimer() @@ -137,8 +153,14 @@ public function testQueryRejectsOnCancellationWithoutClosingSocketButStartsIdleT $promise = $executor->query($query); $promise->cancel(); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); - $promise->then(null, $this->expectCallableOnce()); + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + /** @var \React\Dns\Query\CancellationException $exception */ + $this->assertInstanceOf('React\Dns\Query\CancellationException', $exception); + $this->assertEquals('DNS query for google.com (A) has been cancelled', $exception->getMessage()); } public function testTriggerIdleTimerAfterQueryRejectedOnCancellationWillCloseSocket() @@ -228,21 +250,23 @@ public function testQueryRejectsWhenServerIsNotListening() $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); - $wait = true; + $exception = null; $executor->query($query)->then( null, - function ($e) use (&$wait) { - $wait = false; - throw $e; + function ($e) use (&$exception) { + $exception = $e; } ); \Clue\React\Block\sleep(0.01, $loop); - if ($wait) { + if ($exception === null) { \Clue\React\Block\sleep(0.2, $loop); } - $this->assertFalse($wait); + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('DNS query for google.com (A) failed: Unable to connect to DNS server tcp://127.0.0.1:1 (Connection refused)', $exception->getMessage()); + $this->assertEquals(defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $exception->getCode()); } public function testQueryStaysPendingWhenClientCanNotSendExcessiveMessageInOneChunk() @@ -367,7 +391,7 @@ public function testQueryRejectsWhenClientKeepsSendingWhenServerClosesSocket() // expect EPIPE (Broken pipe), except for macOS kernel race condition or legacy HHVM $this->setExpectedException( 'RuntimeException', - 'Unable to send query to DNS server', + 'Unable to send query to DNS server tcp://' . $address . ' (', defined('SOCKET_EPIPE') && !defined('HHVM_VERSION') ? (PHP_OS !== 'Darwin' || $writePending ? SOCKET_EPIPE : SOCKET_EPROTOTYPE) : null ); throw $exception; @@ -388,21 +412,22 @@ public function testQueryRejectsWhenServerClosesConnection() $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); - $wait = true; + $exception = null; $executor->query($query)->then( null, - function ($e) use (&$wait) { - $wait = false; - throw $e; + function ($e) use (&$exception) { + $exception = $e; } ); \Clue\React\Block\sleep(0.01, $loop); - if ($wait) { + if ($exception === null) { \Clue\React\Block\sleep(0.2, $loop); } - $this->assertFalse($wait); + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('DNS query for google.com (A) failed: Connection to DNS server tcp://' . $address . ' lost', $exception->getMessage()); } public function testQueryKeepsPendingIfServerSendsIncompleteMessageLength() @@ -491,21 +516,22 @@ public function testQueryRejectsWhenServerSendsInvalidMessage() $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); - $wait = true; + $exception = null; $executor->query($query)->then( null, - function ($e) use (&$wait) { - $wait = false; - throw $e; + function ($e) use (&$exception) { + $exception = $e; } ); \Clue\React\Block\sleep(0.01, $loop); - if ($wait) { + if ($exception === null) { \Clue\React\Block\sleep(0.2, $loop); } - $this->assertFalse($wait); + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('DNS query for google.com (A) failed: Invalid message received from DNS server tcp://' . $address, $exception->getMessage()); } public function testQueryRejectsWhenServerSendsInvalidId() @@ -541,21 +567,22 @@ public function testQueryRejectsWhenServerSendsInvalidId() $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); - $wait = true; + $exception = null; $executor->query($query)->then( null, - function ($e) use (&$wait) { - $wait = false; - throw $e; + function ($e) use (&$exception) { + $exception = $e; } ); \Clue\React\Block\sleep(0.01, $loop); - if ($wait) { + if ($exception === null) { \Clue\React\Block\sleep(0.2, $loop); } - $this->assertFalse($wait); + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('DNS query for google.com (A) failed: Invalid response message received from DNS server tcp://' . $address, $exception->getMessage()); } public function testQueryRejectsIfServerSendsTruncatedResponse() @@ -591,21 +618,22 @@ public function testQueryRejectsIfServerSendsTruncatedResponse() $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); - $wait = true; + $exception = null; $executor->query($query)->then( null, - function ($e) use (&$wait) { - $wait = false; - throw $e; + function ($e) use (&$exception) { + $exception = $e; } ); \Clue\React\Block\sleep(0.01, $loop); - if ($wait) { + if ($exception === null) { \Clue\React\Block\sleep(0.2, $loop); } - $this->assertFalse($wait); + /** @var \RuntimeException $exception */ + $this->assertInstanceOf('RuntimeException', $exception); + $this->assertEquals('DNS query for google.com (A) failed: Invalid response message received from DNS server tcp://' . $address, $exception->getMessage()); } public function testQueryResolvesIfServerSendsValidResponse() diff --git a/tests/Query/TimeoutExecutorTest.php b/tests/Query/TimeoutExecutorTest.php index a6c2728e..0df5aa78 100644 --- a/tests/Query/TimeoutExecutorTest.php +++ b/tests/Query/TimeoutExecutorTest.php @@ -105,7 +105,7 @@ public function testWrappedWillBeCancelledOnTimeout() \Clue\React\Block\await($promise, $this->loop); $this->fail(); } catch (TimeoutException $exception) { - $this->assertEquals('DNS query for igor.io timed out' , $exception->getMessage()); + $this->assertEquals('DNS query for igor.io (A) timed out' , $exception->getMessage()); } $this->assertEquals(1, $cancelled); diff --git a/tests/Query/UdpTransportExecutorTest.php b/tests/Query/UdpTransportExecutorTest.php index 25ce4429..08a4a960 100644 --- a/tests/Query/UdpTransportExecutorTest.php +++ b/tests/Query/UdpTransportExecutorTest.php @@ -101,12 +101,20 @@ public function testQueryRejectsIfMessageExceedsUdpSize() $exception = $reason; }); - $this->setExpectedException('RuntimeException', '', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90); + $this->setExpectedException( + 'RuntimeException', + 'DNS query for ' . $query->name . ' (A) failed: Query too large for UDP transport', + defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90 + ); throw $exception; } public function testQueryRejectsIfServerConnectionFails() { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('HHVM reports different error message for invalid addresses'); + } + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addReadStream'); @@ -126,8 +134,10 @@ public function testQueryRejectsIfServerConnectionFails() $exception = $reason; }); - // PHP (Failed to parse address "///") differs from HHVM (Name or service not known) - $this->setExpectedException('RuntimeException', 'Unable to connect to DNS server'); + $this->setExpectedException( + 'RuntimeException', + 'DNS query for google.com (A) failed: Unable to connect to DNS server /// (Failed to parse address "///")' + ); throw $exception; } @@ -154,7 +164,10 @@ public function testQueryRejectsIfSendToServerFailsAfterConnection() }); // ECONNREFUSED (Connection refused) on Linux, EMSGSIZE (Message too long) on macOS - $this->setExpectedException('RuntimeException', 'Unable to send query to DNS server'); + $this->setExpectedException( + 'RuntimeException', + 'DNS query for ' . $query->name . ' (A) failed: Unable to send query to DNS server udp://0.0.0.0:53 (' + ); throw $exception; } @@ -206,8 +219,14 @@ public function testQueryRejectsOnCancellation() $promise = $executor->query($query); $promise->cancel(); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); - $promise->then(null, $this->expectCallableOnce()); + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + /** @var \React\Dns\Query\CancellationException $exception */ + $this->assertInstanceOf('React\Dns\Query\CancellationException', $exception); + $this->assertEquals('DNS query for google.com (A) has been cancelled', $exception->getMessage()); } public function testQueryKeepsPendingIfServerSendsInvalidMessage() @@ -297,7 +316,11 @@ public function testQueryRejectsIfServerSendsTruncatedResponse() $promise = $executor->query($query); - $this->setExpectedException('RuntimeException', '', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90); + $this->setExpectedException( + 'RuntimeException', + 'DNS query for google.com (A) failed: The DNS server udp://' . $address . ' returned a truncated result for a UDP query', + defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90 + ); \Clue\React\Block\await($promise, $loop, 0.1); } diff --git a/tests/Resolver/ResolverTest.php b/tests/Resolver/ResolverTest.php index da7429ac..292d80d2 100644 --- a/tests/Resolver/ResolverTest.php +++ b/tests/Resolver/ResolverTest.php @@ -164,7 +164,7 @@ public function resolveWithNoAnswersShouldCallErrbackIfGiven() })); $errback = $this->expectCallableOnceWith($this->callback(function ($param) { - return ($param instanceof RecordNotFoundException && $param->getCode() === 0 && $param->getMessage() === 'DNS query for igor.io did not return a valid answer (NOERROR / NODATA)'); + return ($param instanceof RecordNotFoundException && $param->getCode() === 0 && $param->getMessage() === 'DNS query for igor.io (A) did not return a valid answer (NOERROR / NODATA)'); })); $resolver = new Resolver($executor); @@ -176,27 +176,27 @@ public function provideRcodeErrors() return array( array( Message::RCODE_FORMAT_ERROR, - 'DNS query for example.com returned an error response (Format Error)', + 'DNS query for example.com (A) returned an error response (Format Error)', ), array( Message::RCODE_SERVER_FAILURE, - 'DNS query for example.com returned an error response (Server Failure)', + 'DNS query for example.com (A) returned an error response (Server Failure)', ), array( Message::RCODE_NAME_ERROR, - 'DNS query for example.com returned an error response (Non-Existent Domain / NXDOMAIN)' + 'DNS query for example.com (A) returned an error response (Non-Existent Domain / NXDOMAIN)' ), array( Message::RCODE_NOT_IMPLEMENTED, - 'DNS query for example.com returned an error response (Not Implemented)' + 'DNS query for example.com (A) returned an error response (Not Implemented)' ), array( Message::RCODE_REFUSED, - 'DNS query for example.com returned an error response (Refused)' + 'DNS query for example.com (A) returned an error response (Refused)' ), array( 99, - 'DNS query for example.com returned an error response (Unknown error response code 99)' + 'DNS query for example.com (A) returned an error response (Unknown error response code 99)' ) ); }