Files
xiaoxia-saas/tests/unit/test_jwt_service.py
T
xiaoxia df38101bd9
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 40s
CI/CD Pipeline / Frontend Lint (push) Successful in 2m26s
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 3m35s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m55s
CI/CD Pipeline / Unit Tests (push) Successful in 4m6s
CI/CD Pipeline / Integration Tests (push) Successful in 1m45s
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m21s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 15m52s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
test(unit): P3-1 核心模块单元测试覆盖率提升 - 新增12个模块400+测试 (#661)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-07-21 00:21:37 +08:00

363 lines
14 KiB
Python
Executable File

"""
JWT Service 单元测试
"""
import time
from datetime import datetime, timedelta
import jwt
import pytest
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
from packages.application.auth.jwt_service import (
JWTConfig,
JWTService,
TokenType,
)
class TestJWTConfig:
"""JWT 配置测试"""
def test_config_init_success(self):
"""测试正常初始化"""
config = JWTConfig(secret_key="a-very-strong-secret-key-for-testing")
assert config.SECRET_KEY == "a-very-strong-secret-key-for-testing"
assert config.ALGORITHM == "HS256"
assert config.ACCESS_TOKEN_EXPIRE_MINUTES == 15
assert config.REFRESH_TOKEN_EXPIRE_DAYS == 7
def test_config_custom_values(self):
"""测试自定义配置值"""
config = JWTConfig(
secret_key="test-secret",
algorithm="HS512",
access_token_expire_minutes=30,
refresh_token_expire_days=14,
)
assert config.ALGORITHM == "HS512"
assert config.ACCESS_TOKEN_EXPIRE_MINUTES == 30
assert config.REFRESH_TOKEN_EXPIRE_DAYS == 14
def test_config_empty_secret_raises(self):
"""测试空密钥报错"""
with pytest.raises(ValueError, match="secret_key must be provided"):
JWTConfig(secret_key="")
def test_config_whitespace_secret_raises(self):
"""测试全空格密钥报错"""
with pytest.raises(ValueError, match="secret_key must be provided"):
JWTConfig(secret_key=" ")
def test_config_insecure_default_secret_raises(self):
"""测试不安全的默认密钥报错"""
insecure_keys = [
"your-secret-key-change-in-production",
"your-secret-key",
"secret",
"changeme",
"password",
"YOUR-SECRET-KEY",
"Secret",
]
for key in insecure_keys:
with pytest.raises(ValueError, match="insecure"):
JWTConfig(secret_key=key)
class TestJWTService:
"""JWT 服务测试"""
@pytest.fixture
def config(self):
return JWTConfig(
secret_key="test-secret-key-for-jwt-unit-tests-12345",
algorithm="HS256",
access_token_expire_minutes=30,
refresh_token_expire_days=7,
)
@pytest.fixture
def service(self, config):
return JWTService(config=config)
def test_service_init_without_config_raises(self):
"""测试无 config 初始化报错"""
with pytest.raises(ValueError, match="requires a JWTConfig"):
JWTService(config=None)
# --- create_access_token ---
def test_create_access_token_success(self, service):
"""测试创建 access token 成功"""
token = service.create_access_token(user_id="user-123")
assert isinstance(token, str)
assert len(token) > 0
def test_create_access_token_contains_user_id(self, service, config):
"""测试 access token 包含正确的 user_id"""
token = service.create_access_token(user_id="user-456")
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[config.ALGORITHM])
assert payload["sub"] == "user-456"
def test_create_access_token_has_correct_type(self, service, config):
"""测试 access token 类型正确"""
token = service.create_access_token(user_id="user-123")
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[config.ALGORITHM])
assert payload["type"] == TokenType.ACCESS
def test_create_access_token_contains_role(self, service, config):
"""测试 access token 包含角色"""
token = service.create_access_token(user_id="user-123", role="admin")
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[config.ALGORITHM])
assert payload["role"] == "admin"
def test_create_access_token_additional_claims(self, service, config):
"""测试 access token 包含额外声明"""
token = service.create_access_token(
user_id="user-123",
additional_claims={"custom_field": "custom_value", "sid": "session-abc"},
)
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[config.ALGORITHM])
assert payload["custom_field"] == "custom_value"
assert payload["sid"] == "session-abc"
def test_create_access_token_has_iat_and_exp(self, service, config):
"""测试 access token 包含 iat 和 exp"""
before = datetime.utcnow() - timedelta(seconds=1)
token = service.create_access_token(user_id="user-123")
after = datetime.utcnow() + timedelta(seconds=1)
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[config.ALGORITHM])
assert "iat" in payload
assert "exp" in payload
iat = datetime.utcfromtimestamp(payload["iat"])
exp = datetime.utcfromtimestamp(payload["exp"])
assert before <= iat <= after
assert exp > iat
# 过期时间约等于配置的分钟数
expected_expiry = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
actual_expiry = exp - iat
assert abs((actual_expiry - expected_expiry).total_seconds()) < 5
# --- create_refresh_token ---
def test_create_refresh_token_success(self, service):
"""测试创建 refresh token 成功"""
token = service.create_refresh_token(user_id="user-123", session_id="sess-abc")
assert isinstance(token, str)
assert len(token) > 0
def test_create_refresh_token_contains_correct_data(self, service, config):
"""测试 refresh token 包含正确数据"""
token = service.create_refresh_token(user_id="user-789", session_id="sess-xyz")
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[config.ALGORITHM])
assert payload["sub"] == "user-789"
assert payload["session_id"] == "sess-xyz"
assert payload["type"] == TokenType.REFRESH
def test_create_refresh_token_expiry(self, service, config):
"""测试 refresh token 过期时间正确"""
token = service.create_refresh_token(user_id="user-123", session_id="sess-abc")
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[config.ALGORITHM])
iat = datetime.utcfromtimestamp(payload["iat"])
exp = datetime.utcfromtimestamp(payload["exp"])
expected_expiry = timedelta(days=config.REFRESH_TOKEN_EXPIRE_DAYS)
actual_expiry = exp - iat
assert abs((actual_expiry - expected_expiry).total_seconds()) < 5
# --- verify_token ---
def test_verify_valid_token(self, service):
"""测试验证有效 token"""
token = service.create_access_token(user_id="user-123")
payload = service.verify_token(token)
assert payload["sub"] == "user-123"
def test_verify_expired_token_raises(self, service, config):
"""测试验证过期 token 报错"""
# 创建一个已经过期的 token
payload = {
"sub": "user-123",
"type": TokenType.ACCESS,
"iat": datetime.utcnow() - timedelta(hours=1),
"exp": datetime.utcnow() - timedelta(minutes=30),
}
expired_token = jwt.encode(payload, config.SECRET_KEY, algorithm=config.ALGORITHM)
with pytest.raises(ExpiredSignatureError, match="expired"):
service.verify_token(expired_token)
def test_verify_invalid_token_raises(self, service):
"""测试验证无效 token 报错"""
with pytest.raises(InvalidTokenError):
service.verify_token("this-is-not-a-valid-jwt-token")
def test_verify_token_with_wrong_secret_raises(self, service, config):
"""测试用错误密钥签发的 token 验证失败"""
wrong_config = JWTConfig(secret_key="different-secret-key")
wrong_service = JWTService(config=wrong_config)
token = wrong_service.create_access_token(user_id="user-123")
with pytest.raises(InvalidTokenError):
service.verify_token(token)
# --- verify_access_token ---
def test_verify_access_token_success(self, service):
"""测试验证有效的 access token"""
token = service.create_access_token(user_id="user-123", role="user")
payload = service.verify_access_token(token)
assert payload["sub"] == "user-123"
assert payload["type"] == TokenType.ACCESS
def test_verify_access_token_with_refresh_token_raises(self, service):
"""测试用 refresh token 调用 verify_access_token 报错"""
refresh_token = service.create_refresh_token(user_id="user-123", session_id="sess-abc")
with pytest.raises(ValueError, match="Token type must be 'access'"):
service.verify_access_token(refresh_token)
# --- verify_refresh_token ---
def test_verify_refresh_token_success(self, service):
"""测试验证有效的 refresh token"""
token = service.create_refresh_token(user_id="user-123", session_id="sess-abc")
payload = service.verify_refresh_token(token)
assert payload["sub"] == "user-123"
assert payload["session_id"] == "sess-abc"
assert payload["type"] == TokenType.REFRESH
def test_verify_refresh_token_with_access_token_raises(self, service):
"""测试用 access token 调用 verify_refresh_token 报错"""
access_token = service.create_access_token(user_id="user-123")
with pytest.raises(ValueError, match="Token type must be 'refresh'"):
service.verify_refresh_token(access_token)
def test_access_and_refresh_tokens_are_different(self, service):
"""测试 access token 和 refresh token 不相同"""
access = service.create_access_token(user_id="user-123")
refresh = service.create_refresh_token(user_id="user-123", session_id="sess-abc")
assert access != refresh
class TestJWTHandler:
"""JWT Handler 委托层测试"""
def test_create_access_token(self):
"""测试创建 access token"""
from packages.application.auth.jwt_handler import JWTHandler
handler = JWTHandler(secret_key="test-secret-key")
token = handler.create_access_token(user_id="user-123", role="admin")
assert token is not None
assert len(token) > 20
# 验证token内容
payload = jwt.decode(token, "test-secret-key", algorithms=["HS256"])
assert payload["sub"] == "user-123"
assert payload["role"] == "admin"
assert payload["type"] == "access"
def test_create_access_token_with_additional_claims(self):
"""测试带额外声明创建 token"""
from packages.application.auth.jwt_handler import JWTHandler
handler = JWTHandler(secret_key="test-secret-key")
token = handler.create_access_token(
user_id="user-456",
additional_claims={"custom_field": "custom_value"},
)
payload = jwt.decode(token, "test-secret-key", algorithms=["HS256"])
assert payload["sub"] == "user-456"
assert payload["custom_field"] == "custom_value"
def test_verify_access_token(self):
"""测试验证 access token"""
from packages.application.auth.jwt_handler import JWTHandler
handler = JWTHandler(secret_key="test-secret-key")
token = handler.create_access_token(user_id="user-123", role="user")
payload = handler.verify_access_token(token)
assert payload["sub"] == "user-123"
assert payload["role"] == "user"
assert payload["type"] == "access"
def test_verify_access_token_expired(self):
"""测试验证过期的 access token"""
from packages.application.auth.jwt_handler import JWTHandler
handler = JWTHandler(secret_key="test-secret-key", access_token_expire_minutes=0)
token = handler.create_access_token(user_id="user-123")
time.sleep(1) # 确保过期
with pytest.raises(ExpiredSignatureError):
handler.verify_access_token(token)
def test_verify_token(self):
"""测试验证任意类型 token"""
from packages.application.auth.jwt_handler import JWTHandler
handler = JWTHandler(secret_key="test-secret-key")
token = handler.create_access_token(user_id="user-123")
payload = handler.verify_token(token)
assert payload["sub"] == "user-123"
def test_verify_invalid_token(self):
"""测试验证无效 token"""
from packages.application.auth.jwt_handler import JWTHandler
handler = JWTHandler(secret_key="test-secret-key")
with pytest.raises(InvalidTokenError):
handler.verify_token("invalid.token.here")
def test_configure_and_get_default_handler(self):
"""测试配置和获取全局默认 handler"""
from packages.application.auth import jwt_handler as handler_module
from packages.application.auth.jwt_handler import (
configure_jwt_handler,
get_jwt_handler,
)
# 重置全局状态
handler_module._default_handler = None
# 配置
handler = configure_jwt_handler(
secret_key="global-secret",
algorithm="HS256",
access_token_expire_minutes=60,
)
assert handler is not None
# 获取
same_handler = get_jwt_handler()
assert same_handler is handler
# 验证能正常工作
token = same_handler.create_access_token(user_id="global-user")
payload = jwt.decode(token, "global-secret", algorithms=["HS256"])
assert payload["sub"] == "global-user"
# 重置全局状态,避免影响其他测试
handler_module._default_handler = None
def test_get_jwt_handler_not_configured(self):
"""测试未配置时获取 handler 抛出异常"""
from packages.application.auth import jwt_handler as handler_module
from packages.application.auth.jwt_handler import get_jwt_handler
# 确保未配置
handler_module._default_handler = None
with pytest.raises(RuntimeError, match="JWT handler not configured"):
get_jwt_handler()