PayrollManagementSystem/backend/tests/unit/test_token_blacklist.py
liuwenxin 273ebf4657 chore: 初始化项目结构与登录鉴权模块
后端 FastAPI 六层结构(api/services/repositories/models/schemas/core),
统一响应信封、全局异常、日志脱敏;6 张表经 Alembic 迁移建库,SQL 留存
db/migration;登录鉴权含图形验证码(Redis GETDEL 防重放)、JWT、登录限流、
登出黑名单(TTL=令牌剩余有效期)、登录日志。

前端 Vue3 + TS + Element Plus 骨架,含登录页、路由守卫、请求封装与 Vite 代理。

工程配置:uv 锁定 Python 3.12、ruff 检查与格式化、editorconfig、gitattributes。

测试 184 项(单元 155 / 集成 29)全绿,浏览器端到端 8 项通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-09 18:46:36 +08:00

61 lines
2.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""登出黑名单单元测试。
守的是:黑名单的 **TTL 必须等于令牌剩余有效期**。
写死固定值会出现「登出 N 分钟后令牌又活了」;
完全不设过期则 Redis 会随登出次数无限增长。两者都是真实事故。
"""
from app.core.constants import REDIS_KEY_TOKEN_BLACKLIST
from app.services import token_blacklist
def _ttl(jti: str) -> int:
from app.core.redis_client import get_redis
return get_redis().ttl(REDIS_KEY_TOKEN_BLACKLIST.format(jti=jti))
class TestRevoke:
"""拉黑。"""
def test_拉黑后可检出(self, fake_redis) -> None:
token_blacklist.revoke("jti-1", 600)
assert token_blacklist.is_revoked("jti-1") is True
def test_TTL等于传入的剩余秒数(self, fake_redis) -> None:
"""容忍 1 秒偏差:Redis 的 TTL 以毫秒精度存绝对过期时刻,
命令返回时向下取整,刚写入就查询必然是 N 或 N-1。
断言死等会变成偶发失败的"闪烁测试"。"""
token_blacklist.revoke("jti-1", 1234)
assert _ttl("jti-1") in (1234, 1233)
def test_键带过期时间而非永久存在(self, fake_redis) -> None:
"""永久存储会让 Redis 随登出次数无限增长。"""
token_blacklist.revoke("jti-1", 600)
assert _ttl("jti-1") > 0
def test_已过期令牌不写入(self, fake_redis) -> None:
"""剩余有效期为 0 或负数时无凭证可撤销,写入只会浪费内存。"""
token_blacklist.revoke("jti-expired", 0)
token_blacklist.revoke("jti-expired2", -100)
assert token_blacklist.is_revoked("jti-expired") is False
assert token_blacklist.is_revoked("jti-expired2") is False
def test_重复拉黑同一jti不报错且TTL更新(self, fake_redis) -> None:
token_blacklist.revoke("jti-1", 600)
token_blacklist.revoke("jti-1", 300)
assert _ttl("jti-1") in (300, 299)
class TestIsRevoked:
"""检测。"""
def test_未拉黑的令牌返回False(self, fake_redis) -> None:
assert token_blacklist.is_revoked("never-revoked") is False
def test_不同jti之间互不影响(self, fake_redis) -> None:
"""jti 是唯一键,串号会导致误踢其他用户的登录态。"""
token_blacklist.revoke("jti-1", 600)
assert token_blacklist.is_revoked("jti-1") is True
assert token_blacklist.is_revoked("jti-2") is False