import json import urllib.error import pytest import web class FakeResp: """Minimal stand-in for an http.client.HTTPResponse context manager.""" def __init__(self, payload): self._b = json.dumps(payload).encode("utf-8") def read(self): return self._b def __enter__(self): return self def __exit__(self, *exc): return False def test_endpoints_defaults(monkeypatch): for var in ("WEBTOOLS_HOST", "SEARXNG_PORT", "FIRECRAWL_PORT"): monkeypatch.delenv(var, raising=False) searx, fire = web.endpoints() assert searx == "http://10.10.20.37:8080" assert fire == "http://10.10.20.37:3002" def test_endpoints_env_override(monkeypatch): monkeypatch.setenv("WEBTOOLS_HOST", "192.168.1.5") monkeypatch.setenv("SEARXNG_PORT", "9999") monkeypatch.setenv("FIRECRAWL_PORT", "4000") searx, fire = web.endpoints() assert searx == "http://192.168.1.5:9999" assert fire == "http://192.168.1.5:4000" def test_request_parses_json(monkeypatch): monkeypatch.setattr(web.urllib.request, "urlopen", lambda req, timeout=None: FakeResp({"ok": True})) assert web._request("http://x/y") == {"ok": True} def test_request_retries_then_succeeds(monkeypatch): calls = {"n": 0} def flaky(req, timeout=None): calls["n"] += 1 if calls["n"] < 3: raise urllib.error.URLError("boom") return FakeResp({"ok": True}) monkeypatch.setattr(web.urllib.request, "urlopen", flaky) monkeypatch.setattr(web.time, "sleep", lambda *_: None) assert web._request("http://x/y", retries=2) == {"ok": True} assert calls["n"] == 3 def test_request_raises_after_exhausting_retries(monkeypatch): def always_fail(req, timeout=None): raise urllib.error.URLError("down") monkeypatch.setattr(web.urllib.request, "urlopen", always_fail) monkeypatch.setattr(web.time, "sleep", lambda *_: None) with pytest.raises(web.WebToolsError): web._request("http://x/y", retries=2) def test_search_parses_and_caps(monkeypatch): payload = {"results": [ {"title": "A", "url": "http://a", "content": "snip a", "engine": "ddg"}, {"title": "B", "url": "http://b", "content": "snip b", "engine": "bing"}, {"title": "no-url", "content": "dropped"}, {"title": "C", "url": "http://c", "content": "snip c", "engine": "ddg"}, ]} monkeypatch.setattr(web, "_request", lambda url, **kw: payload) hits = web.search("demons", max_results=2) assert hits == [ {"title": "A", "url": "http://a", "snippet": "snip a", "engine": "ddg"}, {"title": "B", "url": "http://b", "snippet": "snip b", "engine": "bing"}, ] def test_search_builds_json_query_url(monkeypatch): captured = {} def fake_request(url, **kw): captured["url"] = url return {"results": []} monkeypatch.setattr(web, "_request", fake_request) monkeypatch.setenv("WEBTOOLS_HOST", "10.10.20.37") monkeypatch.delenv("SEARXNG_PORT", raising=False) web.search("demon hierarchy") assert captured["url"].startswith("http://10.10.20.37:8080/search?") assert "q=demon+hierarchy" in captured["url"] assert "format=json" in captured["url"] def test_scrape_returns_markdown(monkeypatch): monkeypatch.setattr(web, "_request", lambda url, **kw: {"success": True, "data": {"markdown": "# Hi"}}) assert web.scrape("http://a") == "# Hi" def test_scrape_posts_url_and_format(monkeypatch): captured = {} def fake_request(url, **kw): captured["url"] = url captured["data"] = kw.get("data") return {"success": True, "data": {"markdown": "ok"}} monkeypatch.setattr(web, "_request", fake_request) web.scrape("http://example.com/page") assert captured["url"].endswith(":3002/v1/scrape") assert captured["data"] == {"url": "http://example.com/page", "formats": ["markdown"]} def test_scrape_raises_on_failure_flag(monkeypatch): monkeypatch.setattr(web, "_request", lambda url, **kw: {"success": False, "error": "blocked"}) with pytest.raises(web.WebToolsError): web.scrape("http://a") def test_scrape_raises_on_empty_markdown(monkeypatch): monkeypatch.setattr(web, "_request", lambda url, **kw: {"success": True, "data": {"markdown": ""}}) with pytest.raises(web.WebToolsError): web.scrape("http://a") def test_main_search_json(monkeypatch, capsys): monkeypatch.setattr(web, "search", lambda q, max_results=12: [{"title": "T", "url": "http://u", "snippet": "s", "engine": "e"}]) rc = web.main(["search", "demons", "--max", "5", "--json"]) assert rc == 0 out = capsys.readouterr().out assert json.loads(out) == [{"title": "T", "url": "http://u", "snippet": "s", "engine": "e"}] def test_main_scrape_to_stdout(monkeypatch, capsys): monkeypatch.setattr(web, "scrape", lambda url: "# Page") rc = web.main(["scrape", "http://a"]) assert rc == 0 assert "# Page" in capsys.readouterr().out def test_main_scrape_to_file(monkeypatch, tmp_path): monkeypatch.setattr(web, "scrape", lambda url: "# Saved") out = tmp_path / "p.md" rc = web.main(["scrape", "http://a", "--out", str(out)]) assert rc == 0 assert out.read_text(encoding="utf-8") == "# Saved" def test_main_returns_1_on_error(monkeypatch, capsys): def boom(*a, **k): raise web.WebToolsError("nope") monkeypatch.setattr(web, "search", boom) rc = web.main(["search", "x"]) assert rc == 1 assert "error:" in capsys.readouterr().err def test_request_does_not_retry_on_bad_json(monkeypatch): calls = {"n": 0} class BadResp: def read(self): return b"not json" def __enter__(self): return self def __exit__(self, *exc): return False def once(req, timeout=None): calls["n"] += 1 return BadResp() monkeypatch.setattr(web.urllib.request, "urlopen", once) monkeypatch.setattr(web.time, "sleep", lambda *_: None) with pytest.raises(web.WebToolsError): web._request("http://x/y", retries=2) assert calls["n"] == 1 # deterministic failure, not retried def test_scrape_raises_on_null_data(monkeypatch): monkeypatch.setattr(web, "_request", lambda url, **kw: {"success": True, "data": None}) with pytest.raises(web.WebToolsError): web.scrape("http://a") def test_main_search_human_format(monkeypatch, capsys): monkeypatch.setattr(web, "search", lambda q, max_results=12: [{"title": "T", "url": "http://u", "snippet": "s" * 200, "engine": "e"}]) rc = web.main(["search", "x"]) assert rc == 0 out = capsys.readouterr().out assert "T" in out and "http://u" in out def test_main_scrape_out_prints_status(monkeypatch, capsys, tmp_path): monkeypatch.setattr(web, "scrape", lambda url: "# X") out = tmp_path / "p.md" rc = web.main(["scrape", "http://a", "--out", str(out)]) assert rc == 0 assert f"wrote {out}" in capsys.readouterr().out