"""Tests for the weather service.""" import pytest import responses from app.services.weather_service import WeatherService, WeatherServiceError from app.utils.http_client import HttpClient SAMPLE_API_RESPONSE = { "address": "viola,tn", "resolvedAddress": "Viola, TN, United States", "timezone": "America/Chicago", "days": [ { "datetime": "2024-01-15", "hours": [ { "datetime": "12:00:00", "datetimeEpoch": 2000000000, # Future timestamp "temp": 45.0, "feelslike": 42.0, "humidity": 65.0, "precip": 0.0, "precipprob": 20.0, "snow": 0.0, "snowdepth": 0.0, "windspeed": 10.0, "windgust": 15.0, "winddir": 180.0, "pressure": 30.1, "visibility": 10.0, "cloudcover": 50.0, "uvindex": 3, "conditions": "Partially cloudy", "icon": "partly-cloudy-day", }, { "datetime": "13:00:00", "datetimeEpoch": 2000003600, "temp": 48.0, "feelslike": 45.0, "humidity": 60.0, "precip": 0.0, "precipprob": 15.0, "snow": 0.0, "snowdepth": 0.0, "windspeed": 12.0, "windgust": 18.0, "winddir": 185.0, "pressure": 30.0, "visibility": 10.0, "cloudcover": 45.0, "uvindex": 4, "conditions": "Partially cloudy", "icon": "partly-cloudy-day", }, ], } ], "alerts": [ { "event": "Wind Advisory", "headline": "Wind Advisory in effect until 6 PM", "description": "Gusty winds expected throughout the day.", "onset": "2024-01-15T08:00:00", "ends": "2024-01-15T18:00:00", "id": "NWS-456", "language": "en", "link": "https://weather.gov/alerts/456", } ], } class TestWeatherService: """Test weather service functionality.""" def test_requires_api_key(self) -> None: """Test that service requires an API key.""" with pytest.raises(WeatherServiceError, match="API key is required"): WeatherService(api_key="") @responses.activate def test_get_forecast_success(self) -> None: """Test successful forecast retrieval.""" responses.add( responses.GET, "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/viola%2Ctn", json=SAMPLE_API_RESPONSE, status=200, ) service = WeatherService(api_key="test-key") forecast = service.get_forecast("viola,tn", hours_ahead=24) assert forecast.location == "viola,tn" assert forecast.resolved_address == "Viola, TN, United States" assert len(forecast.hourly_forecasts) == 2 assert len(forecast.alerts) == 1 assert forecast.alerts[0].event == "Wind Advisory" @responses.activate def test_get_forecast_parses_hourly_data(self) -> None: """Test that hourly forecast data is correctly parsed.""" responses.add( responses.GET, "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/viola%2Ctn", json=SAMPLE_API_RESPONSE, status=200, ) service = WeatherService(api_key="test-key") forecast = service.get_forecast("viola,tn") hour = forecast.hourly_forecasts[0] assert hour.temp == 45.0 assert hour.feelslike == 42.0 assert hour.humidity == 65.0 assert hour.precip_prob == 20.0 assert hour.wind_speed == 10.0 assert hour.wind_gust == 15.0 assert hour.conditions == "Partially cloudy" @responses.activate def test_get_forecast_handles_api_error(self) -> None: """Test handling of API errors.""" responses.add( responses.GET, "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/viola%2Ctn", json={"error": "Invalid API key"}, status=401, ) service = WeatherService(api_key="bad-key") with pytest.raises(WeatherServiceError, match="Failed to fetch forecast"): service.get_forecast("viola,tn") @responses.activate def test_get_forecast_handles_invalid_json(self) -> None: """Test handling of invalid JSON response.""" responses.add( responses.GET, "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/viola%2Ctn", body="not json", status=200, content_type="text/plain", ) service = WeatherService(api_key="test-key") with pytest.raises(WeatherServiceError, match="Invalid JSON"): service.get_forecast("viola,tn") @responses.activate def test_location_encoding(self) -> None: """Test that location is properly URL encoded.""" responses.add( responses.GET, "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/Nashville%2C%20TN", json={ "address": "Nashville, TN", "resolvedAddress": "Nashville, TN, United States", "timezone": "America/Chicago", "days": [], "alerts": [], }, status=200, ) service = WeatherService(api_key="test-key") forecast = service.get_forecast("Nashville, TN") assert forecast.resolved_address == "Nashville, TN, United States"