compliance/ddms_compliance_suite/test_orchestrator.py
2025-05-16 15:18:02 +08:00

802 lines
31 KiB
Python
Raw 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.

"""
测试编排器模块
负责组合API解析器、API调用器、验证器和规则执行器进行端到端的API测试
"""
import logging
import json
import time
from typing import Dict, List, Any, Optional, Union, Tuple
from enum import Enum
import datetime
from .input_parser.parser import InputParser, YAPIEndpoint, SwaggerEndpoint, ParsedYAPISpec, ParsedSwaggerSpec
from .api_caller.caller import APICaller, APIRequest, APIResponse
from .json_schema_validator.validator import JSONSchemaValidator
from .rule_repository.repository import RuleRepository
from .rule_executor.executor import RuleExecutor
from .models.rule_models import RuleQuery, TargetType, RuleCategory, RuleLifecycle, RuleScope
from .models.config_models import RuleRepositoryConfig, RuleStorageConfig
class TestResult:
"""测试结果类"""
class Status(str, Enum):
"""测试状态枚举"""
PASSED = "通过"
FAILED = "失败"
ERROR = "错误"
SKIPPED = "跳过"
def __init__(self,
endpoint_id: str,
endpoint_name: str,
status: Status,
message: str = "",
api_request: Optional[APIRequest] = None,
api_response: Optional[APIResponse] = None,
validation_details: Optional[Dict[str, Any]] = None,
elapsed_time: float = 0.0):
"""
初始化测试结果
Args:
endpoint_id: API端点ID通常是方法+路径的组合)
endpoint_name: API端点名称
status: 测试状态
message: 测试结果消息
api_request: API请求对象
api_response: API响应对象
validation_details: 验证详情
elapsed_time: 执行耗时(秒)
"""
self.endpoint_id = endpoint_id
self.endpoint_name = endpoint_name
self.status = status
self.message = message
self.api_request = api_request
self.api_response = api_response
self.validation_details = validation_details or {}
self.elapsed_time = elapsed_time
self.timestamp = datetime.datetime.now()
def to_dict(self) -> Dict[str, Any]:
"""将测试结果转换为字典"""
result = {
"endpoint_id": self.endpoint_id,
"endpoint_name": self.endpoint_name,
"status": self.status,
"message": self.message,
"elapsed_time": self.elapsed_time,
"timestamp": self.timestamp.isoformat(),
}
if self.api_request:
result["api_request"] = {
"method": self.api_request.method,
"url": str(self.api_request.url),
"params": self.api_request.params,
"body": self.api_request.json_data
}
if self.api_response:
result["api_response"] = {
"status_code": self.api_response.status_code,
"content": self.api_response.json_content if self.api_response.json_content else str(self.api_response.content),
"elapsed_time": self.api_response.elapsed_time
}
if self.validation_details:
result["validation_details"] = self.validation_details
return result
class TestSummary:
"""测试结果摘要"""
def __init__(self):
"""初始化测试结果摘要"""
self.total = 0
self.passed = 0
self.failed = 0
self.error = 0
self.skipped = 0
self.start_time = datetime.datetime.now()
self.end_time: Optional[datetime.datetime] = None
self.results: List[TestResult] = []
def add_result(self, result: TestResult):
"""添加测试结果"""
self.total += 1
if result.status == TestResult.Status.PASSED:
self.passed += 1
elif result.status == TestResult.Status.FAILED:
self.failed += 1
elif result.status == TestResult.Status.ERROR:
self.error += 1
elif result.status == TestResult.Status.SKIPPED:
self.skipped += 1
self.results.append(result)
def finalize(self):
"""完成测试,记录结束时间"""
self.end_time = datetime.datetime.now()
@property
def duration(self) -> float:
"""测试持续时间(秒)"""
if not self.end_time:
return 0.0
return (self.end_time - self.start_time).total_seconds()
@property
def success_rate(self) -> float:
"""测试成功率"""
if self.total == 0:
return 0.0
return self.passed / self.total * 100
def to_dict(self) -> Dict[str, Any]:
"""将测试结果摘要转换为字典"""
return {
"total": self.total,
"passed": self.passed,
"failed": self.failed,
"error": self.error,
"skipped": self.skipped,
"success_rate": f"{self.success_rate:.2f}%",
"start_time": self.start_time.isoformat(),
"end_time": self.end_time.isoformat() if self.end_time else None,
"duration": f"{self.duration:.2f}",
"results": [result.to_dict() for result in self.results]
}
def to_json(self, pretty=True) -> str:
"""将测试结果摘要转换为JSON字符串"""
indent = 2 if pretty else None
return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)
def print_summary(self):
"""打印测试结果摘要"""
print(f"\n测试结果摘要:")
print(f"总测试数: {self.total}")
print(f"通过: {self.passed}")
print(f"失败: {self.failed}")
print(f"错误: {self.error}")
print(f"跳过: {self.skipped}")
print(f"成功率: {self.success_rate:.2f}%")
print(f"总耗时: {self.duration:.2f}")
class APITestOrchestrator:
"""API测试编排器"""
def __init__(self, base_url: str, rule_repo_path: str = "./rules"):
"""
初始化API测试编排器
Args:
base_url: API基础URL
rule_repo_path: 规则库路径
"""
self.base_url = base_url.rstrip('/')
self.logger = logging.getLogger(__name__)
# 初始化组件
self.parser = InputParser()
self.api_caller = APICaller()
self.validator = JSONSchemaValidator()
# 初始化规则库和规则执行器
rule_config = RuleRepositoryConfig(
storage=RuleStorageConfig(path=rule_repo_path)
)
self.rule_repo = RuleRepository(rule_config)
self.rule_executor = RuleExecutor(self.rule_repo)
def _build_api_request(self, endpoint: Union[YAPIEndpoint, SwaggerEndpoint]) -> Tuple[APIRequest, Dict[str, Any]]:
"""
构建API请求对象
Args:
endpoint: API端点对象
Returns:
Tuple[APIRequest, Dict[str, Any]]: API请求对象和测试数据
"""
# 获取端点信息
if hasattr(endpoint, 'method'):
method = endpoint.method
else:
method = "GET" # 默认方法
if hasattr(endpoint, 'path'):
path = endpoint.path
else:
path = "/" # 默认路径
# 替换路径中的参数占位符
path_params = {}
if "{" in path and "}" in path:
# 查找路径中的所有参数
import re
param_matches = re.findall(r'\{([^}]+)\}', path)
for param in param_matches:
# 生成一个随机值作为参数
path_params[param] = f"test_{param}"
# 查找请求参数
params = {}
headers = {"Content-Type": "application/json", "Accept": "application/json"}
body = None
# YAPI端点特有属性
if hasattr(endpoint, 'req_headers') and endpoint.req_headers:
for header in endpoint.req_headers:
if 'name' in header and 'value' in header:
headers[header['name']] = header['value']
if hasattr(endpoint, 'req_query') and endpoint.req_query:
for query in endpoint.req_query:
if 'name' in query:
params[query['name']] = query.get('value', '')
if hasattr(endpoint, 'req_body_type') and endpoint.req_body_type == 'json' and hasattr(endpoint, 'req_body_other'):
try:
# 如果req_body_other是JSON字符串它可能包含请求体schema
req_body_schema = json.loads(endpoint.req_body_other) if isinstance(endpoint.req_body_other, str) else None
if req_body_schema and isinstance(req_body_schema, dict):
# 使用schema生成请求体
body = self._generate_data_from_schema(req_body_schema)
else:
# 如果不是有效的schema使用它作为请求体示例
body = req_body_schema
except json.JSONDecodeError:
# 如果解析失败,使用一个默认的请求体
self.logger.warning(f"无法解析YAPI请求体JSON: {endpoint.req_body_other}")
body = {"test": "data"}
# Swagger端点特有属性
if hasattr(endpoint, 'parameters') and endpoint.parameters:
for param in endpoint.parameters:
param_in = param.get('in', '')
param_name = param.get('name', '')
param_schema = param.get('schema', {})
if not param_name:
continue
# 生成参数值优先使用example
param_value = param.get('example', None)
if param_value is None and param_schema:
param_value = self._generate_data_from_schema(param_schema)
if param_value is None:
# 使用默认值
param_value = param.get('default', 'test_value')
if param_in == 'query':
params[param_name] = param_value
elif param_in == 'header':
headers[param_name] = str(param_value)
elif param_in == 'path' and param_name in path_params:
path_params[param_name] = str(param_value)
if hasattr(endpoint, 'request_body') and endpoint.request_body:
content = endpoint.request_body.get('content', {})
json_content = content.get('application/json', {})
if 'example' in json_content:
body = json_content['example']
elif 'schema' in json_content:
# 基于schema创建请求体
body = self._generate_data_from_schema(json_content['schema'])
# 构建完整URL替换路径参数
url = self.base_url + path
for param, value in path_params.items():
url = url.replace(f"{{{param}}}", str(value))
# 创建API请求
request = None
try:
request = APIRequest(
method=method,
url=url,
headers=headers,
params=params,
json_data=body
)
except Exception as e:
self.logger.error(f"创建API请求时发生错误: {e}")
raise e
# 执行请求准备阶段的规则
endpoint_id = f"{method.upper()} {path}"
# 创建规则执行上下文
context = {
'api_request': request,
'endpoint_id': endpoint_id,
'endpoint': endpoint,
'path_params': path_params,
'query_params': params,
'headers': headers,
'body': body
}
# 执行请求准备阶段的规则
rule_results = self.rule_executor.execute_rules_for_lifecycle(
lifecycle=RuleLifecycle.REQUEST_PREPARATION,
context=context
)
# 保存规则执行结果,以便在测试结果中使用
self.last_request_rule_results = rule_results
# 保存请求对象,以便在响应验证时使用
self.last_request = request
# 收集测试数据
test_data = {
"path_params": path_params,
"query_params": params,
"headers": headers,
"body": body,
"rule_results": [
{
"rule_id": result.rule.id,
"rule_name": result.rule.name,
"is_valid": result.is_valid,
"message": result.message
} for result in rule_results
]
}
return request, test_data
def _validate_response(self, response: APIResponse, endpoint: Union[YAPIEndpoint, SwaggerEndpoint]) -> Dict[str, Any]:
"""
验证API响应
Args:
response: API响应对象
endpoint: API端点对象
Returns:
Dict[str, Any]: 验证结果
"""
validation_results = {
"status_code": {
"is_valid": 200 <= response.status_code < 300,
"expected": "2XX",
"actual": response.status_code
},
"json_format": {
"is_valid": response.json_content is not None,
"message": "响应应为有效的JSON格式" if response.json_content is None else "响应是有效的JSON格式"
}
}
# 尝试从API定义中提取响应schema进行验证
schema = None
schema_source = "未知"
# 从YAPI定义中提取响应schema
if hasattr(endpoint, 'res_body') and endpoint.res_body and response.json_content:
try:
# YAPI中的res_body通常是JSON字符串格式的schema
if isinstance(endpoint.res_body, str) and endpoint.res_body.strip():
schema = json.loads(endpoint.res_body)
schema_source = "YAPI响应定义"
except json.JSONDecodeError:
self.logger.warning(f"无法解析YAPI响应schema: {endpoint.res_body}")
# 从Swagger定义中提取响应schema
elif hasattr(endpoint, 'responses') and endpoint.responses and response.json_content:
# Swagger中通常以状态码为key包含schema定义
success_responses = endpoint.responses.get('200', {}) or endpoint.responses.get('201', {})
if not success_responses and any(str(k).startswith('2') for k in endpoint.responses.keys()):
# 尝试查找任何2xx响应
for k in endpoint.responses.keys():
if str(k).startswith('2'):
success_responses = endpoint.responses[k]
break
if success_responses:
schema_obj = None
if 'schema' in success_responses:
schema_obj = success_responses['schema']
elif 'content' in success_responses and 'application/json' in success_responses['content']:
schema_obj = success_responses['content']['application/json'].get('schema')
if schema_obj:
schema = schema_obj
schema_source = "Swagger响应定义"
# 使用提取的schema进行验证
if schema and response.json_content:
try:
result = self.validator.validate(response.json_content, schema)
validation_results["schema_validation"] = {
"source": schema_source,
"is_valid": result.is_valid,
"errors": result.errors if not result.is_valid else []
}
except Exception as e:
self.logger.error(f"验证响应时发生错误: {str(e)}")
validation_results["schema_validation"] = {
"source": schema_source,
"is_valid": False,
"errors": [f"验证过程中发生错误: {str(e)}"]
}
# 如果我们有JSON Schema规则可以验证响应体
endpoint_id = ""
if hasattr(endpoint, 'path'):
path = endpoint.path
method = getattr(endpoint, 'method', "GET")
endpoint_id = f"{method.upper()} {path}"
schema_rules = self.rule_repo.get_rules_for_target(
target_type=TargetType.API_RESPONSE,
target_id=endpoint_id
)
if schema_rules:
# 使用找到的第一个规则
from .models.rule_models import JSONSchemaDefinition
for schema_rule in schema_rules:
if isinstance(schema_rule, JSONSchemaDefinition):
# 验证响应体
if response.json_content:
result = self.validator.validate_with_rule(response.json_content, schema_rule)
validation_results[f"rule_schema_validation_{schema_rule.id}"] = {
"source": f"规则库 ({schema_rule.id})",
"is_valid": result.is_valid,
"errors": result.errors if not result.is_valid else []
}
# 使用规则执行器验证规则
# 创建执行上下文
if hasattr(endpoint, 'path'):
api_request = None
if hasattr(self, 'last_request'):
api_request = self.last_request
context = {
'api_response': response,
'api_request': api_request,
'endpoint_id': endpoint_id,
'endpoint': endpoint
}
# 执行响应验证阶段的规则
rule_results = self.rule_executor.execute_rules_for_lifecycle(
lifecycle=RuleLifecycle.RESPONSE_VALIDATION,
context=context
)
# 将规则执行结果添加到验证结果中
for i, rule_result in enumerate(rule_results):
validation_results[f"rule_execution_{i}"] = {
"rule_id": rule_result.rule.id,
"rule_name": rule_result.rule.name,
"is_valid": rule_result.is_valid,
"message": rule_result.message,
"details": rule_result.details
}
# 基本验证: 检查返回码、响应时间等
validation_results["response_time"] = {
"value": response.elapsed_time,
"message": f"响应时间: {response.elapsed_time:.4f}"
}
return validation_results
def run_test_for_endpoint(self, endpoint: Union[YAPIEndpoint, SwaggerEndpoint]) -> TestResult:
"""
运行单个API端点的测试
Args:
endpoint: API端点对象
Returns:
TestResult: 测试结果
"""
# 获取端点信息
endpoint_id = f"{getattr(endpoint, 'method', 'GET')} {getattr(endpoint, 'path', '/')}"
endpoint_name = getattr(endpoint, 'title', '') or getattr(endpoint, 'summary', '') or endpoint_id
self.logger.info(f"测试端点: {endpoint_id} - {endpoint_name}")
try:
# 构建API请求
request, test_data = self._build_api_request(endpoint)
# 检查请求准备阶段的规则验证结果
request_rule_failures = []
for rule_result in test_data.get("rule_results", []):
if not rule_result.get("is_valid", True):
request_rule_failures.append(f"{rule_result.get('rule_name', '未知规则')}: {rule_result.get('message', '验证失败')}")
# 如果有关键性的请求验证失败可以选择跳过API调用
if request_rule_failures and any("严重错误" in failure for failure in request_rule_failures):
return TestResult(
endpoint_id=endpoint_id,
endpoint_name=endpoint_name,
status=TestResult.Status.FAILED,
message=f"请求准备阶段验证失败: {'; '.join(request_rule_failures)}",
api_request=request,
api_response=None,
validation_details={"request_rule_failures": request_rule_failures},
elapsed_time=0.0
)
# 发送请求
start_time = time.time()
response = self.api_caller.call_api(request)
elapsed_time = time.time() - start_time
# 验证响应
validation_results = self._validate_response(response, endpoint)
# 执行请求后处理规则
context = {
'api_request': request,
'api_response': response,
'endpoint_id': endpoint_id,
'endpoint': endpoint,
'elapsed_time': elapsed_time
}
post_rule_results = self.rule_executor.execute_rules_for_lifecycle(
lifecycle=RuleLifecycle.POST_VALIDATION,
context=context
)
# 将后处理规则结果添加到验证结果中
for i, rule_result in enumerate(post_rule_results):
validation_results[f"post_rule_execution_{i}"] = {
"rule_id": rule_result.rule.id,
"rule_name": rule_result.rule.name,
"is_valid": rule_result.is_valid,
"message": rule_result.message,
"details": rule_result.details
}
# 判断测试是否通过
# 检查所有验证结果是否有失败的
rule_failures = []
validation_failures = []
for key, result in validation_results.items():
if isinstance(result, dict) and 'is_valid' in result and not result['is_valid']:
if key.startswith('rule_execution_') or key.startswith('post_rule_execution_'):
rule_name = result.get('rule_name', '未知规则')
rule_message = result.get('message', '验证失败')
rule_failures.append(f"{rule_name}: {rule_message}")
else:
validation_failures.append(result.get('message', f"{key}验证失败"))
# 合并请求规则失败和响应规则失败
all_rule_failures = request_rule_failures + rule_failures
# 决定测试结果状态
if not validation_failures and not all_rule_failures:
# 所有验证和规则都通过
result = TestResult(
endpoint_id=endpoint_id,
endpoint_name=endpoint_name,
status=TestResult.Status.PASSED,
message="API测试通过",
api_request=request,
api_response=response,
validation_details=validation_results,
elapsed_time=elapsed_time
)
elif not validation_failures and all_rule_failures:
# 基本验证通过,但规则验证失败
result = TestResult(
endpoint_id=endpoint_id,
endpoint_name=endpoint_name,
status=TestResult.Status.FAILED,
message=f"API规则验证失败: {'; '.join(all_rule_failures)}",
api_request=request,
api_response=response,
validation_details=validation_results,
elapsed_time=elapsed_time
)
self.logger.error(f"接口{endpoint_id} 规则验证失败: {'; '.join(all_rule_failures)}")
else:
# 基本验证失败
result = TestResult(
endpoint_id=endpoint_id,
endpoint_name=endpoint_name,
status=TestResult.Status.FAILED,
message=f"API测试失败: {'; '.join(validation_failures)}",
api_request=request,
api_response=response,
validation_details=validation_results,
elapsed_time=elapsed_time
)
self.logger.error(f"接口{endpoint_id} 测试失败: {'; '.join(validation_failures)}")
return result
except Exception as e:
self.logger.error(f"测试端点 {endpoint_id} 时发生错误: {str(e)}")
return TestResult(
endpoint_id=endpoint_id,
endpoint_name=endpoint_name,
status=TestResult.Status.ERROR,
message=f"测试执行错误: {str(e)}",
elapsed_time=0.0
)
def run_tests_from_yapi(self, yapi_file_path: str, categories: Optional[List[str]] = None) -> TestSummary:
"""
从YAPI定义文件运行API测试
Args:
yapi_file_path: YAPI定义文件路径
categories: 要测试的API分类列表如果为None则测试所有分类
Returns:
TestSummary: 测试结果摘要
"""
# 解析YAPI文件
self.logger.info(f"从YAPI文件加载API定义: {yapi_file_path}")
parsed_yapi = self.parser.parse_yapi_spec(yapi_file_path)
if not parsed_yapi:
self.logger.error(f"解析YAPI文件失败: {yapi_file_path}")
# 创建一个空的测试摘要
summary = TestSummary()
summary.finalize()
return summary
# 筛选端点
endpoints = parsed_yapi.endpoints
if categories:
endpoints = [endpoint for endpoint in endpoints if endpoint.category_name in categories]
# 运行测试
summary = TestSummary()
for endpoint in endpoints:
result = self.run_test_for_endpoint(endpoint)
summary.add_result(result)
summary.finalize()
return summary
def run_tests_from_swagger(self, swagger_file_path: str, tags: Optional[List[str]] = None) -> TestSummary:
"""
从Swagger定义文件运行API测试
Args:
swagger_file_path: Swagger定义文件路径
tags: 要测试的API标签列表如果为None则测试所有标签
Returns:
TestSummary: 测试结果摘要
"""
# 解析Swagger文件
self.logger.info(f"从Swagger文件加载API定义: {swagger_file_path}")
parsed_swagger = self.parser.parse_swagger_spec(swagger_file_path)
if not parsed_swagger:
self.logger.error(f"解析Swagger文件失败: {swagger_file_path}")
# 创建一个空的测试摘要
summary = TestSummary()
summary.finalize()
return summary
# 筛选端点
endpoints = parsed_swagger.endpoints
if tags:
endpoints = [endpoint for endpoint in endpoints if any(tag in endpoint.tags for tag in tags)]
# 运行测试
summary = TestSummary()
for endpoint in endpoints:
result = self.run_test_for_endpoint(endpoint)
summary.add_result(result)
summary.finalize()
return summary
def _generate_data_from_schema(self, schema: Dict[str, Any]) -> Any:
"""
根据JSON Schema生成测试数据
Args:
schema: JSON Schema
Returns:
生成的测试数据
"""
if not schema:
return None
schema_type = schema.get('type')
if schema_type == 'object':
result = {}
properties = schema.get('properties', {})
for prop_name, prop_schema in properties.items():
# 首先检查是否有example或default值
if 'example' in prop_schema:
result[prop_name] = prop_schema['example']
elif 'default' in prop_schema:
result[prop_name] = prop_schema['default']
else:
# 递归生成子属性的值
result[prop_name] = self._generate_data_from_schema(prop_schema)
return result
elif schema_type == 'array':
# 为数组生成一个样本项
items_schema = schema.get('items', {})
# 默认生成1个元素对于测试来说通常足够
return [self._generate_data_from_schema(items_schema)]
elif schema_type == 'string':
# 处理不同的字符串格式
string_format = schema.get('format', '')
if string_format == 'date':
return '2023-01-01'
elif string_format == 'date-time':
return '2023-01-01T12:00:00Z'
elif string_format == 'email':
return 'test@example.com'
elif string_format == 'uuid':
return '00000000-0000-0000-0000-000000000000'
elif 'enum' in schema:
# 如果有枚举值,选择第一个
return schema['enum'][0] if schema['enum'] else 'enum_value'
elif 'pattern' in schema:
# 如果有正则表达式模式,返回一个简单的符合模式的字符串
# 注意:这里只是一个简单处理,不能处理所有正则表达式
return f"pattern_{schema['pattern']}_value"
else:
return 'test_string'
elif schema_type == 'number' or schema_type == 'integer':
# 处理数值类型
if 'minimum' in schema and 'maximum' in schema:
# 如果有最小值和最大值,取中间值
return (schema['minimum'] + schema['maximum']) / 2
elif 'minimum' in schema:
return schema['minimum']
elif 'maximum' in schema:
return schema['maximum']
elif schema_type == 'integer':
return 1
else:
return 1.0
elif schema_type == 'boolean':
return True
elif schema_type == 'null':
return None
# 如果是复杂类型或未知类型,返回一个默认值
return 'test_value'
# python run_api_tests.py --base-url http://127.0.0.1:4523/m1/6386850-6083489-default --yapi assets/doc/井筒API示例.json