Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. import os
  2. import tempfile
  3. import unittest
  4. from datetime import datetime, timezone
  5. from unittest.mock import patch, MagicMock
  6. from services.feeds import looks_like_feed, parse_feed
  7. from services.sources import get_sources
  8. from services.headlines import prepare_headlines
  9. from structs.headline import Headline
  10. RSS_2_0 = """<?xml version="1.0" encoding="UTF-8"?>
  11. <rss version="2.0"><channel>
  12. <title>Test feed</title>
  13. <item><title>First news story</title><link>https://x/1</link><pubDate>Mon, 14 Sep 2026 12:00:00 GMT</pubDate></item>
  14. <item><title>Second news story</title><link>https://x/2</link></item>
  15. </channel></rss>
  16. """
  17. ATOM = """<?xml version="1.0" encoding="utf-8"?>
  18. <feed xmlns="http://www.w3.org/2005/Atom">
  19. <entry><title>Atom story one</title><published>2026-09-14T14:30:00Z</published></entry>
  20. <entry><title>Atom story two</title><updated>2026-09-13T10:00:00Z</updated></entry>
  21. </feed>
  22. """
  23. class TestLooksLikeFeed(unittest.TestCase):
  24. def test_rss_and_atom_detected(self):
  25. self.assertTrue(looks_like_feed(RSS_2_0))
  26. self.assertTrue(looks_like_feed(ATOM))
  27. def test_html_not_detected(self):
  28. self.assertFalse(looks_like_feed("<html><body><a>Story</a></body></html>"))
  29. def test_empty_not_detected(self):
  30. self.assertFalse(looks_like_feed(""))
  31. self.assertFalse(looks_like_feed(None))
  32. class TestParseFeed(unittest.TestCase):
  33. def test_rss_2_0(self):
  34. items = parse_feed(RSS_2_0)
  35. titles = [t for t, _ in items]
  36. self.assertEqual(titles, ["First news story", "Second news story"])
  37. self.assertEqual(items[0][1].year, 2026)
  38. self.assertIsNone(items[1][1])
  39. def test_atom(self):
  40. items = parse_feed(ATOM)
  41. titles = [t for t, _ in items]
  42. self.assertEqual(titles, ["Atom story one", "Atom story two"])
  43. self.assertIsNotNone(items[0][1])
  44. self.assertIsNotNone(items[1][1])
  45. def test_malformed_returns_empty(self):
  46. self.assertEqual(parse_feed("not xml at all <<<"), [])
  47. class TestGetSources(unittest.TestCase):
  48. def test_skips_comments_and_blanks(self):
  49. path = None
  50. try:
  51. with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as f:
  52. f.write("# a comment\n\nhttps://a.com\n\nhttps://b.com\n# another\n")
  53. path = f.name
  54. self.assertEqual(get_sources(path), ["https://a.com", "https://b.com"])
  55. finally:
  56. if path:
  57. os.unlink(path)
  58. class TestFeedDomainNormalization(unittest.TestCase):
  59. def test_feed_subdomain_stripped(self):
  60. self.assertEqual(Headline("a", ["a"], "https://feeds.npr.org/1001/rss.xml").domain, "npr.org")
  61. self.assertEqual(Headline("a", ["a"], "https://rss.nytimes.com/x").domain, "nytimes.com")
  62. self.assertEqual(Headline("a", ["a"], "https://www.vox.com/rss/index.xml").domain, "vox.com")
  63. class TestPrepareHeadlinesFeed(unittest.TestCase):
  64. def setUp(self):
  65. self.stopwords = {"the", "a", "an", "in", "on", "and", "of", "to"}
  66. @patch("services.headlines.requests.get")
  67. def test_feed_source_is_parsed(self, mock_get):
  68. mock_response = MagicMock()
  69. mock_response.status_code = 200
  70. mock_response.content = RSS_2_0.encode("utf-8")
  71. mock_response.text = RSS_2_0
  72. mock_get.return_value = mock_response
  73. headlines = prepare_headlines(["https://feeds.bbci.co.uk/news/world/rss.xml"], self.stopwords)
  74. titles = [h.display_text for h in headlines]
  75. self.assertEqual(titles, ["First news story", "Second news story"])
  76. # Domain comes from the feed host, normalized to the outlet.
  77. self.assertEqual(headlines[0].domain, "bbci.co.uk")
  78. self.assertIsNotNone(headlines[0].published_at)
  79. if __name__ == "__main__":
  80. unittest.main()