DnsPlugin Guide¶
DnsPlugin intercepts stdlib DNS resolution functions (socket.getaddrinfo and socket.gethostbyname) at the module level. When dnspython is installed, it also intercepts dns.resolver.resolve and dns.resolver.Resolver.resolve. Each hostname has its own independent FIFO queue, so mocks for different hosts do not interfere with each other.
Setup¶
DnsPlugin intercepts stdlib socket functions, so no extra installation is needed. If you also want to intercept dnspython resolution, install it separately.
In pytest, access DnsPlugin through the tripwire.dns proxy. It auto-creates the plugin for the current test on first use:
import socket
import tripwire
def test_hostname_resolution():
tripwire.dns.mock_gethostbyname("api.example.com", returns="93.184.216.34")
with tripwire:
ip = socket.gethostbyname("api.example.com")
assert ip == "93.184.216.34"
tripwire.dns.assert_gethostbyname(hostname="api.example.com")
For manual use outside pytest, construct DnsPlugin explicitly:
from tripwire import StrictVerifier
from tripwire.plugins.dns_plugin import DnsPlugin
verifier = StrictVerifier()
dns = DnsPlugin(verifier)
Each verifier may have at most one DnsPlugin. A second DnsPlugin(verifier) raises ValueError.
Registering mock lookups¶
DnsPlugin provides three mock methods, one for each intercepted function.
mock_getaddrinfo(hostname, *, returns, ...)¶
Register a mock for socket.getaddrinfo():
tripwire.dns.mock_getaddrinfo(
"api.example.com",
returns=[(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.216.34", 443))],
)
| Parameter | Type | Default | Description |
|---|---|---|---|
hostname |
str |
required | The hostname to match |
returns |
Any |
required | Value to return when this mock is consumed |
raises |
BaseException \| None |
None |
Exception to raise instead of returning |
required |
bool |
True |
Whether an unused mock causes UnusedMocksError at teardown |
mock_gethostbyname(hostname, *, returns, ...)¶
Register a mock for socket.gethostbyname():
| Parameter | Type | Default | Description |
|---|---|---|---|
hostname |
str |
required | The hostname to match |
returns |
Any |
required | Value to return when this mock is consumed |
raises |
BaseException \| None |
None |
Exception to raise instead of returning |
required |
bool |
True |
Whether an unused mock causes UnusedMocksError at teardown |
mock_resolve(qname, rdtype, *, returns, ...)¶
Register a mock for dns.resolver.resolve() (requires dnspython):
| Parameter | Type | Default | Description |
|---|---|---|---|
qname |
str |
required | The query name (hostname) to match |
rdtype |
str |
required | The DNS record type (e.g., "A", "MX", "CNAME") |
returns |
Any |
required | Value to return when this mock is consumed |
raises |
BaseException \| None |
None |
Exception to raise instead of returning |
required |
bool |
True |
Whether an unused mock causes UnusedMocksError at teardown |
Per-hostname FIFO queues¶
Each hostname (scoped by operation type) has its own independent FIFO queue. Multiple mocks for the same hostname are consumed in registration order:
def test_multiple_resolutions():
tripwire.dns.mock_gethostbyname("api.example.com", returns="93.184.216.34")
tripwire.dns.mock_gethostbyname("api.example.com", returns="93.184.216.35")
with tripwire:
ip1 = socket.gethostbyname("api.example.com")
ip2 = socket.gethostbyname("api.example.com")
assert ip1 == "93.184.216.34"
assert ip2 == "93.184.216.35"
tripwire.dns.assert_gethostbyname(hostname="api.example.com")
tripwire.dns.assert_gethostbyname(hostname="api.example.com")
Asserting interactions¶
Use the typed assertion helpers on tripwire.dns. Each helper requires all detail fields for its operation type.
assert_getaddrinfo(host, port, family, type, proto)¶
tripwire.dns.assert_getaddrinfo(
host="api.example.com",
port=443,
family=socket.AF_INET,
type=socket.SOCK_STREAM,
proto=0,
)
| Parameter | Type | Description |
|---|---|---|
host |
str |
The hostname that was resolved |
port |
Any |
The port passed to getaddrinfo |
family |
int |
Address family (e.g., socket.AF_INET) |
type |
int |
Socket type (e.g., socket.SOCK_STREAM) |
proto |
int |
Protocol number |
assert_gethostbyname(hostname)¶
| Parameter | Type | Description |
|---|---|---|
hostname |
str |
The hostname that was resolved |
assert_resolve(qname, rdtype)¶
| Parameter | Type | Description |
|---|---|---|
qname |
str |
The query name that was resolved |
rdtype |
str |
The DNS record type that was queried |
Simulating errors¶
Use the raises parameter to simulate DNS resolution failures:
import socket
import tripwire
def test_dns_resolution_failure():
tripwire.dns.mock_gethostbyname(
"nonexistent.example.com",
returns=None,
raises=socket.gaierror(8, "nodename nor servname provided, or not known"),
)
with tripwire:
with pytest.raises(socket.gaierror):
socket.gethostbyname("nonexistent.example.com")
tripwire.dns.assert_gethostbyname(hostname="nonexistent.example.com")
Full example¶
Production code (examples/dns_lookup/app.py):
"""Service endpoint DNS resolution."""
import socket
def resolve_service_endpoint(service_name, port=443):
"""Resolve a service hostname and return (ip, port) tuple."""
results = socket.getaddrinfo(service_name, port, socket.AF_INET, socket.SOCK_STREAM)
if not results:
raise RuntimeError(f"Could not resolve {service_name}")
family, socktype, proto, canonname, sockaddr = results[0]
return sockaddr
Test (examples/dns_lookup/test_app.py):
"""Test DNS service resolution using tripwire dns_mock."""
import socket
import tripwire
from .app import resolve_service_endpoint
def test_resolve_service_endpoint():
tripwire.dns.mock_getaddrinfo(
"payments.internal",
returns=[
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("10.0.2.15", 443)),
],
)
with tripwire:
addr = resolve_service_endpoint("payments.internal")
assert addr == ("10.0.2.15", 443)
tripwire.dns.assert_getaddrinfo(
host="payments.internal",
port=443,
family=socket.AF_INET,
type=socket.SOCK_STREAM,
proto=0,
)
Optional mocks¶
Mark a mock as optional with required=False:
An optional mock that is never triggered does not cause UnusedMocksError at teardown.
UnmockedInteractionError¶
When code calls a DNS function for a hostname that has no remaining mocks in its queue, tripwire raises UnmockedInteractionError: