Initial commit - production deployment

This commit is contained in:
2026-01-21 17:17:16 +01:00
commit c23d00dd92
2289 changed files with 638440 additions and 0 deletions

314
services/external/tests/conftest.py vendored Normal file
View File

@@ -0,0 +1,314 @@
# services/external/tests/conftest.py
"""
Pytest configuration and fixtures for External Service tests
"""
import pytest
import asyncio
from datetime import datetime, timezone
from typing import AsyncGenerator
from uuid import uuid4, UUID
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.pool import StaticPool
from fastapi.testclient import TestClient
from app.main import app
from app.core.config import settings
from app.core.database import Base, get_db
from app.models.weather import WeatherData, WeatherStation
from app.models.traffic import TrafficData, TrafficMeasurementPoint
# Test database configuration
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
@pytest.fixture(scope="session")
def event_loop():
"""Create event loop for the test session"""
loop = asyncio.new_event_loop()
yield loop
loop.close()
@pytest.fixture
async def test_engine():
"""Create test database engine"""
engine = create_async_engine(
TEST_DATABASE_URL,
poolclass=StaticPool,
connect_args={"check_same_thread": False}
)
# Create tables
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
await engine.dispose()
@pytest.fixture
async def test_db_session(test_engine) -> AsyncGenerator[AsyncSession, None]:
"""Create test database session"""
async_session = async_sessionmaker(
test_engine, class_=AsyncSession, expire_on_commit=False
)
async with async_session() as session:
yield session
@pytest.fixture
def test_client():
"""Create test client"""
return TestClient(app)
@pytest.fixture
async def override_get_db(test_db_session):
"""Override get_db dependency for testing"""
async def _override_get_db():
yield test_db_session
app.dependency_overrides[get_db] = _override_get_db
yield
app.dependency_overrides.clear()
# Test data fixtures
@pytest.fixture
def sample_tenant_id() -> UUID:
"""Sample tenant ID for testing"""
return uuid4()
@pytest.fixture
def sample_weather_data() -> dict:
"""Sample weather data for testing"""
return {
"city": "madrid",
"location_id": "40.4168,-3.7038",
"date": datetime.now(timezone.utc),
"temperature": 18.5,
"humidity": 65.0,
"pressure": 1013.2,
"wind_speed": 10.2,
"condition": "partly_cloudy",
"description": "Parcialmente nublado",
"source": "aemet",
"data_type": "current",
"is_forecast": False,
"data_quality_score": 95.0
}
@pytest.fixture
def sample_traffic_data() -> dict:
"""Sample traffic data for testing"""
return {
"city": "madrid",
"location_id": "PM_M30_001",
"date": datetime.now(timezone.utc),
"measurement_point_id": "PM_M30_001",
"measurement_point_name": "M-30 Norte - Nudo Norte",
"measurement_point_type": "M30",
"traffic_volume": 850,
"average_speed": 65.2,
"congestion_level": "medium",
"occupation_percentage": 45.8,
"latitude": 40.4501,
"longitude": -3.6919,
"district": "Chamartín",
"source": "madrid_opendata",
"data_quality_score": 92.0,
"is_synthetic": False
}
@pytest.fixture
def sample_weather_forecast() -> list[dict]:
"""Sample weather forecast data"""
base_date = datetime.now(timezone.utc)
return [
{
"city": "madrid",
"location_id": "40.4168,-3.7038",
"date": base_date,
"forecast_date": base_date,
"temperature": 20.0,
"temperature_min": 15.0,
"temperature_max": 25.0,
"precipitation": 0.0,
"humidity": 60.0,
"wind_speed": 12.0,
"condition": "sunny",
"description": "Soleado",
"source": "aemet",
"data_type": "forecast",
"is_forecast": True,
"data_quality_score": 85.0
}
]
@pytest.fixture
async def populated_weather_db(test_db_session: AsyncSession, sample_weather_data: dict):
"""Database populated with weather test data"""
weather_record = WeatherData(**sample_weather_data)
test_db_session.add(weather_record)
await test_db_session.commit()
yield test_db_session
@pytest.fixture
async def populated_traffic_db(test_db_session: AsyncSession, sample_traffic_data: dict):
"""Database populated with traffic test data"""
traffic_record = TrafficData(**sample_traffic_data)
test_db_session.add(traffic_record)
await test_db_session.commit()
yield test_db_session
# Mock external API fixtures
@pytest.fixture
def mock_aemet_response():
"""Mock AEMET API response"""
return {
"date": datetime.now(timezone.utc),
"temperature": 18.5,
"humidity": 65.0,
"pressure": 1013.2,
"wind_speed": 10.2,
"description": "Parcialmente nublado",
"source": "aemet"
}
@pytest.fixture
def mock_madrid_traffic_xml():
"""Mock Madrid Open Data traffic XML"""
return """<?xml version="1.0" encoding="UTF-8"?>
<pms>
<pm codigo="PM_M30_001" nombre="M-30 Norte - Nudo Norte">
<intensidad>850</intensidad>
<ocupacion>45</ocupacion>
<velocidad>65</velocidad>
<fechahora>2024-01-15T10:30:00</fechahora>
</pm>
<pm codigo="PM_URB_002" nombre="Gran Vía - Plaza España">
<intensidad>320</intensidad>
<ocupacion>78</ocupacion>
<velocidad>25</velocidad>
<fechahora>2024-01-15T10:30:00</fechahora>
</pm>
</pms>"""
@pytest.fixture
def mock_messaging():
"""Mock messaging service"""
class MockMessaging:
def __init__(self):
self.published_events = []
async def publish_weather_updated(self, data):
self.published_events.append(("weather_updated", data))
return True
async def publish_traffic_updated(self, data):
self.published_events.append(("traffic_updated", data))
return True
async def publish_collection_job_started(self, data):
self.published_events.append(("job_started", data))
return True
async def publish_collection_job_completed(self, data):
self.published_events.append(("job_completed", data))
return True
return MockMessaging()
# Mock external clients
@pytest.fixture
def mock_aemet_client():
"""Mock AEMET client"""
class MockAEMETClient:
async def get_current_weather(self, lat, lon):
return {
"date": datetime.now(timezone.utc),
"temperature": 18.5,
"humidity": 65.0,
"pressure": 1013.2,
"wind_speed": 10.2,
"description": "Parcialmente nublado",
"source": "aemet"
}
async def get_forecast(self, lat, lon, days):
return [
{
"forecast_date": datetime.now(timezone.utc),
"temperature": 20.0,
"temperature_min": 15.0,
"temperature_max": 25.0,
"precipitation": 0.0,
"humidity": 60.0,
"wind_speed": 12.0,
"description": "Soleado",
"source": "aemet"
}
]
return MockAEMETClient()
@pytest.fixture
def mock_madrid_client():
"""Mock Madrid traffic client"""
class MockMadridClient:
async def fetch_current_traffic_xml(self):
return """<?xml version="1.0" encoding="UTF-8"?>
<pms>
<pm codigo="PM_TEST_001" nombre="Test Point">
<intensidad>500</intensidad>
<ocupacion>50</ocupacion>
<velocidad>50</velocidad>
<fechahora>2024-01-15T10:30:00</fechahora>
</pm>
</pms>"""
return MockMadridClient()
@pytest.fixture
def mock_madrid_processor():
"""Mock Madrid traffic processor"""
class MockMadridProcessor:
async def process_current_traffic_xml(self, xml_content):
return [
{
"city": "madrid",
"location_id": "PM_TEST_001",
"date": datetime.now(timezone.utc),
"measurement_point_id": "PM_TEST_001",
"measurement_point_name": "Test Point",
"measurement_point_type": "TEST",
"traffic_volume": 500,
"average_speed": 50.0,
"congestion_level": "medium",
"occupation_percentage": 50.0,
"latitude": 40.4168,
"longitude": -3.7038,
"district": "Centro",
"source": "madrid_opendata",
"data_quality_score": 90.0,
"is_synthetic": False
}
]
return MockMadridProcessor()

View File

@@ -0,0 +1,9 @@
# Testing dependencies for External Service
pytest==7.4.3
pytest-asyncio==0.21.1
pytest-mock==3.12.0
httpx==0.25.2
fastapi[all]==0.104.1
sqlalchemy[asyncio]==2.0.23
aiosqlite==0.19.0
coverage==7.3.2

View File

@@ -0,0 +1,393 @@
# services/external/tests/unit/test_repositories.py
"""
Unit tests for External Service Repositories
"""
import pytest
from datetime import datetime, timezone, timedelta
from uuid import uuid4
from app.repositories.weather_repository import WeatherRepository
from app.repositories.traffic_repository import TrafficRepository
from app.models.weather import WeatherData, WeatherStation, WeatherDataJob
from app.models.traffic import TrafficData, TrafficMeasurementPoint, TrafficDataJob
@pytest.mark.asyncio
class TestWeatherRepository:
"""Test Weather Repository operations"""
async def test_create_weather_data(self, test_db_session, sample_weather_data):
"""Test creating weather data"""
repository = WeatherRepository(test_db_session)
record = await repository.create_weather_data(sample_weather_data)
assert record is not None
assert record.id is not None
assert record.city == sample_weather_data["city"]
assert record.temperature == sample_weather_data["temperature"]
async def test_get_current_weather(self, populated_weather_db, sample_weather_data):
"""Test getting current weather data"""
repository = WeatherRepository(populated_weather_db)
result = await repository.get_current_weather("madrid")
assert result is not None
assert result.city == "madrid"
assert result.temperature == sample_weather_data["temperature"]
async def test_get_weather_forecast(self, test_db_session, sample_weather_forecast):
"""Test getting weather forecast"""
repository = WeatherRepository(test_db_session)
# Create forecast data
for forecast_item in sample_weather_forecast:
await repository.create_weather_data(forecast_item)
result = await repository.get_weather_forecast("madrid", 7)
assert len(result) == 1
assert result[0].is_forecast is True
async def test_get_historical_weather(self, test_db_session, sample_weather_data):
"""Test getting historical weather data"""
repository = WeatherRepository(test_db_session)
# Create historical data
historical_data = sample_weather_data.copy()
historical_data["date"] = datetime.now(timezone.utc) - timedelta(days=1)
await repository.create_weather_data(historical_data)
start_date = datetime.now(timezone.utc) - timedelta(days=2)
end_date = datetime.now(timezone.utc)
result = await repository.get_historical_weather("madrid", start_date, end_date)
assert len(result) >= 1
async def test_create_weather_station(self, test_db_session):
"""Test creating weather station"""
repository = WeatherRepository(test_db_session)
station_data = {
"station_id": "TEST_001",
"name": "Test Station",
"city": "madrid",
"latitude": 40.4168,
"longitude": -3.7038,
"altitude": 650.0,
"is_active": True
}
station = await repository.create_weather_station(station_data)
assert station is not None
assert station.station_id == "TEST_001"
assert station.name == "Test Station"
async def test_get_weather_stations(self, test_db_session):
"""Test getting weather stations"""
repository = WeatherRepository(test_db_session)
# Create test station
station_data = {
"station_id": "TEST_001",
"name": "Test Station",
"city": "madrid",
"latitude": 40.4168,
"longitude": -3.7038,
"is_active": True
}
await repository.create_weather_station(station_data)
stations = await repository.get_weather_stations("madrid")
assert len(stations) == 1
assert stations[0].station_id == "TEST_001"
async def test_create_weather_job(self, test_db_session, sample_tenant_id):
"""Test creating weather data collection job"""
repository = WeatherRepository(test_db_session)
job_data = {
"job_type": "current",
"city": "madrid",
"status": "pending",
"scheduled_at": datetime.utcnow(),
"tenant_id": sample_tenant_id
}
job = await repository.create_weather_job(job_data)
assert job is not None
assert job.job_type == "current"
assert job.status == "pending"
async def test_update_weather_job(self, test_db_session, sample_tenant_id):
"""Test updating weather job"""
repository = WeatherRepository(test_db_session)
# Create job first
job_data = {
"job_type": "current",
"city": "madrid",
"status": "pending",
"scheduled_at": datetime.utcnow(),
"tenant_id": sample_tenant_id
}
job = await repository.create_weather_job(job_data)
# Update job
update_data = {
"status": "completed",
"completed_at": datetime.utcnow(),
"success_count": 1
}
success = await repository.update_weather_job(job.id, update_data)
assert success is True
async def test_get_weather_jobs(self, test_db_session, sample_tenant_id):
"""Test getting weather jobs"""
repository = WeatherRepository(test_db_session)
# Create test job
job_data = {
"job_type": "forecast",
"city": "madrid",
"status": "completed",
"scheduled_at": datetime.utcnow(),
"tenant_id": sample_tenant_id
}
await repository.create_weather_job(job_data)
jobs = await repository.get_weather_jobs()
assert len(jobs) >= 1
assert any(job.job_type == "forecast" for job in jobs)
@pytest.mark.asyncio
class TestTrafficRepository:
"""Test Traffic Repository operations"""
async def test_create_traffic_data(self, test_db_session, sample_traffic_data):
"""Test creating traffic data"""
repository = TrafficRepository(test_db_session)
# Convert sample data to list for bulk create
traffic_list = [sample_traffic_data]
count = await repository.bulk_create_traffic_data(traffic_list)
assert count == 1
async def test_get_current_traffic(self, populated_traffic_db, sample_traffic_data):
"""Test getting current traffic data"""
repository = TrafficRepository(populated_traffic_db)
result = await repository.get_current_traffic("madrid")
assert len(result) >= 1
assert result[0].city == "madrid"
async def test_get_current_traffic_with_filters(self, populated_traffic_db):
"""Test getting current traffic with filters"""
repository = TrafficRepository(populated_traffic_db)
result = await repository.get_current_traffic("madrid", district="Chamartín")
# Should return results based on filter
assert isinstance(result, list)
async def test_get_historical_traffic(self, test_db_session, sample_traffic_data):
"""Test getting historical traffic data"""
repository = TrafficRepository(test_db_session)
# Create historical data
historical_data = sample_traffic_data.copy()
historical_data["date"] = datetime.now(timezone.utc) - timedelta(days=1)
await repository.bulk_create_traffic_data([historical_data])
start_date = datetime.now(timezone.utc) - timedelta(days=2)
end_date = datetime.now(timezone.utc)
result = await repository.get_historical_traffic("madrid", start_date, end_date)
assert len(result) >= 1
async def test_create_measurement_point(self, test_db_session):
"""Test creating traffic measurement point"""
repository = TrafficRepository(test_db_session)
point_data = {
"point_id": "TEST_POINT_001",
"name": "Test Measurement Point",
"city": "madrid",
"point_type": "TEST",
"latitude": 40.4168,
"longitude": -3.7038,
"district": "Centro",
"road_name": "Test Road",
"is_active": True
}
point = await repository.create_measurement_point(point_data)
assert point is not None
assert point.point_id == "TEST_POINT_001"
assert point.name == "Test Measurement Point"
async def test_get_measurement_points(self, test_db_session):
"""Test getting measurement points"""
repository = TrafficRepository(test_db_session)
# Create test point
point_data = {
"point_id": "TEST_POINT_001",
"name": "Test Point",
"city": "madrid",
"point_type": "TEST",
"latitude": 40.4168,
"longitude": -3.7038,
"is_active": True
}
await repository.create_measurement_point(point_data)
points = await repository.get_measurement_points("madrid")
assert len(points) == 1
assert points[0].point_id == "TEST_POINT_001"
async def test_get_measurement_points_with_filters(self, test_db_session):
"""Test getting measurement points with filters"""
repository = TrafficRepository(test_db_session)
# Create test points with different types
for i, point_type in enumerate(["M30", "URB", "TEST"]):
point_data = {
"point_id": f"TEST_POINT_{i:03d}",
"name": f"Test Point {i}",
"city": "madrid",
"point_type": point_type,
"latitude": 40.4168,
"longitude": -3.7038,
"is_active": True
}
await repository.create_measurement_point(point_data)
# Filter by type
points = await repository.get_measurement_points("madrid", road_type="M30")
assert len(points) == 1
assert points[0].point_type == "M30"
async def test_get_traffic_analytics(self, populated_traffic_db):
"""Test getting traffic analytics"""
repository = TrafficRepository(populated_traffic_db)
analytics = await repository.get_traffic_analytics("madrid")
assert isinstance(analytics, dict)
assert "total_measurements" in analytics
assert "average_volume" in analytics
async def test_create_traffic_job(self, test_db_session, sample_tenant_id):
"""Test creating traffic collection job"""
repository = TrafficRepository(test_db_session)
job_data = {
"job_type": "current",
"city": "madrid",
"status": "pending",
"scheduled_at": datetime.utcnow(),
"tenant_id": sample_tenant_id
}
job = await repository.create_traffic_job(job_data)
assert job is not None
assert job.job_type == "current"
assert job.status == "pending"
async def test_update_traffic_job(self, test_db_session, sample_tenant_id):
"""Test updating traffic job"""
repository = TrafficRepository(test_db_session)
# Create job first
job_data = {
"job_type": "current",
"city": "madrid",
"status": "pending",
"scheduled_at": datetime.utcnow(),
"tenant_id": sample_tenant_id
}
job = await repository.create_traffic_job(job_data)
# Update job
update_data = {
"status": "completed",
"completed_at": datetime.utcnow(),
"success_count": 10
}
success = await repository.update_traffic_job(job.id, update_data)
assert success is True
async def test_get_traffic_jobs(self, test_db_session, sample_tenant_id):
"""Test getting traffic jobs"""
repository = TrafficRepository(test_db_session)
# Create test job
job_data = {
"job_type": "historical",
"city": "madrid",
"status": "completed",
"scheduled_at": datetime.utcnow(),
"tenant_id": sample_tenant_id
}
await repository.create_traffic_job(job_data)
jobs = await repository.get_traffic_jobs()
assert len(jobs) >= 1
assert any(job.job_type == "historical" for job in jobs)
async def test_bulk_create_performance(self, test_db_session):
"""Test bulk create performance"""
repository = TrafficRepository(test_db_session)
# Create large dataset
bulk_data = []
for i in range(100):
data = {
"city": "madrid",
"location_id": f"PM_TEST_{i:03d}",
"date": datetime.now(timezone.utc),
"measurement_point_id": f"PM_TEST_{i:03d}",
"measurement_point_name": f"Test Point {i}",
"measurement_point_type": "TEST",
"traffic_volume": 100 + i,
"average_speed": 50.0,
"congestion_level": "medium",
"occupation_percentage": 50.0,
"latitude": 40.4168,
"longitude": -3.7038,
"source": "test"
}
bulk_data.append(data)
import time
start_time = time.time()
count = await repository.bulk_create_traffic_data(bulk_data)
end_time = time.time()
execution_time = end_time - start_time
assert count == 100
assert execution_time < 3.0 # Should complete in under 3 seconds

View File

@@ -0,0 +1,445 @@
# 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