steam-backlog-enforcer/steam_backlog_enforcer/tests/_no_internet.py
Krzysztof kuhy Rudnicki db44ca5b0b
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Split ci-mirror gate into <60 s steps
The single ci-mirror manual hook (clean venv + pre-commit --all-files +
serial pytest) ran past the 60 s per-command cap, so the tree could not be
stamped for a push. It is now nine gate steps, each under 55 s at load ~40:

- ci-mirror-venv / -precommit / -pytest: scripts/ci_mirror.sh parts;
  pytest runs on 4 xdist workers, pytest-cov still enforces 100 %.
- ci-mirror-pylint-{pkg,tests-1,tests-2,tests-3}: per-file checks over a
  partition of the pylint hook's targets; -dup and -cycles: the two
  cross-file checks on the whole tree. pylint is now a local hook so all of
  them share one env (&pylint_env, pylint==4.0.9).
- scripts/pylint_symilar_cache.py: pylint plugin that hashes each file once
  for duplicate-code instead of once per file pair (same R0801 output).

Tests: an autouse guard refuses non-loopback DNS (several tests made live
HLTB requests), two web fixtures poll at 0.01 s, and the rate-limit test
uses a fake clock. CLAUDE.md and AGENTS.md get the push line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWWjUkRCFGNMnnXZrSP
2026-11-11 16:29:08 +01:00

68 lines
2.3 KiB
Python

"""Autouse guard: tests resolve loopback names only, never the internet.
Without it a test that walked into an unpatched HLTB lookup (the poll-count
backfill after a pick, for example) made real HTTPS requests to
howlongtobeat.com: ~1.5 s per test, and a result that depended on the network
and on HLTB's live data. Every outbound call starts with ``getaddrinfo``
(requests/urllib3 directly, aiohttp through asyncio's thread resolver), so
refusing non-loopback hosts there turns each leak into the same fast failure
the package already handles when the machine is offline.
Tests that need a resolver patch ``socket.getaddrinfo`` themselves; their patch
sits on top of this one for the test's duration.
Split out of ``conftest.py`` to keep every file inside the 250-line cap;
``conftest`` imports the fixture by name, which is what registers it.
"""
from __future__ import annotations
import ipaddress
import socket
from typing import TYPE_CHECKING
from unittest.mock import patch
import pytest
if TYPE_CHECKING:
from collections.abc import Iterator
_REAL_GETADDRINFO = socket.getaddrinfo
_LOCAL_NAMES = frozenset({"", "localhost"})
def is_local_host(host: bytes | str | None) -> bool:
"""Whether *host* names this machine (loopback, unspecified, or None)."""
if host is None:
return True
if isinstance(host, bytes):
host = host.decode("ascii", "replace")
name = str(host)
if name.lower() in _LOCAL_NAMES:
return True
try:
address = ipaddress.ip_address(name)
except ValueError:
return False
return address.is_loopback or address.is_unspecified
def local_only_getaddrinfo(
host: bytes | str | None,
port: bytes | str | int | None,
*args: int,
**kwargs: int,
) -> list[tuple[object, ...]]:
"""``socket.getaddrinfo`` for loopback hosts; offline for anything else."""
if not is_local_host(host):
msg = f"tests may not resolve {host!r}: patch the network call instead"
raise socket.gaierror(socket.EAI_NONAME, msg)
return list(_REAL_GETADDRINFO(host, port, *args, **kwargs))
@pytest.fixture(autouse=True)
def _no_internet() -> Iterator[None]:
"""Refuse DNS for non-loopback hosts for the whole test."""
with patch("socket.getaddrinfo", local_only_getaddrinfo):
yield