REFACTOR data service
This commit is contained in:
314
services/external/tests/conftest.py
vendored
Normal file
314
services/external/tests/conftest.py
vendored
Normal 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()
|
||||
9
services/external/tests/requirements.txt
vendored
Normal file
9
services/external/tests/requirements.txt
vendored
Normal 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
|
||||
393
services/external/tests/unit/test_repositories.py
vendored
Normal file
393
services/external/tests/unit/test_repositories.py
vendored
Normal 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
|
||||
445
services/external/tests/unit/test_services.py
vendored
Normal file
445
services/external/tests/unit/test_services.py
vendored
Normal 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
|
||||
Reference in New Issue
Block a user