Files
wehub-resource-sync e4dcfc49aa
Tests / Lint and Format (push) Waiting to run
Tests / Web Node Tests (push) Waiting to run
Tests / Import Check (Python 3.11) (push) Waiting to run
Tests / Import Check (Python 3.12) (push) Waiting to run
Tests / Import Check (Python 3.13) (push) Waiting to run
Tests / Import Check (Python 3.14) (push) Waiting to run
Tests / Python Tests (Python 3.11) (push) Blocked by required conditions
Tests / Python Tests (Python 3.12) (push) Blocked by required conditions
Tests / Python Tests (Python 3.13) (push) Blocked by required conditions
Tests / Python Tests (Python 3.14) (push) Blocked by required conditions
Tests / Test Summary (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

184 lines
5.6 KiB
Python

"""Unit tests for the ``web_fetch`` tool's pure helpers."""
from __future__ import annotations
import pytest
from deeptutor.tools.web_fetch import (
DEFAULT_MAX_CHARS,
FetchOutcome,
_extract_readable,
_is_disallowed_host,
fetch_url_as_markdown,
)
# ---------------------------------------------------------------------------
# Host validation
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"host",
[
"127.0.0.1",
"localhost",
"10.0.0.1",
"192.168.1.1",
"169.254.1.1",
"::1",
"[::1]",
"metadata.local",
],
)
def test_is_disallowed_host_blocks_private_addresses(host: str) -> None:
assert _is_disallowed_host(host) is True, f"{host!r} should be disallowed"
def test_is_disallowed_host_allows_public_hostname() -> None:
# The DNS-dependent positive test is environment-fragile (CI sandboxes
# often block outbound DNS). The negative coverage above plus the
# injectable ``host_validator`` (used in fetch tests) makes a fully-
# offline public-host assertion unnecessary.
pytest.skip("public DNS check skipped; relies on injectable validator in tests")
# ---------------------------------------------------------------------------
# HTML readability extraction
# ---------------------------------------------------------------------------
def test_extract_readable_strips_scripts_and_styles() -> None:
html = """
<html><head><title>Hello</title><style>body {color:red;}</style></head>
<body><p>Visible.</p><script>alert('no');</script></body></html>
"""
title, body = _extract_readable(html)
assert title == "Hello"
assert "Visible." in body
assert "alert" not in body
assert "color:red" not in body
# Title is prepended as h1 markdown
assert body.startswith("# Hello")
def test_extract_readable_passes_through_plain_text() -> None:
title, body = _extract_readable("Plain text payload\nwith two lines.")
assert title == ""
assert "Plain text payload" in body
assert "with two lines" in body
# ---------------------------------------------------------------------------
# Top-level fetch — uses injected client_factory so no real network I/O.
# ---------------------------------------------------------------------------
class _StubResponse:
def __init__(
self,
*,
body: bytes = b"<html><title>T</title><body><p>x</p></body></html>",
status: int = 200,
url: str = "https://example.com/p",
encoding: str = "utf-8",
) -> None:
self._body = body
self.status_code = status
self.url = url
self.encoding = encoding
self.headers = {"content-type": "text/html; charset=utf-8"}
async def aiter_bytes(self):
yield self._body
def raise_for_status(self) -> None:
if self.status_code >= 400:
raise RuntimeError(f"HTTP {self.status_code}")
class _StubAsyncClient:
def __init__(self, response: _StubResponse) -> None:
self._response = response
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
return False
def stream(self, _method, _url, **_kwargs):
outer = self
class _Ctx:
async def __aenter__(self):
return outer._response
async def __aexit__(self, exc_type, exc, tb):
return False
return _Ctx()
def _factory_returning(response: _StubResponse):
def _factory(*, timeout: float, user_agent: str):
return _StubAsyncClient(response)
return _factory
@pytest.mark.asyncio
async def test_fetch_rejects_unsupported_scheme() -> None:
outcome = await fetch_url_as_markdown("ftp://example.com/x")
assert outcome.ok is False
assert "scheme" in outcome.error.lower()
@pytest.mark.asyncio
async def test_fetch_rejects_private_host() -> None:
outcome = await fetch_url_as_markdown("http://127.0.0.1/x")
assert outcome.ok is False
assert "private" in outcome.error.lower() or "loopback" in outcome.error.lower()
# Bypass DNS in every stubbed-network test — the validator is treated as
# trusted here because ``client_factory`` already pins the response.
_ALLOW_ALL = lambda host: False # noqa: E731 — single-use stub
@pytest.mark.asyncio
async def test_fetch_extracts_html_via_stubbed_client() -> None:
outcome = await fetch_url_as_markdown(
"https://example.com/p",
client_factory=_factory_returning(_StubResponse()),
host_validator=_ALLOW_ALL,
)
assert outcome.ok is True
assert outcome.title == "T"
assert "x" in outcome.markdown
@pytest.mark.asyncio
async def test_fetch_truncates_at_max_chars() -> None:
big_body = b"<html><body>" + (b"a" * 5000) + b"</body></html>"
outcome = await fetch_url_as_markdown(
"https://example.com/big",
max_chars=200,
client_factory=_factory_returning(_StubResponse(body=big_body)),
host_validator=_ALLOW_ALL,
)
assert outcome.ok is True
assert outcome.truncated is True
assert outcome.markdown.endswith("…[truncated]")
assert len(outcome.markdown) <= 220 # cap + marker headroom
@pytest.mark.asyncio
async def test_fetch_propagates_http_error_as_outcome_not_exception() -> None:
outcome = await fetch_url_as_markdown(
"https://example.com/missing",
client_factory=_factory_returning(_StubResponse(status=404, body=b"<p>missing</p>")),
host_validator=_ALLOW_ALL,
)
assert outcome.ok is False
assert "404" in outcome.error