"""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)