Files

169 lines
6.1 KiB
Python

from __future__ import annotations
import ipaddress
import os
import socket
from typing import Any
import pytest
NETWORK_GUARD = pytest.StashKey[pytest.MonkeyPatch]()
PROVIDER_CONTRACT_SOURCES = pytest.StashKey[tuple[str, ...]]()
_getaddrinfo = socket.getaddrinfo
_gethostbyaddr = socket.gethostbyaddr
_gethostbyname = socket.gethostbyname
_gethostbyname_ex = socket.gethostbyname_ex
_getnameinfo = socket.getnameinfo
_connect = socket.socket.connect
_connect_ex = socket.socket.connect_ex
_sendto = socket.socket.sendto
_NETWORK_GUARD_ERROR = (
'External networking through Python socket APIs is disabled in routine tests. '
'Mock the boundary or mark the test with @pytest.mark.live_network and pass '
'--run-live-network -m live_network.'
)
def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption(
'--run-live-network',
action='store_true',
default=False,
help='run tests that contact external services',
)
@pytest.fixture
def live_test_domain() -> str:
return os.environ.get('SMOKE_TEST_DOMAIN', 'mozilla.org')
@pytest.fixture
def provider_contract_sources(request: pytest.FixtureRequest) -> tuple[str, ...]:
return request.config.stash.get(PROVIDER_CONTRACT_SOURCES, ())
def pytest_sessionstart(session: pytest.Session) -> None:
guard = pytest.MonkeyPatch()
guard.setattr(socket, 'getaddrinfo', _guarded_getaddrinfo)
guard.setattr(socket, 'gethostbyaddr', _guarded_gethostbyaddr)
guard.setattr(socket, 'gethostbyname', _guarded_gethostbyname)
guard.setattr(socket, 'gethostbyname_ex', _guarded_gethostbyname_ex)
guard.setattr(socket, 'getnameinfo', _guarded_getnameinfo)
guard.setattr(socket.socket, 'connect', _guarded_connect)
guard.setattr(socket.socket, 'connect_ex', _guarded_connect_ex)
guard.setattr(socket.socket, 'sendto', _guarded_sendto)
session.config.stash[NETWORK_GUARD] = guard
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
contract_modules: set[tuple[str, str]] = set()
for item in items:
if item.get_closest_marker('live_network') is not None:
continue
for marker in item.iter_markers('provider_contract'):
if len(marker.args) != 1 or marker.kwargs or not isinstance(marker.args[0], str):
raise pytest.UsageError(f'{item.nodeid}: provider_contract requires one canonical source name')
contract_modules.add((marker.args[0], str(item.path)))
config.stash[PROVIDER_CONTRACT_SOURCES] = tuple(source for source, _path in sorted(contract_modules))
if config.getoption('--run-live-network'):
if config.getoption('markexpr') != 'live_network':
raise pytest.UsageError('--run-live-network requires -m live_network')
return
skip_live = pytest.mark.skip(reason='requires --run-live-network -m live_network')
for item in items:
if item.get_closest_marker('live_network') is not None:
item.add_marker(skip_live)
def pytest_collection_finish(session: pytest.Session) -> None:
if session.config.getoption('--run-live-network'):
_remove_network_guard(session.config)
def pytest_sessionfinish(session: pytest.Session) -> None:
_remove_network_guard(session.config)
def _remove_network_guard(config: pytest.Config) -> None:
guard = config.stash.get(NETWORK_GUARD, None)
if guard is not None:
guard.undo()
del config.stash[NETWORK_GUARD]
def _is_loopback_host(host: object) -> bool:
if host is None:
return True
if isinstance(host, bytes):
host = host.decode(errors='ignore')
if not isinstance(host, str):
return False
normalized = host.strip('[]').split('%', 1)[0].lower()
if normalized in {'localhost', 'localhost.localdomain'}:
return True
try:
return ipaddress.ip_address(normalized).is_loopback
except ValueError:
return False
def _is_loopback_address(family: int, address: object) -> bool:
if family == socket.AF_UNIX:
return True
return isinstance(address, tuple) and bool(address) and _is_loopback_host(address[0])
def _guarded_getaddrinfo(host: bytes | str | None, *args: Any, **kwargs: Any) -> list[tuple[Any, ...]]:
if not _is_loopback_host(host):
raise AssertionError(f'{_NETWORK_GUARD_ERROR} Attempted host: {host!r}.')
return _getaddrinfo(host, *args, **kwargs)
def _guarded_gethostbyaddr(host: str) -> tuple[str, list[str], list[str]]:
if not _is_loopback_host(host):
raise AssertionError(f'{_NETWORK_GUARD_ERROR} Attempted host: {host!r}.')
return _gethostbyaddr(host)
def _guarded_gethostbyname(host: str) -> str:
if not _is_loopback_host(host):
raise AssertionError(f'{_NETWORK_GUARD_ERROR} Attempted host: {host!r}.')
return _gethostbyname(host)
def _guarded_gethostbyname_ex(host: str) -> tuple[str, list[str], list[str]]:
if not _is_loopback_host(host):
raise AssertionError(f'{_NETWORK_GUARD_ERROR} Attempted host: {host!r}.')
return _gethostbyname_ex(host)
def _guarded_getnameinfo(address: tuple[Any, ...], flags: int) -> tuple[str, str]:
if not address or not _is_loopback_host(address[0]):
raise AssertionError(f'{_NETWORK_GUARD_ERROR} Attempted address: {address!r}.')
return _getnameinfo(address, flags)
def _guarded_connect(sock: socket.socket, address: object) -> None:
if not _is_loopback_address(sock.family, address):
raise AssertionError(f'{_NETWORK_GUARD_ERROR} Attempted address: {address!r}.')
_connect(sock, address) # type: ignore[arg-type]
def _guarded_connect_ex(sock: socket.socket, address: object) -> int:
if not _is_loopback_address(sock.family, address):
raise AssertionError(f'{_NETWORK_GUARD_ERROR} Attempted address: {address!r}.')
return _connect_ex(sock, address) # type: ignore[arg-type]
def _guarded_sendto(sock: socket.socket, data: bytes, *args: Any) -> int:
address = args[-1] if args else None
if not _is_loopback_address(sock.family, address):
raise AssertionError(f'{_NETWORK_GUARD_ERROR} Attempted address: {address!r}.')
return _sendto(sock, data, *args)