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
265 lines
9.2 KiB
Python
Executable File
265 lines
9.2 KiB
Python
Executable File
"""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_id(sub字段)"""
|
||
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)
|