在准备环境前提交次全部更改。
This commit is contained in:
147
apps/backend/tests/test_auth_jwt.py
Normal file
147
apps/backend/tests/test_auth_jwt.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user