2972f19954
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 58s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m38s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m46s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m42s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m18s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m23s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m27s
CI/CD Pipeline / Unit Tests (push) Failing after 5m54s
CI/CD Pipeline / Integration Tests (push) Successful in 2m26s
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 14s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m3s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m14s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
171 lines
5.7 KiB
Python
Executable File
171 lines
5.7 KiB
Python
Executable File
"""JWT Handler 单元测试."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
import pytest
|
|
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
|
|
|
|
from packages.application.auth.jwt_handler import (
|
|
JWTHandler,
|
|
configure_jwt_handler,
|
|
get_jwt_handler,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def jwt_handler():
|
|
return JWTHandler(
|
|
secret_key="test-secret-key-12345",
|
|
algorithm="HS256",
|
|
access_token_expire_minutes=30,
|
|
)
|
|
|
|
|
|
class TestJWTHandler:
|
|
"""JWTHandler 测试"""
|
|
|
|
def test_create_access_token_returns_string(self, jwt_handler):
|
|
"""创建 access_token 返回非空字符串"""
|
|
token = jwt_handler.create_access_token(user_id="user_001")
|
|
|
|
assert isinstance(token, str)
|
|
assert len(token) > 0
|
|
|
|
def test_create_access_token_with_role(self, jwt_handler):
|
|
"""创建带 role 的 access_token"""
|
|
token = jwt_handler.create_access_token(user_id="user_001", role="admin")
|
|
payload = jwt_handler.verify_access_token(token)
|
|
|
|
assert payload["sub"] == "user_001"
|
|
assert payload["role"] == "admin"
|
|
|
|
def test_create_access_token_with_additional_claims(self, jwt_handler):
|
|
"""创建带额外声明的 access_token"""
|
|
token = jwt_handler.create_access_token(
|
|
user_id="user_001",
|
|
additional_claims={"email": "test@example.com", "tenant": "t1"},
|
|
)
|
|
payload = jwt_handler.verify_access_token(token)
|
|
|
|
assert payload["sub"] == "user_001"
|
|
assert payload["email"] == "test@example.com"
|
|
assert payload["tenant"] == "t1"
|
|
|
|
def test_verify_access_token_success(self, jwt_handler):
|
|
"""验证有效 access_token"""
|
|
token = jwt_handler.create_access_token(user_id="user_001")
|
|
payload = jwt_handler.verify_access_token(token)
|
|
|
|
assert payload["sub"] == "user_001"
|
|
assert "exp" in payload
|
|
assert "iat" in payload
|
|
|
|
def test_verify_access_token_type_check(self, jwt_handler):
|
|
"""verify_access_token 验证 token 类型为 access"""
|
|
token = jwt_handler.create_access_token(user_id="user_001")
|
|
payload = jwt_handler.verify_access_token(token)
|
|
|
|
assert payload.get("type") == "access" or "type" in payload
|
|
|
|
def test_verify_token_no_type_restriction(self, jwt_handler):
|
|
"""verify_token 不限制 token 类型"""
|
|
token = jwt_handler.create_access_token(user_id="user_001")
|
|
payload = jwt_handler.verify_token(token)
|
|
|
|
assert payload["sub"] == "user_001"
|
|
|
|
def test_expired_token_raises_error(self):
|
|
"""过期 token 验证失败"""
|
|
handler = JWTHandler(
|
|
secret_key="test-secret",
|
|
access_token_expire_minutes=-1, # 立即过期
|
|
)
|
|
token = handler.create_access_token(user_id="user_001")
|
|
|
|
# 等待一小段时间确保过期
|
|
time.sleep(0.1)
|
|
|
|
with pytest.raises(ExpiredSignatureError):
|
|
handler.verify_access_token(token)
|
|
|
|
def test_invalid_token_raises_error(self, jwt_handler):
|
|
"""无效 token 验证失败"""
|
|
with pytest.raises(InvalidTokenError):
|
|
jwt_handler.verify_access_token("invalid.token.here")
|
|
|
|
def test_empty_token_raises_error(self, jwt_handler):
|
|
"""空字符串 token 验证失败"""
|
|
with pytest.raises(InvalidTokenError):
|
|
jwt_handler.verify_access_token("")
|
|
|
|
def test_different_secret_fails_verification(self):
|
|
"""不同密钥生成的 token 无法互相验证"""
|
|
handler1 = JWTHandler(secret_key="secret-one")
|
|
handler2 = JWTHandler(secret_key="secret-two")
|
|
|
|
token = handler1.create_access_token(user_id="user_001")
|
|
|
|
with pytest.raises(InvalidTokenError):
|
|
handler2.verify_access_token(token)
|
|
|
|
def test_custom_algorithm(self):
|
|
"""支持自定义算法"""
|
|
handler = JWTHandler(
|
|
secret_key="test-secret",
|
|
algorithm="HS256",
|
|
)
|
|
token = handler.create_access_token(user_id="user_001")
|
|
payload = handler.verify_access_token(token)
|
|
|
|
assert payload["sub"] == "user_001"
|
|
|
|
def test_default_role_is_empty_string(self, jwt_handler):
|
|
"""不传 role 时默认为空字符串"""
|
|
token = jwt_handler.create_access_token(user_id="user_001")
|
|
payload = jwt_handler.verify_access_token(token)
|
|
|
|
assert payload.get("role", "") == ""
|
|
|
|
|
|
class TestGlobalJWTHandler:
|
|
"""全局 JWT handler 配置测试"""
|
|
|
|
def test_configure_creates_handler(self):
|
|
"""configure_jwt_handler 创建并返回 handler"""
|
|
import packages.application.auth.jwt_handler as jwt_module
|
|
|
|
# 重置全局状态
|
|
jwt_module._default_handler = None
|
|
|
|
handler = configure_jwt_handler(
|
|
secret_key="global-secret",
|
|
access_token_expire_minutes=60,
|
|
)
|
|
|
|
assert isinstance(handler, JWTHandler)
|
|
assert get_jwt_handler() is handler
|
|
|
|
def test_get_jwt_handler_without_config_raises(self):
|
|
"""未配置时调用 get_jwt_handler 抛出 RuntimeError"""
|
|
import packages.application.auth.jwt_handler as jwt_module
|
|
|
|
# 重置全局状态
|
|
jwt_module._default_handler = None
|
|
|
|
with pytest.raises(RuntimeError, match="JWT handler not configured"):
|
|
get_jwt_handler()
|
|
|
|
def test_configure_overwrites_existing(self):
|
|
"""重新配置会覆盖之前的 handler"""
|
|
import packages.application.auth.jwt_handler as jwt_module
|
|
|
|
jwt_module._default_handler = None
|
|
|
|
handler1 = configure_jwt_handler(secret_key="first-secret")
|
|
handler2 = configure_jwt_handler(secret_key="second-secret")
|
|
|
|
assert handler1 is not handler2
|
|
assert get_jwt_handler() is handler2
|