"""Tests for the EIA RSS feed widget and helpers.""" import time import aiohttp import feedparser import pytest from openbb_us_eia import rss as rss_widget from openbb_us_eia.utils import rss SAMPLE_RSS = b""" Sample Feed First Article https://www.eia.gov/todayinenergy/detail.php?id=1 <p>A short summary.</p> Mon, 19 May 2026 13:00:00 GMT Jane Doe Second Article detail.php?id=2 Plain text summary. Mon, 19 May 2026 12:00:00 GMT """ PRESENTATION_RSS = b""" Presentations Deck https://www.eia.gov/pressroom/index.php A talk. Wed, 04 Mar 2026 13:00:00 GMT /pressroom/presentations/deck.pdf /pressroom/presentations/deck.pptx Energy A. Person Video Talk https://www.youtube.com/watch?v=abc123 Recorded. Tue, 03 Mar 2026 13:00:00 GMT Landing Page Only https://www.eia.gov/pressroom/other.php No document. Mon, 02 Mar 2026 13:00:00 GMT """ PRICE_RSS = b""" Gasoline Prices https://www.eia.gov/petroleum/gasdiesel/ Summary excerpt here<br/>Regular Gasoline<br/>3.081 .. U.S.<br/>2.855 .. Gulf Coast<br/>Diesel<br/>3.624 .. U.S. Mon, 06 Jul 2026 13:00:00 GMT """ class _FakeResponse: def __init__(self, body=b"", status=200): self._body = body self.status = status async def read(self): return self._body def raise_for_status(self): if self.status >= 400: raise aiohttp.ClientError(f"HTTP {self.status}") async def __aenter__(self): return self async def __aexit__(self, *_args): return None class _FakeSession: def __init__(self, results): self.results = list(results) self.calls = 0 self.closed = False async def get(self, _url, **_kwargs): self.calls += 1 result = self.results.pop(0) if isinstance(result, BaseException): raise result return result async def __aenter__(self): return self async def __aexit__(self, *_args): self.closed = True class TestRegistry: def test_feed_specs_complete(self): assert set(rss.EIA_RSS_FEEDS) == { "today_in_energy", "whats_new", "press_releases", "congressional_testimony", "presentations", "gasoline_diesel", "heating_oil_propane", } for spec in rss.EIA_RSS_FEEDS.values(): assert set(spec) == {"label", "description", "url", "base", "viewer"} assert spec["viewer"] in ("page", "pdf", "table") def test_feed_choices(self): choices = rss.feed_choices() assert choices[0] == {"label": "Today in Energy", "value": "today_in_energy"} assert len(choices) == len(rss.EIA_RSS_FEEDS) class TestTextHelpers: def test_strip_html(self): assert rss.strip_html("

A B\n C

") == "A B C" def test_strip_html_empty(self): assert rss.strip_html("") == "" def test_article_text_drops_script_and_style(self): html = ( "" "" "

Real prose.

" ) assert rss.article_text(html) == "Real prose." def test_article_text_empty(self): assert rss.article_text("") == "" def test_article_text_truncates_to_limit(self): assert rss.article_text("

" + "ab " * 100 + "

", limit=5) == "ab ab" def test_excerpt_short_text_unchanged(self): assert rss.clean_excerpt("short") == "short" def test_excerpt_cuts_at_word_boundary(self): assert rss.clean_excerpt("word " * 100, limit=22) == "word word word word…" def test_excerpt_keeps_whitelisted_inline_markup(self): out = rss.clean_excerpt("

Crude rose 4%

") assert out == "Crude rose 4%" def test_excerpt_drops_unlisted_tags_but_keeps_their_text(self): out = rss.clean_excerpt('link and emphasis') assert out == "link and emphasis" def test_excerpt_discards_script_source(self): assert ( rss.clean_excerpt("Prices fell") == "Prices fell" ) def test_excerpt_discards_style_source(self): assert rss.clean_excerpt("Prices fell") == ( "Prices fell" ) def test_excerpt_decodes_entities_into_markup(self): out = rss.clean_excerpt("CO<sub>2</sub> & NG") assert out == "CO2 & NG" def test_excerpt_closes_tags_left_open_by_truncation(self): out = rss.clean_excerpt("very long bold text to cut", limit=20) assert out == "very long bold text…" def test_excerpt_of_empty_summary(self): assert rss.clean_excerpt("") == "" assert rss.clean_excerpt(None) == "" def test_excerpt_stops_once_the_budget_is_exhausted(self): assert rss.clean_excerpt("abcdefgh", limit=4) == "abcd" def test_struct_time_to_iso(self): value = time.gmtime(0) assert rss.struct_time_to_iso(value) == "1970-01-01T00:00:00+00:00" def test_struct_time_none_returns_now(self): assert rss.struct_time_to_iso(None).endswith("+00:00") def test_escape(self): assert rss._escape("&") == "<a>&" assert rss._escape("") == "" def test_youtube_embed(self): assert ( rss._youtube_embed("https://www.youtube.com/watch?v=abc_1-2") == "https://www.youtube.com/embed/abc_1-2" ) assert ( rss._youtube_embed("https://youtu.be/xyz") == "https://www.youtube.com/embed/xyz" ) assert rss._youtube_embed("https://www.eia.gov/page") is None class TestFetchFeed: @pytest.mark.asyncio async def test_parses_first_success(self): session = _FakeSession([_FakeResponse(SAMPLE_RSS)]) parsed = await rss.fetch_feed(session, "http://feed") assert len(parsed.entries) == 2 assert session.calls == 1 @pytest.mark.asyncio async def test_retries_transient_failure(self): session = _FakeSession([aiohttp.ClientError("boom"), _FakeResponse(SAMPLE_RSS)]) parsed = await rss.fetch_feed(session, "http://feed") assert len(parsed.entries) == 2 assert session.calls == 2 @pytest.mark.asyncio async def test_exhausted_retries_return_empty(self): session = _FakeSession([TimeoutError(), TimeoutError(), TimeoutError()]) parsed = await rss.fetch_feed(session, "http://feed") assert parsed.entries == [] assert session.calls == 3 @pytest.mark.asyncio async def test_http_error_counts_as_failure(self): session = _FakeSession( [_FakeResponse(b"", status=503), _FakeResponse(SAMPLE_RSS)] ) parsed = await rss.fetch_feed(session, "http://feed") assert len(parsed.entries) == 2 class TestRenderPriceTable: def test_sections_and_rows(self): description = ( "Summary excerpt ignored
Regular Gasoline
" "3.081 .. U.S.
2.855 .. Gulf Coast
Diesel
3.624 .. U.S." ) out = rss.render_price_table(description) assert out.count("

") == 2 assert out.count("U.S.3.081" in out def test_empty_description(self): assert rss.render_price_table("") == "" def test_plain_text_without_markup(self): out = rss.render_price_table("Header\n1.23 .. Item") assert "

Header

" in out assert "1.23" in out class TestBuildFeed: def test_page_feed_rewrites_src_through_proxy(self): parsed = feedparser.parse(SAMPLE_RSS) data = rss.build_feed("today_in_energy", parsed, 50, "http://t/eia_proxy") assert data["label"] == "Today in Energy" first, second = data["items"] assert first["title"] == "First Article" assert first["src"] == "http://t/eia_proxy/todayinenergy/detail.php?id=1" assert first["url"] == "https://www.eia.gov/todayinenergy/detail.php?id=1" assert first["excerpt"] == "A short summary." assert second["author"] == "Sample Feed" assert second["url"] == "https://www.eia.gov/todayinenergy/detail.php?id=2" def test_without_proxy_keeps_original_src(self): parsed = feedparser.parse(SAMPLE_RSS) data = rss.build_feed("today_in_energy", parsed, 50, "") assert data["items"][0]["src"].startswith("https://www.eia.gov/") def test_limit_and_sort_desc(self): parsed = feedparser.parse(SAMPLE_RSS) data = rss.build_feed("today_in_energy", parsed, 1, "") assert len(data["items"]) == 1 assert data["items"][0]["title"] == "First Article" def test_presentation_variants(self): parsed = feedparser.parse(PRESENTATION_RSS) data = rss.build_feed("presentations", parsed, 50, "http://t/eia_proxy") deck, video, page = data["items"] assert deck["viewer"] == "pdf" assert deck["src"] == "http://t/eia_proxy/pressroom/presentations/deck.pdf" assert ( deck["ppt_url"] == "https://www.eia.gov/pressroom/presentations/deck.pptx" ) assert {"label": "Subject", "value": "Energy"} in deck["meta"] assert video["viewer"] == "embed" assert video["src"] == "https://www.youtube.com/embed/abc123" assert page["viewer"] == "page" assert page["src"] == "http://t/eia_proxy/pressroom/other.php" def test_table_feed_renders_body_html(self): parsed = feedparser.parse(PRICE_RSS) data = rss.build_feed("gasoline_diesel", parsed, 50, "http://t/eia_proxy") item = data["items"][0] assert item["viewer"] == "table" assert "") data = rss.build_feed("today_in_energy", parsed, 50, "") assert data["items"] == [] assert data["label"] == "Today in Energy" class TestRenderRssHtml: def test_escapes_blob_and_sets_theme(self): html = rss.render_rss_html({"feed": "x", "label": "&", "items": []}, "light") assert "__EIA_RSS_DATA__" not in html assert "\\u003cL\\u003e\\u0026" in html assert '"theme": "light"' in html or '"light"' in html def test_dark_default(self): html = rss.render_rss_html({"feed": "x", "items": []}, "anything") assert '"dark"' in html class TestRssFeedEndpoint: @pytest.fixture def stubbed(self, monkeypatch): session = _FakeSession([_FakeResponse(SAMPLE_RSS)]) async def fake_get_session(): return session monkeypatch.setattr( "openbb_core.provider.utils.helpers.get_async_requests_session", fake_get_session, ) return session INFO = {"url": "http://t/api/v1/eia/rss_feed", "query": "", "path": ""} @pytest.mark.asyncio async def test_renders_feed_with_proxy_base(self, stubbed): response = await rss_widget.rss_feed( info=self.INFO, feed="today_in_energy", theme="dark" ) text = response.body.decode() assert "First Article" in text assert "eia_proxy/todayinenergy" in text assert '"endpoint": "http://t/api/v1/eia/rss_feed"' in text assert stubbed.closed is True @pytest.mark.asyncio async def test_unknown_feed_falls_back(self, stubbed): response = await rss_widget.rss_feed( info=self.INFO, feed="bogus", theme="light" ) assert '"today_in_energy"' in response.body.decode() class TestRouterRegistration: def test_route_and_widget_config(self): routes = {route.path: route for route in rss_widget.router._api_router.routes} widget = routes["/rss_feed"] config = widget.openapi_extra["widget_config"] params = {p["paramName"]: p for p in config["params"]} assert params["theme"]["show"] is False assert [o["value"] for o in params["feed"]["options"]] == list( rss.EIA_RSS_FEEDS )