Files
xiaoxia-saas/tests/unit/test_jwt_service.py
T
xiaoxia 7e505a04c7
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
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 / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
test: P3-1 第40波单元测试(wechat_oauth + wechat_sync) (#821)
2026-07-24 16:44:35 +08:00

265 lines
9.2 KiB
Python
Executable File
Raw 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.
"""JWT 服务单元测试."""
from __future__ import annotations
import time
import pytest
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
from packages.application.auth.jwt_service import JWTConfig, JWTService, TokenType
@pytest.fixture
def jwt_config():
return JWTConfig(
secret_key="test-secret-key-strong-enough-123456",
algorithm="HS256",
access_token_expire_minutes=30,
refresh_token_expire_days=7,
)
@pytest.fixture
def jwt_service(jwt_config):
return JWTService(jwt_config)
class TestJWTConfig:
"""JWTConfig 测试"""
def test_empty_secret_raises(self):
"""空 secret_key 抛出 ValueError"""
with pytest.raises(ValueError, match="must be provided"):
JWTConfig(secret_key="")
def test_whitespace_secret_raises(self):
"""纯空白 secret_key 抛出 ValueError"""
with pytest.raises(ValueError, match="must be provided"):
JWTConfig(secret_key=" ")
def test_insecure_default_secret_raises(self):
"""不安全的默认 secret 抛出 ValueError"""
insecure_secrets = [
"your-secret-key-change-in-production",
"your-secret-key",
"secret",
"changeme",
"password",
"SECRET",
]
for secret in insecure_secrets:
with pytest.raises(ValueError, match="insecure"):
JWTConfig(secret_key=secret)
def test_strong_secret_accepted(self):
"""强 secret 可以正常创建"""
config = JWTConfig(secret_key="my-strong-secret-key-1234567890")
assert config.SECRET_KEY == "my-strong-secret-key-1234567890"
def test_default_values(self):
"""默认配置值正确"""
config = JWTConfig(secret_key="test-secret-12345")
assert config.ALGORITHM == "HS256"
assert config.ACCESS_TOKEN_EXPIRE_MINUTES == 15
assert config.REFRESH_TOKEN_EXPIRE_DAYS == 7
def test_custom_expiry_values(self):
"""自定义过期时间"""
config = JWTConfig(
secret_key="test-secret-12345",
access_token_expire_minutes=60,
refresh_token_expire_days=30,
)
assert config.ACCESS_TOKEN_EXPIRE_MINUTES == 60
assert config.REFRESH_TOKEN_EXPIRE_DAYS == 30
class TestTokenType:
"""TokenType 测试"""
def test_access_token_type(self):
"""access token 类型值"""
assert TokenType.ACCESS == "access"
def test_refresh_token_type(self):
"""refresh token 类型值"""
assert TokenType.REFRESH == "refresh"
class TestJWTServiceInit:
"""JWTService 初始化测试"""
def test_none_config_raises(self):
"""不传 config 抛出 ValueError"""
with pytest.raises(ValueError, match="requires a JWTConfig"):
JWTService(None)
def test_with_config_creates_service(self, jwt_config):
"""传入 config 正常创建"""
service = JWTService(jwt_config)
assert service.config is jwt_config
class TestCreateAccessToken:
"""create_access_token 测试"""
def test_returns_string(self, jwt_service):
"""返回非空字符串"""
token = jwt_service.create_access_token(user_id="user_001")
assert isinstance(token, str)
assert len(token) > 0
def test_contains_user_id(self, jwt_service):
"""payload 包含正确的 user_idsub字段)"""
token = jwt_service.create_access_token(user_id="user_123")
payload = jwt_service.verify_token(token)
assert payload["sub"] == "user_123"
def test_contains_role(self, jwt_service):
"""payload 包含 role"""
token = jwt_service.create_access_token(user_id="user_001", role="admin")
payload = jwt_service.verify_token(token)
assert payload["role"] == "admin"
def test_default_role_empty(self, jwt_service):
"""不传 role 默认为空字符串"""
token = jwt_service.create_access_token(user_id="user_001")
payload = jwt_service.verify_token(token)
assert payload["role"] == ""
def test_token_type_is_access(self, jwt_service):
"""access token 的 type 为 access"""
token = jwt_service.create_access_token(user_id="user_001")
payload = jwt_service.verify_token(token)
assert payload["type"] == TokenType.ACCESS
def test_additional_claims(self, jwt_service):
"""额外声明被包含在 payload 中"""
token = jwt_service.create_access_token(
user_id="user_001",
additional_claims={"email": "test@example.com", "tenant": "t1"},
)
payload = jwt_service.verify_token(token)
assert payload["email"] == "test@example.com"
assert payload["tenant"] == "t1"
def test_has_iat_and_exp(self, jwt_service):
"""payload 包含 iat 和 exp"""
token = jwt_service.create_access_token(user_id="user_001")
payload = jwt_service.verify_token(token)
assert "iat" in payload
assert "exp" in payload
assert payload["exp"] > payload["iat"]
def test_expiry_correct_duration(self, jwt_service):
"""过期时间设置正确"""
token = jwt_service.create_access_token(user_id="user_001")
payload = jwt_service.verify_token(token)
# 30分钟 = 1800秒
duration = payload["exp"] - payload["iat"]
assert 1790 <= duration <= 1810 # 允许10秒误差
class TestCreateRefreshToken:
"""create_refresh_token 测试"""
def test_returns_string(self, jwt_service):
"""返回非空字符串"""
token = jwt_service.create_refresh_token(user_id="user_001", session_id="sess_001")
assert isinstance(token, str)
assert len(token) > 0
def test_contains_user_and_session(self, jwt_service):
"""包含 user_id 和 session_id"""
token = jwt_service.create_refresh_token(user_id="user_123", session_id="sess_456")
payload = jwt_service.verify_token(token)
assert payload["sub"] == "user_123"
assert payload["session_id"] == "sess_456"
def test_token_type_is_refresh(self, jwt_service):
"""refresh token 的 type 为 refresh"""
token = jwt_service.create_refresh_token(user_id="user_001", session_id="s1")
payload = jwt_service.verify_token(token)
assert payload["type"] == TokenType.REFRESH
class TestVerifyToken:
"""verify_token 测试"""
def test_valid_token(self, jwt_service):
"""有效 token 验证通过"""
token = jwt_service.create_access_token(user_id="u1")
payload = jwt_service.verify_token(token)
assert payload["sub"] == "u1"
def test_invalid_token_raises(self, jwt_service):
"""无效 token 抛出 InvalidTokenError"""
with pytest.raises(InvalidTokenError):
jwt_service.verify_token("not.a.valid.token")
def test_empty_token_raises(self, jwt_service):
"""空字符串 token 抛出异常"""
with pytest.raises(InvalidTokenError):
jwt_service.verify_token("")
def test_wrong_secret_fails(self, jwt_config):
"""不同密钥的 token 无法验证"""
service1 = JWTService(JWTConfig(secret_key="secret-one-123456"))
service2 = JWTService(JWTConfig(secret_key="secret-two-1234567"))
token = service1.create_access_token(user_id="u1")
with pytest.raises(InvalidTokenError):
service2.verify_token(token)
class TestVerifyAccessToken:
"""verify_access_token 测试"""
def test_valid_access_token(self, jwt_service):
"""有效 access token 验证通过"""
token = jwt_service.create_access_token(user_id="u1")
payload = jwt_service.verify_access_token(token)
assert payload["sub"] == "u1"
def test_refresh_token_fails(self, jwt_service):
"""refresh token 不能当 access token 用"""
token = jwt_service.create_refresh_token(user_id="u1", session_id="s1")
with pytest.raises(ValueError, match="Token type must be 'access'"):
jwt_service.verify_access_token(token)
class TestVerifyRefreshToken:
"""verify_refresh_token 测试"""
def test_valid_refresh_token(self, jwt_service):
"""有效 refresh token 验证通过"""
token = jwt_service.create_refresh_token(user_id="u1", session_id="s1")
payload = jwt_service.verify_refresh_token(token)
assert payload["sub"] == "u1"
assert payload["session_id"] == "s1"
def test_access_token_fails(self, jwt_service):
"""access token 不能当 refresh token 用"""
token = jwt_service.create_access_token(user_id="u1")
with pytest.raises(ValueError, match="Token type must be 'refresh'"):
jwt_service.verify_refresh_token(token)
class TestExpiredToken:
"""过期 token 测试"""
def test_expired_access_token_raises(self):
"""过期 token 验证抛出 ExpiredSignatureError"""
config = JWTConfig(
secret_key="test-secret-12345",
access_token_expire_minutes=-1, # 立即过期
)
service = JWTService(config)
token = service.create_access_token(user_id="u1")
time.sleep(0.1)
with pytest.raises(ExpiredSignatureError):
service.verify_access_token(token)