"""Parser tests."""
import time
from lxml import html
from openbb_news import parser
def test_strip_html_empty():
assert parser.strip_html("") == ""
def test_strip_html_plain():
assert parser.strip_html("hello world") == "hello world"
def test_strip_html_tags_and_whitespace():
assert parser.strip_html("
Hello world\n!
") == "Hello world !"
def test_html_to_markdown_empty():
assert parser.html_to_markdown("") == ""
def test_html_to_markdown_preserves_anchor_links():
out = parser.html_to_markdown(
'NYT headline '
'BBC headline'
)
assert "[NYT headline](https://nyt.test/x)" in out
assert "[BBC headline](https://bbc.test/x)" in out
def test_html_to_markdown_strips_non_anchor_tags():
out = parser.html_to_markdown("
Hello bold world
")
assert "bold" in out
assert "<" not in out
def test_html_to_markdown_anchor_without_href():
out = parser.html_to_markdown('just text')
assert "just text" in out
assert "[" not in out
def test_html_to_markdown_anchor_without_text():
out = parser.html_to_markdown('')
assert "[" not in out
def test_html_to_markdown_handles_paragraphs_and_breaks():
out = parser.html_to_markdown("
Line one
Line two
")
assert "Line one" in out
assert "Line two" in out
assert "Line one" in out.split("\n")[0]
def test_html_to_markdown_handles_br():
out = parser.html_to_markdown("First Second")
assert "First" in out
assert "Second" in out
def test_html_to_markdown_paragraph_after_text():
out = parser.html_to_markdown("intro text
paragraph
")
assert "intro text" in out
assert "paragraph" in out
lines = [line for line in out.split("\n") if line.strip()]
assert lines.index("intro text") < lines.index("paragraph")
def test_html_to_markdown_lxml_parse_failure_falls_back(monkeypatch):
from lxml import html as _html
def boom(*_a, **_kw):
raise ValueError("synthetic parse failure")
monkeypatch.setattr(_html, "fragment_fromstring", boom)
out = parser.html_to_markdown('link')
assert out == "link"
def test_truncate_short():
assert parser.truncate("abc", limit=10) == "abc"
def test_truncate_long():
text = "word " * 100
truncated = parser.truncate(text, limit=50)
assert len(truncated) <= 60
assert truncated.endswith("…")
def test_struct_time_to_iso_none():
iso = parser.struct_time_to_iso(None)
assert "T" in iso and iso.endswith("+00:00")
def test_struct_time_to_iso_value():
st = time.struct_time((2026, 5, 19, 12, 0, 0, 0, 139, 0))
assert parser.struct_time_to_iso(st) == "2026-05-19T12:00:00+00:00"
def test_article_markdown_joins_with_blank_lines(sample_article_html):
doc = html.fromstring(sample_article_html)
article = doc.xpath("//article")[0]
text = parser._article_markdown(article)
assert "First paragraph of the article." in text
assert text.count("\n\n") == 2
def test_article_markdown_requires_two_paragraphs():
doc = html.fromstring(b"
"
)
text = parser._article_markdown(doc.xpath("//div")[0])
assert "2:26" not in text
assert "12:44" not in text
assert "0:58" not in text
assert "1:30:00" not in text
assert "Real opening paragraph" in text
assert "Real closing paragraph" in text
def test_article_markdown_keeps_timestamp_inside_sentence():
doc = html.fromstring(
b"
"
b"
The video begins at 2:26 with the host introducing.
"
b"
By 12:44 the segment closes.
"
b"
"
)
text = parser._article_markdown(doc.xpath("//div")[0])
assert "2:26" in text
assert "12:44" in text
def test_article_markdown_ignores_non_paragraph_text():
doc = html.fromstring(b"
"
b""
)
text = parser._article_markdown(doc.xpath("//article")[0])
assert "![" not in text
assert "One." in text
assert "Two." in text
def test_article_markdown_skips_img_without_src():
doc = html.fromstring(b"
One.
Two.
")
text = parser._article_markdown(doc.xpath("//article")[0])
assert "![" not in text
def test_article_markdown_figure_without_caption():
doc = html.fromstring(
b""
b"
One.
"
b''
b"
Two.
"
b""
)
text = parser._article_markdown(doc.xpath("//article")[0])
assert "" in text
def test_strip_chrome_removes_chrome_tags():
doc = html.fromstring(
b""
b"h"
b""
b""
b"
kept one
kept two
"
b""
)
parser._strip_chrome(doc)
leftover_tags = {el.tag for el in doc.iter() if isinstance(el.tag, str)}
for tag in parser._CHROME_TAGS:
assert tag not in leftover_tags
assert "article" in leftover_tags
def test_looks_clean_rejects_templates_and_empty():
assert parser._looks_clean("Some real text.")
assert not parser._looks_clean("")
assert not parser._looks_clean("[[ type === 'moc' ]] body text")
assert not parser._looks_clean("body with {{ binding }} marker")
async def test_fetch_feed_parses(stub_session_factory, sample_rss):
session = stub_session_factory({"http://feed.test/rss": sample_rss})
parsed = await parser.fetch_feed(session, "http://feed.test/rss")
assert len(parsed.entries) == 2
assert parsed.entries[0].title == "First Article"
async def test_fetch_feed_client_error(stub_session_factory):
import aiohttp
session = stub_session_factory(
{"http://feed.test/rss": aiohttp.ClientError("boom")}
)
parsed = await parser.fetch_feed(session, "http://feed.test/rss")
assert len(parsed.entries) == 0
async def test_fetch_feed_http_error(stub_session_factory):
session = stub_session_factory({"http://feed.test/rss": (b"", 500)})
parsed = await parser.fetch_feed(session, "http://feed.test/rss")
assert len(parsed.entries) == 0
def test_extract_body_jsonld_top_level(sample_jsonld_html):
assert parser._extract_body(sample_jsonld_html) == "JSON-LD top-level body."
def test_extract_body_jsonld_graph():
payload = (
b'"
)
assert parser._extract_body(payload) == "Body from graph."
def test_extract_body_jsonld_list():
payload = (
b'"
)
assert parser._extract_body(payload) == "Found in list."
def test_extract_body_jsonld_invalid_then_article():
payload = (
b""
b''
b"
First sentence.
Second sentence.
"
b""
)
assert parser._extract_body(payload) == "First sentence.\n\nSecond sentence."
def test_extract_body_article_selector(sample_article_html):
body = parser._extract_body(sample_article_html)
assert body is not None
assert "First paragraph" in body
assert body.count("\n\n") == 2
def test_extract_body_picks_largest_article():
payload = (
b""
b"
Related card one.
Related card two.
"
b""
b"
Main story paragraph one with more substance.
"
b"
Main story paragraph two with detail.
"
b"
Main story paragraph three closing the piece.
"
b""
b""
)
body = parser._extract_body(payload)
assert body is not None
assert "Main story paragraph one" in body
assert "Related card" not in body
def test_extract_body_main_fallback_when_no_article():
payload = (
b""
b""
b""
b"
Story paragraph one with content.
"
b"
Story paragraph two with more detail.
"
b""
b""
)
assert (
parser._extract_body(payload)
== "Story paragraph one with content.\n\nStory paragraph two with more detail."
)
def test_extract_body_main_requires_two_paragraphs():
payload = b"
"
""
).encode()
body = parser._extract_body(payload)
assert body is not None
assert "Court’s" in body
assert "Dominion’s" in body
assert "“we won”" in body
assert "—" in body
for artifact in ("’", "–", " "):
assert artifact not in body
async def test_fetch_article_body_end_to_end(stub_session_factory, sample_article_html):
session = stub_session_factory({"https://x.test/a": sample_article_html})
body = await parser.fetch_article_body(session, "https://x.test/a")
assert body is not None
assert "First paragraph" in body
async def test_fetch_article_body_client_error(stub_session_factory):
import aiohttp
session = stub_session_factory({"https://x.test/a": aiohttp.ClientError("boom")})
assert await parser.fetch_article_body(session, "https://x.test/a") is None
async def test_fetch_article_body_timeout(stub_session_factory):
session = stub_session_factory(
{"https://x.test/a": TimeoutError("simulated timeout")}
)
assert await parser.fetch_article_body(session, "https://x.test/a") is None
async def test_fetch_article_body_http_error(stub_session_factory):
session = stub_session_factory({"https://x.test/a": (b"", 403)})
assert await parser.fetch_article_body(session, "https://x.test/a") is None
async def test_fetch_article_body_no_article_returns_none(stub_session_factory):
session = stub_session_factory(
{"https://x.test/a": b"nothing"}
)
assert await parser.fetch_article_body(session, "https://x.test/a") is None
def test_extract_body_preserves_inline_img():
payload = (
b""
b"
Opening paragraph.
"
b''
b"
Closing paragraph.
"
b""
)
body = parser._extract_body(payload)
assert body is not None
lines = body.split("\n\n")
assert lines == [
"Opening paragraph.",
"",
"Closing paragraph.",
]
def test_extract_body_preserves_figure_with_caption():
payload = (
b""
b"
Lead in.
"
b''
b"A long caption describing the photo."
b""
b"
After the photo.
"
b""
)
body = parser._extract_body(payload)
assert body is not None
assert "" in body
assert "Lead in." in body
assert "After the photo." in body
def test_extract_body_prepends_og_image_when_no_inline_image():
payload = (
b""
b''
b""
b"
Paragraph one.
Paragraph two.
"
b""
)
body = parser._extract_body(payload)
assert body is not None
assert body.split("\n\n")[0] == ""
assert "Paragraph one." in body
def test_extract_body_skips_hero_when_inline_image_present():
payload = (
b""
b''
b""
b"
Lead.
"
b''
b"
Body.
"
b""
)
body = parser._extract_body(payload)
assert body is not None
assert "https://x.test/hero.jpg" not in body
assert "" in body
def test_extract_body_jsonld_articlebody_with_og_hero():
payload = (
b""
b''
b""
b'"
)
body = parser._extract_body(payload)
assert body == "\n\nPure text body."
def test_html_to_markdown_preserves_inline_img():
out = parser.html_to_markdown(
'
Lead
'
)
assert "" in out
assert "Lead" in out
def test_html_to_markdown_img_tail_text():
out = parser.html_to_markdown(
' trailing text after img'
)
assert "" in out
assert "trailing text after img" in out
def test_jsonld_field_list_of_strings():
payload = (
b""
b'"
)
body = parser._extract_body(payload)
assert body is None or body.startswith("")
def test_jsonld_field_list_of_image_objects():
payload = (
b""
b'"
b"
One.
Two.
"
b""
)
body = parser._extract_body(payload)
assert body is not None
assert body.startswith("")
def test_jsonld_field_image_object_with_url():
payload = (
b""
b'"
b"
One.
Two.
"
b""
)
body = parser._extract_body(payload)
assert body is not None
assert body.startswith("")
def test_extract_image_jsonld_fallback_path():
payload = (
b""
b'"
b"
One.
Two.
"
b""
)
body = parser._extract_body(payload)
assert body is not None
assert body.startswith("")