Files
live-hub-py/tests/test_auth_sliding_session.py
T
yml2213 6fc453017c feat(auth): 后台登录改为滑动会话
- JWT 记录 iat,exp=min(now+24h, iat+7d)
- 已认证请求自动续签 httpOnly Cookie
- 新增 ACCESS_TOKEN_IDLE_HOURS / ACCESS_TOKEN_MAX_HOURS
2026-10-25 14:07:44 +08:00

119 lines
3.8 KiB
Python

"""后台登录滑动会话:24h 空闲 + 7d 绝对上限。"""
from datetime import UTC, datetime, timedelta
from types import SimpleNamespace
from jose import jwt
from starlette.responses import Response
from web.backend import security
from web.backend.session import _should_renew, set_session_cookie
NOW = datetime.now(UTC).replace(microsecond=0)
def test_issue_token_uses_24h_idle_window():
issued = security.issue_access_token({"sub": "7"}, now=NOW)
payload = security.decode_access_token(issued.token)
assert payload is not None
assert payload["iat"] == int(NOW.timestamp())
assert issued.expires_at == NOW + timedelta(hours=24)
assert not security.is_session_expired(payload, now=NOW + timedelta(hours=23))
assert security.is_session_expired(payload, now=NOW + timedelta(hours=24))
def test_renew_extends_idle_window_but_keeps_session_start():
issued = security.issue_access_token({"sub": "7"}, now=NOW)
renewed = security.renew_access_token(issued.token, now=NOW + timedelta(hours=23))
assert renewed is not None
assert renewed.issued_at == NOW
assert renewed.expires_at == NOW + timedelta(hours=47)
payload = security.decode_access_token(renewed.token)
assert payload is not None
assert payload["iat"] == int(NOW.timestamp())
def test_renew_never_passes_seven_day_absolute_cap():
# 模拟一个已经连续续期到第 6 天 23:30 的会话(仍有效),
# 下一次续期只能把 exp 顶到 7d,不能推到 7d23h。
near_cap = jwt.encode(
{
"sub": "7",
"iat": NOW,
"exp": NOW + timedelta(days=6, hours=23, minutes=30),
},
security.SECRET_KEY_TYPED,
algorithm=security.ALGORITHM,
)
renewed = security.renew_access_token(
near_cap, now=NOW + timedelta(days=6, hours=23)
)
assert renewed is not None
assert renewed.expires_at == NOW + timedelta(days=7)
payload = security.decode_access_token(renewed.token)
assert payload is not None
assert security.is_session_expired(payload, now=NOW + timedelta(days=7))
# 已经顶到绝对上限后不再续签。
assert (
security.renew_access_token(
renewed.token, now=NOW + timedelta(days=6, hours=23, minutes=30)
)
is None
)
def test_legacy_token_without_iat_anchors_cap_to_original_issue_time():
legacy = jwt.encode(
{
"sub": "7",
"exp": NOW + timedelta(hours=1),
},
security.SECRET_KEY_TYPED,
algorithm=security.ALGORITHM,
)
renewed = security.renew_access_token(legacy, now=NOW)
assert renewed is not None
# 旧 Token 固定 24h,因此签发时间 = exp - 24h。
assert renewed.issued_at == NOW - timedelta(hours=23)
assert renewed.expires_at == NOW + timedelta(hours=24)
def test_set_session_cookie_matches_token_expiry():
response = Response()
expires_at = NOW + timedelta(hours=24)
set_session_cookie(response, "token-value", expires_at, now=NOW)
cookie = response.headers["set-cookie"].lower()
assert "access_token=token-value" in cookie
assert "httponly" in cookie
assert "samesite=lax" in cookie
assert "max-age=86400" in cookie
assert "path=/" in cookie
def test_should_renew_only_authenticated_api_requests():
request = SimpleNamespace(
url=SimpleNamespace(path="/api/accounts"),
state=SimpleNamespace(current_user=object()),
)
assert _should_renew(request, 200)
assert _should_renew(request, 403)
assert not _should_renew(request, 401)
request.url.path = "/api/auth/logout"
assert not _should_renew(request, 200)
request.url.path = "/assets/app.js"
assert not _should_renew(request, 200)
request.url.path = "/api/accounts"
request.state.current_user = None
assert not _should_renew(request, 200)