import os
import tempfile
import unittest
from datetime import datetime, timezone
from unittest.mock import patch, MagicMock
from services.feeds import looks_like_feed, parse_feed
from services.sources import get_sources
from services.headlines import prepare_headlines
from structs.headline import Headline
RSS_2_0 = """
Test feed
- First news storyhttps://x/1Mon, 14 Sep 2026 12:00:00 GMT
- Second news storyhttps://x/2
"""
ATOM = """
Atom story one2026-09-14T14:30:00Z
Atom story two2026-09-13T10:00:00Z
"""
class TestLooksLikeFeed(unittest.TestCase):
def test_rss_and_atom_detected(self):
self.assertTrue(looks_like_feed(RSS_2_0))
self.assertTrue(looks_like_feed(ATOM))
def test_html_not_detected(self):
self.assertFalse(looks_like_feed("
Story"))
def test_empty_not_detected(self):
self.assertFalse(looks_like_feed(""))
self.assertFalse(looks_like_feed(None))
class TestParseFeed(unittest.TestCase):
def test_rss_2_0(self):
items = parse_feed(RSS_2_0)
titles = [t for t, _ in items]
self.assertEqual(titles, ["First news story", "Second news story"])
self.assertEqual(items[0][1].year, 2026)
self.assertIsNone(items[1][1])
def test_atom(self):
items = parse_feed(ATOM)
titles = [t for t, _ in items]
self.assertEqual(titles, ["Atom story one", "Atom story two"])
self.assertIsNotNone(items[0][1])
self.assertIsNotNone(items[1][1])
def test_malformed_returns_empty(self):
self.assertEqual(parse_feed("not xml at all <<<"), [])
class TestGetSources(unittest.TestCase):
def test_skips_comments_and_blanks(self):
path = None
try:
with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as f:
f.write("# a comment\n\nhttps://a.com\n\nhttps://b.com\n# another\n")
path = f.name
self.assertEqual(get_sources(path), ["https://a.com", "https://b.com"])
finally:
if path:
os.unlink(path)
class TestFeedDomainNormalization(unittest.TestCase):
def test_feed_subdomain_stripped(self):
self.assertEqual(Headline("a", ["a"], "https://feeds.npr.org/1001/rss.xml").domain, "npr.org")
self.assertEqual(Headline("a", ["a"], "https://rss.nytimes.com/x").domain, "nytimes.com")
self.assertEqual(Headline("a", ["a"], "https://www.vox.com/rss/index.xml").domain, "vox.com")
class TestPrepareHeadlinesFeed(unittest.TestCase):
def setUp(self):
self.stopwords = {"the", "a", "an", "in", "on", "and", "of", "to"}
@patch("services.headlines.requests.get")
def test_feed_source_is_parsed(self, mock_get):
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.content = RSS_2_0.encode("utf-8")
mock_response.text = RSS_2_0
mock_get.return_value = mock_response
headlines = prepare_headlines(["https://feeds.bbci.co.uk/news/world/rss.xml"], self.stopwords)
titles = [h.display_text for h in headlines]
self.assertEqual(titles, ["First news story", "Second news story"])
# Domain comes from the feed host, normalized to the outlet.
self.assertEqual(headlines[0].domain, "bbci.co.uk")
self.assertIsNotNone(headlines[0].published_at)
if __name__ == "__main__":
unittest.main()