"""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()