"""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 = """ Example & docs A useful result. """ 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 = """ Projeto https://example.org/project Uma <b>descrição</b>. """ 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 = """ Operação acontece no Rio de Janeiro https://news.google.com/rss/articles/example Operação acontece no Rio de Janeiro

Forças de segurança divulgaram o balanço.

O GLOBO ]]>
Tue, 04 Aug 2026 17:11:11 GMT O GLOBO
""" 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 = """ Resultado inseguro javascript:alert(1) Tue, 04 Aug 2026 17:11:11 GMT Fonte """ self.assertEqual(parse_google_news_rss(payload), []) def test_invalid_result_scheme_is_rejected(self) -> None: payload = """ Inseguro Não deve aparecer. """ self.assertEqual(parse_duckduckgo_lite(payload), []) def test_invalid_result_does_not_replace_previous_snippet(self) -> None: payload = """ Valido Descricao valida. Inseguro Descricao insegura. """ 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()