Spaces:
Running on Zero
Running on Zero
| """Unit tests for the fixed-source local web-search parser.""" | |
| from __future__ import annotations | |
| import unittest | |
| from unittest.mock import patch | |
| import httpx | |
| import web_search | |
| from web_search import ( | |
| SearchUnavailable, | |
| parse_bing_rss, | |
| parse_duckduckgo_lite, | |
| parse_google_news_rss, | |
| search_web, | |
| ) | |
| class FakeClient: | |
| def __enter__(self) -> "FakeClient": | |
| return self | |
| def __exit__(self, *_args: object) -> None: | |
| return None | |
| class WebSearchParserTests(unittest.TestCase): | |
| def test_duckduckgo_lite_results_and_redirects(self) -> None: | |
| payload = """ | |
| <html><body> | |
| <a class="result-link" | |
| href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.com%2Fdocs&rut=x"> | |
| Example & docs | |
| </a> | |
| <td class="result-snippet">A <b>useful</b> result.</td> | |
| </body></html> | |
| """ | |
| self.assertEqual( | |
| parse_duckduckgo_lite(payload), | |
| [ | |
| { | |
| "title": "Example & docs", | |
| "url": "https://example.com/docs", | |
| "description": "A useful result.", | |
| "source": "example.com", | |
| } | |
| ], | |
| ) | |
| def test_bing_rss_results(self) -> None: | |
| payload = """<?xml version="1.0"?> | |
| <rss><channel><item> | |
| <title>Projeto</title> | |
| <link>https://example.org/project</link> | |
| <description>Uma <b>descrição</b>.</description> | |
| </item></channel></rss> | |
| """ | |
| self.assertEqual( | |
| parse_bing_rss(payload), | |
| [ | |
| { | |
| "title": "Projeto", | |
| "url": "https://example.org/project", | |
| "description": "Uma descrição.", | |
| "source": "example.org", | |
| } | |
| ], | |
| ) | |
| def test_google_news_rss_parses_date_and_source(self) -> None: | |
| payload = """<?xml version="1.0" encoding="UTF-8"?> | |
| <rss><channel><item> | |
| <title>Operação acontece no Rio de Janeiro</title> | |
| <link>https://news.google.com/rss/articles/example</link> | |
| <description><![CDATA[ | |
| <a href="https://example.com/rj">Operação acontece no Rio de Janeiro</a> | |
| <p>Forças de segurança divulgaram o balanço.</p> | |
| <font>O GLOBO</font> | |
| ]]></description> | |
| <pubDate>Tue, 04 Aug 2026 17:11:11 GMT</pubDate> | |
| <source url="https://oglobo.globo.com">O GLOBO</source> | |
| </item></channel></rss> | |
| """ | |
| self.assertEqual( | |
| parse_google_news_rss(payload), | |
| [ | |
| { | |
| "title": "Operação acontece no Rio de Janeiro", | |
| "url": "https://news.google.com/rss/articles/example", | |
| "description": ( | |
| "Publicado em 04/08/2026 17:11 UTC — Fonte: O GLOBO. " | |
| "Forças de segurança divulgaram o balanço." | |
| ), | |
| "source": "O GLOBO", | |
| } | |
| ], | |
| ) | |
| def test_google_news_rss_rejects_unsafe_link(self) -> None: | |
| payload = """<rss><channel><item> | |
| <title>Resultado inseguro</title> | |
| <link>javascript:alert(1)</link> | |
| <pubDate>Tue, 04 Aug 2026 17:11:11 GMT</pubDate> | |
| <source>Fonte</source> | |
| </item></channel></rss>""" | |
| self.assertEqual(parse_google_news_rss(payload), []) | |
| def test_invalid_result_scheme_is_rejected(self) -> None: | |
| payload = """ | |
| <a class="result-link" href="javascript:alert(1)">Inseguro</a> | |
| <td class="result-snippet">Não deve aparecer.</td> | |
| """ | |
| self.assertEqual(parse_duckduckgo_lite(payload), []) | |
| def test_invalid_result_does_not_replace_previous_snippet(self) -> None: | |
| payload = """ | |
| <a class="result-link" href="https://example.com/valid">Valido</a> | |
| <td class="result-snippet">Descricao valida.</td> | |
| <a class="result-link" href="javascript:alert(1)">Inseguro</a> | |
| <td class="result-snippet">Descricao insegura.</td> | |
| """ | |
| self.assertEqual( | |
| parse_duckduckgo_lite(payload), | |
| [ | |
| { | |
| "title": "Valido", | |
| "url": "https://example.com/valid", | |
| "description": "Descricao valida.", | |
| "source": "example.com", | |
| } | |
| ], | |
| ) | |
| class WebSearchAggregationTests(unittest.TestCase): | |
| def test_recent_news_aggregates_and_prioritizes_rio_de_janeiro(self) -> None: | |
| google_results = [ | |
| { | |
| "title": "Agenda cultural em Porto Alegre, RS", | |
| "url": "https://gauchazh.clicrbs.com.br/porto-alegre/noticia", | |
| "description": "Publicado em 04/08/2026 17:00 UTC — Fonte: GZH.", | |
| "source": "GZH", | |
| }, | |
| { | |
| "title": "Prefeitura do Rio de Janeiro anuncia medida", | |
| "url": "https://example.com/rj/prefeitura", | |
| "description": "Publicado em 04/08/2026 16:00 UTC — Fonte: Jornal RJ.", | |
| "source": "Jornal RJ", | |
| }, | |
| ] | |
| duck_results = [ | |
| { | |
| "title": "Prefeitura do Rio de Janeiro anuncia medida", | |
| "url": "https://duplicate.example/noticia", | |
| "description": "Resultado duplicado vindo de outro provedor.", | |
| "source": "duplicate.example", | |
| }, | |
| { | |
| "title": "Trânsito no RJ tem alteração nesta terça", | |
| "url": "https://example.org/noticias/rj/transito", | |
| "description": "Mudança afeta vias da capital fluminense.", | |
| "source": "example.org", | |
| }, | |
| ] | |
| with ( | |
| patch("web_search.httpx.Client", return_value=FakeClient()), | |
| patch( | |
| "web_search._google_news_rss", | |
| return_value=google_results, | |
| ) as google, | |
| patch( | |
| "web_search._duckduckgo_lite", | |
| return_value=duck_results, | |
| ) as duck, | |
| patch("web_search._bing_rss") as bing, | |
| ): | |
| result = search_web("últimas notícias do RJ hoje") | |
| self.assertEqual(result["provider"], "google-news+duckduckgo-lite") | |
| self.assertEqual( | |
| [item["title"] for item in result["results"]], | |
| [ | |
| "Prefeitura do Rio de Janeiro anuncia medida", | |
| "Trânsito no RJ tem alteração nesta terça", | |
| "Agenda cultural em Porto Alegre, RS", | |
| ], | |
| ) | |
| google.assert_called_once() | |
| duck.assert_called_once() | |
| bing.assert_not_called() | |
| def test_recent_news_falls_back_when_primary_providers_fail(self) -> None: | |
| bing_result = { | |
| "title": "Notícia do Rio de Janeiro", | |
| "url": "https://example.net/rj/noticia", | |
| "description": "Informação atualizada.", | |
| "source": "example.net", | |
| } | |
| network_error = httpx.ConnectError("offline") | |
| with ( | |
| patch("web_search.httpx.Client", return_value=FakeClient()), | |
| patch("web_search._google_news_rss", side_effect=network_error), | |
| patch("web_search._duckduckgo_lite", return_value=[]), | |
| patch("web_search._bing_rss", return_value=[bing_result]), | |
| ): | |
| result = search_web("notícias recentes do Rio de Janeiro") | |
| self.assertEqual(result["provider"], "bing-rss") | |
| self.assertEqual(result["results"], [bing_result]) | |
| def test_general_search_aggregates_existing_providers(self) -> None: | |
| duck_result = { | |
| "title": "Documentação do projeto", | |
| "url": "https://example.com/docs", | |
| "description": "Guia principal.", | |
| "source": "example.com", | |
| } | |
| bing_result = { | |
| "title": "Repositório do projeto", | |
| "url": "https://github.com/example/project", | |
| "description": "Código fonte.", | |
| "source": "github.com", | |
| } | |
| with ( | |
| patch("web_search.httpx.Client", return_value=FakeClient()), | |
| patch("web_search._duckduckgo_lite", return_value=[duck_result]), | |
| patch("web_search._bing_rss", return_value=[bing_result]), | |
| patch("web_search._wikipedia") as wikipedia, | |
| patch("web_search._google_news_rss") as google_news, | |
| ): | |
| result = search_web("documentação projeto") | |
| self.assertEqual(result["provider"], "duckduckgo-lite+bing-rss") | |
| self.assertEqual(len(result["results"]), 2) | |
| wikipedia.assert_not_called() | |
| google_news.assert_not_called() | |
| def test_all_providers_unavailable_preserves_search_error(self) -> None: | |
| network_error = httpx.ConnectError("offline") | |
| with ( | |
| patch("web_search.httpx.Client", return_value=FakeClient()), | |
| patch("web_search._google_news_rss", side_effect=network_error), | |
| patch("web_search._duckduckgo_lite", return_value=[]), | |
| patch("web_search._bing_rss", side_effect=network_error), | |
| ): | |
| with self.assertRaisesRegex(SearchUnavailable, "google-news"): | |
| search_web("últimas notícias") | |
| if __name__ == "__main__": | |
| unittest.main() | |