Skip to content

Commit 7001232

Browse files
authored
test: block external DNS before resolution
Merge reviewed offline DNS guard and regression coverage.
2 parents 724bb3a + 8d35b18 commit 7001232

2 files changed

Lines changed: 46 additions & 3 deletions

File tree

tests/conftest.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from __future__ import annotations
88

9+
from ipaddress import ip_address
910
import socket
1011
from unittest.mock import MagicMock, patch
1112

@@ -51,14 +52,39 @@ def block_all_network(request: pytest.FixtureRequest):
5152
return
5253

5354
orig_connect = socket.socket.connect
55+
orig_getaddrinfo = socket.getaddrinfo
56+
57+
def is_allowed_host(host) -> bool:
58+
if host is None:
59+
return True
60+
if isinstance(host, bytes):
61+
try:
62+
host = host.decode("ascii")
63+
except UnicodeDecodeError:
64+
return False
65+
if host == "localhost":
66+
return True
67+
try:
68+
address = ip_address(host)
69+
except (TypeError, ValueError):
70+
return False
71+
return address.is_loopback or address.is_unspecified
5472

5573
def guarded_connect(self, address, *args, **kwargs):
5674
host = address[0] if isinstance(address, (tuple, list)) else address
57-
if host in ("127.0.0.1", "localhost", "::1"):
75+
if is_allowed_host(host):
5876
return orig_connect(self, address, *args, **kwargs)
5977
raise RuntimeError(f"Blocked unmocked external network connection to {address} during test execution.")
6078

61-
with patch.object(socket.socket, "connect", guarded_connect):
79+
def guarded_getaddrinfo(host, port, *args, **kwargs):
80+
if is_allowed_host(host):
81+
return orig_getaddrinfo(host, port, *args, **kwargs)
82+
raise RuntimeError(f"Blocked unmocked external network connection to {(host, port)} during test execution.")
83+
84+
with (
85+
patch.object(socket, "getaddrinfo", guarded_getaddrinfo),
86+
patch.object(socket.socket, "connect", guarded_connect),
87+
):
6288
yield
6389

6490

tests/test_test_safety.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Regression coverage for the opt-in live-test safety contract."""
22

33
from pathlib import Path
4-
from unittest.mock import MagicMock
4+
from unittest.mock import MagicMock, patch
55

66
import conftest
77
import pytest
@@ -61,6 +61,23 @@ def test_collection_does_not_skip_live_tests_after_opt_in() -> None:
6161
item.add_marker.assert_not_called()
6262

6363

64+
def test_network_guard_blocks_dns_before_native_resolution() -> None:
65+
native_getaddrinfo = MagicMock(return_value=[])
66+
67+
with patch.object(conftest.socket, "getaddrinfo", native_getaddrinfo):
68+
fixture = conftest.block_all_network.__wrapped__(_request(marked=False, opted_in=False))
69+
next(fixture)
70+
try:
71+
with pytest.raises(RuntimeError, match="Blocked unmocked external network connection"):
72+
conftest.socket.getaddrinfo("example.com", 443)
73+
native_getaddrinfo.assert_not_called()
74+
75+
assert conftest.socket.getaddrinfo("localhost", 443) == []
76+
native_getaddrinfo.assert_called_once_with("localhost", 443)
77+
finally:
78+
fixture.close()
79+
80+
6481
def test_default_subprocess_skips_marked_live_test(pytester: pytest.Pytester) -> None:
6582
_load_project_conftest(pytester)
6683
pytester.makepyfile(

0 commit comments

Comments
 (0)