138 lines
4.4 KiB
Python
138 lines
4.4 KiB
Python
"""
|
||
认证模块属性测试(Property-Based Testing)。
|
||
|
||
使用 hypothesis 验证认证系统的通用正确性属性:
|
||
- Property 2: 无效凭据始终被拒绝
|
||
- Property 3: 有效 JWT 令牌授权访问
|
||
"""
|
||
|
||
import os
|
||
|
||
os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key-for-property-tests")
|
||
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
from hypothesis import given, settings
|
||
from hypothesis import strategies as st
|
||
|
||
from app.auth.dependencies import CurrentUser, get_current_user
|
||
from app.auth.jwt import create_access_token
|
||
from app.main import app
|
||
from app.routers.auth import router
|
||
|
||
# 确保路由已挂载
|
||
if router not in [r for r in app.routes]:
|
||
app.include_router(router)
|
||
|
||
from fastapi.testclient import TestClient
|
||
|
||
client = TestClient(app)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 策略(Strategies)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# 用户名策略:1~64 字符的可打印字符串(排除控制字符)
|
||
_username_st = st.text(
|
||
alphabet=st.characters(whitelist_categories=("L", "N", "P", "S")),
|
||
min_size=1,
|
||
max_size=64,
|
||
)
|
||
|
||
# 密码策略:1~128 字符的可打印字符串
|
||
_password_st = st.text(
|
||
alphabet=st.characters(whitelist_categories=("L", "N", "P", "S")),
|
||
min_size=1,
|
||
max_size=128,
|
||
)
|
||
|
||
# user_id 策略:正整数
|
||
_user_id_st = st.integers(min_value=1, max_value=2**31 - 1)
|
||
|
||
# site_id 策略:正整数
|
||
_site_id_st = st.integers(min_value=1, max_value=2**63 - 1)
|
||
|
||
|
||
def _mock_db_returning(row):
|
||
"""构造 mock get_connection,cursor.fetchone() 返回指定行。"""
|
||
mock_conn = MagicMock()
|
||
mock_cursor = MagicMock()
|
||
mock_cursor.fetchone.return_value = row
|
||
mock_conn.cursor.return_value.__enter__ = lambda _: mock_cursor
|
||
mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
|
||
return mock_conn
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Feature: admin-web-console, Property 2: 无效凭据始终被拒绝
|
||
# **Validates: Requirements 1.2**
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@settings(max_examples=100)
|
||
@given(username=_username_st, password=_password_st)
|
||
@patch("app.routers.auth.get_connection")
|
||
def test_invalid_credentials_always_rejected(mock_get_conn, username, password):
|
||
"""
|
||
Property 2: 无效凭据始终被拒绝。
|
||
|
||
对于任意用户名/密码组合,当数据库中不存在该用户时(fetchone 返回 None),
|
||
登录接口应始终返回 401 状态码。
|
||
"""
|
||
# mock 数据库返回 None — 用户不存在
|
||
mock_get_conn.return_value = _mock_db_returning(None)
|
||
|
||
resp = client.post(
|
||
"/api/auth/login",
|
||
json={"username": username, "password": password},
|
||
)
|
||
assert resp.status_code == 401, (
|
||
f"期望 401,实际 {resp.status_code},username={username!r}"
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Feature: admin-web-console, Property 3: 有效 JWT 令牌授权访问
|
||
# **Validates: Requirements 1.3**
|
||
# ---------------------------------------------------------------------------
|
||
|
||
import asyncio
|
||
|
||
from fastapi.security import HTTPAuthorizationCredentials
|
||
|
||
|
||
def _run_async(coro):
|
||
"""在同步上下文中执行异步协程,避免 DeprecationWarning。"""
|
||
loop = asyncio.new_event_loop()
|
||
try:
|
||
return loop.run_until_complete(coro)
|
||
finally:
|
||
loop.close()
|
||
|
||
|
||
@settings(max_examples=100)
|
||
@given(user_id=_user_id_st, site_id=_site_id_st)
|
||
def test_valid_jwt_grants_access(user_id, site_id):
|
||
"""
|
||
Property 3: 有效 JWT 令牌授权访问。
|
||
|
||
对于任意 user_id 和 site_id,由系统签发的未过期 access_token
|
||
应能被 get_current_user 依赖成功解析为 CurrentUser 对象,
|
||
且解析出的 user_id 和 site_id 与签发时一致。
|
||
"""
|
||
# 生成有效的 access_token
|
||
token = create_access_token(user_id=user_id, site_id=site_id)
|
||
|
||
# 直接调用依赖函数验证令牌解析
|
||
credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials=token)
|
||
result = _run_async(get_current_user(credentials))
|
||
|
||
assert isinstance(result, CurrentUser)
|
||
assert result.user_id == user_id, (
|
||
f"user_id 不匹配:期望 {user_id},实际 {result.user_id}"
|
||
)
|
||
assert result.site_id == site_id, (
|
||
f"site_id 不匹配:期望 {site_id},实际 {result.site_id}"
|
||
)
|