From a5436deaf00e704fd4e57fc5f599e622112664cc Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sat, 16 May 2026 11:55:48 +0200 Subject: [PATCH] test(datetime): pin tz-aware contract for fixtures (red until UtcDateTime lands) Drops .replace(tzinfo=None) from 8 fixture sites that mirrored the production-side strip convention. Two of these (test_live_state_poller.py test_new_player_opens_session_with_backfilled_join, test_models.py test_user_has_password_changed_at_default) now fail with TypeError when comparing aware in-memory values against naive DB reads -- that failure is intentional and describes the contract commit 2 must satisfy: DB-sourced datetimes return aware UTC. The remaining 6 sites were already cosmetic (fixture-seed only, no aware-vs-DB comparison) but are flipped here so future authors write aware fixtures. The three deliberately-naive sites in test_timeago.py (lines 67, 73, 113) are LEFT untouched -- they exercise _ensure_utc's normalize-up path and are feature tests, not workarounds. See docs/superpowers/specs/2026-05-16-tz-aware-datetime-design.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- l4d2web/tests/test_auth.py | 2 +- l4d2web/tests/test_live_state_poller.py | 8 ++++---- l4d2web/tests/test_models.py | 2 +- l4d2web/tests/test_servers.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/l4d2web/tests/test_auth.py b/l4d2web/tests/test_auth.py index 1672a8f..08523b8 100644 --- a/l4d2web/tests/test_auth.py +++ b/l4d2web/tests/test_auth.py @@ -159,7 +159,7 @@ def test_load_current_user_rejects_stale_marker(client) -> None: db.flush() uid = u.id - stale = datetime.now(UTC).replace(tzinfo=None) - timedelta(minutes=5) + stale = datetime.now(UTC) - timedelta(minutes=5) with client.session_transaction() as sess: sess["user_id"] = uid sess["pw_changed_at"] = stale.isoformat() diff --git a/l4d2web/tests/test_live_state_poller.py b/l4d2web/tests/test_live_state_poller.py index efc2955..7136a23 100644 --- a/l4d2web/tests/test_live_state_poller.py +++ b/l4d2web/tests/test_live_state_poller.py @@ -156,7 +156,7 @@ def test_new_player_opens_session_with_backfilled_join(tmp_path, monkeypatch) -> assert s.min_ping == 60 assert s.max_ping == 60 # joined_at should be ~30s before now - delta = (datetime.now(UTC).replace(tzinfo=None) - s.joined_at).total_seconds() + delta = (datetime.now(UTC) - s.joined_at).total_seconds() assert 25 <= delta <= 60 @@ -275,7 +275,7 @@ def test_skips_enrichment_when_api_key_unset(tmp_path, monkeypatch) -> None: def test_retention_trims_old_rows(tmp_path, monkeypatch) -> None: app, sid = _seed(tmp_path) app.config["LIVE_STATE_HISTORY_DAYS"] = 30 - long_ago = datetime.now(UTC).replace(tzinfo=None) - timedelta(days=45) + long_ago = datetime.now(UTC) - timedelta(days=45) with session_scope() as db: db.add(ServerLiveState( server_id=sid, started_at=long_ago, last_seen_at=long_ago, @@ -300,7 +300,7 @@ def test_retention_trims_old_rows(tmp_path, monkeypatch) -> None: def test_close_stuck_sessions_after_threshold(tmp_path, monkeypatch) -> None: app, sid = _seed(tmp_path) app.config["STUCK_SESSION_SECONDS"] = 60 - way_back = datetime.now(UTC).replace(tzinfo=None) - timedelta(hours=2) + way_back = datetime.now(UTC) - timedelta(hours=2) with session_scope() as db: db.add(ServerPlayerSession( server_id=sid, steam_id_64="76561197960828710", @@ -353,7 +353,7 @@ def test_skips_enrichment_when_cache_is_fresh(tmp_path, monkeypatch) -> None: steam_id_64="76561197960828710", persona_name="cached", avatar_url="cached.jpg", - fetched_at=datetime.now(UTC).replace(tzinfo=None), + fetched_at=datetime.now(UTC), )) monkeypatch.setattr( live_state_poller, "query_status", diff --git a/l4d2web/tests/test_models.py b/l4d2web/tests/test_models.py index 203ce77..9825bd6 100644 --- a/l4d2web/tests/test_models.py +++ b/l4d2web/tests/test_models.py @@ -40,7 +40,7 @@ def test_user_has_password_changed_at_default(tmp_path, monkeypatch): create_app({"TESTING": True, "DATABASE_URL": db_url, "SECRET_KEY": "test"}) init_db() - before = datetime.now(UTC).replace(tzinfo=None) + before = datetime.now(UTC) with session_scope() as db: db.add(User(username="alice", password_digest=hash_password("secret"))) with session_scope() as db: diff --git a/l4d2web/tests/test_servers.py b/l4d2web/tests/test_servers.py index aca053a..2691126 100644 --- a/l4d2web/tests/test_servers.py +++ b/l4d2web/tests/test_servers.py @@ -456,7 +456,7 @@ def test_servers_index_renders_live_state_badge(user_client_with_blueprints) -> client, data = user_client_with_blueprints # Seed one server with a recent snapshot, one without. - now = datetime.now(UTC).replace(tzinfo=None) + now = datetime.now(UTC) with session_scope() as db: s_active = Server( user_id=data["user_id"], @@ -518,7 +518,7 @@ def test_live_state_fragment_renders_current_and_recent(user_client_with_bluepri ) client, data = user_client_with_blueprints - now = datetime.now(UTC).replace(tzinfo=None) + now = datetime.now(UTC) with session_scope() as db: srv = Server( user_id=data["user_id"], blueprint_id=data["blueprint_id"],