|
- import unittest
- from datetime import date, datetime, timezone
- from unittest.mock import patch, MagicMock
-
- from services.dates import (
- parse_datetime,
- parse_date_arg,
- date_part,
- filter_by_date_window,
- )
- from services.headlines import prepare_headlines
- from structs.headline import Headline
-
-
- def h(text, tokens, dt=None, url="https://a.com"):
- return Headline(text, tokens, url, dt)
-
-
- class TestParseDatetime(unittest.TestCase):
- def test_iso_with_z(self):
- dt = parse_datetime("2026-09-14T14:30:00Z")
- self.assertIsNotNone(dt)
- self.assertEqual(dt.year, 2026)
-
- def test_iso_with_offset(self):
- dt = parse_datetime("2026-09-14T10:30:00-04:00")
- self.assertEqual((dt.month, dt.day), (9, 14))
-
- def test_rfc2822(self):
- dt = parse_datetime("Mon, 14 Sep 2026 12:30:00 GMT")
- self.assertEqual((dt.year, dt.month, dt.day), (2026, 9, 14))
-
- def test_human_month_day_year(self):
- dt = parse_datetime("September 14, 2026")
- self.assertEqual((dt.month, dt.day), (9, 14))
-
- def test_garbage_and_relative_return_none(self):
- self.assertIsNone(parse_datetime("3 hours ago"))
- self.assertIsNone(parse_datetime("not a date"))
- self.assertIsNone(parse_datetime(""))
- self.assertIsNone(parse_datetime(None))
-
-
- class TestParseDateArg(unittest.TestCase):
- def test_valid(self):
- self.assertEqual(parse_date_arg("2026-09-14"), date(2026, 9, 14))
-
- def test_invalid(self):
- with self.assertRaises(ValueError):
- parse_date_arg("14-09-2026")
-
-
- class TestDatePart(unittest.TestCase):
- def test_aware_normalized_to_utc(self):
- dt = datetime(2026, 9, 14, 23, 30, tzinfo=timezone.utc)
- self.assertEqual(date_part(dt), date(2026, 9, 14))
-
- def test_naive_kept(self):
- dt = datetime(2026, 9, 14, 23, 30)
- self.assertEqual(date_part(dt), date(2026, 9, 14))
-
-
- class TestFilterByDateWindow(unittest.TestCase):
- def setUp(self):
- self.headlines = [
- h("old story", ["old"], datetime(2026, 9, 10)),
- h("recent story", ["recent"], datetime(2026, 9, 14)),
- h("undated story", ["undated"], None),
- ]
-
- def test_no_bounds_keeps_all(self):
- kept, dropped = filter_by_date_window(self.headlines)
- self.assertEqual(len(kept), 3)
- self.assertEqual(dropped, 0)
-
- def test_since_drops_undated_and_old(self):
- kept, dropped = filter_by_date_window(self.headlines, since=date(2026, 9, 13))
- self.assertEqual([x.display_text for x in kept], ["recent story"])
- self.assertEqual(dropped, 1)
-
- def test_until(self):
- kept, _ = filter_by_date_window(self.headlines, until=date(2026, 9, 12))
- self.assertEqual([x.display_text for x in kept], ["old story"])
-
- def test_inclusive_bounds(self):
- kept, _ = filter_by_date_window(self.headlines, since=date(2026, 9, 14), until=date(2026, 9, 14))
- self.assertEqual([x.display_text for x in kept], ["recent story"])
-
-
- class TestDateCapturedFromHtml(unittest.TestCase):
- def setUp(self):
- self.stopwords = {"the", "a", "an", "in", "on"}
-
- @patch("services.headlines.requests.get")
- def test_time_element_associated_with_headline(self, mock_get):
- html = (
- "<html><body>"
- "<article><time datetime='2026-09-14T14:30:00Z'>Sep 14</time>"
- "<a href='/news'>Breaking News Story</a></article>"
- "<article><span>Another Story Here</span></article>"
- "</body></html>"
- )
- mock_response = MagicMock()
- mock_response.status_code = 200
- mock_response.content = html.encode("utf-8")
- mock_response.text = html
- mock_get.return_value = mock_response
-
- headlines = prepare_headlines(["https://example.com/news"], self.stopwords)
-
- by_text = {x.display_text: x for x in headlines}
- self.assertIsNotNone(by_text["Breaking News Story"].published_at)
- self.assertIsNone(by_text["Another Story Here"].published_at)
-
-
- if __name__ == "__main__":
- unittest.main()
|