or id="answer" populates answers list."""
html = '
The answer is 42
'
parser = SearXNGHTMLParser()
parser.feed(html)
assert len(parser.answers) == 1
assert "42" in parser.answers[0]
def test_parser_answer_by_id():
html = '
42
'
parser = SearXNGHTMLParser()
parser.feed(html)
assert len(parser.answers) == 1
# ----- parse_html_results -----
def test_parse_html_results_shape():
"""parse_html_results returns a dict with all expected keys."""
html = """
c
a1
"""
r = parse_html_results(html, query="test")
assert r["query"] == "test"
assert len(r["results"]) == 1
assert r["suggestions"] == ["s1"]
assert r["answers"] == ["a1"]
assert r["corrections"] == []
assert r["infoboxes"] == []
assert r["unresponsive_engines"] == []
assert r["_fallback"] == "html"
assert r["number_of_results"] == 1
def test_parse_html_results_empty():
"""Empty HTML yields an empty-but-well-shaped result."""
r = parse_html_results("", query="q")
assert r["results"] == []
assert r["query"] == "q"
# ----- search_html -----
def _mock_urlopen(data: bytes, content_type="text/html"):
resp = MagicMock()
resp.read.return_value = data
resp.headers = {"Content-Type": content_type}
resp.__enter__.return_value = resp
resp.__exit__.return_value = None
return resp
def test_search_html_success():
"""search_html fetches and parses an HTML results page."""
html = """
snippet
"""
with patch("urllib.request.urlopen",
return_value=_mock_urlopen(html.encode())):
r = search_html("https://s.example.com", {"q": "test"})
assert r["results"][0]["title"] == "R"
assert r["results"][0]["url"] == "https://r.com"
def test_search_html_strips_format_param():
"""search_html must not pass format=json to the HTML endpoint."""
captured_req = {}
def _capture(req, timeout=None):
captured_req["url"] = req.full_url
return _mock_urlopen(b"
")
with patch("urllib.request.urlopen", side_effect=_capture):
search_html("https://s.example.com", {"q": "test", "format": "json"})
assert "format=json" not in captured_req["url"]
assert "q=test" in captured_req["url"]
def test_search_html_error_raises_runtime():
"""Network errors are wrapped in RuntimeError with context."""
err = urllib.error.URLError("connection refused")
with patch("urllib.request.urlopen", side_effect=err):
try:
search_html("https://s.example.com", {"q": "test"})
assert False, "should raise"
except RuntimeError as e:
assert "HTML search failed" in str(e)
# ----- search_single -----
def test_search_single_prefers_json():
"""When JSON works, search_single returns JSON results directly."""
import json as json_mod
payload = json_mod.dumps({"results": [{"title": "json", "url": "https://j.com"}]})
with patch("urllib.request.urlopen",
return_value=_mock_urlopen(payload.encode(),
content_type="application/json")):
r = search_single("https://s.example.com", {"q": "t", "format": "json"})
assert r["results"][0]["title"] == "json"
def test_search_single_falls_back_to_html():
"""When JSON returns None, search_single falls back to HTML parsing."""
html = """
"""
# First call returns HTML (JSON unsupported), second call (HTML search) also HTML
with patch("urllib.request.urlopen",
return_value=_mock_urlopen(html.encode())):
r = search_single("https://s.example.com", {"q": "t", "format": "json"})
assert r["results"][0]["title"] == "html"
assert r.get("_fallback") == "html"
def test_search_single_html_fallback_marks_fallback():
"""HTML fallback path sets _fallback='html' in the result."""
html = '
'
with patch("urllib.request.urlopen",
return_value=_mock_urlopen(html.encode())):
r = search_single("https://s.example.com", {"q": "t", "format": "json"})
assert r["_fallback"] == "html"