445 lines
20 KiB
Python
445 lines
20 KiB
Python
# 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 |