7ac291b8ac
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
114 lines
4.1 KiB
Python
114 lines
4.1 KiB
Python
"""UTC in the DB, explicit +00:00 on the wire, local time only on presentation (#42)."""
|
|
from datetime import datetime, timezone
|
|
from unittest.mock import patch
|
|
|
|
from httpx import AsyncClient
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from src.backend.models.orm.user import User
|
|
from src.backend.services import report_service
|
|
from src.backend.tests.conftest import auth_headers, create_test_recipe
|
|
from src.backend.utils.tempo import as_utc_naive, to_local, utcnow
|
|
|
|
|
|
def test_utcnow_naive_and_close_to_utc():
|
|
now = utcnow()
|
|
assert now.tzinfo is None
|
|
delta = datetime.now(timezone.utc).replace(tzinfo=None) - now
|
|
assert abs(delta.total_seconds()) < 5
|
|
|
|
|
|
def test_as_utc_naive():
|
|
parsed = datetime.fromisoformat("2026-10-06T10:00:00+02:00")
|
|
assert as_utc_naive(parsed) == datetime(2026, 10, 6, 8, 0)
|
|
assert as_utc_naive(parsed).tzinfo is None
|
|
naive = datetime(2026, 10, 6, 8, 0)
|
|
assert as_utc_naive(naive) is naive
|
|
assert as_utc_naive(None) is None
|
|
|
|
|
|
def test_to_local_is_rome():
|
|
assert to_local(datetime(2026, 10, 6, 8, 0)).strftime("%H:%M %z") == "10:00 +0200"
|
|
assert to_local(datetime(2026, 1, 6, 8, 0)).strftime("%H:%M %z") == "09:00 +0100"
|
|
|
|
|
|
async def _make_measurement(client, user, recipe_id) -> None:
|
|
tasks = (await client.get(f"/api/recipes/{recipe_id}/tasks", headers=auth_headers(user))).json()
|
|
await client.post(
|
|
"/api/measurements/",
|
|
headers=auth_headers(user),
|
|
json={
|
|
"subtask_id": tasks[0]["subtasks"][0]["id"],
|
|
"version_id": tasks[0]["version_id"],
|
|
"value": 10.0,
|
|
},
|
|
)
|
|
|
|
|
|
async def test_api_datetimes_carry_utc_offset_and_filter_equivalence(
|
|
client: AsyncClient,
|
|
measurement_tec_user: User,
|
|
metrologist_user: User,
|
|
db_session: AsyncSession,
|
|
):
|
|
recipe = await create_test_recipe(db_session, measurement_tec_user.id)
|
|
await _make_measurement(client, measurement_tec_user, recipe.id)
|
|
|
|
resp = await client.get("/api/measurements/", headers=auth_headers(metrologist_user))
|
|
item = resp.json()["items"][0]
|
|
assert item["measured_at"].endswith("+00:00")
|
|
|
|
# Same instant written with +02:00 and as UTC: identical results.
|
|
future_utc = "2099-01-01T08:00:00+00:00"
|
|
future_rome = "2099-01-01T10:00:00+02:00"
|
|
past_utc = "2000-01-01T08:00:00+00:00"
|
|
past_rome = "2000-01-01T10:00:00+02:00"
|
|
for frm, to in ((past_utc, future_utc), (past_rome, future_rome)):
|
|
r = await client.get(
|
|
"/api/measurements/",
|
|
headers=auth_headers(metrologist_user),
|
|
params={"date_from": frm, "date_to": to},
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["total"] == 1
|
|
|
|
# Boundary: measurement is "now" in UTC; a date_from 1h ahead in UTC,
|
|
# written as +02:00 (now+3h local wall clock), must exclude it.
|
|
ahead = (utcnow().replace(microsecond=0)).replace(tzinfo=timezone.utc)
|
|
from datetime import timedelta
|
|
ahead_utc = (ahead + timedelta(hours=1)).isoformat()
|
|
ahead_rome = (ahead + timedelta(hours=1)).astimezone(
|
|
timezone(timedelta(hours=2))
|
|
).isoformat()
|
|
totals = []
|
|
for frm in (ahead_utc, ahead_rome):
|
|
r = await client.get(
|
|
"/api/measurements/",
|
|
headers=auth_headers(metrologist_user),
|
|
params={"date_from": frm},
|
|
)
|
|
totals.append(r.json()["total"])
|
|
assert totals == [0, 0]
|
|
|
|
|
|
async def test_report_generated_at_is_local_time(
|
|
db_session: AsyncSession, metrologist_user: User
|
|
):
|
|
recipe = await create_test_recipe(db_session, metrologist_user.id)
|
|
captured: dict[str, str] = {}
|
|
|
|
def fake_render(html: str) -> bytes:
|
|
captured["html"] = html
|
|
return b"%PDF"
|
|
|
|
with patch.object(report_service, "utcnow", return_value=datetime(2026, 10, 6, 8, 0)), \
|
|
patch.object(report_service, "_render_pdf", side_effect=fake_render):
|
|
await report_service.generate_measurement_report(db_session, recipe.id)
|
|
|
|
assert "Generated 2026-10-06 10:00" in captured["html"]
|
|
|
|
|
|
def test_report_filter_day_is_local():
|
|
"""UTC naive 22:00 of Oct 5 is already Oct 6 in Europe/Rome (#42)."""
|
|
assert report_service._giorno_locale(datetime(2026, 10, 5, 22, 0, 0)) == "2026-10-06"
|