from unittest.mock import Mock from pigway_frame_flow.models import WeatherConfig from pigway_frame_flow.providers.weather import ConfigurableWeatherProvider def transport(): response = Mock() response.json.return_value = { "current": { "temperature_2m": 24, "weather_code": 3, "time": "2026-10-02T10:00", }, "daily": { "time": ["2026-10-02"], "weather_code": [61], "temperature_2m_min": [20], "temperature_2m_max": [26], "precipitation_probability_max": [80], }, } return Mock(return_value=response) def test_current_forecast_and_cache(service): get = transport() provider = ConfigurableWeatherProvider(service.db, get) provider.configure(WeatherConfig(provider="open-meteo")) assert provider.get_current()["temperature"] == "24°C" assert provider.get_forecast()[0]["rain_probability"] == 80 assert get.call_count == 1 assert get.call_args.kwargs["timeout"] == 8 assert not get.call_args.kwargs["allow_redirects"] def test_weather_stale_and_error_backoff(service): get = transport() provider = ConfigurableWeatherProvider(service.db, get) provider.configure(WeatherConfig(provider="open-meteo")) provider.get_current() provider.next_fetch = 0 get.side_effect = TimeoutError("private transport detail") assert provider.get_current()["stale"] assert provider.get_current()["temperature"] == "24°C" assert get.call_count == 2 assert "private" not in str(provider.get_current()) def test_unavailable_never_masquerades_as_mock(service): get = Mock(side_effect=TimeoutError("secret")) provider = ConfigurableWeatherProvider(service.db, get) provider.configure(WeatherConfig(provider="open-meteo")) current = provider.get_current() assert not current["mock"] and not current["available"] and current["stale"] assert "secret" not in str(current) def test_provider_settings_persist_and_validate(client, service): assert client.get("/api/v1/providers/weather").json()["current"]["mock"] assert ( client.put("/api/v1/settings/weather", json={"latitude": 100}).status_code == 422 ) assert ( client.put( "/api/v1/settings/weather", json={ "provider": "mock", "location": "Test town", "latitude": 30, "longitude": 121, }, ).status_code == 200 ) assert ConfigurableWeatherProvider(service.db).config().location == "Test town" def test_manual_job_run_and_preview_are_distinct(client, service): job = client.post( "/api/v1/jobs", json={ "name": "Preview", "enabled": False, "schedule": {"type": "daily", "time": "07:00"}, "source": {"type": "template", "template": "quote", "data": {}}, }, ).json() assert client.get("/api/v1/jobs/" + job["id"]).status_code == 200 preview = client.post("/api/v1/jobs/" + job["id"] + "/preview").json() service.process_one() assert service.operation(preview["operation_id"])["status"] == "COMPLETED" assert service.registry.get("living-room").client.uploads == 0 service.settings.mock = False assert client.post("/api/v1/jobs/" + job["id"] + "/run").status_code == 403 service.settings.mock = True first = client.post( "/api/v1/jobs/" + job["id"] + "/run", headers={"Idempotency-Key": "manual-test"} ) second = client.post( "/api/v1/jobs/" + job["id"] + "/run", headers={"Idempotency-Key": "manual-test"} ) assert first.status_code == 202 and first.json() == second.json() def test_image_url_allowlist_is_persistent_and_restrictive(client, service): assert client.get("/api/v1/settings/image-url").json()["allowed_hosts"] == [] assert ( client.put( "/api/v1/settings/image-url", json={"allowed_hosts": ["https://example.com"]}, ).status_code == 422 ) assert ( client.put( "/api/v1/settings/image-url", json={"allowed_hosts": ["*.example.com"]} ).status_code == 422 ) assert client.put( "/api/v1/settings/image-url", json={"allowed_hosts": ["EXAMPLE.com", "example.com"]}, ).json()["allowed_hosts"] == ["example.com"] assert service.settings.image_url_hosts == ["example.com"] assert service.db.one("SELECT payload FROM app_config WHERE key='image_url_hosts'") def test_weather_template_resolves_forecast(service): provider = ConfigurableWeatherProvider(service.db, transport()) provider.configure(WeatherConfig(provider="open-meteo")) service.renderer.weather = provider snapshot = service.renderer.resolve("weather", {}) assert snapshot["forecast"][0]["maximum_c"] == 26 assert snapshot["weather_attribution"] == "Open-Meteo / CC BY 4.0" assert service.renderer.render("weather", snapshot)