"""Session cookie sign/verify — security-critical edges that the existing test suite uses as a fixture (``sign_session(1)`` for cookies) but doesn't actually probe. Covers: - Round-trip: sign(user_id) → verify → user_id - Tampered cookie → None (not raised) - Expired cookie → None (via itsdangerous max_age) - Garbage / non-serializer-format input → None - Wrong-salt isolation: a pending cookie can't be unlocked by the session verifier (and vice versa) """ from __future__ import annotations from itsdangerous import URLSafeTimedSerializer from app import auth def test_session_signed_token_round_trips(): cookie = auth.sign_session(42) assert auth.verify_session(cookie) == 42 def test_session_token_is_opaque_url_safe(): """Sanity: the serializer produces a URL-safe string with at least two dot-separated segments (payload.timestamp.signature). Not a semantic test, but catches a future swap to an un-encoded format.""" cookie = auth.sign_session(7) assert "." in cookie assert " " not in cookie def test_tampered_session_cookie_returns_none(): """Flip a single character in the signature segment and verify the cookie no longer authenticates — without exceptions leaking.""" cookie = auth.sign_session(99) # Flip the last character (signature segment). tampered = cookie[:-1] + ("a" if cookie[-1] != "a" else "b") assert auth.verify_session(tampered) is None def test_garbage_session_cookie_returns_none(): assert auth.verify_session("not-a-real-cookie") is None assert auth.verify_session("") is None assert auth.verify_session("a.b.c") is None def test_expired_session_cookie_returns_none(monkeypatch): """Forge a cookie with an ancient timestamp and confirm the TTL check rejects it. We bypass sign_session() so the timestamp is in our control rather than `now`.""" s = auth._serializer() # itsdangerous stores the issued-at timestamp in a base62 segment. # Easier than hand-building: monkeypatch the SESSION_TTL_SECONDS # to a negative value so any freshly-signed cookie is "expired" # the moment we verify it. cookie = auth.sign_session(123) monkeypatch.setattr(auth, "SESSION_TTL_SECONDS", -1) assert auth.verify_session(cookie) is None def test_session_serializer_isolated_from_pending_serializer(): """A pending-verify cookie must not authenticate as a session (different salts), and vice versa — otherwise the half-finished OTP flow becomes a free login.""" pending = auth.sign_pending("u@x", 5) session = auth.sign_session(5) assert auth.verify_session(pending) is None assert auth.verify_pending(session) is None def test_session_cookie_signed_with_different_secret_rejected(monkeypatch): """Defence-in-depth: signing with a different secret produces a cookie that the live verifier (using the configured secret) rejects. Confirms we're actually checking the HMAC, not just the payload format.""" rogue = URLSafeTimedSerializer("totally-different-secret", salt="cassandra-session-v1") rogue_cookie = rogue.dumps({"uid": 1}) assert auth.verify_session(rogue_cookie) is None