159 lines
5.8 KiB
Python
159 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
from django.apps import apps
|
|
from django.test import SimpleTestCase, TestCase, override_settings
|
|
from unittest.mock import Mock, patch
|
|
|
|
from location_data.models import SoilLocation
|
|
from weather.adapters import MockWeatherAdapter, OpenMeteoWeatherAdapter, OpenWeatherOneCallAdapter
|
|
from weather.models import WeatherForecast
|
|
from weather.services import fetch_weather_from_api, update_weather_for_location
|
|
|
|
|
|
class MockWeatherAdapterTests(SimpleTestCase):
|
|
def setUp(self):
|
|
self.adapter = MockWeatherAdapter(delay_seconds=0)
|
|
|
|
def test_same_coordinate_returns_same_forecast(self):
|
|
first = self.adapter.fetch_forecast(35.71, 51.4)
|
|
second = self.adapter.fetch_forecast(35.71, 51.4)
|
|
|
|
self.assertEqual(first, second)
|
|
|
|
def test_nearby_coordinates_produce_nearby_forecast(self):
|
|
first = self.adapter.fetch_forecast(35.71, 51.4)
|
|
second = self.adapter.fetch_forecast(35.715, 51.405)
|
|
|
|
first_daily = first["daily"]
|
|
second_daily = second["daily"]
|
|
self.assertLess(
|
|
abs(first_daily["temperature_2m_mean"][0] - second_daily["temperature_2m_mean"][0]),
|
|
2.5,
|
|
)
|
|
self.assertLess(
|
|
abs(first_daily["relative_humidity_2m_mean"][0] - second_daily["relative_humidity_2m_mean"][0]),
|
|
8.0,
|
|
)
|
|
self.assertLess(
|
|
abs(first_daily["wind_speed_10m_max"][0] - second_daily["wind_speed_10m_max"][0]),
|
|
6.0,
|
|
)
|
|
|
|
def test_shape_matches_open_meteo_daily_contract(self):
|
|
forecast = self.adapter.fetch_forecast(35.71, 51.4)
|
|
daily = forecast["daily"]
|
|
|
|
self.assertEqual(len(daily["time"]), 7)
|
|
self.assertEqual(len(daily["temperature_2m_max"]), 7)
|
|
self.assertEqual(len(daily["weather_code"]), 7)
|
|
|
|
|
|
class WeatherAdapterSelectionTests(SimpleTestCase):
|
|
def tearDown(self):
|
|
apps.get_app_config("weather").__dict__.pop("weather_data_adapter", None)
|
|
|
|
@override_settings(WEATHER_DATA_PROVIDER="mock", WEATHER_MOCK_DELAY_SECONDS=0)
|
|
def test_app_config_returns_mock_adapter(self):
|
|
config = apps.get_app_config("weather")
|
|
config.__dict__.pop("weather_data_adapter", None)
|
|
|
|
adapter = config.get_weather_data_adapter()
|
|
|
|
self.assertIsInstance(adapter, MockWeatherAdapter)
|
|
|
|
@override_settings(WEATHER_DATA_PROVIDER="open-meteo", WEATHER_TIMEOUT_SECONDS=12)
|
|
def test_app_config_returns_live_adapter(self):
|
|
config = apps.get_app_config("weather")
|
|
config.__dict__.pop("weather_data_adapter", None)
|
|
|
|
adapter = config.get_weather_data_adapter()
|
|
|
|
self.assertIsInstance(adapter, OpenMeteoWeatherAdapter)
|
|
self.assertEqual(adapter.timeout, 12)
|
|
|
|
@override_settings(
|
|
WEATHER_DATA_PROVIDER="openweather",
|
|
WEATHER_API_BASE_URL="https://api.openweathermap.org/data/3.0/onecall",
|
|
WEATHER_API_KEY="test-key",
|
|
WEATHER_TIMEOUT_SECONDS=18,
|
|
)
|
|
def test_app_config_returns_openweather_adapter(self):
|
|
config = apps.get_app_config("weather")
|
|
config.__dict__.pop("weather_data_adapter", None)
|
|
|
|
adapter = config.get_weather_data_adapter()
|
|
|
|
self.assertIsInstance(adapter, OpenWeatherOneCallAdapter)
|
|
self.assertEqual(adapter.timeout, 18)
|
|
|
|
|
|
class OpenWeatherOneCallAdapterTests(SimpleTestCase):
|
|
@patch("weather.adapters.requests.get")
|
|
def test_adapter_maps_openweather_daily_payload_to_internal_shape(self, mock_get):
|
|
response = Mock()
|
|
response.json.return_value = {
|
|
"lat": 35.71,
|
|
"lon": 51.4,
|
|
"timezone": "Asia/Tehran",
|
|
"daily": [
|
|
{
|
|
"dt": 1775001600,
|
|
"temp": {"min": 12.0, "max": 24.0, "day": 19.0},
|
|
"humidity": 44,
|
|
"wind_speed": 5.0,
|
|
"pop": 0.35,
|
|
"rain": 2.4,
|
|
"clouds": 25,
|
|
"uvi": 7.0,
|
|
"weather": [{"id": 500, "main": "Rain"}],
|
|
}
|
|
],
|
|
}
|
|
mock_get.return_value = response
|
|
|
|
adapter = OpenWeatherOneCallAdapter(
|
|
base_url="https://api.openweathermap.org/data/3.0/onecall",
|
|
api_key="test-key",
|
|
timeout=10,
|
|
)
|
|
payload = adapter.fetch_forecast(35.71, 51.4, days=1)
|
|
|
|
self.assertEqual(payload["daily"]["time"], ["2026-04-01"])
|
|
self.assertEqual(payload["daily"]["temperature_2m_mean"], [19.0])
|
|
self.assertEqual(payload["daily"]["precipitation_sum"], [2.4])
|
|
self.assertEqual(payload["daily"]["precipitation_probability_max"], [35.0])
|
|
self.assertEqual(payload["daily"]["wind_speed_10m_max"], [18.0])
|
|
self.assertEqual(payload["daily"]["weather_code"], [61])
|
|
|
|
|
|
@override_settings(WEATHER_DATA_PROVIDER="mock", WEATHER_MOCK_DELAY_SECONDS=0)
|
|
class WeatherServiceTests(TestCase):
|
|
def setUp(self):
|
|
self.location = SoilLocation.objects.create(
|
|
latitude="35.710000",
|
|
longitude="51.400000",
|
|
)
|
|
|
|
def test_fetch_weather_from_api_uses_mock_provider(self):
|
|
payload = fetch_weather_from_api(35.71, 51.4)
|
|
|
|
self.assertIn("daily", payload)
|
|
self.assertEqual(len(payload["daily"]["time"]), 7)
|
|
|
|
def test_update_weather_for_location_persists_seven_days(self):
|
|
result = update_weather_for_location(self.location)
|
|
|
|
self.assertEqual(result["status"], "success")
|
|
self.assertEqual(result["days_updated"], 7)
|
|
self.assertEqual(
|
|
WeatherForecast.objects.filter(location=self.location).count(),
|
|
7,
|
|
)
|
|
self.assertTrue(
|
|
WeatherForecast.objects.filter(
|
|
location=self.location,
|
|
precipitation__isnull=False,
|
|
weather_code__isnull=False,
|
|
).exists()
|
|
)
|