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 = ( "
" "