M8-R11: make business timezone deterministic
frontend / frontend (push) Successful in 46s
pytest / test (push) Successful in 3m56s

This commit is contained in:
2026-08-27 12:16:52 +02:00
parent d5623b9fcb
commit 33ca3da593
6 changed files with 65 additions and 10 deletions
+15
View File
@@ -72,6 +72,21 @@ def test_compose_uses_migration_job_before_app() -> None:
assert dev["services"]["app"]["build"] == "."
def test_compose_defaults_business_timezone_and_dev_inherits_it() -> None:
"""Both production services receive an overridable Amsterdam timezone.
Compose merges service ``environment`` mappings, and the dev override does
not replace either mapping, so the base default applies to base+dev too.
"""
base = _read_yaml("docker-compose.yml")
dev = _read_yaml("docker-compose.dev.yml")
default_tz = "${TZ:-Europe/Amsterdam}"
for service_name in ("migration", "app"):
assert base["services"][service_name]["environment"]["TZ"] == default_tz
assert "TZ" not in dev["services"].get(service_name, {}).get("environment", {})
def test_compose_keeps_app_non_root_and_maps_minimal_warmtelink_serial_access() -> None:
"""Base Compose maps the configured device with only pyserial's required access.
+8 -3
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from unittest.mock import patch
from zoneinfo import ZoneInfo
import pytest
from fastapi.testclient import TestClient
@@ -139,7 +140,9 @@ def test_meter_cost_summary_empty_and_recompute_csrf_window_validation(meter_cos
).status_code == 422
def test_meter_cost_summary_returns_five_fixed_components_and_totals(meter_cost_client) -> None:
def test_meter_cost_summary_returns_five_fixed_components_and_totals(
meter_cost_client, monkeypatch: pytest.MonkeyPatch
) -> None:
client, engine = meter_cost_client
now = datetime.now(UTC)
values = {
@@ -166,8 +169,10 @@ def test_meter_cost_summary_returns_five_fixed_components_and_totals(meter_cost_
db.add_all((heating, water))
db.commit()
_login(client)
with patch("app.api.routes.api.meter_costs.local_now", return_value=datetime(2026, 6, 25, 2, tzinfo=UTC)):
response = client.get("/api/energy/meter-costs/summary?scope=thermal&start=2026-06-23T00:00:00Z&end=2026-06-24T00:00:00Z")
monkeypatch.setattr("app.services.timezone.local_tz", lambda: ZoneInfo("Europe/Amsterdam"))
response = client.get(
"/api/energy/meter-costs/summary?scope=thermal&start=2026-06-23T00:00:00Z&end=2026-06-24T00:00:00Z"
)
assert response.status_code == 200
body = response.json()
assert body["heating"] == "1.000000000" and body["hot_water_heating"] == "0.8"
+33 -1
View File
@@ -7,7 +7,7 @@ on any CI host timezone.
from __future__ import annotations
from datetime import UTC, date, datetime
from zoneinfo import ZoneInfo
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
import pytest
@@ -56,6 +56,38 @@ def test_local_tz_env_var_overrides(monkeypatch):
monkeypatch.delenv("TZ")
@pytest.mark.parametrize("value", [None, "", " "])
def test_local_tz_defaults_to_dst_aware_amsterdam(monkeypatch, value: str | None):
"""Unset or blank TZ must use the deterministic Amsterdam business zone."""
if value is None:
monkeypatch.delenv("TZ", raising=False)
else:
monkeypatch.setenv("TZ", value)
tz = tz_mod.local_tz()
assert isinstance(tz, ZoneInfo)
assert tz.key == "Europe/Amsterdam"
winter = datetime(2026, 1, 15, 12, tzinfo=tz)
summer = datetime(2026, 7, 15, 12, tzinfo=tz)
assert winter.utcoffset().total_seconds() == 3600
assert summer.utcoffset().total_seconds() == 7200
def test_local_tz_explicit_utc_override(monkeypatch):
"""A non-empty TZ remains an operator-controlled override."""
monkeypatch.setenv("TZ", "UTC")
tz = tz_mod.local_tz()
assert isinstance(tz, ZoneInfo)
assert tz.key == "UTC"
def test_local_tz_invalid_explicit_override_fails_loudly(monkeypatch):
"""A non-empty invalid override must not silently become Amsterdam."""
monkeypatch.setenv("TZ", "Invalid/Timezone")
with pytest.raises(ZoneInfoNotFoundError):
tz_mod.local_tz()
# ---------------------------------------------------------------------------
# to_local() — conversion correctness
# ---------------------------------------------------------------------------