""" 认证模块属性测试(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}" )