""" JWT 认证模块单元测试。 覆盖:令牌生成、验证、过期、类型校验、密码哈希、依赖注入。 """ import os import time import pytest from jose import jwt as jose_jwt # 测试前设置 JWT_SECRET_KEY,避免空密钥 os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key-for-unit-tests") from app.auth.jwt import ( create_access_token, create_refresh_token, create_token_pair, decode_access_token, decode_refresh_token, decode_token, hash_password, verify_password, ) from app import config # --------------------------------------------------------------------------- # 密码哈希 # --------------------------------------------------------------------------- class TestPasswordHashing: def test_hash_and_verify(self): raw = "my_secure_password" hashed = hash_password(raw) assert verify_password(raw, hashed) def test_wrong_password_rejected(self): hashed = hash_password("correct") assert not verify_password("wrong", hashed) def test_hash_is_not_plaintext(self): raw = "plaintext123" hashed = hash_password(raw) assert hashed != raw # --------------------------------------------------------------------------- # 令牌生成与解码 # --------------------------------------------------------------------------- class TestTokenCreation: def test_access_token_contains_expected_fields(self): token = create_access_token(user_id=1, site_id=100) payload = decode_token(token) assert payload["sub"] == "1" assert payload["site_id"] == 100 assert payload["type"] == "access" assert "exp" in payload def test_refresh_token_contains_expected_fields(self): token = create_refresh_token(user_id=2, site_id=200) payload = decode_token(token) assert payload["sub"] == "2" assert payload["site_id"] == 200 assert payload["type"] == "refresh" assert "exp" in payload def test_token_pair_returns_both_tokens(self): pair = create_token_pair(user_id=3, site_id=300) assert "access_token" in pair assert "refresh_token" in pair assert pair["token_type"] == "bearer" # 验证两个令牌类型不同 access_payload = decode_token(pair["access_token"]) refresh_payload = decode_token(pair["refresh_token"]) assert access_payload["type"] == "access" assert refresh_payload["type"] == "refresh" # --------------------------------------------------------------------------- # 令牌类型校验 # --------------------------------------------------------------------------- class TestTokenTypeValidation: def test_decode_access_token_rejects_refresh(self): """access 解码器拒绝 refresh 令牌。""" token = create_refresh_token(user_id=1, site_id=1) with pytest.raises(Exception): decode_access_token(token) def test_decode_refresh_token_rejects_access(self): """refresh 解码器拒绝 access 令牌。""" token = create_access_token(user_id=1, site_id=1) with pytest.raises(Exception): decode_refresh_token(token) def test_decode_access_token_accepts_access(self): token = create_access_token(user_id=5, site_id=50) payload = decode_access_token(token) assert payload["sub"] == "5" assert payload["site_id"] == 50 def test_decode_refresh_token_accepts_refresh(self): token = create_refresh_token(user_id=6, site_id=60) payload = decode_refresh_token(token) assert payload["sub"] == "6" assert payload["site_id"] == 60 # --------------------------------------------------------------------------- # 令牌过期 # --------------------------------------------------------------------------- class TestTokenExpiry: def test_expired_token_rejected(self): """手动构造已过期令牌,验证解码失败。""" payload = { "sub": "1", "site_id": 1, "type": "access", "exp": int(time.time()) - 10, # 10 秒前过期 } token = jose_jwt.encode( payload, config.JWT_SECRET_KEY, algorithm=config.JWT_ALGORITHM ) with pytest.raises(Exception): decode_token(token) # --------------------------------------------------------------------------- # 无效令牌 # --------------------------------------------------------------------------- class TestInvalidToken: def test_garbage_token_rejected(self): with pytest.raises(Exception): decode_token("not.a.valid.jwt") def test_wrong_secret_rejected(self): """用不同密钥签发的令牌应被拒绝。""" payload = {"sub": "1", "site_id": 1, "type": "access", "exp": int(time.time()) + 3600} token = jose_jwt.encode(payload, "wrong-secret", algorithm="HS256") with pytest.raises(Exception): decode_token(token)