Files
Neo-ZQYY/apps/backend/tests/test_auth_properties.py

138 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
认证模块属性测试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_connectioncursor.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}"
)