You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

117 lines
4.0 KiB

  1. import unittest
  2. from datetime import date, datetime, timezone
  3. from unittest.mock import patch, MagicMock
  4. from services.dates import (
  5. parse_datetime,
  6. parse_date_arg,
  7. date_part,
  8. filter_by_date_window,
  9. )
  10. from services.headlines import prepare_headlines
  11. from structs.headline import Headline
  12. def h(text, tokens, dt=None, url="https://a.com"):
  13. return Headline(text, tokens, url, dt)
  14. class TestParseDatetime(unittest.TestCase):
  15. def test_iso_with_z(self):
  16. dt = parse_datetime("2026-09-14T14:30:00Z")
  17. self.assertIsNotNone(dt)
  18. self.assertEqual(dt.year, 2026)
  19. def test_iso_with_offset(self):
  20. dt = parse_datetime("2026-09-14T10:30:00-04:00")
  21. self.assertEqual((dt.month, dt.day), (9, 14))
  22. def test_rfc2822(self):
  23. dt = parse_datetime("Mon, 14 Sep 2026 12:30:00 GMT")
  24. self.assertEqual((dt.year, dt.month, dt.day), (2026, 9, 14))
  25. def test_human_month_day_year(self):
  26. dt = parse_datetime("September 14, 2026")
  27. self.assertEqual((dt.month, dt.day), (9, 14))
  28. def test_garbage_and_relative_return_none(self):
  29. self.assertIsNone(parse_datetime("3 hours ago"))
  30. self.assertIsNone(parse_datetime("not a date"))
  31. self.assertIsNone(parse_datetime(""))
  32. self.assertIsNone(parse_datetime(None))
  33. class TestParseDateArg(unittest.TestCase):
  34. def test_valid(self):
  35. self.assertEqual(parse_date_arg("2026-09-14"), date(2026, 9, 14))
  36. def test_invalid(self):
  37. with self.assertRaises(ValueError):
  38. parse_date_arg("14-09-2026")
  39. class TestDatePart(unittest.TestCase):
  40. def test_aware_normalized_to_utc(self):
  41. dt = datetime(2026, 9, 14, 23, 30, tzinfo=timezone.utc)
  42. self.assertEqual(date_part(dt), date(2026, 9, 14))
  43. def test_naive_kept(self):
  44. dt = datetime(2026, 9, 14, 23, 30)
  45. self.assertEqual(date_part(dt), date(2026, 9, 14))
  46. class TestFilterByDateWindow(unittest.TestCase):
  47. def setUp(self):
  48. self.headlines = [
  49. h("old story", ["old"], datetime(2026, 9, 10)),
  50. h("recent story", ["recent"], datetime(2026, 9, 14)),
  51. h("undated story", ["undated"], None),
  52. ]
  53. def test_no_bounds_keeps_all(self):
  54. kept, dropped = filter_by_date_window(self.headlines)
  55. self.assertEqual(len(kept), 3)
  56. self.assertEqual(dropped, 0)
  57. def test_since_drops_undated_and_old(self):
  58. kept, dropped = filter_by_date_window(self.headlines, since=date(2026, 9, 13))
  59. self.assertEqual([x.display_text for x in kept], ["recent story"])
  60. self.assertEqual(dropped, 1)
  61. def test_until(self):
  62. kept, _ = filter_by_date_window(self.headlines, until=date(2026, 9, 12))
  63. self.assertEqual([x.display_text for x in kept], ["old story"])
  64. def test_inclusive_bounds(self):
  65. kept, _ = filter_by_date_window(self.headlines, since=date(2026, 9, 14), until=date(2026, 9, 14))
  66. self.assertEqual([x.display_text for x in kept], ["recent story"])
  67. class TestDateCapturedFromHtml(unittest.TestCase):
  68. def setUp(self):
  69. self.stopwords = {"the", "a", "an", "in", "on"}
  70. @patch("services.headlines.requests.get")
  71. def test_time_element_associated_with_headline(self, mock_get):
  72. html = (
  73. "<html><body>"
  74. "<article><time datetime='2026-09-14T14:30:00Z'>Sep 14</time>"
  75. "<a href='/news'>Breaking News Story</a></article>"
  76. "<article><span>Another Story Here</span></article>"
  77. "</body></html>"
  78. )
  79. mock_response = MagicMock()
  80. mock_response.status_code = 200
  81. mock_response.content = html.encode("utf-8")
  82. mock_response.text = html
  83. mock_get.return_value = mock_response
  84. headlines = prepare_headlines(["https://example.com/news"], self.stopwords)
  85. by_text = {x.display_text: x for x in headlines}
  86. self.assertIsNotNone(by_text["Breaking News Story"].published_at)
  87. self.assertIsNone(by_text["Another Story Here"].published_at)
  88. if __name__ == "__main__":
  89. unittest.main()