agenda/tests/test_bristol_waste.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

371 lines
13 KiB
Python

"""Test Bristol waste collection module."""
import json
import os
import tempfile
from datetime import date, datetime, timedelta
from unittest.mock import AsyncMock, Mock, patch
import httpx
import pytest
from agenda import bristol_waste
from agenda.event import Event
@pytest.fixture
def temp_data_dir():
"""Create a temporary directory for test data."""
with tempfile.TemporaryDirectory() as tmpdir:
yield tmpdir
@pytest.fixture
def sample_bristol_data():
"""Sample Bristol waste collection data."""
return [
{
"containerName": "Recycling Container",
"collection": [
{
"nextCollectionDate": "2024-07-15T00:00:00Z",
"lastCollectionDate": "2024-07-01T00:00:00Z",
}
],
},
{
"containerName": "General Waste Container",
"collection": [
{
"nextCollectionDate": "2024-07-22T00:00:00Z",
"lastCollectionDate": "2024-07-08T00:00:00Z",
}
],
},
{
"containerName": "Food Waste Container",
"collection": [
{
"nextCollectionDate": "2024-07-16T00:00:00Z",
"lastCollectionDate": "2024-07-02T00:00:00Z",
}
],
},
]
@pytest.fixture
def mock_response(sample_bristol_data):
"""Mock HTTP response for Bristol waste API."""
response = Mock(spec=httpx.Response)
response.content = json.dumps({"data": sample_bristol_data}).encode()
response.json.return_value = {"data": sample_bristol_data}
return response
class TestGetService:
"""Test get_service function."""
def test_recycling_container(self):
"""Test extracting recycling service name."""
item = {"containerName": "Recycling Container"}
result = bristol_waste.get_service(item)
assert result == "Recycling"
def test_general_waste_container(self):
"""Test extracting general waste service name."""
item = {"containerName": "General Waste Container"}
result = bristol_waste.get_service(item)
assert result == "Waste Container"
def test_food_waste_container(self):
"""Test extracting food waste service name."""
item = {"containerName": "Food Waste Container"}
result = bristol_waste.get_service(item)
assert result == "Waste Container"
def test_garden_waste_container(self):
"""Test extracting garden waste service name."""
item = {"containerName": "Garden Waste Container"}
result = bristol_waste.get_service(item)
assert result == "Waste Container"
class TestCollections:
"""Test collections function."""
def test_single_collection_dates(self):
"""Test extracting dates from a single collection."""
item = {
"collection": [
{
"nextCollectionDate": "2024-07-15T00:00:00Z",
"lastCollectionDate": "2024-07-01T00:00:00Z",
}
]
}
dates = list(bristol_waste.collections(item))
expected = [date(2024, 7, 15), date(2024, 7, 1)]
assert dates == expected
def test_multiple_collection_dates(self):
"""Test extracting dates from multiple collections."""
item = {
"collection": [
{
"nextCollectionDate": "2024-07-15T00:00:00Z",
"lastCollectionDate": "2024-07-01T00:00:00Z",
},
{
"nextCollectionDate": "2024-07-22T00:00:00Z",
"lastCollectionDate": "2024-07-08T00:00:00Z",
},
]
}
dates = list(bristol_waste.collections(item))
expected = [
date(2024, 7, 15),
date(2024, 7, 1),
date(2024, 7, 22),
date(2024, 7, 8),
]
assert dates == expected
def test_empty_collection(self):
"""Test extracting dates from empty collection."""
item = {"collection": []}
dates = list(bristol_waste.collections(item))
assert dates == []
class TestGetWebData:
"""Test get_web_data function."""
@pytest.mark.asyncio
async def test_get_web_data_success(self, mock_response):
"""Test successful web data retrieval."""
uprn = "123456789012"
with patch("httpx.AsyncClient") as mock_client:
mock_async_client = AsyncMock()
mock_client.return_value.__aenter__.return_value = mock_async_client
mock_async_client.get.return_value = mock_response
mock_async_client.post.return_value = mock_response
result = await bristol_waste.get_web_data(uprn)
assert result == mock_response
assert mock_async_client.get.call_count == 1
assert mock_async_client.post.call_count == 2
@pytest.mark.asyncio
async def test_get_web_data_zero_padded_uprn(self, mock_response):
"""Test UPRN is zero-padded to 12 digits."""
uprn = "123456"
with patch("httpx.AsyncClient") as mock_client:
mock_async_client = AsyncMock()
mock_client.return_value.__aenter__.return_value = mock_async_client
mock_async_client.get.return_value = mock_response
mock_async_client.post.return_value = mock_response
await bristol_waste.get_web_data(uprn)
# Check that the UPRN was zero-padded in the requests
calls = mock_async_client.post.call_args_list
assert calls[0][1]["json"] == {"Uprn": "UPRN000000123456"}
assert calls[1][1]["json"] == {"uprn": "000000123456"}
class TestGetData:
"""Test get_data function."""
@pytest.mark.asyncio
async def test_get_data_force_cache(self, temp_data_dir, sample_bristol_data):
"""Test using forced cache."""
uprn = "123456789012"
os.makedirs(os.path.join(temp_data_dir, "waste"))
# Create a cached file
cache_file = os.path.join(
temp_data_dir, "waste", f"2024-07-01_12:00_{uprn}.json"
)
with open(cache_file, "w") as f:
json.dump({"data": sample_bristol_data}, f)
result = await bristol_waste.get_data(temp_data_dir, uprn, "force")
assert result == sample_bristol_data
@pytest.mark.asyncio
async def test_get_data_refresh_cache(
self, temp_data_dir, sample_bristol_data, mock_response
):
"""Test refreshing cache."""
uprn = "123456789012"
with patch("agenda.bristol_waste.get_web_data", return_value=mock_response):
with patch("agenda.bristol_waste.datetime") as mock_datetime:
mock_now = datetime(2024, 7, 15, 14, 30)
mock_datetime.now.return_value = mock_now
mock_datetime.strptime = datetime.strptime
result = await bristol_waste.get_data(temp_data_dir, uprn, "refresh")
assert result == sample_bristol_data
# Check that cache file was created
waste_dir = os.path.join(temp_data_dir, "waste")
cache_files = [
f for f in os.listdir(waste_dir) if f.endswith(f"_{uprn}.json")
]
assert len(cache_files) == 1
assert cache_files[0] == f"2024-07-15_14:30_{uprn}.json"
@pytest.mark.asyncio
async def test_get_data_recent_cache(self, temp_data_dir, sample_bristol_data):
"""Test using recent cache within TTL."""
uprn = "123456789012"
os.makedirs(os.path.join(temp_data_dir, "waste"))
# Create a recent cached file (within TTL)
recent_time = datetime.now() - timedelta(hours=6)
cache_file = os.path.join(
temp_data_dir,
"waste",
f"{recent_time.strftime('%Y-%m-%d_%H:%M')}_{uprn}.json",
)
with open(cache_file, "w") as f:
json.dump({"data": sample_bristol_data}, f)
result = await bristol_waste.get_data(temp_data_dir, uprn, "auto")
assert result == sample_bristol_data
@pytest.mark.asyncio
async def test_get_data_expired_cache(
self, temp_data_dir, sample_bristol_data, mock_response
):
"""Test with expired cache, should fetch new data."""
uprn = "123456789012"
os.makedirs(os.path.join(temp_data_dir, "waste"))
# Create an old cached file (beyond TTL)
old_time = datetime.now() - timedelta(hours=25)
cache_file = os.path.join(
temp_data_dir, "waste", f"{old_time.strftime('%Y-%m-%d_%H:%M')}_{uprn}.json"
)
with open(cache_file, "w") as f:
json.dump({"data": [{"old": "data"}]}, f)
with patch("agenda.bristol_waste.get_web_data", return_value=mock_response):
result = await bristol_waste.get_data(temp_data_dir, uprn, "auto")
assert result == sample_bristol_data
@pytest.mark.asyncio
async def test_get_data_timeout_fallback(self, temp_data_dir, sample_bristol_data):
"""Test fallback to cache when web request times out."""
uprn = "123456789012"
os.makedirs(os.path.join(temp_data_dir, "waste"))
# Create a cached file
cache_file = os.path.join(
temp_data_dir, "waste", f"2024-07-01_12:00_{uprn}.json"
)
with open(cache_file, "w") as f:
json.dump({"data": sample_bristol_data}, f)
with patch(
"agenda.bristol_waste.get_web_data",
side_effect=httpx.ReadTimeout("Timeout"),
):
result = await bristol_waste.get_data(temp_data_dir, uprn, "refresh")
assert result == sample_bristol_data
class TestGet:
"""Test main get function."""
@pytest.mark.asyncio
async def test_get_events(self, temp_data_dir, sample_bristol_data):
"""Test generating events from Bristol waste data."""
start_date = date(2024, 7, 10)
uprn = "123456789012"
with patch("agenda.bristol_waste.get_data", return_value=sample_bristol_data):
events = await bristol_waste.get(start_date, temp_data_dir, uprn, "auto")
assert len(events) == 3 # Only dates before start_date
# Check event properties
for event in events:
assert isinstance(event, Event)
assert event.name == "waste_schedule"
assert event.title.startswith("Bristol: ")
assert event.date < start_date
@pytest.mark.asyncio
async def test_get_events_filtered_by_start_date(
self, temp_data_dir, sample_bristol_data
):
"""Test events are filtered by start date."""
start_date = date(2024, 7, 20) # Later start date
uprn = "123456789012"
with patch("agenda.bristol_waste.get_data", return_value=sample_bristol_data):
events = await bristol_waste.get(start_date, temp_data_dir, uprn, "auto")
# Should get all events before start_date (all dates in sample data)
assert len(events) == 5 # All collection dates are before 2024-07-20
for event in events:
assert event.date < start_date
@pytest.mark.asyncio
async def test_get_events_combined_services(self, temp_data_dir):
"""Test services are combined for same date."""
# Create data with multiple services on same date
data = [
{
"containerName": "Recycling Container",
"collection": [
{
"nextCollectionDate": "2024-07-15T00:00:00Z",
"lastCollectionDate": "2024-07-01T00:00:00Z",
}
],
},
{
"containerName": "Food Waste Container",
"collection": [
{
"nextCollectionDate": "2024-07-15T00:00:00Z",
"lastCollectionDate": "2024-07-01T00:00:00Z",
}
],
},
]
start_date = date(2024, 7, 10)
uprn = "123456789012"
with patch("agenda.bristol_waste.get_data", return_value=data):
events = await bristol_waste.get(start_date, temp_data_dir, uprn, "auto")
# Should have 1 event (only the 2024-07-01 date is before start_date)
assert len(events) == 1
# Check that the event for 2024-07-01 combines both services
july_1_event = events[0]
assert july_1_event.date == date(2024, 7, 1)
assert "Recycling" in july_1_event.title
assert "Waste Container" in july_1_event.title
@pytest.mark.asyncio
async def test_get_empty_data(self, temp_data_dir):
"""Test with empty data."""
start_date = date(2024, 7, 10)
uprn = "123456789012"
with patch("agenda.bristol_waste.get_data", return_value=[]):
events = await bristol_waste.get(start_date, temp_data_dir, uprn, "auto")
assert events == []