Skip to content

test: block outbound network access from the test suite - #256

Merged
Olen merged 1 commit into
mainfrom
test/block-outbound-network
Aug 15, 2026
Merged

test: block outbound network access from the test suite#256
Olen merged 1 commit into
mainfrom
test/block-outbound-network

Conversation

@Olen

@Olen Olen commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Summary

Adds pytest-socket and disables sockets for the whole suite, so a gap in a mock fails immediately instead of sending a real request to api.spond.com.

Motivation

This is not hypothetical. CI on #236 currently fails like this:

ValueError: Request failed with status 401: {"message":"User is not logged in.","errorKey":"notLoggedIn"}

That JSON came from the production Spond API — a GitHub Actions runner made a real outbound call because a change moved _get_entity() onto an HTTP path that the test's mocks did not cover. The test failed for the right reason there, but only by luck: an unmocked request that happens to succeed passes the test for entirely the wrong reason, and the result depends on network conditions and credentials CI does not have.

Under this guard the same situation fails immediately and legibly:

pytest_socket.SocketBlockedError: A test tried to use socket.getaddrinfo.

Why pytest-socket rather than a conftest.py fixture

A hand-rolled guard typically patches socket.socket.connect, which has real gaps: aiohttp resolves DNS through loop.getaddrinfo before connecting, and asyncio connects non-blocking sockets via the selector. A guard with holes is worse than no guard, because it manufactures false confidence. pytest-socket intercepts at socket creation and getaddrinfo, which is where the traffic actually starts.

Why --allow-unix-socket

Load-bearing, not decoration. asyncio's BaseSelectorEventLoop builds its self-pipe with socket.socketpair(), which is AF_UNIX on Linux. Blocking every socket unconditionally breaks the event loop itself, and with it every async test in the suite.

Escape hatch

A test that genuinely needs the network can opt in with @pytest.mark.enable_socket. Nothing in the suite currently does.

Testing

  • poetry run pytest → 30 passed with the guard active, confirming the existing suite is already hermetic and the AF_UNIX exception is correct
  • Deliberately verified the guard bites: a temporary test issuing a real aiohttp GET to https://api.spond.com/... was blocked at socket.getaddrinfo with SocketBlockedError naming the host. Probe removed before commit
  • poetry run ruff check → all checks passed
  • poetry run ruff format --check → 13 files already formatted

🤖 Generated with Claude Code

A gap in a mock currently fails silently in the worst possible way: the
request escapes to the real api.spond.com instead of erroring. That makes
the suite non-hermetic and non-deterministic, and a test that reaches the
network can pass for the wrong reason as easily as it can fail.

Add pytest-socket and disable sockets by default, so an unmocked request
fails immediately with SocketBlockedError naming the host it tried to
resolve, rather than depending on network conditions or credentials the
CI runner does not have.

`--allow-unix-socket` is required: asyncio builds its event loop self-pipe
with socket.socketpair() (AF_UNIX on Linux), so blocking every socket
would break the event loop and with it every async test.

Verified by attempting a real aiohttp GET to api.spond.com under the
guard: blocked at socket.getaddrinfo. The existing 30 tests are unaffected
and still pass, confirming the suite is already hermetic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Olen
Olen merged commit 12a492c into main Aug 15, 2026
8 checks passed
@Olen
Olen deleted the test/block-outbound-network branch August 15, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant