1e3dc11d59
feat(test-suite): 新增测试套件模块,包含API测试客户端和测试配置 fix(api): 修复数据库实体和仓库的删除操作返回值 style(api): 统一数据库表名和字段命名 perf(api): 添加缓存注解提升配置查询性能 test(api): 添加H2测试数据库配置支持 chore: 清理旧的测试文件和脚本
226 lines
7.2 KiB
Python
226 lines
7.2 KiB
Python
# API 集成测试 - 基础API客户端
|
|
import pytest
|
|
import requests
|
|
import time
|
|
import os
|
|
from typing import Optional, Dict, Any, Union
|
|
from requests.adapters import HTTPAdapter, Retry
|
|
from dotenv import load_dotenv
|
|
import httpx
|
|
|
|
# 加载环境变量
|
|
load_dotenv()
|
|
|
|
|
|
class BaseAPIClient:
|
|
"""基础 API 客户端,提供通用的 HTTP 请求方法"""
|
|
|
|
def __init__(self, base_url: Optional[str] = None, timeout: int = 30):
|
|
self.base_url = base_url or os.getenv('BASE_URL', 'http://localhost:8084')
|
|
self.timeout = timeout
|
|
self.session = requests.Session()
|
|
self.token: Optional[str] = None
|
|
self.user_id: Optional[int] = None
|
|
|
|
# 配置重试策略
|
|
retries = Retry(
|
|
total=3,
|
|
backoff_factor=0.1,
|
|
status_forcelist=[500, 502, 503, 504]
|
|
)
|
|
self.session.mount('http', HTTPAdapter(max_retries=retries))
|
|
self.session.mount('https', HTTPAdapter(max_retries=retries))
|
|
|
|
def login(self, username: str, password: str) -> bool:
|
|
"""登录并获取 Token"""
|
|
response = self.post(
|
|
'/api/auth/login',
|
|
json={'username': username, 'password': password},
|
|
include_auth=False
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
self.token = data.get('token')
|
|
self.user_id = data.get('userId')
|
|
print(f"✅ 登录成功: {username} (User ID: {self.user_id})")
|
|
return True
|
|
else:
|
|
print(f"❌ 登录失败: {response.status_code}")
|
|
return False
|
|
|
|
def _build_url(self, path: str) -> str:
|
|
"""构建完整 URL"""
|
|
if path.startswith('http'):
|
|
return path
|
|
return f"{self.base_url}{path}"
|
|
|
|
def _get_headers(self, include_auth: bool = True) -> Dict[str, str]:
|
|
"""获取请求头"""
|
|
headers = {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
}
|
|
|
|
if include_auth and self.token:
|
|
headers['Authorization'] = f"Bearer {self.token}"
|
|
|
|
return headers
|
|
|
|
def get(self, path: str, params: Optional[Dict] = None, include_auth: bool = True) -> requests.Response:
|
|
"""GET 请求"""
|
|
url = self._build_url(path)
|
|
headers = self._get_headers(include_auth)
|
|
|
|
response = self.session.get(
|
|
url,
|
|
headers=headers,
|
|
params=params,
|
|
timeout=self.timeout
|
|
)
|
|
|
|
return response
|
|
|
|
def post(self, path: str, data: Optional[Dict] = None, json: Optional[Dict] = None,
|
|
include_auth: bool = True) -> requests.Response:
|
|
"""POST 请求"""
|
|
url = self._build_url(path)
|
|
headers = self._get_headers(include_auth)
|
|
|
|
response = self.session.post(
|
|
url,
|
|
headers=headers,
|
|
data=data,
|
|
json=json,
|
|
timeout=self.timeout
|
|
)
|
|
|
|
return response
|
|
|
|
def put(self, path: str, data: Optional[Dict] = None, json: Optional[Dict] = None,
|
|
include_auth: bool = True) -> requests.Response:
|
|
"""PUT 请求"""
|
|
url = self._build_url(path)
|
|
headers = self._get_headers(include_auth)
|
|
|
|
response = self.session.put(
|
|
url,
|
|
headers=headers,
|
|
data=data,
|
|
json=json,
|
|
timeout=self.timeout
|
|
)
|
|
|
|
return response
|
|
|
|
def delete(self, path: str, include_auth: bool = True) -> requests.Response:
|
|
"""DELETE 请求"""
|
|
url = self._build_url(path)
|
|
headers = self._get_headers(include_auth)
|
|
|
|
response = self.session.delete(
|
|
url,
|
|
headers=headers,
|
|
timeout=self.timeout
|
|
)
|
|
|
|
return response
|
|
|
|
def cleanup_resource(self, resource_type: str, resource_id: int) -> bool:
|
|
"""清理测试资源"""
|
|
try:
|
|
response = self.delete(f'/api/{resource_type}/{resource_id}')
|
|
return response.status_code == 200
|
|
except Exception as e:
|
|
print(f"清理资源失败: {e}")
|
|
return False
|
|
|
|
|
|
class APIFixture:
|
|
"""API 测试固定装置,提供测试数据管理"""
|
|
|
|
def __init__(self, api_client: BaseAPIClient):
|
|
self.api_client = api_client
|
|
self.created_users = []
|
|
self.created_roles = []
|
|
self.created_menus = []
|
|
self.created_configs = []
|
|
self.created_dicts = []
|
|
|
|
def cleanup(self):
|
|
"""清理所有创建的测试数据"""
|
|
print("\n🧹 清理测试数据...")
|
|
|
|
# 清理用户
|
|
for user_id in self.created_users:
|
|
self.api_client.cleanup_resource('users', user_id)
|
|
self.created_users.clear()
|
|
|
|
# 清理角色
|
|
for role_id in self.created_roles:
|
|
self.api_client.cleanup_resource('roles', role_id)
|
|
self.created_roles.clear()
|
|
|
|
# 清理菜单
|
|
for menu_id in self.created_menus:
|
|
self.api_client.cleanup_resource('menus', menu_id)
|
|
self.created_menus.clear()
|
|
|
|
# 清理配置
|
|
for config_id in self.created_configs:
|
|
self.api_client.cleanup_resource('config', config_id)
|
|
self.created_configs.clear()
|
|
|
|
# 清理字典
|
|
for dict_id in self.created_dicts:
|
|
self.api_client.cleanup_resource('dict', dict_id)
|
|
self.created_dicts.clear()
|
|
|
|
print("✅ 测试数据清理完成")
|
|
|
|
|
|
class AsyncAPIClient:
|
|
"""异步 API 客户端,使用 httpx"""
|
|
|
|
def __init__(self, client: httpx.AsyncClient):
|
|
self.client = client
|
|
self.token: Optional[str] = None
|
|
self.user_id: Optional[int] = None
|
|
|
|
def set_auth(self, token: str, user_id: int = None):
|
|
"""设置认证信息"""
|
|
self.token = token
|
|
self.user_id = user_id
|
|
self.client.headers.update({'Authorization': f'Bearer {token}'})
|
|
|
|
async def login(self, username: str, password: str) -> httpx.Response:
|
|
"""登录并获取 Token"""
|
|
response = await self.client.post(
|
|
'/api/auth/login',
|
|
json={'username': username, 'password': password}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
self.token = data.get('token')
|
|
self.user_id = data.get('userId')
|
|
print(f"✅ 登录成功: {username} (User ID: {self.user_id})")
|
|
|
|
return response
|
|
|
|
async def get(self, path: str, params: Optional[Dict] = None) -> httpx.Response:
|
|
"""GET 请求"""
|
|
return await self.client.get(path, params=params)
|
|
|
|
async def post(self, path: str, json: Optional[Dict] = None) -> httpx.Response:
|
|
"""POST 请求"""
|
|
return await self.client.post(path, json=json)
|
|
|
|
async def put(self, path: str, json: Optional[Dict] = None) -> httpx.Response:
|
|
"""PUT 请求"""
|
|
return await self.client.put(path, json=json)
|
|
|
|
async def delete(self, path: str) -> httpx.Response:
|
|
"""DELETE 请求"""
|
|
return await self.client.delete(path)
|