Files
pigway-frame-flow/tests/test_weather.py
T

138 lines
4.9 KiB
Python
Raw Permalink Normal View History

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)