Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix AsyncResolver to match ThreadedResolver behavior (#8270)
Browse files Browse the repository at this point in the history
Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) <sviat@redhat.com>
(cherry picked from commit 012f986)
bdraco committed Apr 5, 2024

Verified

This commit was signed with the committer’s verified signature.
bdraco J. Nick Koston
1 parent 6643115 commit 65d051d
Showing 10 changed files with 343 additions and 111 deletions.
9 changes: 9 additions & 0 deletions CHANGES/8270.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Fix ``AsyncResolver`` to match ``ThreadedResolver`` behavior
-- by :user:`bdraco`.

On system with IPv6 support, the :py:class:`~aiohttp.resolver.AsyncResolver` would not fallback
to providing A records when AAAA records were not available.
Additionally, unlike the :py:class:`~aiohttp.resolver.ThreadedResolver`, the :py:class:`~aiohttp.resolver.AsyncResolver`
did not handle link-local addresses correctly.

This change makes the behavior consistent with the :py:class:`~aiohttp.resolver.ThreadedResolver`.
28 changes: 27 additions & 1 deletion aiohttp/abc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import logging
import socket
from abc import ABC, abstractmethod
from collections.abc import Sized
from http.cookies import BaseCookie, Morsel
@@ -14,6 +15,7 @@
List,
Optional,
Tuple,
TypedDict,
)

from multidict import CIMultiDict
@@ -119,11 +121,35 @@ def __await__(self) -> Generator[Any, None, StreamResponse]:
"""Execute the view handler."""


class ResolveResult(TypedDict):
"""Resolve result.
This is the result returned from an AbstractResolver's
resolve method.
:param hostname: The hostname that was provided.
:param host: The IP address that was resolved.
:param port: The port that was resolved.
:param family: The address family that was resolved.
:param proto: The protocol that was resolved.
:param flags: The flags that were resolved.
"""

hostname: str
host: str
port: int
family: int
proto: int
flags: int


class AbstractResolver(ABC):
"""Abstract DNS resolver."""

@abstractmethod
async def resolve(self, host: str, port: int, family: int) -> List[Dict[str, Any]]:
async def resolve(
self, host: str, port: int = 0, family: int = socket.AF_INET
) -> List[ResolveResult]:
"""Return IP address for given hostname"""

@abstractmethod
16 changes: 8 additions & 8 deletions aiohttp/connector.py
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@
import attr

from . import hdrs, helpers
from .abc import AbstractResolver
from .abc import AbstractResolver, ResolveResult
from .client_exceptions import (
ClientConnectionError,
ClientConnectorCertificateError,
@@ -693,14 +693,14 @@ async def _create_connection(

class _DNSCacheTable:
def __init__(self, ttl: Optional[float] = None) -> None:
self._addrs_rr: Dict[Tuple[str, int], Tuple[Iterator[Dict[str, Any]], int]] = {}
self._addrs_rr: Dict[Tuple[str, int], Tuple[Iterator[ResolveResult], int]] = {}
self._timestamps: Dict[Tuple[str, int], float] = {}
self._ttl = ttl

def __contains__(self, host: object) -> bool:
return host in self._addrs_rr

def add(self, key: Tuple[str, int], addrs: List[Dict[str, Any]]) -> None:
def add(self, key: Tuple[str, int], addrs: List[ResolveResult]) -> None:
self._addrs_rr[key] = (cycle(addrs), len(addrs))

if self._ttl is not None:
@@ -716,7 +716,7 @@ def clear(self) -> None:
self._addrs_rr.clear()
self._timestamps.clear()

def next_addrs(self, key: Tuple[str, int]) -> List[Dict[str, Any]]:
def next_addrs(self, key: Tuple[str, int]) -> List[ResolveResult]:
loop, length = self._addrs_rr[key]
addrs = list(islice(loop, length))
# Consume one more element to shift internal state of `cycle`
@@ -834,7 +834,7 @@ def clear_dns_cache(

async def _resolve_host(
self, host: str, port: int, traces: Optional[List["Trace"]] = None
) -> List[Dict[str, Any]]:
) -> List[ResolveResult]:
"""Resolve host and return list of addresses."""
if is_ip_address(host):
return [
@@ -890,7 +890,7 @@ async def _resolve_host(
return await asyncio.shield(resolved_host_task)
except asyncio.CancelledError:

def drop_exception(fut: "asyncio.Future[List[Dict[str, Any]]]") -> None:
def drop_exception(fut: "asyncio.Future[List[ResolveResult]]") -> None:
with suppress(Exception, asyncio.CancelledError):
fut.result()

@@ -903,7 +903,7 @@ async def _resolve_host_with_throttle(
host: str,
port: int,
traces: Optional[List["Trace"]],
) -> List[Dict[str, Any]]:
) -> List[ResolveResult]:
"""Resolve host with a dns events throttle."""
if key in self._throttle_dns_events:
# get event early, before any await (#4014)
@@ -1217,7 +1217,7 @@ async def _start_tls_connection(
return tls_transport, tls_proto

def _convert_hosts_to_addr_infos(
self, hosts: List[Dict[str, Any]]
self, hosts: List[ResolveResult]
) -> List[aiohappyeyeballs.AddrInfoType]:
"""Converts the list of hosts to a list of addr_infos.
94 changes: 62 additions & 32 deletions aiohttp/resolver.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import asyncio
import socket
from typing import Any, Dict, List, Optional, Type, Union
import sys
from typing import Any, Dict, List, Optional, Tuple, Type, Union

from .abc import AbstractResolver
from .abc import AbstractResolver, ResolveResult
from .helpers import get_running_loop

__all__ = ("ThreadedResolver", "AsyncResolver", "DefaultResolver")

try:
import aiodns

# aiodns_default = hasattr(aiodns.DNSResolver, 'gethostbyname')
# aiodns_default = hasattr(aiodns.DNSResolver, 'getaddrinfo')
except ImportError: # pragma: no cover
aiodns = None


aiodns_default = False

_NUMERIC_SOCKET_FLAGS = socket.AI_NUMERICHOST | socket.AI_NUMERICSERV
_SUPPORTS_SCOPE_ID = sys.version_info >= (3, 9, 0)


class ThreadedResolver(AbstractResolver):
"""Threaded resolver.
@@ -28,45 +33,45 @@ def __init__(self, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
self._loop = get_running_loop(loop)

async def resolve(
self, hostname: str, port: int = 0, family: int = socket.AF_INET
) -> List[Dict[str, Any]]:
self, host: str, port: int = 0, family: int = socket.AF_INET
) -> List[ResolveResult]:
infos = await self._loop.getaddrinfo(
hostname,
host,
port,
type=socket.SOCK_STREAM,
family=family,
flags=socket.AI_ADDRCONFIG,
)

hosts = []
hosts: List[ResolveResult] = []
for family, _, proto, _, address in infos:
if family == socket.AF_INET6:
if len(address) < 3:
# IPv6 is not supported by Python build,
# or IPv6 is not enabled in the host
continue
if address[3]:
if address[3] and _SUPPORTS_SCOPE_ID:
# This is essential for link-local IPv6 addresses.
# LL IPv6 is a VERY rare case. Strictly speaking, we should use
# getnameinfo() unconditionally, but performance makes sense.
host, _port = socket.getnameinfo(
address, socket.NI_NUMERICHOST | socket.NI_NUMERICSERV
resolved_host, _port = await self._loop.getnameinfo(
address, _NUMERIC_SOCKET_FLAGS
)
port = int(_port)
else:
host, port = address[:2]
resolved_host, port = address[:2]
else: # IPv4
assert family == socket.AF_INET
host, port = address # type: ignore[misc]
resolved_host, port = address # type: ignore[misc]
hosts.append(
{
"hostname": hostname,
"host": host,
"port": port,
"family": family,
"proto": proto,
"flags": socket.AI_NUMERICHOST | socket.AI_NUMERICSERV,
}
ResolveResult(
hostname=host,
host=resolved_host,
port=port,
family=family,
proto=proto,
flags=_NUMERIC_SOCKET_FLAGS,
)
)

return hosts
@@ -96,23 +101,48 @@ def __init__(

async def resolve(
self, host: str, port: int = 0, family: int = socket.AF_INET
) -> List[Dict[str, Any]]:
) -> List[ResolveResult]:
try:
resp = await self._resolver.gethostbyname(host, family)
resp = await self._resolver.getaddrinfo(
host,
port=port,
type=socket.SOCK_STREAM,
family=family,
flags=socket.AI_ADDRCONFIG,
)
except aiodns.error.DNSError as exc:
msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed"
raise OSError(msg) from exc
hosts = []
for address in resp.addresses:
hosts: List[ResolveResult] = []
for node in resp.nodes:
address: Union[Tuple[bytes, int], Tuple[bytes, int, int, int]] = node.addr
family = node.family
if family == socket.AF_INET6:
if len(address) > 3 and address[3] and _SUPPORTS_SCOPE_ID:
# This is essential for link-local IPv6 addresses.
# LL IPv6 is a VERY rare case. Strictly speaking, we should use
# getnameinfo() unconditionally, but performance makes sense.
result = await self._resolver.getnameinfo(
(address[0].decode("ascii"), *address[1:]),
_NUMERIC_SOCKET_FLAGS,
)
resolved_host = result.node
else:
resolved_host = address[0].decode("ascii")
port = address[1]
else: # IPv4
assert family == socket.AF_INET
resolved_host = address[0].decode("ascii")
port = address[1]
hosts.append(
{
"hostname": host,
"host": address,
"port": port,
"family": family,
"proto": 0,
"flags": socket.AI_NUMERICHOST | socket.AI_NUMERICSERV,
}
ResolveResult(
hostname=host,
host=resolved_host,
port=port,
family=family,
proto=0,
flags=_NUMERIC_SOCKET_FLAGS,
)
)

if not hosts:
54 changes: 54 additions & 0 deletions docs/abc.rst
Original file line number Diff line number Diff line change
@@ -181,3 +181,57 @@ Abstract Access Logger
:param response: :class:`aiohttp.web.Response` object.

:param float time: Time taken to serve the request.


Abstract Resolver
-------------------------------

.. class:: AbstractResolver

An abstract class, base for all resolver implementations.

Method ``resolve`` should be overridden.

.. method:: resolve(host, port, family)

Resolve host name to IP address.

:param str host: host name to resolve.

:param int port: port number.

:param int family: socket family.

:return: list of :class:`aiohttp.abc.ResolveResult` instances.

.. method:: close()

Release resolver.

.. class:: ResolveResult

Result of host name resolution.

.. attribute:: hostname

The host name that was provided.

.. attribute:: host

The IP address that was resolved.

.. attribute:: port

The port that was resolved.

.. attribute:: family

The address family that was resolved.

.. attribute:: proto

The protocol that was resolved.

.. attribute:: flags

The flags that were resolved.
3 changes: 2 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
@@ -394,7 +394,8 @@
("py:class", "aiohttp.protocol.HttpVersion"), # undocumented
("py:class", "aiohttp.ClientRequest"), # undocumented
("py:class", "aiohttp.payload.Payload"), # undocumented
("py:class", "aiohttp.abc.AbstractResolver"), # undocumented
("py:class", "aiohttp.resolver.AsyncResolver"), # undocumented
("py:class", "aiohttp.resolver.ThreadedResolver"), # undocumented
("py:func", "aiohttp.ws_connect"), # undocumented
("py:meth", "start"), # undocumented
("py:exc", "aiohttp.ClientHttpProxyError"), # undocumented
10 changes: 8 additions & 2 deletions examples/fake_server.py
Original file line number Diff line number Diff line change
@@ -3,10 +3,11 @@
import pathlib
import socket
import ssl
from typing import List, Union

import aiohttp
from aiohttp import web
from aiohttp.abc import AbstractResolver
from aiohttp.abc import AbstractResolver, ResolveResult
from aiohttp.resolver import DefaultResolver
from aiohttp.test_utils import unused_port

@@ -19,7 +20,12 @@ def __init__(self, fakes, *, loop):
self._fakes = fakes
self._resolver = DefaultResolver(loop=loop)

async def resolve(self, host, port=0, family=socket.AF_INET):
async def resolve(
self,
host: str,
port: int = 0,
family: Union[socket.AddressFamily, int] = socket.AF_INET,
) -> List[ResolveResult]:
fake_port = self._fakes.get(host)
if fake_port is not None:
return [
2 changes: 1 addition & 1 deletion requirements/runtime-deps.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Extracted from `setup.cfg` via `make sync-direct-runtime-deps`

aiodns; sys_platform=="linux" or sys_platform=="darwin"
aiodns >= 3.2.0; sys_platform=="linux" or sys_platform=="darwin"
aiohappyeyeballs >= 2.3.0
aiosignal >= 1.1.2
async-timeout >= 4.0, < 5.0 ; python_version < "3.11"
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -65,7 +65,7 @@ install_requires =
[options.extras_require]
speedups =
# required c-ares (aiodns' backend) will not build on windows
aiodns; sys_platform=="linux" or sys_platform=="darwin"
aiodns >= 3.2.0; sys_platform=="linux" or sys_platform=="darwin"
Brotli; platform_python_implementation == 'CPython'
brotlicffi; platform_python_implementation != 'CPython'

236 changes: 171 additions & 65 deletions tests/test_resolver.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,88 @@
import asyncio
import ipaddress
import socket
from typing import Any, List
from ipaddress import ip_address
from typing import Any, Awaitable, Callable, Collection, List, NamedTuple, Tuple, Union
from unittest.mock import Mock, patch

import pytest

from aiohttp.resolver import AsyncResolver, DefaultResolver, ThreadedResolver
from aiohttp.resolver import (
_NUMERIC_SOCKET_FLAGS,
_SUPPORTS_SCOPE_ID,
AsyncResolver,
DefaultResolver,
ThreadedResolver,
)

try:
import aiodns

gethostbyname = hasattr(aiodns.DNSResolver, "gethostbyname")
getaddrinfo: Any = hasattr(aiodns.DNSResolver, "getaddrinfo")
except ImportError:
aiodns = None
gethostbyname = False
getaddrinfo = False


class FakeResult:
def __init__(self, addresses):
self.addresses = addresses
class FakeAIODNSAddrInfoNode(NamedTuple):

family: int
addr: Union[Tuple[bytes, int], Tuple[bytes, int, int, int]]


class FakeAIODNSAddrInfoIPv4Result:
def __init__(self, hosts: Collection[str]) -> None:
self.nodes = [
FakeAIODNSAddrInfoNode(socket.AF_INET, (h.encode(), 0)) for h in hosts
]


class FakeAIODNSAddrInfoIPv6Result:
def __init__(self, hosts: Collection[str]) -> None:
self.nodes = [
FakeAIODNSAddrInfoNode(
socket.AF_INET6,
(h.encode(), 0, 0, 3 if ip_address(h).is_link_local else 0),
)
for h in hosts
]


class FakeAIODNSNameInfoIPv6Result:
def __init__(self, host: str) -> None:
self.node = host
self.service = None


class FakeQueryResult:
def __init__(self, host):
self.host = host


async def fake_result(addresses):
return FakeResult(addresses=tuple(addresses))
async def fake_aiodns_getaddrinfo_ipv4_result(
hosts: Collection[str],
) -> FakeAIODNSAddrInfoIPv4Result:
return FakeAIODNSAddrInfoIPv4Result(hosts=hosts)


async def fake_aiodns_getaddrinfo_ipv6_result(
hosts: Collection[str],
) -> FakeAIODNSAddrInfoIPv6Result:
return FakeAIODNSAddrInfoIPv6Result(hosts=hosts)


async def fake_aiodns_getnameinfo_ipv6_result(
host: str,
) -> FakeAIODNSNameInfoIPv6Result:
return FakeAIODNSNameInfoIPv6Result(host)


async def fake_query_result(result):
return [FakeQueryResult(host=h) for h in result]


def fake_addrinfo(hosts):
async def fake(*args, **kwargs):
def fake_addrinfo(hosts: Collection[str]) -> Callable[..., Awaitable[Any]]:
async def fake(*args: Any, **kwargs: Any) -> List[Any]:
if not hosts:
raise socket.gaierror

@@ -45,33 +91,83 @@ async def fake(*args, **kwargs):
return fake


@pytest.mark.skipif(not gethostbyname, reason="aiodns 1.1 required")
async def test_async_resolver_positive_lookup(loop) -> None:
def fake_ipv6_addrinfo(hosts: Collection[str]) -> Callable[..., Awaitable[Any]]:
async def fake(*args: Any, **kwargs: Any) -> List[Any]:
if not hosts:
raise socket.gaierror

return [
(
socket.AF_INET6,
None,
socket.SOCK_STREAM,
None,
(h, 0, 0, 3 if ip_address(h).is_link_local else 0),
)
for h in hosts
]

return fake


def fake_ipv6_nameinfo(host: str) -> Callable[..., Awaitable[Any]]:
async def fake(*args: Any, **kwargs: Any) -> Tuple[str, int]:
return host, 0

return fake


@pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required")
async def test_async_resolver_positive_ipv4_lookup(loop: Any) -> None:
with patch("aiodns.DNSResolver") as mock:
mock().gethostbyname.return_value = fake_result(["127.0.0.1"])
resolver = AsyncResolver(loop=loop)
mock().getaddrinfo.return_value = fake_aiodns_getaddrinfo_ipv4_result(
["127.0.0.1"]
)
resolver = AsyncResolver()
real = await resolver.resolve("www.python.org")
ipaddress.ip_address(real[0]["host"])
mock().gethostbyname.assert_called_with("www.python.org", socket.AF_INET)


@pytest.mark.skipif(aiodns is None, reason="aiodns required")
async def test_async_resolver_query_positive_lookup(loop) -> None:
mock().getaddrinfo.assert_called_with(
"www.python.org",
family=socket.AF_INET,
flags=socket.AI_ADDRCONFIG,
port=0,
type=socket.SOCK_STREAM,
)


@pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required")
@pytest.mark.skipif(
not _SUPPORTS_SCOPE_ID, reason="python version does not support scope id"
)
async def test_async_resolver_positive_link_local_ipv6_lookup(loop: Any) -> None:
with patch("aiodns.DNSResolver") as mock:
del mock().gethostbyname
mock().query.return_value = fake_query_result(["127.0.0.1"])
resolver = AsyncResolver(loop=loop)
mock().getaddrinfo.return_value = fake_aiodns_getaddrinfo_ipv6_result(
["fe80::1"]
)
mock().getnameinfo.return_value = fake_aiodns_getnameinfo_ipv6_result(
"fe80::1%eth0"
)
resolver = AsyncResolver()
real = await resolver.resolve("www.python.org")
ipaddress.ip_address(real[0]["host"])
mock().query.assert_called_with("www.python.org", "A")


@pytest.mark.skipif(not gethostbyname, reason="aiodns 1.1 required")
async def test_async_resolver_multiple_replies(loop) -> None:
mock().getaddrinfo.assert_called_with(
"www.python.org",
family=socket.AF_INET,
flags=socket.AI_ADDRCONFIG,
port=0,
type=socket.SOCK_STREAM,
)
mock().getnameinfo.assert_called_with(
("fe80::1", 0, 0, 3), _NUMERIC_SOCKET_FLAGS
)


@pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required")
async def test_async_resolver_multiple_replies(loop: Any) -> None:
with patch("aiodns.DNSResolver") as mock:
ips = ["127.0.0.1", "127.0.0.2", "127.0.0.3", "127.0.0.4"]
mock().gethostbyname.return_value = fake_result(ips)
resolver = AsyncResolver(loop=loop)
mock().getaddrinfo.return_value = fake_aiodns_getaddrinfo_ipv4_result(ips)
resolver = AsyncResolver()
real = await resolver.resolve("www.google.com")
ips = [ipaddress.ip_address(x["host"]) for x in real]
assert len(ips) > 3, "Expecting multiple addresses"
@@ -88,40 +184,20 @@ async def test_async_resolver_query_multiple_replies(loop) -> None:
ips = [ipaddress.ip_address(x["host"]) for x in real]


@pytest.mark.skipif(not gethostbyname, reason="aiodns 1.1 required")
async def test_async_resolver_negative_lookup(loop) -> None:
with patch("aiodns.DNSResolver") as mock:
mock().gethostbyname.side_effect = aiodns.error.DNSError()
resolver = AsyncResolver(loop=loop)
with pytest.raises(OSError):
await resolver.resolve("doesnotexist.bla")


@pytest.mark.skipif(aiodns is None, reason="aiodns required")
async def test_async_resolver_query_negative_lookup(loop) -> None:
@pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required")
async def test_async_resolver_negative_lookup(loop: Any) -> None:
with patch("aiodns.DNSResolver") as mock:
del mock().gethostbyname
mock().query.side_effect = aiodns.error.DNSError()
resolver = AsyncResolver(loop=loop)
with pytest.raises(OSError):
await resolver.resolve("doesnotexist.bla")


@pytest.mark.skipif(aiodns is None, reason="aiodns required")
async def test_async_resolver_no_hosts_in_query(loop) -> None:
with patch("aiodns.DNSResolver") as mock:
del mock().gethostbyname
mock().query.return_value = fake_query_result([])
resolver = AsyncResolver(loop=loop)
mock().getaddrinfo.side_effect = aiodns.error.DNSError()
resolver = AsyncResolver()
with pytest.raises(OSError):
await resolver.resolve("doesnotexist.bla")


@pytest.mark.skipif(not gethostbyname, reason="aiodns 1.1 required")
async def test_async_resolver_no_hosts_in_gethostbyname(loop) -> None:
@pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required")
async def test_async_resolver_no_hosts_in_getaddrinfo(loop: Any) -> None:
with patch("aiodns.DNSResolver") as mock:
mock().gethostbyname.return_value = fake_result([])
resolver = AsyncResolver(loop=loop)
mock().getaddrinfo.return_value = fake_aiodns_getaddrinfo_ipv4_result([])
resolver = AsyncResolver()
with pytest.raises(OSError):
await resolver.resolve("doesnotexist.bla")

@@ -135,6 +211,20 @@ async def test_threaded_resolver_positive_lookup() -> None:
ipaddress.ip_address(real[0]["host"])


@pytest.mark.skipif(
not _SUPPORTS_SCOPE_ID, reason="python version does not support scope id"
)
async def test_threaded_resolver_positive_ipv6_link_local_lookup() -> None:
loop = Mock()
loop.getaddrinfo = fake_ipv6_addrinfo(["fe80::1"])
loop.getnameinfo = fake_ipv6_nameinfo("fe80::1%eth0")
resolver = ThreadedResolver()
resolver._loop = loop
real = await resolver.resolve("www.python.org")
assert real[0]["hostname"] == "www.python.org"
ipaddress.ip_address(real[0]["host"])


async def test_threaded_resolver_multiple_replies() -> None:
loop = Mock()
ips = ["127.0.0.1", "127.0.0.2", "127.0.0.3", "127.0.0.4"]
@@ -154,6 +244,16 @@ async def test_threaded_negative_lookup() -> None:
await resolver.resolve("doesnotexist.bla")


async def test_threaded_negative_ipv6_lookup() -> None:
loop = Mock()
ips: List[Any] = []
loop.getaddrinfo = fake_ipv6_addrinfo(ips)
resolver = ThreadedResolver()
resolver._loop = loop
with pytest.raises(socket.gaierror):
await resolver.resolve("doesnotexist.bla")


async def test_threaded_negative_lookup_with_unknown_result() -> None:
loop = Mock()

@@ -202,14 +302,20 @@ async def test_default_loop_for_async_resolver(loop) -> None:
assert resolver._loop is loop


@pytest.mark.skipif(not gethostbyname, reason="aiodns 1.1 required")
async def test_async_resolver_ipv6_positive_lookup(loop) -> None:
@pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required")
async def test_async_resolver_ipv6_positive_lookup(loop: Any) -> None:
with patch("aiodns.DNSResolver") as mock:
mock().gethostbyname.return_value = fake_result(["::1"])
resolver = AsyncResolver(loop=loop)
real = await resolver.resolve("www.python.org", family=socket.AF_INET6)
mock().getaddrinfo.return_value = fake_aiodns_getaddrinfo_ipv6_result(["::1"])
resolver = AsyncResolver()
real = await resolver.resolve("www.python.org")
ipaddress.ip_address(real[0]["host"])
mock().gethostbyname.assert_called_with("www.python.org", socket.AF_INET6)
mock().getaddrinfo.assert_called_with(
"www.python.org",
family=socket.AF_INET,
flags=socket.AI_ADDRCONFIG,
port=0,
type=socket.SOCK_STREAM,
)


@pytest.mark.skipif(aiodns is None, reason="aiodns required")
@@ -230,7 +336,7 @@ async def test_async_resolver_aiodns_not_present(loop, monkeypatch) -> None:


def test_default_resolver() -> None:
# if gethostbyname:
# if getaddrinfo:
# assert DefaultResolver is AsyncResolver
# else:
# assert DefaultResolver is ThreadedResolver

0 comments on commit 65d051d

Please sign in to comment.