Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions csp_gateway/tests/utils/test_id_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import importlib
from datetime import datetime, timezone
from unittest.mock import patch

# csp_gateway.utils gets shadowed in sys.modules by csp_gateway.server.web.utils,
# so load the real module by file path.
_id_gen_mod = importlib.import_module("csp_gateway.utils.id_generator")


def _reset_global_counter():
"""Reset the global counter so _get_global_counter re-initialises."""
_id_gen_mod._global_counter = None


def test_base_uses_utc_midnight():
"""Regression: naive datetime caused the base to be interpreted as local
time. When the process started between midnight-local and midnight-UTC
(e.g. 9 PM EDT = 01:00 UTC), Rust's ``Utc::now_nanos - base_nanos``
underflowed a u64, producing a near-2^64 counter value."""
_reset_global_counter()
try:
# Simulate a time where UTC hour < local-timezone UTC offset would
# have caused underflow with the old naive-datetime code.
# 2026-04-15 01:00 UTC (9 PM EDT the previous evening)
fake_now = datetime(2026, 4, 15, 1, 0, 0, tzinfo=timezone.utc)

with patch.object(_id_gen_mod, "datetime") as mock_dt:
mock_dt.now.return_value = fake_now
# Ensure the real datetime constructor is used for building `base`
mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)

counter = _id_gen_mod._get_global_counter()
value = counter.current()

# Value must be reasonable (well below 2^63) — not a wrapped u64
assert value < 2**63, f"Counter value {value} looks like a u64 underflow wrap"

# The Rust Counter stores Utc::now_nanos - base_nanos.
# We can't control Rust's Utc::now(), but we can verify the base
# we passed is sane: it must equal UTC midnight in nanos.
# Counter(base_nanos) → Rust sees base=base_nanos.
# If naive, base would have been 4 h later (EST offset) and could
# exceed Utc::now for early-UTC times.
# Just assert the value is not astronomically large.
assert value < 200_000 * 1_000_000_000, f"Counter value {value} is unreasonably large"
finally:
_reset_global_counter()
2 changes: 1 addition & 1 deletion csp_gateway/utils/id_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def _get_global_counter() -> Counter:
global _global_counter
if _global_counter is None:
nowish = datetime.now(timezone.utc)
base = datetime(nowish.year, nowish.month, nowish.day)
base = datetime(nowish.year, nowish.month, nowish.day, tzinfo=timezone.utc)
_global_counter = Counter(int(base.timestamp()) * 1_000_000_000)
return _global_counter

Expand Down
Loading