test: the cookie signer and test modules read the never-back clock too

The docker VM's clock steps back: itsdangerous refused a cookie signed in the next second (a
random 401 right after a sign-in), and a test compared an app stamp with the raw wall clock.
This commit is contained in:
2026-11-14 19:32:02 +01:00
parent f1d0845923
commit 4dc9846eba
2 changed files with 78 additions and 2 deletions
+27 -2
View File
@@ -337,20 +337,45 @@ class _NeverBack(_dt.datetime):
return t return t
_last_signature_second = [0]
def _signature_second_never_back(self) -> int:
"""The session cookie's signing clock (itsdangerous, whole seconds), never going back: it refuses
a signature from the future, so a step back across a second boundary refused the cookie a
sign-in had just set. Read through the module at call time, so a probe can step it."""
import itsdangerous.timed
t = max(int(itsdangerous.timed.time.time()), _last_signature_second[0])
_last_signature_second[0] = t
return t
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _clock_never_goes_back(monkeypatch): def _clock_never_goes_back(monkeypatch):
"""Rows a test makes one after another are stamped in that order. The wall clock of the docker """Rows a test makes one after another are stamped in that order. The wall clock of the docker
VM the suite runs in steps BACK now and then (measured: 2 steps in 20 s, up to 0.5 s), and a VM the suite runs in steps BACK now and then (measured: 2 steps in 20 s, up to 0.5 s), and a
test that makes three files and reads them newest first then got them out of order. Every loaded test that makes three files and reads them newest first then got them out of order. Every loaded
`app.*` module that took `datetime` from the datetime module reads this clock instead (a sweep, `app.*` module that took `datetime` from the datetime module reads this clock instead (a sweep,
as the limiter reset below does, so a new module is covered the day it lands). A database as the limiter reset below does, so a new module is covered the day it lands) — and so does every
test module, so a test comparing a stamp the app wrote with "now" reads the same clock. The
session cookie's signature has a clock of its own (`_signature_second_never_back`). A database
default (`server_default=func.now()`) is Postgres's clock and is not covered: a test that orders default (`server_default=func.now()`) is Postgres's clock and is not covered: a test that orders
such rows made within a second of each other gives them distinct times itself.""" such rows made within a second of each other gives them distinct times itself."""
import sys import sys
from itsdangerous import TimestampSigner
tests_dir = os.path.dirname(os.path.abspath(__file__))
for name, mod in list(sys.modules.items()): for name, mod in list(sys.modules.items()):
if (name == "app" or name.startswith("app.")) and vars(mod).get("datetime") is _dt.datetime: if vars(mod).get("datetime") is not _dt.datetime:
continue
mod_file = getattr(mod, "__file__", None) or ""
if name == "app" or name.startswith("app.") or mod_file.startswith(tests_dir + os.sep):
monkeypatch.setattr(mod, "datetime", _NeverBack) monkeypatch.setattr(mod, "datetime", _NeverBack)
# Per test: a cookie never outlives its test, and a probe's stepped clock must not carry over.
_last_signature_second[0] = 0
monkeypatch.setattr(TimestampSigner, "get_timestamp", _signature_second_never_back)
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
+51
View File
@@ -0,0 +1,51 @@
"""The suite's clock (conftest `_clock_never_goes_back`), DB-lane.
The wall clock of the docker VM the suite runs in steps BACK now and then (measured: up to 0.5 s).
These hold the two places that read it besides the app: the session cookie's signature, and a test
comparing a stamp the app wrote with "now".
"""
import time
from datetime import datetime, timezone
from types import SimpleNamespace
import itsdangerous.timed
_PW = "correct horse battery"
def test_a_cookie_signed_just_before_the_clock_steps_back_still_signs_in(
client, make_user, monkeypatch
):
# The signature carries whole seconds and itsdangerous refuses one from the future: a step back
# across a second boundary refused the cookie a sign-in had just set (a 401 at random).
clock = [int(time.time()) + 0.2]
monkeypatch.setattr(itsdangerous.timed, "time", SimpleNamespace(time=lambda: clock[0]))
make_user(email="clock@example.com", password=_PW)
r = client.post("/auth/password-login", json={"email": "clock@example.com", "password": _PW})
assert r.status_code == 200
clock[0] -= 0.5
assert client.get("/api/me/notifications").status_code == 200
def test_a_test_reads_the_clock_the_app_stamps_with(monkeypatch):
# A test asserting "the app's stamp is not in the future" compared it with the wall clock, which
# may have stepped back since the app read its own.
import conftest
from app import diagnostics
# The app reads 12:00:01, then the wall clock steps back half a second (a time past any real one,
# so a test reading the wall clock instead is behind the stamp).
later = datetime(2099, 1, 1, 12, 0, 1, tzinfo=timezone.utc)
stepped_back = datetime(2099, 1, 1, 12, 0, 0, 500000, tzinfo=timezone.utc)
readings = iter([later])
class Stepping(datetime):
@classmethod
def now(cls, tz=None):
return next(readings, stepped_back)
monkeypatch.setattr(conftest._NeverBack, "clock", Stepping)
monkeypatch.setattr(conftest._NeverBack, "last", None)
stamped = diagnostics.datetime.now(timezone.utc)
assert datetime.now(timezone.utc) >= stamped