"""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 == []