agenda/tests/test_geomob.py
Edward Betts fac73962b2 Add comprehensive tests and fix geomob URL bug
- 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>
2025-07-20 01:31:19 +02:00

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()