- Add complete test suite for geomob module (19 tests) - Add comprehensive Bristol waste collection tests - Fix geomob_email() double slash assertion bug for HTTPS URLs - Fix utils.py human_readable_delta days pluralization - Update agenda tests with better coverage 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
389 lines
12 KiB
Python
389 lines
12 KiB
Python
"""Test geomob module."""
|
|
|
|
from datetime import date
|
|
from unittest.mock import Mock, mock_open, patch
|
|
|
|
import lxml.html
|
|
import pytest
|
|
|
|
from agenda.geomob import (
|
|
GeomobEvent,
|
|
extract_events,
|
|
find_new_events,
|
|
geomob_email,
|
|
get_cached_upcoming_events_list,
|
|
update,
|
|
)
|
|
|
|
|
|
def test_geomob_event_dataclass():
|
|
"""Test GeomobEvent dataclass creation and properties."""
|
|
event = GeomobEvent(
|
|
date=date(2024, 7, 15), href="/event/london-2024-07-15", hashtag="#geomobLDN"
|
|
)
|
|
|
|
assert event.date == date(2024, 7, 15)
|
|
assert event.href == "/event/london-2024-07-15"
|
|
assert event.hashtag == "#geomobLDN"
|
|
|
|
|
|
def test_geomob_event_frozen():
|
|
"""Test that GeomobEvent is frozen (immutable)."""
|
|
event = GeomobEvent(
|
|
date=date(2024, 7, 15), href="/event/london-2024-07-15", hashtag="#geomobLDN"
|
|
)
|
|
|
|
with pytest.raises(AttributeError):
|
|
event.date = date(2024, 8, 15)
|
|
|
|
|
|
def test_extract_events_with_valid_html():
|
|
"""Test extracting events from valid HTML."""
|
|
html_content = """
|
|
<html>
|
|
<body>
|
|
<ol class="event-list">
|
|
<li><a href="/event/london-2024-07-15">July 15, 2024 #geomobLDN</a></li>
|
|
<li><a href="/event/berlin-2024-08-20">August 20, 2024 #geomobBER</a></li>
|
|
</ol>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
tree = lxml.html.fromstring(html_content)
|
|
events = extract_events(tree)
|
|
|
|
assert len(events) == 2
|
|
|
|
assert events[0].date == date(2024, 7, 15)
|
|
assert events[0].href == "/event/london-2024-07-15"
|
|
assert events[0].hashtag == "#geomobLDN"
|
|
|
|
assert events[1].date == date(2024, 8, 20)
|
|
assert events[1].href == "/event/berlin-2024-08-20"
|
|
assert events[1].hashtag == "#geomobBER"
|
|
|
|
|
|
def test_extract_events_empty_list():
|
|
"""Test extracting events from HTML with no events."""
|
|
html_content = """
|
|
<html>
|
|
<body>
|
|
<ol class="event-list">
|
|
</ol>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
tree = lxml.html.fromstring(html_content)
|
|
events = extract_events(tree)
|
|
|
|
assert events == []
|
|
|
|
|
|
def test_extract_events_no_event_list():
|
|
"""Test extracting events from HTML with no event list."""
|
|
html_content = """
|
|
<html>
|
|
<body>
|
|
<p>No events here</p>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
tree = lxml.html.fromstring(html_content)
|
|
events = extract_events(tree)
|
|
|
|
assert events == []
|
|
|
|
|
|
def test_find_new_events_with_new_events():
|
|
"""Test finding new events when there are some."""
|
|
prev_events = [
|
|
GeomobEvent(date(2024, 7, 15), "/event/london-2024-07-15", "#geomobLDN"),
|
|
GeomobEvent(date(2024, 8, 20), "/event/berlin-2024-08-20", "#geomobBER"),
|
|
]
|
|
|
|
cur_events = [
|
|
GeomobEvent(date(2024, 7, 15), "/event/london-2024-07-15", "#geomobLDN"),
|
|
GeomobEvent(date(2024, 8, 20), "/event/berlin-2024-08-20", "#geomobBER"),
|
|
GeomobEvent(date(2024, 9, 25), "/event/paris-2024-09-25", "#geomobPAR"),
|
|
]
|
|
|
|
new_events = find_new_events(prev_events, cur_events)
|
|
|
|
assert len(new_events) == 1
|
|
assert new_events[0].date == date(2024, 9, 25)
|
|
assert new_events[0].href == "/event/paris-2024-09-25"
|
|
assert new_events[0].hashtag == "#geomobPAR"
|
|
|
|
|
|
def test_find_new_events_no_new_events():
|
|
"""Test finding new events when there are none."""
|
|
events = [
|
|
GeomobEvent(date(2024, 7, 15), "/event/london-2024-07-15", "#geomobLDN"),
|
|
GeomobEvent(date(2024, 8, 20), "/event/berlin-2024-08-20", "#geomobBER"),
|
|
]
|
|
|
|
new_events = find_new_events(events, events)
|
|
assert new_events == []
|
|
|
|
|
|
def test_find_new_events_empty_previous():
|
|
"""Test finding new events when previous list is empty."""
|
|
cur_events = [
|
|
GeomobEvent(date(2024, 7, 15), "/event/london-2024-07-15", "#geomobLDN"),
|
|
GeomobEvent(date(2024, 8, 20), "/event/berlin-2024-08-20", "#geomobBER"),
|
|
]
|
|
|
|
new_events = find_new_events([], cur_events)
|
|
assert len(new_events) == 2
|
|
assert set(new_events) == set(cur_events)
|
|
|
|
|
|
def test_geomob_email_single_event():
|
|
"""Test generating email for a single new event."""
|
|
events = [GeomobEvent(date(2024, 7, 15), "/event/london-2024-07-15", "#geomobLDN")]
|
|
|
|
subject, body = geomob_email(events, "https://thegeomob.com")
|
|
|
|
assert subject == "1 New Geomob Event(s) Announced"
|
|
assert "Hello," in body
|
|
assert "Here are the new Geomob events:" in body
|
|
assert "Date: 2024-07-15" in body
|
|
assert "URL: https://thegeomob.com/event/london-2024-07-15" in body
|
|
assert "Hashtag: #geomobLDN" in body
|
|
assert "----------------------------------------" in body
|
|
|
|
|
|
def test_geomob_email_single_event_no_protocol():
|
|
"""Test generating email for a single new event with base URL without protocol."""
|
|
events = [GeomobEvent(date(2024, 7, 15), "/event/london-2024-07-15", "#geomobLDN")]
|
|
|
|
subject, body = geomob_email(events, "thegeomob.com")
|
|
|
|
assert subject == "1 New Geomob Event(s) Announced"
|
|
assert "Hello," in body
|
|
assert "Here are the new Geomob events:" in body
|
|
assert "Date: 2024-07-15" in body
|
|
assert "URL: thegeomob.com/event/london-2024-07-15" in body
|
|
assert "Hashtag: #geomobLDN" in body
|
|
assert "----------------------------------------" in body
|
|
|
|
|
|
def test_geomob_email_multiple_events():
|
|
"""Test generating email for multiple new events."""
|
|
events = [
|
|
GeomobEvent(date(2024, 7, 15), "/event/london-2024-07-15", "#geomobLDN"),
|
|
GeomobEvent(date(2024, 8, 20), "/event/berlin-2024-08-20", "#geomobBER"),
|
|
]
|
|
|
|
subject, body = geomob_email(events, "https://thegeomob.com")
|
|
|
|
assert subject == "2 New Geomob Event(s) Announced"
|
|
assert "Date: 2024-07-15" in body
|
|
assert "Date: 2024-08-20" in body
|
|
assert body.count("----------------------------------------") == 2
|
|
|
|
|
|
def test_geomob_email_no_double_slash():
|
|
"""Test that URL construction doesn't create double slashes."""
|
|
events = [GeomobEvent(date(2024, 7, 15), "/event/london-2024-07-15", "#geomobLDN")]
|
|
|
|
subject, body = geomob_email(events, "https://thegeomob.com")
|
|
|
|
# This should not raise an AssertionError due to double slashes
|
|
assert "https://thegeomob.com/event/london-2024-07-15" in body
|
|
|
|
|
|
def test_geomob_email_empty_list():
|
|
"""Test that geomob_email raises assertion error with empty list."""
|
|
with pytest.raises(AssertionError):
|
|
geomob_email([], "https://thegeomob.com")
|
|
|
|
|
|
def test_geomob_email_detects_double_slash_in_path():
|
|
"""Test that the function detects double slashes in the URL path."""
|
|
events = [GeomobEvent(date(2024, 7, 15), "//event/london-2024-07-15", "#geomobLDN")]
|
|
|
|
# This should raise an AssertionError due to double slash in path
|
|
with pytest.raises(AssertionError, match="Double slash found in URL"):
|
|
geomob_email(events, "https://thegeomob.com")
|
|
|
|
|
|
@patch("agenda.utils.get_most_recent_file")
|
|
@patch("lxml.html.parse")
|
|
def test_get_cached_upcoming_events_list_with_file(mock_parse, mock_get_file):
|
|
"""Test getting cached events when file exists."""
|
|
mock_get_file.return_value = "/path/to/recent.html"
|
|
|
|
# Mock the HTML parsing
|
|
mock_tree = Mock()
|
|
mock_parse.return_value.getroot.return_value = mock_tree
|
|
|
|
# Mock extract_events by patching it
|
|
with patch("agenda.geomob.extract_events") as mock_extract:
|
|
mock_events = [
|
|
GeomobEvent(date(2024, 7, 15), "/event/london-2024-07-15", "#geomobLDN")
|
|
]
|
|
mock_extract.return_value = mock_events
|
|
|
|
result = get_cached_upcoming_events_list("/geomob/dir")
|
|
|
|
assert result == mock_events
|
|
mock_get_file.assert_called_once_with("/geomob/dir", "html")
|
|
mock_parse.assert_called_once_with("/path/to/recent.html")
|
|
mock_extract.assert_called_once_with(mock_tree)
|
|
|
|
|
|
@patch("agenda.utils.get_most_recent_file")
|
|
def test_get_cached_upcoming_events_list_no_file(mock_get_file):
|
|
"""Test getting cached events when no file exists."""
|
|
mock_get_file.return_value = None
|
|
|
|
result = get_cached_upcoming_events_list("/geomob/dir")
|
|
|
|
assert result == []
|
|
mock_get_file.assert_called_once_with("/geomob/dir", "html")
|
|
|
|
|
|
@patch("agenda.mail.send_mail")
|
|
@patch("requests.get")
|
|
@patch("agenda.geomob.get_cached_upcoming_events_list")
|
|
@patch("os.path.join")
|
|
@patch("builtins.open", new_callable=mock_open)
|
|
@patch("agenda.geomob.datetime")
|
|
def test_update_with_new_events(
|
|
mock_datetime,
|
|
mock_open_file,
|
|
mock_join,
|
|
mock_get_cached,
|
|
mock_requests,
|
|
mock_send_mail,
|
|
):
|
|
"""Test update function when there are new events."""
|
|
# Mock config
|
|
config = {"DATA_DIR": "/data"}
|
|
|
|
# Mock datetime
|
|
mock_now = Mock()
|
|
mock_now.strftime.return_value = "2024-07-15_12:00:00"
|
|
mock_datetime.now.return_value = mock_now
|
|
|
|
# Mock os.path.join
|
|
mock_join.return_value = "/data/geomob/2024-07-15_12:00:00.html"
|
|
|
|
# Mock previous events
|
|
prev_events = [
|
|
GeomobEvent(date(2024, 7, 15), "/event/london-2024-07-15", "#geomobLDN")
|
|
]
|
|
mock_get_cached.return_value = prev_events
|
|
|
|
# Mock HTTP response
|
|
mock_response = Mock()
|
|
mock_response.text = "<html>mock content</html>"
|
|
mock_response.content = b"<html>mock content</html>"
|
|
mock_requests.return_value = mock_response
|
|
|
|
# Mock current events (with one new event)
|
|
with patch("agenda.geomob.extract_events") as mock_extract:
|
|
cur_events = [
|
|
GeomobEvent(date(2024, 7, 15), "/event/london-2024-07-15", "#geomobLDN"),
|
|
GeomobEvent(date(2024, 8, 20), "/event/berlin-2024-08-20", "#geomobBER"),
|
|
]
|
|
mock_extract.return_value = cur_events
|
|
|
|
update(config)
|
|
|
|
# Verify file was written
|
|
mock_open_file.assert_called_once_with(
|
|
"/data/geomob/2024-07-15_12:00:00.html", "w"
|
|
)
|
|
mock_open_file().write.assert_called_once_with("<html>mock content</html>")
|
|
|
|
# Verify email was sent
|
|
mock_send_mail.assert_called_once()
|
|
args = mock_send_mail.call_args[0]
|
|
assert args[1] == "1 New Geomob Event(s) Announced" # subject
|
|
|
|
|
|
@patch("requests.get")
|
|
@patch("agenda.geomob.get_cached_upcoming_events_list")
|
|
def test_update_no_changes(mock_get_cached, mock_requests):
|
|
"""Test update function when there are no changes."""
|
|
config = {"DATA_DIR": "/data"}
|
|
|
|
# Mock identical events
|
|
events = [GeomobEvent(date(2024, 7, 15), "/event/london-2024-07-15", "#geomobLDN")]
|
|
mock_get_cached.return_value = events
|
|
|
|
# Mock HTTP response
|
|
mock_response = Mock()
|
|
mock_response.content = b"<html>mock content</html>"
|
|
mock_requests.return_value = mock_response
|
|
|
|
with patch("agenda.geomob.extract_events") as mock_extract:
|
|
mock_extract.return_value = events # Same events
|
|
|
|
with patch("builtins.open") as mock_open_file:
|
|
with patch("agenda.mail.send_mail") as mock_send_mail:
|
|
update(config)
|
|
|
|
# Verify no file was written and no email sent
|
|
mock_open_file.assert_not_called()
|
|
mock_send_mail.assert_not_called()
|
|
|
|
|
|
@patch("agenda.mail.send_mail")
|
|
@patch("requests.get")
|
|
@patch("agenda.geomob.get_cached_upcoming_events_list")
|
|
@patch("os.path.join")
|
|
@patch("builtins.open", new_callable=mock_open)
|
|
@patch("agenda.geomob.datetime")
|
|
def test_update_events_changed_but_no_new(
|
|
mock_datetime,
|
|
mock_open_file,
|
|
mock_join,
|
|
mock_get_cached,
|
|
mock_requests,
|
|
mock_send_mail,
|
|
):
|
|
"""Test update function when events changed but no new ones added."""
|
|
config = {"DATA_DIR": "/data"}
|
|
|
|
# Mock datetime
|
|
mock_now = Mock()
|
|
mock_now.strftime.return_value = "2024-07-15_12:00:00"
|
|
mock_datetime.now.return_value = mock_now
|
|
|
|
# Mock os.path.join
|
|
mock_join.return_value = "/data/geomob/2024-07-15_12:00:00.html"
|
|
|
|
# Mock previous events
|
|
prev_events = [
|
|
GeomobEvent(date(2024, 7, 15), "/event/london-2024-07-15", "#geomobLDN"),
|
|
GeomobEvent(date(2024, 8, 20), "/event/berlin-2024-08-20", "#geomobBER"),
|
|
]
|
|
mock_get_cached.return_value = prev_events
|
|
|
|
# Mock HTTP response
|
|
mock_response = Mock()
|
|
mock_response.text = "<html>mock content</html>"
|
|
mock_response.content = b"<html>mock content</html>"
|
|
mock_requests.return_value = mock_response
|
|
|
|
# Mock current events (one event removed, but no new ones)
|
|
with patch("agenda.geomob.extract_events") as mock_extract:
|
|
cur_events = [
|
|
GeomobEvent(date(2024, 7, 15), "/event/london-2024-07-15", "#geomobLDN")
|
|
]
|
|
mock_extract.return_value = cur_events
|
|
|
|
update(config)
|
|
|
|
# Verify file was written (events changed)
|
|
mock_open_file.assert_called_once()
|
|
|
|
# Verify no email was sent (no new events)
|
|
mock_send_mail.assert_not_called()
|