b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
113 lines
4.1 KiB
Python
113 lines
4.1 KiB
Python
"""Tests for network.force_ipv4 — the socket.getaddrinfo monkey-patch."""
|
|
|
|
import importlib
|
|
import socket
|
|
|
|
|
|
|
|
def _reload_constants():
|
|
"""Reload hermes_constants to get a fresh apply_ipv4_preference."""
|
|
import hermes_constants
|
|
importlib.reload(hermes_constants)
|
|
return hermes_constants
|
|
|
|
|
|
class TestApplyIPv4Preference:
|
|
"""Tests for apply_ipv4_preference()."""
|
|
|
|
def setup_method(self):
|
|
"""Save the original getaddrinfo before each test."""
|
|
self._original = socket.getaddrinfo
|
|
|
|
def teardown_method(self):
|
|
"""Restore the original getaddrinfo after each test."""
|
|
socket.getaddrinfo = self._original
|
|
|
|
def test_noop_when_force_false(self):
|
|
"""No patch when force=False."""
|
|
from hermes_constants import apply_ipv4_preference
|
|
original = socket.getaddrinfo
|
|
apply_ipv4_preference(force=False)
|
|
assert socket.getaddrinfo is original
|
|
|
|
def test_patches_getaddrinfo_when_forced(self):
|
|
"""Patches socket.getaddrinfo when force=True."""
|
|
from hermes_constants import apply_ipv4_preference
|
|
original = socket.getaddrinfo
|
|
apply_ipv4_preference(force=True)
|
|
assert socket.getaddrinfo is not original
|
|
assert getattr(socket.getaddrinfo, "_hermes_ipv4_patched", False) is True
|
|
|
|
def test_double_patch_is_safe(self):
|
|
"""Calling apply twice doesn't double-wrap."""
|
|
from hermes_constants import apply_ipv4_preference
|
|
apply_ipv4_preference(force=True)
|
|
first_patch = socket.getaddrinfo
|
|
apply_ipv4_preference(force=True)
|
|
assert socket.getaddrinfo is first_patch
|
|
|
|
def test_af_unspec_becomes_af_inet(self):
|
|
"""AF_UNSPEC (default) calls get rewritten to AF_INET."""
|
|
from hermes_constants import apply_ipv4_preference
|
|
|
|
calls = []
|
|
original = socket.getaddrinfo
|
|
|
|
def mock_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
|
|
calls.append(family)
|
|
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.216.34", 80))]
|
|
|
|
socket.getaddrinfo = mock_getaddrinfo
|
|
apply_ipv4_preference(force=True)
|
|
|
|
# Call with default family (AF_UNSPEC = 0)
|
|
socket.getaddrinfo("example.com", 80)
|
|
assert calls[-1] == socket.AF_INET, "AF_UNSPEC should be rewritten to AF_INET"
|
|
|
|
def test_explicit_family_preserved(self):
|
|
"""Explicit AF_INET6 requests are not intercepted."""
|
|
from hermes_constants import apply_ipv4_preference
|
|
|
|
calls = []
|
|
original = socket.getaddrinfo
|
|
|
|
def mock_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
|
|
calls.append(family)
|
|
return [(family, socket.SOCK_STREAM, 6, "", ("::1", 80))]
|
|
|
|
socket.getaddrinfo = mock_getaddrinfo
|
|
apply_ipv4_preference(force=True)
|
|
|
|
socket.getaddrinfo("example.com", 80, family=socket.AF_INET6)
|
|
assert calls[-1] == socket.AF_INET6, "Explicit AF_INET6 should pass through"
|
|
|
|
def test_fallback_on_gaierror(self):
|
|
"""Falls back to AF_UNSPEC if AF_INET resolution fails."""
|
|
from hermes_constants import apply_ipv4_preference
|
|
|
|
call_families = []
|
|
|
|
def mock_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
|
|
call_families.append(family)
|
|
if family == socket.AF_INET:
|
|
raise socket.gaierror("No A record")
|
|
# AF_UNSPEC fallback returns IPv6
|
|
return [(socket.AF_INET6, socket.SOCK_STREAM, 6, "", ("::1", 80))]
|
|
|
|
socket.getaddrinfo = mock_getaddrinfo
|
|
apply_ipv4_preference(force=True)
|
|
|
|
result = socket.getaddrinfo("ipv6only.example.com", 80)
|
|
# Should have tried AF_INET first, then fallen back to AF_UNSPEC
|
|
assert call_families == [socket.AF_INET, 0]
|
|
assert result[0][0] == socket.AF_INET6
|
|
|
|
|
|
class TestConfigDefault:
|
|
"""Verify network section exists in DEFAULT_CONFIG."""
|
|
|
|
def test_network_section_in_default_config(self):
|
|
from hermes_cli.config import DEFAULT_CONFIG
|
|
assert "network" in DEFAULT_CONFIG
|
|
assert DEFAULT_CONFIG["network"]["force_ipv4"] is False
|