- 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>
371 lines
13 KiB
Python
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 == []
|