📦 EqualifyEverything / equalify-reflow

📄 test_session.py · 85 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85"""Unit tests for the signed-cookie session encoder."""

from __future__ import annotations

import time

import pytest

from src.auth.session import SignedCookieSession, make_identity, should_reissue

SECRET = "x" * 32
OTHER_SECRET = "y" * 32


@pytest.mark.unit
def test_signed_cookie_round_trip() -> None:
    store = SignedCookieSession(secret_key=SECRET, max_age_seconds=3600)
    identity = make_identity(sub="alice", provider_id="basic", ttl_seconds=3600)

    encoded = store.encode(identity)
    decoded = store.decode(encoded)

    assert decoded is not None
    assert decoded.sub == identity.sub
    assert decoded.provider_id == identity.provider_id
    assert decoded.expires_at == identity.expires_at


@pytest.mark.unit
def test_short_secret_rejected() -> None:
    with pytest.raises(ValueError, match="at least 32 characters"):
        SignedCookieSession(secret_key="short", max_age_seconds=3600)


@pytest.mark.unit
def test_tampered_cookie_returns_none() -> None:
    store = SignedCookieSession(secret_key=SECRET, max_age_seconds=3600)
    identity = make_identity(sub="alice", provider_id="basic", ttl_seconds=3600)
    encoded = store.encode(identity)

    # Flip a byte in the middle. itsdangerous will reject the signature.
    tampered = encoded[:-3] + ("a" if encoded[-3] != "a" else "b") + encoded[-2:]
    assert store.decode(tampered) is None


@pytest.mark.unit
def test_wrong_secret_returns_none() -> None:
    encoder = SignedCookieSession(secret_key=SECRET, max_age_seconds=3600)
    decoder = SignedCookieSession(secret_key=OTHER_SECRET, max_age_seconds=3600)
    identity = make_identity(sub="alice", provider_id="basic", ttl_seconds=3600)

    assert decoder.decode(encoder.encode(identity)) is None


@pytest.mark.unit
def test_expired_cookie_returns_none() -> None:
    # itsdangerous timestamps are integer-second resolution; sleep > 2s to
    # guarantee we cross max_age=1 regardless of the encode-time fractional
    # offset.
    store = SignedCookieSession(secret_key=SECRET, max_age_seconds=1)
    identity = make_identity(sub="alice", provider_id="basic", ttl_seconds=1)
    encoded = store.encode(identity)
    time.sleep(2.1)
    assert store.decode(encoded) is None


@pytest.mark.unit
def test_should_reissue_after_half_life() -> None:
    fresh = make_identity(sub="alice", provider_id="basic", ttl_seconds=3600)
    assert should_reissue(fresh) is False

    # Build an aged identity that legitimately predates half-life: shift both
    # issued_at AND expires_at backward together so the total span stays at
    # 3600s. With elapsed=2000 and halflife=1800, should_reissue returns True.
    from datetime import timedelta

    shift = timedelta(seconds=2000)
    aged = fresh.model_copy(
        update={
            "issued_at": fresh.issued_at - shift,
            "expires_at": fresh.expires_at - shift,
        }
    )
    assert should_reissue(aged) is True