"""后台登录滑动会话: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)