# services/external/tests/unit/test_services.py """ Unit tests for External Service Services """ import pytest from datetime import datetime, timezone, timedelta from unittest.mock import AsyncMock, patch from uuid import uuid4 from app.services.weather_service import WeatherService from app.services.traffic_service import TrafficService @pytest.mark.asyncio class TestWeatherService: """Test Weather Service business logic""" @pytest.fixture def weather_service(self): """Create weather service instance""" return WeatherService() async def test_get_current_weather_from_cache(self, weather_service): """Test getting current weather from cache""" with patch('app.services.weather_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() mock_weather = AsyncMock() mock_weather.date = datetime.now(timezone.utc) - timedelta(minutes=30) # Fresh data mock_weather.to_dict.return_value = {"temperature": 18.5, "city": "madrid"} mock_repository.get_current_weather.return_value = mock_weather with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository): result = await weather_service.get_current_weather("madrid") assert result is not None assert result["temperature"] == 18.5 assert result["city"] == "madrid" async def test_get_current_weather_fetch_from_api(self, weather_service, mock_aemet_response): """Test getting current weather from API when cache is stale""" with patch('app.services.weather_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() # No cached data or stale data mock_repository.get_current_weather.return_value = None mock_stored = AsyncMock() mock_stored.to_dict.return_value = {"temperature": 20.0} mock_repository.create_weather_data.return_value = mock_stored # Mock AEMET client mock_client = AsyncMock() mock_client.get_current_weather.return_value = mock_aemet_response with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository): weather_service.aemet_client = mock_client result = await weather_service.get_current_weather("madrid") assert result is not None assert result["temperature"] == 20.0 mock_client.get_current_weather.assert_called_once() async def test_get_weather_forecast_from_cache(self, weather_service): """Test getting weather forecast from cache""" with patch('app.services.weather_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() mock_forecast = [AsyncMock(), AsyncMock()] for item in mock_forecast: item.created_at = datetime.now(timezone.utc) - timedelta(hours=1) # Fresh item.to_dict.return_value = {"temperature": 22.0} mock_repository.get_weather_forecast.return_value = mock_forecast with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository): result = await weather_service.get_weather_forecast("madrid", 7) assert len(result) == 2 assert all(item["temperature"] == 22.0 for item in result) async def test_get_weather_forecast_fetch_from_api(self, weather_service): """Test getting weather forecast from API when cache is stale""" with patch('app.services.weather_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() # No cached data mock_repository.get_weather_forecast.return_value = [] mock_stored = AsyncMock() mock_stored.to_dict.return_value = {"temperature": 25.0} mock_repository.create_weather_data.return_value = mock_stored # Mock AEMET client mock_client = AsyncMock() mock_client.get_forecast.return_value = [ {"forecast_date": datetime.now(), "temperature": 25.0} ] with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository): weather_service.aemet_client = mock_client result = await weather_service.get_weather_forecast("madrid", 7) assert len(result) == 1 assert result[0]["temperature"] == 25.0 mock_client.get_forecast.assert_called_once() async def test_get_historical_weather(self, weather_service, sample_tenant_id): """Test getting historical weather data""" start_date = datetime.now(timezone.utc) - timedelta(days=7) end_date = datetime.now(timezone.utc) with patch('app.services.weather_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() mock_historical = [AsyncMock(), AsyncMock()] for item in mock_historical: item.to_dict.return_value = {"temperature": 18.0} mock_repository.get_historical_weather.return_value = mock_historical with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository): result = await weather_service.get_historical_weather( "madrid", start_date, end_date, sample_tenant_id ) assert len(result) == 2 assert all(item["temperature"] == 18.0 for item in result) async def test_get_weather_stations(self, weather_service): """Test getting weather stations""" with patch('app.services.weather_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() mock_stations = [AsyncMock()] mock_stations[0].to_dict.return_value = {"station_id": "TEST_001"} mock_repository.get_weather_stations.return_value = mock_stations with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository): result = await weather_service.get_weather_stations("madrid") assert len(result) == 1 assert result[0]["station_id"] == "TEST_001" async def test_trigger_weather_collection(self, weather_service, sample_tenant_id): """Test triggering weather data collection""" with patch('app.services.weather_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() mock_job = AsyncMock() mock_job.id = uuid4() mock_job.to_dict.return_value = {"id": str(mock_job.id), "status": "pending"} mock_repository.create_weather_job.return_value = mock_job with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository): result = await weather_service.trigger_weather_collection( "madrid", "current", sample_tenant_id ) assert result["status"] == "pending" mock_repository.create_weather_job.assert_called_once() async def test_process_weather_collection_job(self, weather_service): """Test processing weather collection job""" job_id = uuid4() with patch('app.services.weather_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() # Mock job mock_job = AsyncMock() mock_job.id = job_id mock_job.job_type = "current" mock_job.city = "madrid" mock_repository.get_weather_jobs.return_value = [mock_job] mock_repository.update_weather_job.return_value = True # Mock updated job after completion mock_updated_job = AsyncMock() mock_updated_job.to_dict.return_value = {"id": str(job_id), "status": "completed"} # Mock methods for different calls def mock_get_jobs_side_effect(): return [mock_updated_job] # Return completed job mock_repository.get_weather_jobs.side_effect = [ [mock_job], # First call returns pending job [mock_updated_job] # Second call returns completed job ] with patch('app.services.weather_service.WeatherRepository', return_value=mock_repository): with patch.object(weather_service, '_collect_current_weather', return_value=1): result = await weather_service.process_weather_collection_job(job_id) assert result["status"] == "completed" async def test_map_weather_condition(self, weather_service): """Test weather condition mapping""" test_cases = [ ("Soleado", "clear"), ("Nublado", "cloudy"), ("Parcialmente nublado", "partly_cloudy"), ("Lluvioso", "rainy"), ("Nevando", "snowy"), ("Tormenta", "stormy"), ("Desconocido", "unknown") ] for description, expected in test_cases: result = weather_service._map_weather_condition(description) assert result == expected @pytest.mark.asyncio class TestTrafficService: """Test Traffic Service business logic""" @pytest.fixture def traffic_service(self): """Create traffic service instance""" return TrafficService() async def test_get_current_traffic_from_cache(self, traffic_service): """Test getting current traffic from cache""" with patch('app.services.traffic_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() mock_traffic = [AsyncMock()] mock_traffic[0].date = datetime.now(timezone.utc) - timedelta(minutes=5) # Fresh mock_traffic[0].to_dict.return_value = {"traffic_volume": 850} mock_repository.get_current_traffic.return_value = mock_traffic with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository): result = await traffic_service.get_current_traffic("madrid") assert len(result) == 1 assert result[0]["traffic_volume"] == 850 async def test_get_current_traffic_fetch_from_api(self, traffic_service, mock_madrid_traffic_xml): """Test getting current traffic from API when cache is stale""" with patch('app.services.traffic_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() # No cached data mock_repository.get_current_traffic.return_value = [] mock_repository.bulk_create_traffic_data.return_value = 2 # Mock clients mock_client = AsyncMock() mock_client.fetch_current_traffic_xml.return_value = mock_madrid_traffic_xml mock_processor = AsyncMock() mock_processor.process_current_traffic_xml.return_value = [ {"traffic_volume": 850, "measurement_point_id": "PM_M30_001"}, {"traffic_volume": 320, "measurement_point_id": "PM_URB_002"} ] with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository): traffic_service.madrid_client = mock_client traffic_service.madrid_processor = mock_processor result = await traffic_service.get_current_traffic("madrid") assert len(result) == 2 assert result[0]["traffic_volume"] == 850 mock_client.fetch_current_traffic_xml.assert_called_once() async def test_get_historical_traffic(self, traffic_service, sample_tenant_id): """Test getting historical traffic data""" start_date = datetime.now(timezone.utc) - timedelta(days=7) end_date = datetime.now(timezone.utc) with patch('app.services.traffic_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() mock_historical = [AsyncMock(), AsyncMock()] for item in mock_historical: item.to_dict.return_value = {"traffic_volume": 500} mock_repository.get_historical_traffic.return_value = mock_historical with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository): result = await traffic_service.get_historical_traffic( "madrid", start_date, end_date, tenant_id=sample_tenant_id ) assert len(result) == 2 assert all(item["traffic_volume"] == 500 for item in result) async def test_get_measurement_points(self, traffic_service): """Test getting measurement points""" with patch('app.services.traffic_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() mock_points = [AsyncMock()] mock_points[0].to_dict.return_value = {"point_id": "PM_TEST_001"} mock_repository.get_measurement_points.return_value = mock_points with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository): result = await traffic_service.get_measurement_points("madrid") assert len(result) == 1 assert result[0]["point_id"] == "PM_TEST_001" async def test_get_traffic_analytics(self, traffic_service): """Test getting traffic analytics""" start_date = datetime.now(timezone.utc) - timedelta(days=30) end_date = datetime.now(timezone.utc) with patch('app.services.traffic_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() mock_analytics = { "total_measurements": 1000, "average_volume": 650.5, "peak_hour": "08:00" } mock_repository.get_traffic_analytics.return_value = mock_analytics with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository): result = await traffic_service.get_traffic_analytics( "madrid", start_date, end_date ) assert result["total_measurements"] == 1000 assert result["average_volume"] == 650.5 assert "generated_at" in result async def test_trigger_traffic_collection(self, traffic_service, sample_tenant_id): """Test triggering traffic data collection""" with patch('app.services.traffic_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() mock_job = AsyncMock() mock_job.id = uuid4() mock_job.to_dict.return_value = {"id": str(mock_job.id), "status": "pending"} mock_repository.create_traffic_job.return_value = mock_job with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository): result = await traffic_service.trigger_traffic_collection( "madrid", "current", user_id=sample_tenant_id ) assert result["status"] == "pending" mock_repository.create_traffic_job.assert_called_once() async def test_process_traffic_collection_job(self, traffic_service): """Test processing traffic collection job""" job_id = uuid4() with patch('app.services.traffic_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() # Mock job mock_job = AsyncMock() mock_job.id = job_id mock_job.job_type = "current" mock_job.city = "madrid" mock_job.location_pattern = None mock_repository.get_traffic_jobs.return_value = [mock_job] mock_repository.update_traffic_job.return_value = True # Mock updated job after completion mock_updated_job = AsyncMock() mock_updated_job.to_dict.return_value = {"id": str(job_id), "status": "completed"} mock_repository.get_traffic_jobs.side_effect = [ [mock_job], # First call returns pending job [mock_updated_job] # Second call returns completed job ] with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository): with patch.object(traffic_service, '_collect_current_traffic', return_value=125): result = await traffic_service.process_traffic_collection_job(job_id) assert result["status"] == "completed" async def test_is_traffic_data_fresh(self, traffic_service): """Test traffic data freshness check""" from app.models.traffic import TrafficData # Fresh data (5 minutes old) fresh_data = [AsyncMock()] fresh_data[0].date = datetime.utcnow() - timedelta(minutes=5) result = traffic_service._is_traffic_data_fresh(fresh_data) assert result is True # Stale data (15 minutes old) stale_data = [AsyncMock()] stale_data[0].date = datetime.utcnow() - timedelta(minutes=15) result = traffic_service._is_traffic_data_fresh(stale_data) assert result is False # Empty data result = traffic_service._is_traffic_data_fresh([]) assert result is False async def test_collect_current_traffic(self, traffic_service): """Test current traffic collection""" with patch('app.services.traffic_service.get_db_transaction') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db mock_repository = AsyncMock() mock_repository.bulk_create_traffic_data.return_value = 10 with patch('app.services.traffic_service.TrafficRepository', return_value=mock_repository): with patch.object(traffic_service, '_fetch_current_traffic_from_api', return_value=[{} for _ in range(10)]): result = await traffic_service._collect_current_traffic("madrid", None) assert result == 10