v0-json-schema
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
"""API Caller Module"""
|
||||
import requests
|
||||
from typing import Any, Dict, Optional, Union, List
|
||||
|
||||
from pydantic import BaseModel, Field, HttpUrl
|
||||
|
||||
# It's a good practice to define input/output models,
|
||||
# even for internal components, using Pydantic.
|
||||
|
||||
class APIRequest(BaseModel):
|
||||
method: str # GET, POST, PUT, DELETE, etc.
|
||||
url: HttpUrl
|
||||
headers: Optional[Dict[str, str]] = None
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
json_data: Optional[Any] = None # For POST/PUT with JSON body (can be dict, list, string, etc.)
|
||||
body: Optional[Any] = Field(default=None, description="Alias for json_data") # 添加别名,方便调用
|
||||
data: Optional[Any] = None # For form data etc.
|
||||
timeout: int = 30 # seconds
|
||||
|
||||
def model_post_init(self, __context):
|
||||
"""初始化后处理,将 body 赋值给 json_data(如果提供了body)"""
|
||||
if self.body is not None and self.json_data is None:
|
||||
self.json_data = self.body
|
||||
|
||||
class APIResponse(BaseModel):
|
||||
status_code: int
|
||||
headers: Dict[str, str]
|
||||
content: bytes # Raw content
|
||||
json_content: Optional[Any] = None # Parsed JSON content if applicable
|
||||
elapsed_time: float # in seconds
|
||||
|
||||
class APICaller:
|
||||
"""
|
||||
Responsible for executing HTTP/S API calls to the DDMS services.
|
||||
"""
|
||||
|
||||
def __init__(self, default_timeout: int = 30, default_headers: Optional[Dict[str, str]] = None):
|
||||
self.default_timeout = default_timeout
|
||||
self.default_headers = default_headers or {}
|
||||
|
||||
def call_api(self, request_data: APIRequest) -> APIResponse:
|
||||
"""
|
||||
Makes an API call based on the provided request data.
|
||||
|
||||
Args:
|
||||
request_data: An APIRequest Pydantic model instance.
|
||||
|
||||
Returns:
|
||||
An APIResponse Pydantic model instance.
|
||||
"""
|
||||
merged_headers = {**self.default_headers, **(request_data.headers or {})}
|
||||
timeout = request_data.timeout or self.default_timeout
|
||||
|
||||
try:
|
||||
# 如果提供了 body,使用它作为 json 参数
|
||||
json_payload = request_data.json_data
|
||||
|
||||
response = requests.request(
|
||||
method=request_data.method.upper(),
|
||||
url=str(request_data.url),
|
||||
headers=merged_headers,
|
||||
params=request_data.params,
|
||||
json=json_payload,
|
||||
data=request_data.data,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
# 不立即引发异常,而是捕获状态码
|
||||
status_code = response.status_code
|
||||
|
||||
json_content = None
|
||||
try:
|
||||
if response.headers.get('Content-Type', '').startswith('application/json'):
|
||||
json_content = response.json()
|
||||
except requests.exceptions.JSONDecodeError:
|
||||
# Not a JSON response or invalid JSON, that's fine for some cases.
|
||||
pass
|
||||
|
||||
return APIResponse(
|
||||
status_code=status_code,
|
||||
headers=dict(response.headers),
|
||||
content=response.content,
|
||||
json_content=json_content,
|
||||
elapsed_time=response.elapsed.total_seconds()
|
||||
)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
# 处理 HTTP 错误
|
||||
print(f"API call to {request_data.url} failed: {e}")
|
||||
return APIResponse(
|
||||
status_code=e.response.status_code,
|
||||
headers=dict(e.response.headers),
|
||||
content=e.response.content,
|
||||
json_content=None,
|
||||
elapsed_time=e.response.elapsed.total_seconds()
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
# 处理其他请求异常
|
||||
print(f"API call to {request_data.url} failed: {e}")
|
||||
return APIResponse(
|
||||
status_code=getattr(e.response, 'status_code', 500),
|
||||
headers=dict(getattr(e.response, 'headers', {})),
|
||||
content=str(e).encode(),
|
||||
json_content=None,
|
||||
elapsed_time=0
|
||||
)
|
||||
|
||||
# Example Usage (can be moved to tests or main application logic)
|
||||
if __name__ == '__main__':
|
||||
caller = APICaller(default_headers={"X-App-Name": "DDMSComplianceSuite"})
|
||||
|
||||
# Example GET request
|
||||
get_req_data = APIRequest(
|
||||
method="GET",
|
||||
url=HttpUrl("https://jsonplaceholder.typicode.com/todos/1"),
|
||||
headers={"X-Request-ID": "12345"}
|
||||
)
|
||||
response = caller.call_api(get_req_data)
|
||||
print("GET Response:")
|
||||
if response.json_content:
|
||||
print(f"Status: {response.status_code}, Data: {response.json_content}")
|
||||
else:
|
||||
print(f"Status: {response.status_code}, Content: {response.content.decode()}")
|
||||
print(f"Time taken: {response.elapsed_time:.4f}s")
|
||||
|
||||
print("\n")
|
||||
|
||||
# Example POST request with json_data
|
||||
post_req_data = APIRequest(
|
||||
method="POST",
|
||||
url=HttpUrl("https://jsonplaceholder.typicode.com/posts"),
|
||||
json_data={"title": "foo", "body": "bar", "userId": 1},
|
||||
headers={"Content-Type": "application/json; charset=UTF-8"}
|
||||
)
|
||||
response = caller.call_api(post_req_data)
|
||||
print("POST Response with json_data:")
|
||||
if response.json_content:
|
||||
print(f"Status: {response.status_code}, Data: {response.json_content}")
|
||||
else:
|
||||
print(f"Status: {response.status_code}, Content: {response.content.decode()}")
|
||||
print(f"Time taken: {response.elapsed_time:.4f}s")
|
||||
|
||||
# Example POST request with body (alias for json_data)
|
||||
post_req_data_with_body = APIRequest(
|
||||
method="POST",
|
||||
url=HttpUrl("https://jsonplaceholder.typicode.com/posts"),
|
||||
body={"title": "using body", "body": "testing body alias", "userId": 2},
|
||||
headers={"Content-Type": "application/json; charset=UTF-8"}
|
||||
)
|
||||
response = caller.call_api(post_req_data_with_body)
|
||||
print("\nPOST Response with body:")
|
||||
if response.json_content:
|
||||
print(f"Status: {response.status_code}, Data: {response.json_content}")
|
||||
else:
|
||||
print(f"Status: {response.status_code}, Content: {response.content.decode()}")
|
||||
print(f"Time taken: {response.elapsed_time:.4f}s")
|
||||
|
||||
# Example list body request (demonstrating array body support)
|
||||
array_req_data = APIRequest(
|
||||
method="POST",
|
||||
url=HttpUrl("https://jsonplaceholder.typicode.com/posts"),
|
||||
body=["test_string", "another_value"],
|
||||
headers={"Content-Type": "application/json; charset=UTF-8"}
|
||||
)
|
||||
response = caller.call_api(array_req_data)
|
||||
print("\nPOST Response with array body:")
|
||||
if response.json_content:
|
||||
print(f"Status: {response.status_code}, Data: {response.json_content}")
|
||||
else:
|
||||
print(f"Status: {response.status_code}, Content: {response.content.decode()}")
|
||||
print(f"Time taken: {response.elapsed_time:.4f}s")
|
||||
|
||||
# Example Error request (non-existent domain)
|
||||
error_req_data = APIRequest(
|
||||
method="GET",
|
||||
url=HttpUrl("https://nonexistentdomain.invalid"),
|
||||
)
|
||||
response = caller.call_api(error_req_data)
|
||||
print("\nError GET Response:")
|
||||
print(f"Status: {response.status_code}, Content: {response.content.decode()}")
|
||||
print(f"Time taken: {response.elapsed_time:.4f}s")
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Assertion Engine Module"""
|
||||
|
||||
from typing import Any, Dict, List
|
||||
# from ..models.rule_models import BusinessAssertionTemplate # Assuming rule_models.py will exist
|
||||
|
||||
class AssertionEngine:
|
||||
"""
|
||||
Responsible for verifying test step results based on predefined rules.
|
||||
This is a placeholder and will need significant development based on
|
||||
how assertion rules are defined and evaluated (e.g., Python expressions, JSONPath, etc.).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Initialization, potentially loading common assertion helpers or context
|
||||
pass
|
||||
|
||||
def evaluate_assertion(self, assertion_rule: Any, context_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Evaluates a single assertion rule against the given context data.
|
||||
|
||||
Args:
|
||||
assertion_rule: The rule definition (e.g., a Pydantic model like BusinessAssertionTemplate).
|
||||
The structure of this will depend on your rule design.
|
||||
context_data: Data from the test execution context (e.g., API response, extracted variables).
|
||||
|
||||
Returns:
|
||||
True if the assertion passes, False otherwise.
|
||||
"""
|
||||
# Placeholder logic - this needs to be implemented based on rule type
|
||||
# Example: if rule is a python expression
|
||||
# if assertion_rule.template_language == "python_expression":
|
||||
# try:
|
||||
# # Ensure the expression is safe to eval!
|
||||
# # Consider using ast.literal_eval for simple cases or a safer evaluation library.
|
||||
# # For complex expressions, a dedicated DSL or restricted environment is better.
|
||||
# # The context_data would be made available to the expression.
|
||||
# return bool(eval(assertion_rule.template_expression, {}, context_data))
|
||||
# except Exception as e:
|
||||
# print(f"Error evaluating Python expression assertion: {e}")
|
||||
# return False
|
||||
|
||||
# Example: if rule is a simple equality check (defined differently)
|
||||
# if "expected_value" in assertion_rule and "actual_value_path" in assertion_rule:
|
||||
# actual_value = get_value_from_path(context_data, assertion_rule.actual_value_path) # Needs helper
|
||||
# return actual_value == assertion_rule.expected_value
|
||||
|
||||
print(f"[AssertionEngine] Placeholder: Evaluating rule '{getattr(assertion_rule, "name", "Unnamed Rule")}'. Context: {context_data}")
|
||||
# This is a very basic placeholder. Real implementation depends heavily on rule definition.
|
||||
return True # Default to True for now
|
||||
|
||||
# Helper function example (would likely be more complex or use a library like jsonpath-ng)
|
||||
# def get_value_from_path(data: Dict[str, Any], path: str) -> Any:
|
||||
# """Retrieves a value from a nested dict using a simple dot-separated path."""
|
||||
# keys = path.split('.')
|
||||
# value = data
|
||||
# for key in keys:
|
||||
# if isinstance(value, dict) and key in value:
|
||||
# value = value[key]
|
||||
# else:
|
||||
# return None # Or raise an error
|
||||
# return value
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Configuration Management Module"""
|
||||
import yaml
|
||||
from pydantic import ValidationError # BaseModel is not directly used here anymore for defining AppConfig
|
||||
from typing import Optional, Any
|
||||
|
||||
from ..models.config_models import AppConfig # Import AppConfig from the new location
|
||||
|
||||
class ConfigurationManager:
|
||||
"""
|
||||
Loads and manages application configuration from a YAML file.
|
||||
Uses Pydantic for validation.
|
||||
"""
|
||||
def __init__(self, config_path: str = "configs/config.yaml"):
|
||||
self.config_path = config_path
|
||||
self.config: Optional[AppConfig] = None
|
||||
self._load_config()
|
||||
|
||||
def _load_config(self):
|
||||
try:
|
||||
with open(self.config_path, 'r') as f:
|
||||
raw_config = yaml.safe_load(f)
|
||||
if raw_config is None:
|
||||
raw_config = {} # Allow empty config file, defaults from Pydantic will be used
|
||||
self.config = AppConfig(**raw_config)
|
||||
print(f"Configuration loaded successfully from {self.config_path}")
|
||||
except FileNotFoundError:
|
||||
print(f"Warning: Configuration file {self.config_path} not found. Using default settings.")
|
||||
self.config = AppConfig() # Load with default values
|
||||
except yaml.YAMLError as e:
|
||||
print(f"Error parsing YAML configuration file {self.config_path}: {e}")
|
||||
print("Falling back to default configuration.")
|
||||
self.config = AppConfig()
|
||||
except ValidationError as e:
|
||||
print(f"Configuration validation error from {self.config_path}:\n{e}")
|
||||
print("Falling back to default configuration due to validation errors.")
|
||||
self.config = AppConfig()
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred while loading configuration from {self.config_path}: {e}")
|
||||
print("Falling back to default configuration.")
|
||||
self.config = AppConfig()
|
||||
|
||||
def get_config(self) -> AppConfig:
|
||||
"""Returns the loaded (and validated) application configuration."""
|
||||
if self.config is None:
|
||||
print("Error: Config not loaded. Attempting to load defaults.")
|
||||
self.config = AppConfig()
|
||||
return self.config
|
||||
|
||||
def get_module_config(self, module_name: str) -> Optional[Any]:
|
||||
"""Returns configuration for a specific module."""
|
||||
if self.config:
|
||||
# Ensure the module_name is a valid attribute of AppConfig
|
||||
if hasattr(self.config, module_name):
|
||||
return getattr(self.config, module_name)
|
||||
else:
|
||||
print(f"Warning: Configuration for module '{module_name}' not found in AppConfig.")
|
||||
return None
|
||||
return None
|
||||
|
||||
# Example Usage (can be moved to tests or main application logic)
|
||||
if __name__ == '__main__':
|
||||
# Create a dummy configs directory if it doesn't exist for the example
|
||||
import os
|
||||
if not os.path.exists("configs"):
|
||||
os.makedirs("configs")
|
||||
|
||||
dummy_config_file = "configs/dummy_config.yaml"
|
||||
|
||||
dummy_config_content = {
|
||||
'app_name': 'My DDMS Checker',
|
||||
'logging': {'level': 'DEBUG'},
|
||||
'api_caller': {
|
||||
'default_timeout': 60,
|
||||
'default_headers': {'X-Custom-Header': 'TestValue'}
|
||||
},
|
||||
'rule_repository': {
|
||||
'storage': {
|
||||
'type': 'filesystem',
|
||||
'path': './custom_rules'
|
||||
},
|
||||
'preload_rules': True
|
||||
},
|
||||
'json_schema_validator': { # Added for completeness of the example
|
||||
'default_draft_version': 'draft7'
|
||||
}
|
||||
}
|
||||
with open(dummy_config_file, 'w') as f_yaml:
|
||||
yaml.dump(dummy_config_content, f_yaml)
|
||||
|
||||
manager = ConfigurationManager(config_path=dummy_config_file)
|
||||
app_cfg = manager.get_config()
|
||||
print(f"App Name: {app_cfg.app_name}")
|
||||
print(f"Log Level: {app_cfg.logging.level}")
|
||||
if app_cfg.api_caller:
|
||||
print(f"API Caller Timeout: {app_cfg.api_caller.default_timeout}")
|
||||
if app_cfg.rule_repository and app_cfg.rule_repository.storage:
|
||||
print(f"Rule Storage Type: {app_cfg.rule_repository.storage.type}")
|
||||
print(f"Rule Storage Path: {app_cfg.rule_repository.storage.path}")
|
||||
if app_cfg.json_schema_validator:
|
||||
print(f"JSON Schema Validator Draft: {app_cfg.json_schema_validator.default_draft_version}")
|
||||
|
||||
print("\nLoading with non-existent file (expect defaults):")
|
||||
manager_default = ConfigurationManager(config_path="configs/non_existent_config.yaml")
|
||||
app_cfg_default = manager_default.get_config()
|
||||
print(f"App Name (Default): {app_cfg_default.app_name}")
|
||||
if app_cfg_default.api_caller:
|
||||
print(f"API Caller Timeout (Default): {app_cfg_default.api_caller.default_timeout}")
|
||||
|
||||
# Clean up the dummy file and directory if it was created
|
||||
if os.path.exists(dummy_config_file):
|
||||
os.remove(dummy_config_file)
|
||||
if os.path.exists("configs") and not os.listdir("configs"):
|
||||
os.rmdir("configs")
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
"""Input Parser Module"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any, Dict, Optional, List, Union
|
||||
from pydantic import BaseModel # For defining the structure of parsed inputs
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("InputParser")
|
||||
|
||||
class ParsedOpenAPISpec(BaseModel):
|
||||
# Placeholder for OpenAPI spec details relevant to the compliance suite
|
||||
spec: Dict[str, Any]
|
||||
info: Dict[str, Any] # Swagger 'info' object with title, version, etc.
|
||||
paths: Dict[str, Dict[str, Any]] # API paths and their operations
|
||||
tags: Optional[List[Dict[str, str]]] = None # API tags
|
||||
basePath: Optional[str] = None # Base path for all APIs
|
||||
swagger_version: str # Swagger specification version
|
||||
|
||||
@dataclass
|
||||
class YAPIEndpoint:
|
||||
"""YAPI API端点信息"""
|
||||
path: str
|
||||
method: str
|
||||
title: str = ""
|
||||
description: str = ""
|
||||
category_name: str = ""
|
||||
req_params: List[Dict[str, Any]] = field(default_factory=list)
|
||||
req_query: List[Dict[str, Any]] = field(default_factory=list)
|
||||
req_headers: List[Dict[str, Any]] = field(default_factory=list)
|
||||
req_body_type: str = ""
|
||||
req_body_other: str = ""
|
||||
res_body_type: str = ""
|
||||
res_body: str = ""
|
||||
|
||||
@dataclass
|
||||
class ParsedYAPISpec:
|
||||
"""解析后的YAPI规范"""
|
||||
endpoints: List[YAPIEndpoint]
|
||||
categories: List[Dict[str, Any]]
|
||||
total_count: int
|
||||
|
||||
@dataclass
|
||||
class SwaggerEndpoint:
|
||||
"""Swagger API端点信息"""
|
||||
path: str
|
||||
method: str
|
||||
summary: str = ""
|
||||
description: str = ""
|
||||
operation_id: str = ""
|
||||
tags: List[str] = field(default_factory=list)
|
||||
parameters: List[Dict[str, Any]] = field(default_factory=list)
|
||||
responses: Dict[str, Any] = field(default_factory=dict)
|
||||
consumes: List[str] = field(default_factory=list)
|
||||
produces: List[str] = field(default_factory=list)
|
||||
request_body: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@dataclass
|
||||
class ParsedSwaggerSpec:
|
||||
"""解析后的Swagger规范"""
|
||||
endpoints: List[SwaggerEndpoint]
|
||||
info: Dict[str, Any]
|
||||
swagger_version: str
|
||||
host: str = ""
|
||||
base_path: str = ""
|
||||
schemes: List[str] = field(default_factory=list)
|
||||
tags: List[Dict[str, Any]] = field(default_factory=list)
|
||||
categories: List[Dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
class ParsedBusinessLogic(BaseModel):
|
||||
# Placeholder for parsed business logic flow
|
||||
name: str
|
||||
steps: list # List of steps, each could be another Pydantic model
|
||||
|
||||
class InputParser:
|
||||
"""
|
||||
Responsible for parsing DDMS supplier's input materials like API specs, etc.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def parse_openapi_spec(self, spec_path: str) -> Optional[ParsedOpenAPISpec]:
|
||||
"""
|
||||
Parses an OpenAPI specification from a file path.
|
||||
|
||||
Args:
|
||||
spec_path: The file path of the OpenAPI specification.
|
||||
|
||||
Returns:
|
||||
A ParsedOpenAPISpec object containing the parsed specification,
|
||||
or None if parsing fails.
|
||||
"""
|
||||
try:
|
||||
# Check if file exists
|
||||
if not os.path.exists(spec_path):
|
||||
print(f"Error: File not found: {spec_path}")
|
||||
return None
|
||||
|
||||
# Read and parse JSON file
|
||||
with open(spec_path, 'r', encoding='utf-8') as f:
|
||||
swagger_data = json.load(f)
|
||||
|
||||
# Extract basic information
|
||||
swagger_version = swagger_data.get('swagger', swagger_data.get('openapi', 'Unknown'))
|
||||
info = swagger_data.get('info', {})
|
||||
paths = swagger_data.get('paths', {})
|
||||
tags = swagger_data.get('tags', [])
|
||||
base_path = swagger_data.get('basePath', '')
|
||||
|
||||
# Create and return ParsedOpenAPISpec
|
||||
return ParsedOpenAPISpec(
|
||||
spec=swagger_data,
|
||||
info=info,
|
||||
paths=paths,
|
||||
tags=tags,
|
||||
basePath=base_path,
|
||||
swagger_version=swagger_version
|
||||
)
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"File not found: {spec_path}")
|
||||
return None
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error parsing JSON from {spec_path}: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Error parsing OpenAPI spec from {spec_path}: {e}")
|
||||
return None
|
||||
|
||||
def parse_yapi_spec(self, file_path: str) -> Optional[ParsedYAPISpec]:
|
||||
"""
|
||||
解析YAPI规范文件
|
||||
|
||||
Args:
|
||||
file_path: YAPI JSON文件路径
|
||||
|
||||
Returns:
|
||||
Optional[ParsedYAPISpec]: 解析后的YAPI规范,如果解析失败则返回None
|
||||
"""
|
||||
if not os.path.isfile(file_path):
|
||||
logger.error(f"文件不存在: {file_path}")
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
yapi_data = json.load(f)
|
||||
|
||||
if not isinstance(yapi_data, list):
|
||||
logger.error(f"无效的YAPI文件格式: 顶层元素应该是数组")
|
||||
return None
|
||||
|
||||
endpoints = []
|
||||
categories = []
|
||||
|
||||
# 处理分类
|
||||
for category_data in yapi_data:
|
||||
if not isinstance(category_data, dict):
|
||||
logger.warning(f"YAPI 分类条目格式不正确,应为字典类型,已跳过: {category_data}")
|
||||
continue
|
||||
|
||||
category_name = category_data.get('name', '')
|
||||
category_desc = category_data.get('desc', '')
|
||||
|
||||
# 添加到分类列表
|
||||
categories.append({
|
||||
'name': category_name,
|
||||
'desc': category_desc
|
||||
})
|
||||
|
||||
# 处理API接口
|
||||
api_list = category_data.get('list', [])
|
||||
if not isinstance(api_list, list):
|
||||
logger.warning(f"分类 '{category_name}' 中的 API列表 (list) 格式不正确,应为数组类型,已跳过。")
|
||||
continue
|
||||
|
||||
for api_item in api_list:
|
||||
if not isinstance(api_item, dict):
|
||||
logger.warning(f"分类 '{category_name}' 中的 API条目格式不正确,应为字典类型,已跳过: {api_item}")
|
||||
continue
|
||||
|
||||
# 提取API信息
|
||||
path = api_item.get('path', '')
|
||||
if not path:
|
||||
logger.info(f"分类 '{category_name}' 中的 API条目缺少 'path',使用空字符串。 API: {api_item.get('title', '未命名')}")
|
||||
method = api_item.get('method', 'GET')
|
||||
if api_item.get('method') is None: # 仅当原始数据中完全没有 method 字段时记录
|
||||
logger.info(f"分类 '{category_name}' 中的 API条目 '{path}' 缺少 'method',使用默认值 'GET'。")
|
||||
title = api_item.get('title', '')
|
||||
if not title:
|
||||
logger.info(f"分类 '{category_name}' 中的 API条目 '{path}' ({method}) 缺少 'title',使用空字符串。")
|
||||
description = api_item.get('desc', '')
|
||||
|
||||
# 提取请求参数
|
||||
req_params = api_item.get('req_params', [])
|
||||
req_query = api_item.get('req_query', [])
|
||||
req_headers = api_item.get('req_headers', [])
|
||||
|
||||
# 提取请求体信息
|
||||
req_body_type = api_item.get('req_body_type', '')
|
||||
req_body_other = api_item.get('req_body_other', '')
|
||||
|
||||
# 提取响应体信息
|
||||
res_body_type = api_item.get('res_body_type', '')
|
||||
res_body = api_item.get('res_body', '')
|
||||
|
||||
# 创建端点对象
|
||||
endpoint = YAPIEndpoint(
|
||||
path=path,
|
||||
method=method,
|
||||
title=title,
|
||||
description=description,
|
||||
category_name=category_name,
|
||||
req_params=req_params,
|
||||
req_query=req_query,
|
||||
req_headers=req_headers,
|
||||
req_body_type=req_body_type,
|
||||
req_body_other=req_body_other,
|
||||
res_body_type=res_body_type,
|
||||
res_body=res_body
|
||||
)
|
||||
|
||||
endpoints.append(endpoint)
|
||||
|
||||
return ParsedYAPISpec(
|
||||
endpoints=endpoints,
|
||||
categories=categories,
|
||||
total_count=len(endpoints)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"解析YAPI文件时出错: {str(e)}")
|
||||
return None
|
||||
|
||||
def parse_swagger_spec(self, file_path: str) -> Optional[ParsedSwaggerSpec]:
|
||||
"""
|
||||
解析Swagger规范文件
|
||||
|
||||
Args:
|
||||
file_path: Swagger JSON文件路径
|
||||
|
||||
Returns:
|
||||
Optional[ParsedSwaggerSpec]: 解析后的Swagger规范,如果解析失败则返回None
|
||||
"""
|
||||
if not os.path.isfile(file_path):
|
||||
logger.error(f"文件不存在: {file_path}")
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
swagger_data = json.load(f)
|
||||
|
||||
if not isinstance(swagger_data, dict):
|
||||
logger.error(f"无效的Swagger文件格式: 顶层元素应该是对象")
|
||||
return None
|
||||
|
||||
# 提取基本信息
|
||||
swagger_version = swagger_data.get('swagger', swagger_data.get('openapi', ''))
|
||||
info = swagger_data.get('info', {})
|
||||
host = swagger_data.get('host', '')
|
||||
base_path = swagger_data.get('basePath', '')
|
||||
schemes = swagger_data.get('schemes', [])
|
||||
tags = swagger_data.get('tags', [])
|
||||
|
||||
# 创建分类列表
|
||||
categories = []
|
||||
for tag in tags:
|
||||
categories.append({
|
||||
'name': tag.get('name', ''),
|
||||
'desc': tag.get('description', '')
|
||||
})
|
||||
|
||||
# 处理API路径
|
||||
paths = swagger_data.get('paths', {})
|
||||
endpoints = []
|
||||
|
||||
for path, path_item in paths.items():
|
||||
if not isinstance(path_item, dict):
|
||||
continue
|
||||
|
||||
# 处理每个HTTP方法 (GET, POST, PUT, DELETE等)
|
||||
for method, operation in path_item.items():
|
||||
if method in ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace']:
|
||||
if not isinstance(operation, dict):
|
||||
continue
|
||||
|
||||
# 提取操作信息
|
||||
summary = operation.get('summary', '')
|
||||
description = operation.get('description', '')
|
||||
operation_id = operation.get('operationId', '')
|
||||
operation_tags = operation.get('tags', [])
|
||||
|
||||
# 提取参数信息
|
||||
parameters = operation.get('parameters', [])
|
||||
|
||||
# 提取响应信息
|
||||
responses = operation.get('responses', {})
|
||||
|
||||
# 提取请求和响应的内容类型
|
||||
consumes = operation.get('consumes', swagger_data.get('consumes', []))
|
||||
produces = operation.get('produces', swagger_data.get('produces', []))
|
||||
|
||||
# 提取请求体信息 (OpenAPI 3.0 格式)
|
||||
request_body = operation.get('requestBody', {})
|
||||
|
||||
# 创建端点对象
|
||||
endpoint = SwaggerEndpoint(
|
||||
path=path,
|
||||
method=method.upper(),
|
||||
summary=summary,
|
||||
description=description,
|
||||
operation_id=operation_id,
|
||||
tags=operation_tags,
|
||||
parameters=parameters,
|
||||
responses=responses,
|
||||
consumes=consumes,
|
||||
produces=produces,
|
||||
request_body=request_body
|
||||
)
|
||||
|
||||
endpoints.append(endpoint)
|
||||
|
||||
# 创建返回对象
|
||||
return ParsedSwaggerSpec(
|
||||
endpoints=endpoints,
|
||||
info=info,
|
||||
swagger_version=swagger_version,
|
||||
host=host,
|
||||
base_path=base_path,
|
||||
schemes=schemes,
|
||||
tags=tags,
|
||||
categories=categories
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"解析Swagger文件时出错: {str(e)}")
|
||||
return None
|
||||
|
||||
def parse_business_logic_flow(self, flow_description: str) -> Optional[ParsedBusinessLogic]:
|
||||
"""
|
||||
Parses a business logic flow description.
|
||||
The format of this description is TBD and this parser would need to be built accordingly.
|
||||
|
||||
Args:
|
||||
flow_description: The string content describing the business logic flow.
|
||||
|
||||
Returns:
|
||||
A ParsedBusinessLogic object or None if parsing fails.
|
||||
"""
|
||||
print(f"[InputParser] Placeholder: Parsing business logic flow. Content: {flow_description[:100]}...")
|
||||
# Placeholder: Actual parsing logic will depend on the defined format.
|
||||
return ParsedBusinessLogic(name="Example Flow", steps=["Step 1 API call", "Step 2 Validate Response"])
|
||||
|
||||
# Add other parsers as needed (e.g., for data object definitions)
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
JSON Schema Validator module for DDMS Compliance Suite.
|
||||
"""
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
JSON Schema validator implementation for DDMS Compliance Suite.
|
||||
|
||||
Provides validators to check if data objects conform to defined JSON Schemas.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
import jsonschema
|
||||
from jsonschema import ValidationError
|
||||
|
||||
from ddms_compliance_suite.models.rule_models import JSONSchemaDefinition
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ValidationResult:
|
||||
"""Validation result container"""
|
||||
|
||||
def __init__(self, is_valid: bool, errors: List[str] = None, warnings: List[str] = None):
|
||||
"""
|
||||
Initialize a validation result
|
||||
|
||||
Args:
|
||||
is_valid: Whether the data is valid according to the schema
|
||||
errors: List of error messages (if any)
|
||||
warnings: List of warning messages (if any)
|
||||
"""
|
||||
self.is_valid = is_valid
|
||||
self.errors = errors or []
|
||||
self.warnings = warnings or []
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of validation result"""
|
||||
status = "Valid" if self.is_valid else "Invalid"
|
||||
result = f"Validation Result: {status}\n"
|
||||
|
||||
if self.errors:
|
||||
result += f"Errors ({len(self.errors)}):\n"
|
||||
for i, error in enumerate(self.errors, 1):
|
||||
result += f" {i}. {error}\n"
|
||||
|
||||
if self.warnings:
|
||||
result += f"Warnings ({len(self.warnings)}):\n"
|
||||
for i, warning in enumerate(self.warnings, 1):
|
||||
result += f" {i}. {warning}\n"
|
||||
|
||||
return result
|
||||
|
||||
class JSONSchemaValidator:
|
||||
"""JSON Schema validator implementation"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the JSON Schema validator"""
|
||||
self.validator_cache = {}
|
||||
|
||||
def validate(self, data: Dict[str, Any], schema: Dict[str, Any]) -> ValidationResult:
|
||||
"""
|
||||
Validate data against a JSON schema
|
||||
|
||||
Args:
|
||||
data: The data to validate
|
||||
schema: The JSON schema to validate against
|
||||
|
||||
Returns:
|
||||
ValidationResult: Result of the validation
|
||||
"""
|
||||
if not schema:
|
||||
logger.error("Schema is empty or None")
|
||||
return ValidationResult(False, ["Schema is not provided"])
|
||||
|
||||
if not data:
|
||||
logger.error("Data is empty or None")
|
||||
return ValidationResult(False, ["Data is not provided"])
|
||||
|
||||
try:
|
||||
# Get or create validator
|
||||
schema_str = json.dumps(schema, sort_keys=True)
|
||||
if schema_str not in self.validator_cache:
|
||||
self.validator_cache[schema_str] = jsonschema.Draft7Validator(schema)
|
||||
|
||||
validator = self.validator_cache[schema_str]
|
||||
|
||||
# Collect all validation errors
|
||||
errors = list(validator.iter_errors(data))
|
||||
|
||||
if not errors:
|
||||
return ValidationResult(True)
|
||||
|
||||
# Format error messages
|
||||
error_messages = []
|
||||
for error in errors:
|
||||
path = ".".join(str(path_item) for path_item in error.path) if error.path else "root"
|
||||
|
||||
# Include the error type in the message to make it easier to identify
|
||||
error_type = "unknown"
|
||||
|
||||
# Determine error type based on the validation error
|
||||
if error.validator == 'required':
|
||||
error_type = "required"
|
||||
elif error.validator == 'pattern':
|
||||
error_type = "pattern"
|
||||
elif error.validator == 'enum':
|
||||
error_type = "enum"
|
||||
elif error.validator == 'type':
|
||||
error_type = "type"
|
||||
elif error.validator == 'format':
|
||||
error_type = "format"
|
||||
elif error.validator == 'minimum' or error.validator == 'maximum':
|
||||
error_type = error.validator
|
||||
elif error.validator == 'additionalProperties':
|
||||
error_type = "additionalProperties"
|
||||
else:
|
||||
error_type = error.validator
|
||||
|
||||
error_messages.append(f"Error at {path}: {error_type} - {error.message}")
|
||||
|
||||
return ValidationResult(False, error_messages)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Schema validation failed with error")
|
||||
return ValidationResult(False, [f"Validation error: {str(e)}"])
|
||||
|
||||
def validate_with_rule(self, data: Dict[str, Any], rule: JSONSchemaDefinition) -> ValidationResult:
|
||||
"""
|
||||
Validate data against a schema rule
|
||||
|
||||
Args:
|
||||
data: The data to validate
|
||||
rule: The JSON schema rule to validate against
|
||||
|
||||
Returns:
|
||||
ValidationResult: Result of the validation
|
||||
"""
|
||||
if not rule:
|
||||
logger.error("Rule is empty or None")
|
||||
return ValidationResult(False, ["Rule is not provided"])
|
||||
|
||||
if not rule.schema_content:
|
||||
logger.error(f"Rule {rule.id} does not have schema content")
|
||||
return ValidationResult(False, [f"Rule {rule.id} does not have schema content"])
|
||||
|
||||
return self.validate(data, rule.schema_content)
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
DMS Compliance Suite
|
||||
|
||||
This module is the main entry point or CLI for the application.
|
||||
"""
|
||||
|
||||
def main():
|
||||
print("Initializing DMS Compliance Suite...")
|
||||
# TODO: Initialize ConfigurationManager
|
||||
# TODO: Initialize LoggingService
|
||||
# TODO: Parse arguments (if any)
|
||||
# TODO: Initialize RuleRepository
|
||||
# TODO: Initialize TestExecutor
|
||||
# TODO: Start the validation process
|
||||
print("DDMS Compliance Suite finished.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Pydantic models for application configuration."""
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, Dict, Literal
|
||||
|
||||
# These models were previously in config/manager.py, moved here for better organization.
|
||||
|
||||
class APICallerConfig(BaseModel):
|
||||
default_timeout: int = 30
|
||||
default_headers: Optional[Dict[str, str]] = None
|
||||
# Add other API caller specific configs: e.g., retry_attempts, backoff_factor
|
||||
|
||||
class JSONSchemaValidatorConfig(BaseModel):
|
||||
default_draft_version: Optional[str] = None # e.g., "draft7", "draft2020-12"
|
||||
# Add other schema validator specific configs
|
||||
|
||||
class RuleStorageConfig(BaseModel):
|
||||
type: Literal["filesystem", "database", "in_memory"] = "filesystem"
|
||||
path: Optional[str] = "./rules" # For filesystem adapter: path to rules directory
|
||||
connection_string: Optional[str] = None # For database adapter
|
||||
# Add other storage specific configs, e.g., for filesystem: file_pattern = "*.json"
|
||||
|
||||
class RuleRepositoryConfig(BaseModel):
|
||||
storage: RuleStorageConfig = RuleStorageConfig()
|
||||
default_version_strategy: Literal["latest_enabled", "latest_stable", "exact"] = "latest_enabled"
|
||||
preload_rules: bool = False
|
||||
# Add other rule repository specific configs
|
||||
|
||||
class LoggingConfig(BaseModel):
|
||||
level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
|
||||
format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
# file_path: Optional[str] = None # Optional: if logging to a file
|
||||
|
||||
class TestExecutorConfig(BaseModel):
|
||||
# Add configurations specific to the Test Executor
|
||||
# For example, default behavior for failing a test step, etc.
|
||||
stop_on_first_failure: bool = False
|
||||
|
||||
class ReportGeneratorConfig(BaseModel):
|
||||
# Add configurations specific to the Report Generator
|
||||
output_format: Literal["json", "html", "xml"] = "json"
|
||||
output_path: str = "./reports"
|
||||
|
||||
class AppConfig(BaseModel):
|
||||
app_name: str = "DDMS Compliance Suite"
|
||||
logging: LoggingConfig = LoggingConfig()
|
||||
api_caller: APICallerConfig = APICallerConfig()
|
||||
json_schema_validator: JSONSchemaValidatorConfig = JSONSchemaValidatorConfig()
|
||||
rule_repository: RuleRepositoryConfig = RuleRepositoryConfig()
|
||||
test_executor: TestExecutorConfig = TestExecutorConfig()
|
||||
report_generator: ReportGeneratorConfig = ReportGeneratorConfig()
|
||||
# input_parser: ... # Config for input parser if needed
|
||||
# assertion_engine: ... # Config for assertion engine if needed
|
||||
@@ -0,0 +1,190 @@
|
||||
"""Base Pydantic models for the application."""
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List, Dict, Any, Union, Literal
|
||||
from enum import Enum
|
||||
|
||||
class SeverityLevel(str, Enum):
|
||||
ERROR = "error"
|
||||
WARNING = "warning"
|
||||
INFO = "info"
|
||||
|
||||
class RuleCategory(str, Enum):
|
||||
JSON_SCHEMA = "JSONSchema"
|
||||
API_LINTING = "APILinting"
|
||||
BUSINESS_LOGIC = "BusinessLogic"
|
||||
DATA_QUALITY = "DataQuality"
|
||||
SECURITY = "Security"
|
||||
PERFORMANCE = "Performance"
|
||||
MAPPING = "Mapping"
|
||||
PYTHON_CODE = "PythonCode" # Python代码规则类别
|
||||
API_DESIGN = "APIDesign" # API设计规则类别
|
||||
ERROR_HANDLING = "ErrorHandling" # 错误处理规则类别
|
||||
COMPATIBILITY = "Compatibility" # 兼容性规则类别
|
||||
GENERIC = "Generic"
|
||||
|
||||
class TargetType(str, Enum):
|
||||
API_REQUEST = "APIRequest"
|
||||
API_RESPONSE = "APIResponse"
|
||||
DATA_OBJECT = "DataObject"
|
||||
OPENAPI_SPECIFICATION = "OpenAPISpecification"
|
||||
GENERIC_TARGET = "GenericTarget"
|
||||
|
||||
class RuleLifecycle(str, Enum):
|
||||
"""规则生命周期枚举,定义规则在测试流程中的适用阶段"""
|
||||
REQUEST_PREPARATION = "请求准备阶段"
|
||||
RESPONSE_VALIDATION = "响应验证阶段"
|
||||
POST_VALIDATION = "后处理阶段"
|
||||
API_SPECIFICATION_VALIDATION = "API规范验证阶段" # 新增的生命周期
|
||||
ANY_STAGE = "任意阶段"
|
||||
|
||||
class RuleScope(str, Enum):
|
||||
"""规则作用域枚举,定义规则针对的具体对象"""
|
||||
REQUEST_URL = "请求URL"
|
||||
REQUEST_HEADERS = "请求头"
|
||||
REQUEST_PARAMS = "请求参数"
|
||||
REQUEST_BODY = "请求体"
|
||||
RESPONSE_STATUS = "响应状态码"
|
||||
RESPONSE_HEADERS = "响应头"
|
||||
RESPONSE_BODY = "响应体"
|
||||
RESPONSE_TIME = "响应时间"
|
||||
SECURITY = "安全性"
|
||||
PERFORMANCE = "性能"
|
||||
ANY_SCOPE = "任意作用域"
|
||||
|
||||
class BaseRule(BaseModel):
|
||||
"""通用规则属性 (Pydantic基类 BaseRule)"""
|
||||
id: str # 规则唯一ID
|
||||
name: str # 规则名称
|
||||
description: Optional[str] = None # 规则详细描述
|
||||
category: RuleCategory # 规则类别
|
||||
version: str = "1.0.0" # 版本号
|
||||
severity: SeverityLevel = SeverityLevel.INFO # 严重性
|
||||
source: Optional[str] = None # 规则来源 (例如 "PlatformStandard-XYZ", "OWASP-ASVS")
|
||||
is_enabled: bool = True # 是否启用
|
||||
tags: Optional[List[str]] = None # 规则标签 (如 "critical", "wellbore")
|
||||
target_type: Optional[TargetType] = None # 规则适用目标类型 (描述性元数据)
|
||||
target_identifier: Optional[str] = None # 规则适用目标的具体标识 (描述性元数据)
|
||||
author: Optional[str] = None # 规则作者
|
||||
# 新增字段
|
||||
lifecycle: Optional[RuleLifecycle] = Field(
|
||||
default=RuleLifecycle.ANY_STAGE,
|
||||
description="规则的推荐适用阶段或类型。主要用于识别 API_SPECIFICATION_VALIDATION (静态分析) 规则。"
|
||||
"对于其他动态规则,此字段为元数据提示,实际执行阶段由 TestStep 定义。"
|
||||
)
|
||||
scope: Optional[RuleScope] = Field(
|
||||
default=RuleScope.ANY_SCOPE,
|
||||
description="描述规则逻辑主要关注的API交互部分 (例如,请求头、响应体)。"
|
||||
"此字段为描述性元数据,用于规则分类和理解,不作为执行时的强制约束。"
|
||||
)
|
||||
code: Optional[str] = None # 规则验证代码
|
||||
|
||||
class JSONSchemaDefinition(BaseRule):
|
||||
"""JSON Schema 定义 (用于API请求/响应体结构验证)"""
|
||||
category: Literal[RuleCategory.JSON_SCHEMA] = RuleCategory.JSON_SCHEMA
|
||||
schema_content: Dict[str, Any] # 实际的JSON Schema
|
||||
|
||||
class APILintingRuleset(BaseRule):
|
||||
"""API 设计规范/Linting规则 (例如,基于Spectral的OpenAPI规范校验规则集)"""
|
||||
category: Literal[RuleCategory.API_LINTING] = RuleCategory.API_LINTING
|
||||
ruleset_format: str = "spectral" # 例如 "spectral", "custom_json_rules"
|
||||
ruleset_content: Union[str, Dict[str, Any]] # 规则集内容或其引用(例如,文件路径或URL,或直接嵌入的规则字典)
|
||||
|
||||
class BusinessAssertionTemplate(BaseRule):
|
||||
"""业务逻辑断言模板或具体规则"""
|
||||
category: Literal[RuleCategory.BUSINESS_LOGIC] = RuleCategory.BUSINESS_LOGIC
|
||||
template_language: str = "python_expression" # 如 "python_expression", "jsonpath_assert"
|
||||
template_expression: str
|
||||
expected_parameters: Optional[List[str]] = None # 执行断言模板时需要输入的参数列表
|
||||
|
||||
class DataQualityRule(BaseRule):
|
||||
"""数据质量校验规则"""
|
||||
category: Literal[RuleCategory.DATA_QUALITY] = RuleCategory.DATA_QUALITY
|
||||
# Specific fields for data quality rules, e.g.:
|
||||
field_name: Optional[str] = None
|
||||
validation_type: str # e.g., "regex", "range", "custom_function"
|
||||
validation_expression: str # regex pattern, range boundaries, function name/path
|
||||
|
||||
class PythonCodeRule(BaseRule):
|
||||
"""使用 Python 代码定义的规则
|
||||
|
||||
此类规则允许以 Python 代码片段形式定义复杂的验证逻辑,提供了最大的灵活性。
|
||||
代码将在受控环境中执行,以确保安全性。
|
||||
"""
|
||||
category: Literal[RuleCategory.PYTHON_CODE] = RuleCategory.PYTHON_CODE
|
||||
|
||||
# Python 代码内容或文件路径
|
||||
code_file: Optional[str] = None
|
||||
|
||||
# 脚本入口函数名,默认为 validate
|
||||
entry_function: str = "validate"
|
||||
|
||||
# 预期的入口函数参数
|
||||
expected_parameters: Optional[List[str]] = None
|
||||
|
||||
# 是否允许导入外部模块 (出于安全考虑,默认为 False)
|
||||
allow_imports: bool = False
|
||||
|
||||
# 允许导入的模块列表 (如果 allow_imports 为 True)
|
||||
allowed_modules: Optional[List[str]] = None
|
||||
|
||||
# 超时设置(秒),防止无限循环等恶意代码
|
||||
timeout: int = 5
|
||||
|
||||
# 代码依赖的其他规则ID
|
||||
depends_on: Optional[List[str]] = None
|
||||
|
||||
# 新增规则类型
|
||||
class PerformanceRule(BaseRule):
|
||||
"""性能相关规则,如响应时间、吞吐量等"""
|
||||
category: Literal[RuleCategory.PERFORMANCE] = RuleCategory.PERFORMANCE
|
||||
lifecycle: Literal[RuleLifecycle.RESPONSE_VALIDATION] = RuleLifecycle.RESPONSE_VALIDATION
|
||||
scope: Literal[RuleScope.RESPONSE_TIME] = RuleScope.RESPONSE_TIME
|
||||
threshold: Union[float, int] # 性能阈值
|
||||
metric: str # 性能指标名称
|
||||
unit: str = "ms" # 默认单位:毫秒
|
||||
|
||||
class SecurityRule(BaseRule):
|
||||
"""安全相关规则,如身份验证、授权、加密等"""
|
||||
category: Literal[RuleCategory.SECURITY] = RuleCategory.SECURITY
|
||||
check_type: str # 安全检查类型(如认证、授权、加密等)
|
||||
expected_value: Optional[str] = None # 预期值
|
||||
|
||||
class RESTfulDesignRule(BaseRule):
|
||||
"""RESTful API设计规则,如HTTP方法使用、URL设计等"""
|
||||
category: Literal[RuleCategory.API_DESIGN] = RuleCategory.API_DESIGN
|
||||
design_aspect: str # 设计方面(如URL、HTTP方法、参数等)
|
||||
pattern: Optional[str] = None # 匹配模式(如正则表达式)
|
||||
|
||||
class ErrorHandlingRule(BaseRule):
|
||||
"""错误处理规则,如错误码、错误消息等"""
|
||||
category: Literal[RuleCategory.ERROR_HANDLING] = RuleCategory.ERROR_HANDLING
|
||||
error_code: str # 错误码
|
||||
expected_status: int # 预期HTTP状态码
|
||||
expected_message: Optional[str] = None # 预期错误消息
|
||||
|
||||
# 更新联合类型以包含新规则类型
|
||||
AnyRule = Union[
|
||||
JSONSchemaDefinition,
|
||||
APILintingRuleset,
|
||||
BusinessAssertionTemplate,
|
||||
DataQualityRule,
|
||||
PythonCodeRule,
|
||||
PerformanceRule,
|
||||
SecurityRule,
|
||||
RESTfulDesignRule,
|
||||
ErrorHandlingRule,
|
||||
BaseRule # 通用或未明确分类的规则
|
||||
]
|
||||
|
||||
class RuleQuery(BaseModel):
|
||||
"""规则查询条件对象 (Pydantic模型)"""
|
||||
rule_id: Optional[str] = None
|
||||
category: Optional[RuleCategory] = None
|
||||
target_type: Optional[TargetType] = None
|
||||
target_identifier: Optional[str] = None
|
||||
version: Optional[str] = "latest" # 或特殊标识 ("latest", "stable", or specific version)
|
||||
tags: Optional[List[str]] = None
|
||||
is_enabled: Optional[bool] = True # 默认查询已启用的规则
|
||||
# 新增字段
|
||||
lifecycle: Optional[RuleLifecycle] = None
|
||||
scope: Optional[RuleScope] = None
|
||||
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
Pydantic models for Test Cases, Test Steps, and Test Suites.
|
||||
"""
|
||||
from typing import Optional, List, Dict, Any, Union, Literal
|
||||
from pydantic import BaseModel, Field, HttpUrl
|
||||
from enum import Enum
|
||||
|
||||
from .rule_models import SeverityLevel # Assuming rule_models.py is in the same directory
|
||||
from ..api_caller.caller import APIRequest # Adjust path as necessary
|
||||
|
||||
class TestDataGenerationStrategy(str, Enum):
|
||||
"""Defines how test data for an API request should be generated."""
|
||||
STATIC = "static" # Data is explicitly provided
|
||||
FROM_SCHEMA = "from_schema" # Data is generated based on OpenAPI/JSON schema
|
||||
FROM_PREVIOUS_STEP = "from_previous_step" # Data is extracted from a previous test step's output
|
||||
AI_GENERATED = "ai_generated" # Data is generated by an AI model
|
||||
|
||||
class TestDataGenerationConfig(BaseModel):
|
||||
"""Configuration for generating test data for an API request."""
|
||||
strategy: TestDataGenerationStrategy = TestDataGenerationStrategy.STATIC
|
||||
# For STATIC strategy:
|
||||
static_data: Optional[Dict[str, Any]] = Field(default=None, description="Explicit data for the request (e.g., body, params).")
|
||||
# For FROM_SCHEMA strategy:
|
||||
schema_source: Optional[str] = Field(default=None, description="Identifier for the schema to be used (e.g., operationId, or path to schema file).")
|
||||
# For FROM_PREVIOUS_STEP strategy:
|
||||
source_step_id: Optional[str] = Field(default=None, description="The ID of the previous TestStep whose output will be used.")
|
||||
extraction_rules: Optional[Dict[str, str]] = Field(default_None, description="Rules to extract data (e.g., JSONPath expressions like {'user_id': '$.body.id'}).")
|
||||
# For AI_GENERATED strategy:
|
||||
ai_prompt: Optional[str] = Field(default=None, description="Prompt to be used by an AI model to generate data.")
|
||||
ai_model_config: Optional[Dict[str, Any]] = Field(default_None, description="Configuration for the AI model (e.g., temperature, max_tokens).")
|
||||
|
||||
class APIRequestContext(BaseModel):
|
||||
"""
|
||||
Defines how to construct the APIRequest for a TestStep, potentially using
|
||||
data from previous steps or other dynamic sources.
|
||||
"""
|
||||
method: str = Field(description="HTTP method (GET, POST, PUT, DELETE, etc.).")
|
||||
url_template: str = Field(description="URL template, can contain placeholders like {base_url} or {resource_id}.")
|
||||
path_params_source: Optional[Dict[str, TestDataGenerationConfig]] = Field(default_None, description="How to generate/get path parameters.")
|
||||
query_params_source: Optional[Dict[str, TestDataGenerationConfig]] = Field(default_None, description="How to generate/get query parameters.")
|
||||
headers_source: Optional[Dict[str, TestDataGenerationConfig]] = Field(default_None, description="How to generate/get request headers. Default headers can be added by the orchestrator.")
|
||||
body_source: Optional[TestDataGenerationConfig] = Field(default_None, description="How to generate/get the request body.")
|
||||
|
||||
# Stores the actual APIRequest object after context resolution
|
||||
# This field will be populated by the TestOrchestrator before execution.
|
||||
resolved_request: Optional[APIRequest] = Field(default=None, exclude=True)
|
||||
|
||||
|
||||
class TestStep(BaseModel):
|
||||
"""
|
||||
Represents a single step within a TestCase, typically involving one API call
|
||||
and a set of rules to be validated against its request/response.
|
||||
"""
|
||||
step_id: str = Field(description="Unique identifier for this test step within the TestCase.")
|
||||
description: Optional[str] = Field(default=None, description="Description of what this test step does.")
|
||||
api_request_context: APIRequestContext = Field(description="Context to build the API request for this step.")
|
||||
|
||||
# Rules to be applied at different phases of this step
|
||||
# Applied after the request is prepared but before it's sent.
|
||||
# Useful for validating the constructed request itself.
|
||||
request_preparation_rule_ids: List[str] = Field(default_factory=list, description="Rules to apply to the APIRequest before sending.")
|
||||
|
||||
# Applied after the API response is received. This is the most common place for validation rules.
|
||||
response_validation_rule_ids: List[str] = Field(default_factory=list, description="Rules to apply to the APIResponse.")
|
||||
|
||||
# Applied after response validation, يمكن استخدامه لتنظيف البيانات أو إجراءات ما بعد الاختبار
|
||||
post_validation_rule_ids: List[str] = Field(default_factory=list, description="Rules to apply after response validation (e.g., cleanup).")
|
||||
|
||||
# Configuration for expected outcomes, more specific than just rule pass/fail
|
||||
expected_status_code: Optional[int] = Field(default=None, description="Expected HTTP status code.")
|
||||
# Allows defining assertions on the response body using something like JSONPath
|
||||
# e.g., {"$.data.status": "completed", "$.errors": null}
|
||||
response_body_assertions: Optional[Dict[str, Any]] = Field(default_None, description="Assertions to make on the response body (e.g., using JSONPath).")
|
||||
|
||||
# Execution control
|
||||
skip: bool = Field(default=False, description="If True, this test step will be skipped.")
|
||||
# Allows storing output from this step to be used by subsequent steps
|
||||
# e.g., {"created_user_id": "$.response.body.id"}
|
||||
outputs_to_extract: Optional[Dict[str, str]] = Field(default_None, description="JSONPath expressions to extract data from response to be used in later steps.")
|
||||
|
||||
|
||||
class TestCaseExecutionMode(str, Enum):
|
||||
SEQUENTIAL = "sequential" # Steps are executed one after another
|
||||
PARALLEL = "parallel" # Steps (if independent) can be executed in parallel
|
||||
|
||||
class TestCase(BaseModel):
|
||||
"""
|
||||
Defines a test case, which consists of one or more test steps,
|
||||
metadata, and configurations for execution.
|
||||
"""
|
||||
id: str = Field(description="Unique identifier for the test case.")
|
||||
name: str = Field(description="Name of the test case.")
|
||||
description: Optional[str] = Field(default=None, description="Detailed description of the test case.")
|
||||
tags: List[str] = Field(default_factory=list, description="Tags for categorizing and filtering test cases.")
|
||||
severity: SeverityLevel = Field(default=SeverityLevel.INFO, description="Severity of the test case if it fails.")
|
||||
author: Optional[str] = Field(default=None, description="Author of the test case.")
|
||||
creation_date: Optional[str] = Field(default=None, description="Date when the test case was created (ISO format).") # Consider using datetime
|
||||
version: str = Field(default="1.0.0", description="Version of the test case.")
|
||||
|
||||
# Test Steps
|
||||
steps: List[TestStep] = Field(description="A list of test steps to be executed for this test case.")
|
||||
execution_mode: TestCaseExecutionMode = Field(default=TestCaseExecutionMode.SEQUENTIAL, description="How the steps in this test case should be executed.")
|
||||
|
||||
# Data that can be shared across steps within this test case.
|
||||
# Steps can read from and write to this context.
|
||||
shared_context: Dict[str, Any] = Field(default_factory=dict, description="Data context televisão across steps.")
|
||||
|
||||
# Overall expected outcome for the test case (can be high-level)
|
||||
expected_overall_result: Optional[str] = Field(default=None, description="A high-level description of the expected outcome for the entire test case.")
|
||||
|
||||
# For iterative/load testing
|
||||
execution_count: int = Field(default=1, ge=1, description="Number of times this test case should be executed.")
|
||||
# If > 1, defines delay between iterations in seconds.
|
||||
delay_between_iterations_sec: float = Field(default=0, ge=0)
|
||||
|
||||
|
||||
class TestSuite(BaseModel):
|
||||
"""
|
||||
A collection of TestCases, possibly with a shared configuration or setup/teardown logic.
|
||||
"""
|
||||
id: str = Field(description="Unique identifier for the test suite.")
|
||||
name: str = Field(description="Name of the test suite.")
|
||||
description: Optional[str] = Field(default=None, description="Description of the test suite.")
|
||||
tags: List[str] = Field(default_factory=list, description="Tags for categorizing and filtering test suites.")
|
||||
|
||||
# List of TestCase IDs or full TestCase objects. Using IDs allows for referencing.
|
||||
# For simplicity in this initial design, let's assume full TestCase objects.
|
||||
# Later, we could support referencing TestCase definitions stored elsewhere.
|
||||
test_cases: List[TestCase] = Field(default_factory=list, description="List of test cases included in this suite.")
|
||||
|
||||
# Global parameters or configurations that can apply to all TestCases in the suite
|
||||
# These could override individual TestCase settings or provide defaults.
|
||||
global_parameters: Optional[Dict[str, Any]] = Field(default_None, description="Parameters applicable to all test cases in the suite.")
|
||||
|
||||
# Setup: API calls or actions to perform before running any TestCase in the suite.
|
||||
# Similar structure to TestStep but without specific rule validations, more for setup.
|
||||
setup_steps: Optional[List[TestStep]] = Field(default_None, description="Steps to execute before running test cases in the suite.")
|
||||
|
||||
# Teardown: API calls or actions to perform after all TestCases in the suite have run.
|
||||
teardown_steps: Optional[List[TestStep]] = Field(default_None, description="Steps to execute after running test cases in the suite.")
|
||||
|
||||
# Execution control for the suite
|
||||
# e.g., run all, run tagged, run failed from previous
|
||||
execution_strategy: Optional[str] = Field(default=None, description="Strategy for executing test cases within the suite.")
|
||||
@@ -0,0 +1,531 @@
|
||||
"""规则执行引擎
|
||||
|
||||
该模块负责执行不同类型的规则,包括Python代码规则、断言规则等,
|
||||
支持在API测试的不同生命周期阶段(请求准备、执行、响应验证等)执行规则。
|
||||
"""
|
||||
|
||||
import logging
|
||||
import importlib
|
||||
import inspect
|
||||
import time
|
||||
import threading
|
||||
from typing import Dict, List, Any, Optional, Union, Callable
|
||||
|
||||
from ..models.rule_models import (
|
||||
AnyRule, BaseRule, RuleCategory, TargetType, RuleLifecycle, RuleScope,
|
||||
PythonCodeRule, BusinessAssertionTemplate, PerformanceRule, SecurityRule,
|
||||
RESTfulDesignRule, ErrorHandlingRule
|
||||
)
|
||||
from ..api_caller.caller import APIRequest, APIResponse
|
||||
from ..rule_repository.repository import RuleRepository
|
||||
|
||||
class RuleExecutionError(Exception):
|
||||
"""规则执行过程中发生的错误"""
|
||||
pass
|
||||
|
||||
class RuleExecutionResult:
|
||||
"""规则执行结果"""
|
||||
|
||||
def __init__(self,
|
||||
rule: BaseRule,
|
||||
is_valid: bool,
|
||||
message: str = "",
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
error: Optional[Exception] = None):
|
||||
"""
|
||||
初始化规则执行结果
|
||||
|
||||
Args:
|
||||
rule: 执行的规则
|
||||
is_valid: 验证是否通过
|
||||
message: 执行结果消息
|
||||
details: 详细信息
|
||||
error: 执行过程中发生的异常(如果有)
|
||||
"""
|
||||
self.rule = rule
|
||||
self.rule_id = rule.id
|
||||
self.rule_name = rule.name
|
||||
self.rule_category = rule.category
|
||||
self.is_valid = is_valid
|
||||
self.message = message
|
||||
self.details = details or {}
|
||||
self.error = error
|
||||
|
||||
def __bool__(self):
|
||||
"""允许直接使用结果对象作为布尔值,表示验证是否通过"""
|
||||
return self.is_valid
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""将结果转换为字典"""
|
||||
return {
|
||||
'rule_id': self.rule_id,
|
||||
'rule_name': self.rule_name,
|
||||
'rule_category': self.rule_category.value,
|
||||
'is_valid': self.is_valid,
|
||||
'message': self.message,
|
||||
'details': self.details,
|
||||
'error': str(self.error) if self.error else None
|
||||
}
|
||||
|
||||
class RuleExecutor:
|
||||
"""规则执行引擎"""
|
||||
|
||||
def __init__(self, rule_repository: RuleRepository):
|
||||
"""
|
||||
初始化规则执行引擎
|
||||
|
||||
Args:
|
||||
rule_repository: 规则库实例
|
||||
"""
|
||||
self.rule_repository = rule_repository
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def execute_rule(self, rule: BaseRule, context: Dict[str, Any]) -> RuleExecutionResult:
|
||||
"""
|
||||
执行单个规则
|
||||
|
||||
Args:
|
||||
rule: 要执行的规则
|
||||
context: 执行上下文,包含API请求、响应等信息
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
"""
|
||||
if not rule.is_enabled:
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=True,
|
||||
message=f"规则 {rule.id} 已禁用,跳过执行"
|
||||
)
|
||||
|
||||
try:
|
||||
# 根据规则类型选择适当的执行方法
|
||||
if rule.category == RuleCategory.PYTHON_CODE or hasattr(rule, 'code') and rule.code:
|
||||
return self._execute_python_code_rule(rule, context)
|
||||
elif rule.category == RuleCategory.BUSINESS_LOGIC:
|
||||
return self._execute_business_assertion_rule(rule, context)
|
||||
elif rule.category == RuleCategory.PERFORMANCE:
|
||||
return self._execute_performance_rule(rule, context)
|
||||
elif rule.category == RuleCategory.SECURITY:
|
||||
return self._execute_security_rule(rule, context)
|
||||
elif rule.category == RuleCategory.API_DESIGN:
|
||||
return self._execute_api_design_rule(rule, context)
|
||||
elif rule.category == RuleCategory.ERROR_HANDLING:
|
||||
return self._execute_error_handling_rule(rule, context)
|
||||
else:
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message=f"不支持的规则类型: {rule.category}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"执行规则 {rule.id} 失败: {e}", exc_info=True)
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message=f"规则执行失败: {e}",
|
||||
error=e
|
||||
)
|
||||
|
||||
def _execute_python_code_rule(self, rule: BaseRule, context: Dict[str, Any]) -> RuleExecutionResult:
|
||||
"""执行Python代码规则"""
|
||||
# 获取规则中的Python代码
|
||||
code = getattr(rule, 'code', None)
|
||||
if not code:
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message="规则未提供Python代码"
|
||||
)
|
||||
|
||||
# 准备执行环境
|
||||
namespace = {'__builtins__': __builtins__}
|
||||
namespace.update(context)
|
||||
|
||||
try:
|
||||
# 编译并执行代码
|
||||
compiled_code = compile(code, f"<rule:{rule.id}>", 'exec')
|
||||
exec(compiled_code, namespace)
|
||||
|
||||
# 查找并执行验证函数
|
||||
entry_function = 'validate'
|
||||
if hasattr(rule, 'entry_function') and rule.entry_function:
|
||||
entry_function = rule.entry_function
|
||||
|
||||
if entry_function not in namespace:
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message=f"找不到入口函数 '{entry_function}'"
|
||||
)
|
||||
|
||||
validate_func = namespace[entry_function]
|
||||
if not callable(validate_func):
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message=f"'{entry_function}' 不是可调用的函数"
|
||||
)
|
||||
|
||||
# 调用验证函数
|
||||
timeout = getattr(rule, 'timeout', 5) # 默认5秒超时
|
||||
result = self._execute_with_timeout(validate_func, (context,), {}, timeout)
|
||||
|
||||
# 处理执行结果
|
||||
if isinstance(result, dict):
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=bool(result.get('is_valid', False)),
|
||||
message=result.get('message', ''),
|
||||
details=result.get('details', {})
|
||||
)
|
||||
else:
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=bool(result),
|
||||
message=str(result) if result is not True else "验证通过"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"执行Python代码规则 {rule.id} 失败: {e}", exc_info=True)
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message=f"执行代码失败: {e}",
|
||||
error=e
|
||||
)
|
||||
|
||||
def _execute_with_timeout(self, func: Callable, args: tuple, kwargs: Dict[str, Any], timeout: int) -> Any:
|
||||
"""使用超时执行函数"""
|
||||
result = [None]
|
||||
exception = [None]
|
||||
|
||||
def target():
|
||||
try:
|
||||
result[0] = func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
exception[0] = e
|
||||
|
||||
thread = threading.Thread(target=target)
|
||||
thread.daemon = True
|
||||
|
||||
thread.start()
|
||||
thread.join(timeout)
|
||||
|
||||
if thread.is_alive():
|
||||
raise RuleExecutionError(f"规则执行超时(超过{timeout}秒)")
|
||||
|
||||
if exception[0]:
|
||||
raise exception[0]
|
||||
|
||||
return result[0]
|
||||
|
||||
def _execute_business_assertion_rule(self, rule: BusinessAssertionTemplate, context: Dict[str, Any]) -> RuleExecutionResult:
|
||||
"""执行业务断言规则"""
|
||||
# 验证是否提供了所有必需的参数
|
||||
if rule.expected_parameters:
|
||||
missing_params = [p for p in rule.expected_parameters if p not in context]
|
||||
if missing_params:
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message=f"缺少必需的参数: {', '.join(missing_params)}"
|
||||
)
|
||||
|
||||
if rule.template_language == "python_expression":
|
||||
try:
|
||||
# 使用eval执行Python表达式
|
||||
result = eval(rule.template_expression, {'__builtins__': __builtins__}, context)
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=bool(result),
|
||||
message="断言通过" if result else "断言失败"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"执行Python表达式断言失败: {e}", exc_info=True)
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message=f"表达式执行失败: {e}",
|
||||
error=e
|
||||
)
|
||||
else:
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message=f"不支持的模板语言: {rule.template_language}"
|
||||
)
|
||||
|
||||
def _execute_performance_rule(self, rule: PerformanceRule, context: Dict[str, Any]) -> RuleExecutionResult:
|
||||
"""执行性能规则"""
|
||||
response = context.get('api_response')
|
||||
if not response or not isinstance(response, APIResponse):
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message="缺少有效的API响应对象"
|
||||
)
|
||||
|
||||
# 获取响应时间(毫秒)
|
||||
elapsed_time = response.elapsed_time * 1000 # 转换为毫秒
|
||||
|
||||
# 检查是否超过阈值
|
||||
if elapsed_time > rule.threshold:
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message=f"响应时间({elapsed_time:.2f}ms)超过阈值({rule.threshold}{rule.unit})",
|
||||
details={
|
||||
'actual_time': elapsed_time,
|
||||
'threshold': rule.threshold,
|
||||
'unit': rule.unit
|
||||
}
|
||||
)
|
||||
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=True,
|
||||
message=f"响应时间({elapsed_time:.2f}ms)在阈值范围内",
|
||||
details={
|
||||
'actual_time': elapsed_time,
|
||||
'threshold': rule.threshold,
|
||||
'unit': rule.unit
|
||||
}
|
||||
)
|
||||
|
||||
def _execute_security_rule(self, rule: SecurityRule, context: Dict[str, Any]) -> RuleExecutionResult:
|
||||
"""执行安全规则"""
|
||||
if rule.check_type == "transport_security":
|
||||
request = context.get('api_request')
|
||||
if not request or not isinstance(request, APIRequest):
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message="缺少有效的API请求对象"
|
||||
)
|
||||
|
||||
url = str(request.url)
|
||||
|
||||
# 检查URL是否使用HTTPS
|
||||
if not url.startswith('https://'):
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message="API请求必须使用HTTPS协议",
|
||||
details={
|
||||
'current_url': url,
|
||||
'expected_protocol': 'https'
|
||||
}
|
||||
)
|
||||
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=True,
|
||||
message="API请求使用了HTTPS协议",
|
||||
details={
|
||||
'url': url
|
||||
}
|
||||
)
|
||||
else:
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message=f"不支持的安全检查类型: {rule.check_type}"
|
||||
)
|
||||
|
||||
def _execute_api_design_rule(self, rule: RESTfulDesignRule, context: Dict[str, Any]) -> RuleExecutionResult:
|
||||
"""执行API设计规则"""
|
||||
import re
|
||||
|
||||
request = context.get('api_request')
|
||||
if not request or not isinstance(request, APIRequest):
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message="缺少有效的API请求对象"
|
||||
)
|
||||
|
||||
url = str(request.url)
|
||||
|
||||
# 解析URL,获取路径部分
|
||||
from urllib.parse import urlparse
|
||||
parsed_url = urlparse(url)
|
||||
path = parsed_url.path
|
||||
|
||||
# 使用正则表达式验证路径
|
||||
if rule.pattern and not re.match(rule.pattern, path):
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message=f"API路径不符合{rule.design_aspect}规范",
|
||||
details={
|
||||
'current_path': path,
|
||||
'expected_pattern': rule.pattern
|
||||
}
|
||||
)
|
||||
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=True,
|
||||
message=f"API路径符合{rule.design_aspect}规范",
|
||||
details={
|
||||
'path': path
|
||||
}
|
||||
)
|
||||
|
||||
def _execute_error_handling_rule(self, rule: ErrorHandlingRule, context: Dict[str, Any]) -> RuleExecutionResult:
|
||||
"""执行错误处理规则"""
|
||||
response = context.get('api_response')
|
||||
if not response or not isinstance(response, APIResponse):
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message="缺少有效的API响应对象"
|
||||
)
|
||||
|
||||
# 只验证4xx和5xx状态码
|
||||
if response.status_code < 400:
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=True,
|
||||
message="非错误响应,跳过验证",
|
||||
details={
|
||||
'status_code': response.status_code
|
||||
}
|
||||
)
|
||||
|
||||
# 验证状态码是否匹配
|
||||
if rule.expected_status != -1 and response.status_code != rule.expected_status:
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message=f"响应状态码({response.status_code})与预期({rule.expected_status})不符",
|
||||
details={
|
||||
'actual_status': response.status_code,
|
||||
'expected_status': rule.expected_status
|
||||
}
|
||||
)
|
||||
|
||||
# 验证JSON响应
|
||||
if not response.json_content:
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message="错误响应不是有效的JSON格式",
|
||||
details={
|
||||
'status_code': response.status_code,
|
||||
'content_type': response.headers.get('Content-Type', '未知')
|
||||
}
|
||||
)
|
||||
|
||||
# 检查错误码
|
||||
if rule.error_code != "*" and str(response.json_content.get('code', '')) != rule.error_code:
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message=f"错误码({response.json_content.get('code')})与预期({rule.error_code})不符",
|
||||
details={
|
||||
'actual_code': response.json_content.get('code'),
|
||||
'expected_code': rule.error_code
|
||||
}
|
||||
)
|
||||
|
||||
# 验证错误消息
|
||||
if rule.expected_message and rule.expected_message not in str(response.json_content.get('message', '')):
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=False,
|
||||
message="错误消息与预期不符",
|
||||
details={
|
||||
'actual_message': response.json_content.get('message'),
|
||||
'expected_message': rule.expected_message
|
||||
}
|
||||
)
|
||||
|
||||
return RuleExecutionResult(
|
||||
rule=rule,
|
||||
is_valid=True,
|
||||
message="错误响应符合预期",
|
||||
details={
|
||||
'status_code': response.status_code,
|
||||
'error_code': response.json_content.get('code'),
|
||||
'error_message': response.json_content.get('message')
|
||||
}
|
||||
)
|
||||
|
||||
def execute_rules_for_lifecycle(self, lifecycle: RuleLifecycle, context: Dict[str, Any]) -> List[RuleExecutionResult]:
|
||||
"""
|
||||
执行特定生命周期阶段的所有规则
|
||||
|
||||
Args:
|
||||
lifecycle: 生命周期阶段
|
||||
context: 执行上下文
|
||||
|
||||
Returns:
|
||||
执行结果列表
|
||||
"""
|
||||
# 获取适用于该生命周期阶段的所有规则
|
||||
rules = self.rule_repository.get_rules_by_lifecycle(lifecycle)
|
||||
|
||||
# 执行规则
|
||||
results = []
|
||||
for rule in rules:
|
||||
result = self.execute_rule(rule, context)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
def execute_rules_for_target(self, target_type: TargetType, target_id: str, context: Dict[str, Any]) -> List[RuleExecutionResult]:
|
||||
"""
|
||||
执行特定目标的所有规则
|
||||
|
||||
Args:
|
||||
target_type: 目标类型
|
||||
target_id: 目标ID
|
||||
context: 执行上下文
|
||||
|
||||
Returns:
|
||||
执行结果列表
|
||||
"""
|
||||
# 获取适用于该目标的所有规则
|
||||
rules = self.rule_repository.get_rules_for_target(target_type, target_id)
|
||||
|
||||
# 执行规则
|
||||
results = []
|
||||
for rule in rules:
|
||||
result = self.execute_rule(rule, context)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
def execute_specific_rules(self, rules: List[AnyRule], context: Dict[str, Any], lifecycle_phase: Optional[RuleLifecycle] = None) -> List[RuleExecutionResult]:
|
||||
"""
|
||||
执行一个明确指定的规则列表。
|
||||
|
||||
Args:
|
||||
rules: 要执行的规则对象的列表。
|
||||
context: 执行上下文,包含API请求、响应等信息。
|
||||
lifecycle_phase: 可选的,名义上的生命周期阶段,可能用于上下文或某些规则的内部逻辑。
|
||||
注意:此参数目前主要用于信息传递,核心执行逻辑在 execute_rule 中
|
||||
并不直接依赖它来选择执行路径。
|
||||
|
||||
Returns:
|
||||
一个包含每个规则执行结果的列表。
|
||||
"""
|
||||
results = []
|
||||
if not rules:
|
||||
self.logger.info("execute_specific_rules_called_with_no_rules")
|
||||
return results
|
||||
|
||||
# 如果需要,可以将 lifecycle_phase 添加到 context 中传递给每个规则
|
||||
# updated_context = context.copy()
|
||||
# if lifecycle_phase:
|
||||
# updated_context['current_lifecycle_phase'] = lifecycle_phase
|
||||
|
||||
for rule in rules:
|
||||
# 使用现有的 execute_rule 方法执行单个规则
|
||||
# result = self.execute_rule(rule, updated_context if lifecycle_phase else context)
|
||||
result = self.execute_rule(rule, context) # 简化:暂时不修改context传递
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Base adapter interface for rule storage."""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
|
||||
from ...models.rule_models import AnyRule, RuleQuery, BaseRule
|
||||
|
||||
|
||||
class BaseRuleStorageAdapter(ABC):
|
||||
"""Base class for rule storage adapters."""
|
||||
|
||||
@abstractmethod
|
||||
def initialize(self) -> None:
|
||||
"""初始化适配器(例如,连接到数据库,验证文件路径等)。"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def load_rule_by_id(self, rule_id: str, version: Optional[str] = None) -> Optional[AnyRule]:
|
||||
"""
|
||||
按ID加载单个规则。
|
||||
|
||||
Args:
|
||||
rule_id: 规则的唯一标识符。
|
||||
version: 可选的版本标识符。如果未提供,则使用配置的默认版本策略。
|
||||
|
||||
Returns:
|
||||
找到的规则对象,或者如果未找到则返回None。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def query_rules(self, query: RuleQuery) -> List[AnyRule]:
|
||||
"""
|
||||
根据查询条件返回匹配的规则列表。
|
||||
|
||||
Args:
|
||||
query: 规则查询条件。
|
||||
|
||||
Returns:
|
||||
匹配规则的列表。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_rule(self, rule: BaseRule) -> bool:
|
||||
"""
|
||||
保存单个规则。
|
||||
|
||||
Args:
|
||||
rule: 要保存的规则对象。
|
||||
|
||||
Returns:
|
||||
操作是否成功。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_rule(self, rule_id: str, version: Optional[str] = None) -> bool:
|
||||
"""
|
||||
删除单个规则。
|
||||
|
||||
Args:
|
||||
rule_id: 规则的唯一标识符。
|
||||
version: 可选的版本标识符。如果未提供,通常会删除所有版本。
|
||||
|
||||
Returns:
|
||||
操作是否成功。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def list_all_rule_ids(self) -> List[str]:
|
||||
"""
|
||||
列出存储中的所有规则ID。
|
||||
|
||||
Returns:
|
||||
规则ID的列表。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_rule_versions(self, rule_id: str) -> List[str]:
|
||||
"""
|
||||
获取特定规则ID的所有可用版本。
|
||||
|
||||
Args:
|
||||
rule_id: 规则的唯一标识符。
|
||||
|
||||
Returns:
|
||||
版本标识符的列表。
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,324 @@
|
||||
"""File system adapter for rule storage using JSON files."""
|
||||
import os
|
||||
import json
|
||||
import glob
|
||||
from typing import List, Dict, Optional, Any, Tuple, Type, Union, cast
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from .base_adapter import BaseRuleStorageAdapter
|
||||
from ...models.rule_models import (
|
||||
AnyRule, RuleQuery, BaseRule, RuleCategory, TargetType
|
||||
)
|
||||
from .rule_adapter_utils import parse_rule_data
|
||||
|
||||
class FilesystemAdapter(BaseRuleStorageAdapter):
|
||||
"""
|
||||
基于文件系统的规则存储适配器,使用JSON文件保存规则。
|
||||
|
||||
文件结构约定:
|
||||
规则文件将按以下方式组织:
|
||||
rules/
|
||||
json_schemas/
|
||||
rule_id1/
|
||||
1.0.0.json
|
||||
1.1.0.json
|
||||
rule_id2/
|
||||
1.0.0.json
|
||||
business_logic/
|
||||
rule_id3/
|
||||
1.0.0.json
|
||||
...
|
||||
"""
|
||||
|
||||
def __init__(self, base_path: str = "./rules", file_pattern: str = "*.json"):
|
||||
"""
|
||||
初始化适配器。
|
||||
|
||||
Args:
|
||||
base_path: 存储规则的基本目录路径。
|
||||
file_pattern: 匹配规则文件的glob模式。
|
||||
"""
|
||||
self.base_path = os.path.abspath(base_path)
|
||||
self.file_pattern = file_pattern
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def initialize(self) -> None:
|
||||
"""确保基本目录存在,并验证其可访问性。"""
|
||||
if not os.path.exists(self.base_path):
|
||||
try:
|
||||
os.makedirs(self.base_path)
|
||||
self.logger.info(f"Created rules directory at {self.base_path}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to create rules directory at {self.base_path}: {e}")
|
||||
raise ValueError(f"Rules directory {self.base_path} does not exist and could not be created")
|
||||
|
||||
if not os.access(self.base_path, os.R_OK | os.W_OK):
|
||||
self.logger.error(f"Rules directory {self.base_path} is not readable and writable")
|
||||
raise ValueError(f"Rules directory {self.base_path} is not readable and writable")
|
||||
|
||||
# 确保每个规则类别的子目录存在
|
||||
for category in RuleCategory:
|
||||
category_dir = self._get_category_dir(category)
|
||||
if not os.path.exists(category_dir):
|
||||
try:
|
||||
os.makedirs(category_dir)
|
||||
self.logger.debug(f"Created category directory at {category_dir}")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to create category directory at {category_dir}: {e}")
|
||||
|
||||
def _get_category_dir(self, category: RuleCategory) -> str:
|
||||
"""获取给定规则类别的目录路径。"""
|
||||
return os.path.join(self.base_path, category.value.lower())
|
||||
|
||||
def _get_rule_dir(self, rule_id: str, category: Optional[RuleCategory] = None) -> str:
|
||||
"""
|
||||
获取给定规则ID的目录路径。
|
||||
如果指定了类别,则直接使用该类别的目录;否则尝试在所有类别中查找。
|
||||
"""
|
||||
if category:
|
||||
return os.path.join(self._get_category_dir(category), rule_id)
|
||||
|
||||
# 如果未指定类别,检查所有类别目录
|
||||
for cat in RuleCategory:
|
||||
rule_dir = os.path.join(self._get_category_dir(cat), rule_id)
|
||||
if os.path.exists(rule_dir):
|
||||
return rule_dir
|
||||
|
||||
# 如果未找到任何匹配项,默认返回通用类别目录
|
||||
return os.path.join(self._get_category_dir(RuleCategory.GENERIC), rule_id)
|
||||
|
||||
def _get_rule_file_path(self, rule_id: str, version: str, category: Optional[RuleCategory] = None) -> str:
|
||||
"""获取给定规则ID和版本的文件路径。"""
|
||||
rule_dir = self._get_rule_dir(rule_id, category)
|
||||
return os.path.join(rule_dir, f"{version}.json")
|
||||
|
||||
def _get_rule_from_file(self, file_path: str) -> Optional[AnyRule]:
|
||||
"""从文件加载规则。"""
|
||||
if not os.path.exists(file_path):
|
||||
self.logger.debug(f"Rule file {file_path} does not exist")
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
raw_data = json.load(f)
|
||||
|
||||
# 使用工具函数解析规则数据
|
||||
return parse_rule_data(raw_data)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.error(f"Failed to parse JSON from {file_path}: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading rule from {file_path}: {e}")
|
||||
return None
|
||||
|
||||
def _save_rule_to_file(self, rule: BaseRule, file_path: str) -> bool:
|
||||
"""将规则保存到文件。"""
|
||||
try:
|
||||
# 确保目录存在
|
||||
dir_path = os.path.dirname(file_path)
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
|
||||
# 序列化规则为JSON并写入文件
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(rule.model_dump(), f, indent=2, ensure_ascii=False)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to save rule to {file_path}: {e}")
|
||||
return False
|
||||
|
||||
def _get_latest_version(self, rule_id: str, category: Optional[RuleCategory] = None) -> Optional[str]:
|
||||
"""获取给定规则ID的最新版本。"""
|
||||
versions = self.get_rule_versions(rule_id, category)
|
||||
if not versions:
|
||||
return None
|
||||
|
||||
# 简单地按字符串排序,假设版本格式是类似于"1.0.0"的语义化版本
|
||||
# 如果需要更复杂的版本比较,可以使用packaging.version
|
||||
versions.sort()
|
||||
return versions[-1]
|
||||
|
||||
def get_rule_versions(self, rule_id: str, category: Optional[RuleCategory] = None) -> List[str]:
|
||||
"""获取给定规则ID的所有版本。"""
|
||||
rule_dir = self._get_rule_dir(rule_id, category)
|
||||
if not os.path.exists(rule_dir):
|
||||
return []
|
||||
|
||||
# 查找目录中所有匹配的JSON文件,并提取版本号(文件名)
|
||||
pattern = os.path.join(rule_dir, self.file_pattern)
|
||||
version_files = glob.glob(pattern)
|
||||
|
||||
versions = []
|
||||
for vf in version_files:
|
||||
version = os.path.splitext(os.path.basename(vf))[0] # 移除.json扩展名
|
||||
versions.append(version)
|
||||
|
||||
return versions
|
||||
|
||||
def load_rule_by_id(self, rule_id: str, version: Optional[str] = None) -> Optional[AnyRule]:
|
||||
"""
|
||||
按ID加载单个规则。
|
||||
|
||||
Args:
|
||||
rule_id: 规则的唯一标识符。
|
||||
version: 可选的版本标识符。如果未提供,则加载最新版本。
|
||||
|
||||
Returns:
|
||||
找到的规则对象,或者如果未找到则返回None。
|
||||
"""
|
||||
if not version:
|
||||
version = self._get_latest_version(rule_id)
|
||||
if not version:
|
||||
self.logger.debug(f"No versions found for rule ID {rule_id}")
|
||||
return None
|
||||
|
||||
file_path = self._get_rule_file_path(rule_id, version)
|
||||
return self._get_rule_from_file(file_path)
|
||||
|
||||
def query_rules(self, query: RuleQuery) -> List[AnyRule]:
|
||||
"""
|
||||
根据查询条件查询规则。
|
||||
|
||||
Args:
|
||||
query: 包含筛选条件的查询对象。
|
||||
|
||||
Returns:
|
||||
匹配查询条件的规则列表。
|
||||
"""
|
||||
results = []
|
||||
|
||||
# 如果指定了规则ID,直接加载该规则
|
||||
if query.rule_id:
|
||||
rule = self.load_rule_by_id(query.rule_id, query.version if query.version != "latest" else None)
|
||||
if rule and self._rule_matches_query(rule, query):
|
||||
results.append(rule)
|
||||
return results
|
||||
|
||||
# 否则,根据查询条件扫描规则文件
|
||||
categories_to_search = [query.category] if query.category else list(RuleCategory)
|
||||
|
||||
for category in categories_to_search:
|
||||
category_dir = self._get_category_dir(category)
|
||||
if not os.path.exists(category_dir):
|
||||
continue
|
||||
|
||||
# 获取该类别下的所有规则ID(子目录)
|
||||
rule_dirs = [d for d in os.listdir(category_dir)
|
||||
if os.path.isdir(os.path.join(category_dir, d))]
|
||||
|
||||
for rule_id in rule_dirs:
|
||||
# 对于每个规则ID,加载指定版本或最新版本
|
||||
if query.version and query.version != "latest":
|
||||
file_path = self._get_rule_file_path(rule_id, query.version, category)
|
||||
rule = self._get_rule_from_file(file_path)
|
||||
if rule and self._rule_matches_query(rule, query):
|
||||
results.append(rule)
|
||||
else:
|
||||
latest_version = self._get_latest_version(rule_id, category)
|
||||
if latest_version:
|
||||
file_path = self._get_rule_file_path(rule_id, latest_version, category)
|
||||
rule = self._get_rule_from_file(file_path)
|
||||
if rule and self._rule_matches_query(rule, query):
|
||||
results.append(rule)
|
||||
|
||||
return results
|
||||
|
||||
def _rule_matches_query(self, rule: BaseRule, query: RuleQuery) -> bool:
|
||||
"""检查规则是否匹配查询条件。"""
|
||||
# 检查是否启用
|
||||
if query.is_enabled is not None and rule.is_enabled != query.is_enabled:
|
||||
return False
|
||||
|
||||
# 检查目标类型
|
||||
if query.target_type and rule.target_type != query.target_type:
|
||||
return False
|
||||
|
||||
# 检查目标标识符
|
||||
if query.target_identifier and rule.target_identifier != query.target_identifier:
|
||||
return False
|
||||
|
||||
# 检查标签
|
||||
if query.tags:
|
||||
if not rule.tags:
|
||||
return False
|
||||
# 检查是否所有查询标签都在规则标签中
|
||||
if not all(tag in rule.tags for tag in query.tags):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def save_rule(self, rule: BaseRule) -> bool:
|
||||
"""
|
||||
保存规则到文件系统。
|
||||
|
||||
Args:
|
||||
rule: 要保存的规则对象。
|
||||
|
||||
Returns:
|
||||
操作是否成功。
|
||||
"""
|
||||
file_path = self._get_rule_file_path(rule.id, rule.version, rule.category)
|
||||
return self._save_rule_to_file(rule, file_path)
|
||||
|
||||
def delete_rule(self, rule_id: str, version: Optional[str] = None) -> bool:
|
||||
"""
|
||||
删除规则。
|
||||
|
||||
Args:
|
||||
rule_id: 规则的唯一标识符。
|
||||
version: 如果提供,仅删除该版本;否则删除所有版本。
|
||||
|
||||
Returns:
|
||||
操作是否成功。
|
||||
"""
|
||||
try:
|
||||
if version:
|
||||
# 删除特定版本
|
||||
file_path = self._get_rule_file_path(rule_id, version)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
self.logger.info(f"Deleted rule file: {file_path}")
|
||||
else:
|
||||
self.logger.warning(f"Rule file not found for deletion: {file_path}")
|
||||
return False
|
||||
else:
|
||||
# 删除所有版本(整个规则目录)
|
||||
rule_dir = self._get_rule_dir(rule_id)
|
||||
if os.path.exists(rule_dir):
|
||||
# 递归删除目录及其内容
|
||||
for root, dirs, files in os.walk(rule_dir, topdown=False):
|
||||
for file in files:
|
||||
os.remove(os.path.join(root, file))
|
||||
for dir in dirs:
|
||||
os.rmdir(os.path.join(root, dir))
|
||||
os.rmdir(rule_dir)
|
||||
self.logger.info(f"Deleted rule directory: {rule_dir}")
|
||||
else:
|
||||
self.logger.warning(f"Rule directory not found for deletion: {rule_dir}")
|
||||
return False
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error deleting rule {rule_id} (version={version}): {e}")
|
||||
return False
|
||||
|
||||
def list_all_rule_ids(self) -> List[str]:
|
||||
"""列出所有规则ID。"""
|
||||
all_rule_ids = set()
|
||||
|
||||
# 扫描所有类别目录
|
||||
for category in RuleCategory:
|
||||
category_dir = self._get_category_dir(category)
|
||||
if not os.path.exists(category_dir):
|
||||
continue
|
||||
|
||||
# 获取该类别下的所有规则ID(子目录)
|
||||
rule_dirs = [d for d in os.listdir(category_dir)
|
||||
if os.path.isdir(os.path.join(category_dir, d))]
|
||||
|
||||
all_rule_ids.update(rule_dirs)
|
||||
|
||||
return list(all_rule_ids)
|
||||
@@ -0,0 +1,60 @@
|
||||
"""规则适配器工具函数"""
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, Type
|
||||
|
||||
from ...models.rule_models import (
|
||||
AnyRule, BaseRule, RuleCategory,
|
||||
JSONSchemaDefinition, APILintingRuleset, BusinessAssertionTemplate, DataQualityRule,
|
||||
PythonCodeRule, PerformanceRule, SecurityRule, RESTfulDesignRule, ErrorHandlingRule
|
||||
)
|
||||
|
||||
# 规则类别到Pydantic模型类的映射
|
||||
RULE_CLASSES = {
|
||||
RuleCategory.JSON_SCHEMA: JSONSchemaDefinition,
|
||||
RuleCategory.API_LINTING: APILintingRuleset,
|
||||
RuleCategory.BUSINESS_LOGIC: BusinessAssertionTemplate,
|
||||
RuleCategory.DATA_QUALITY: DataQualityRule,
|
||||
RuleCategory.PYTHON_CODE: PythonCodeRule,
|
||||
RuleCategory.PERFORMANCE: PerformanceRule,
|
||||
RuleCategory.SECURITY: SecurityRule,
|
||||
RuleCategory.API_DESIGN: RESTfulDesignRule,
|
||||
RuleCategory.ERROR_HANDLING: ErrorHandlingRule,
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_rule_class_by_category(category: RuleCategory) -> Type[BaseRule]:
|
||||
"""根据规则类别获取对应的规则类"""
|
||||
return RULE_CLASSES.get(category, BaseRule)
|
||||
|
||||
def parse_rule_data(raw_data: Dict[str, Any]) -> Optional[AnyRule]:
|
||||
"""
|
||||
解析规则数据,返回对应类型的规则对象
|
||||
|
||||
Args:
|
||||
raw_data: 从文件加载的原始规则数据
|
||||
|
||||
Returns:
|
||||
规则对象,如果无法解析则返回None
|
||||
"""
|
||||
try:
|
||||
# 确保数据包含类别信息
|
||||
if 'category' not in raw_data:
|
||||
logger.warning("Rule data missing 'category' field")
|
||||
return None
|
||||
|
||||
# 尝试将字符串类别转换为枚举值
|
||||
try:
|
||||
category = RuleCategory(raw_data['category'])
|
||||
except ValueError:
|
||||
logger.warning(f"Rule data has invalid category: {raw_data['category']}")
|
||||
return None
|
||||
|
||||
# 获取对应的Pydantic模型类
|
||||
rule_class = get_rule_class_by_category(category)
|
||||
|
||||
# 使用Pydantic模型解析数据
|
||||
return rule_class(**raw_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing rule data: {e}")
|
||||
return None
|
||||
@@ -0,0 +1,314 @@
|
||||
"""
|
||||
Python代码规则执行器
|
||||
|
||||
负责安全地执行规则中包含的Python代码,确保代码在隔离的环境中运行,
|
||||
并对可执行的操作进行限制,以防恶意代码。
|
||||
"""
|
||||
|
||||
import ast
|
||||
import logging
|
||||
import importlib
|
||||
import inspect
|
||||
import time
|
||||
import threading
|
||||
import os
|
||||
from typing import Dict, Any, Optional, List, Callable, Tuple
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from ..models.rule_models import PythonCodeRule
|
||||
from ..models.rule_models import RuleCategory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CodeExecutionError(Exception):
|
||||
"""执行Python代码时发生的错误"""
|
||||
pass
|
||||
|
||||
class TimeoutError(CodeExecutionError):
|
||||
"""代码执行超时错误"""
|
||||
pass
|
||||
|
||||
class ImportError(CodeExecutionError):
|
||||
"""非法导入模块错误"""
|
||||
pass
|
||||
|
||||
class ValidationResult:
|
||||
"""Python代码验证的结果"""
|
||||
def __init__(self,
|
||||
is_valid: bool,
|
||||
message: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
exception: Optional[Exception] = None):
|
||||
self.is_valid = is_valid
|
||||
self.message = message
|
||||
self.details = details or {}
|
||||
self.exception = exception
|
||||
|
||||
def __bool__(self):
|
||||
return self.is_valid
|
||||
|
||||
class PythonRuleExecutor:
|
||||
"""
|
||||
Python代码规则执行器
|
||||
|
||||
负责安全地执行规则中的Python代码,并返回验证结果。
|
||||
"""
|
||||
|
||||
def __init__(self, rules_base_path: str = "./rules"):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.rules_base_path = os.path.abspath(rules_base_path)
|
||||
|
||||
def _load_code_from_file(self, rule: PythonCodeRule) -> str:
|
||||
"""
|
||||
从文件加载代码
|
||||
|
||||
Args:
|
||||
rule: Python代码规则对象,包含code_file属性
|
||||
|
||||
Returns:
|
||||
加载的代码内容
|
||||
|
||||
Raises:
|
||||
CodeExecutionError: 如果加载代码失败
|
||||
"""
|
||||
if not rule.code_file:
|
||||
raise CodeExecutionError("规则未指定code_file属性")
|
||||
|
||||
# 构建代码文件的绝对路径
|
||||
# 如果规则ID和版本都存在,则在python_code目录下查找
|
||||
if rule.id and rule.version:
|
||||
# 代码文件路径格式: rules/python_code/{rule_id}/{version}.py
|
||||
file_path = os.path.join(self.rules_base_path, "python_code", rule.id, f"{rule.version}.py")
|
||||
else:
|
||||
# 否则使用传入的相对路径
|
||||
file_path = os.path.join(self.rules_base_path, rule.code_file)
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.isfile(file_path):
|
||||
raise CodeExecutionError(f"代码文件不存在: {file_path}")
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
raise CodeExecutionError(f"读取代码文件失败: {e}")
|
||||
|
||||
def analyze_code(self, code: str) -> List[str]:
|
||||
"""
|
||||
分析代码,检查其中使用的导入模块
|
||||
|
||||
Args:
|
||||
code: 要分析的Python代码
|
||||
|
||||
Returns:
|
||||
代码中导入的模块列表
|
||||
|
||||
Raises:
|
||||
SyntaxError: 如果代码存在语法错误
|
||||
"""
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
except SyntaxError as e:
|
||||
raise CodeExecutionError(f"代码语法错误: {e}")
|
||||
|
||||
imports = []
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for name in node.names:
|
||||
imports.append(name.name)
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
imports.append(node.module)
|
||||
|
||||
return imports
|
||||
|
||||
def _execute_with_timeout(self,
|
||||
func: Callable,
|
||||
args: Tuple,
|
||||
kwargs: Dict[str, Any],
|
||||
timeout: int) -> Any:
|
||||
"""
|
||||
使用超时执行函数
|
||||
|
||||
Args:
|
||||
func: 要执行的函数
|
||||
args: 函数的位置参数
|
||||
kwargs: 函数的关键字参数
|
||||
timeout: 超时时间(秒)
|
||||
|
||||
Returns:
|
||||
函数的返回值
|
||||
|
||||
Raises:
|
||||
TimeoutError: 如果函数执行超过指定的超时时间
|
||||
"""
|
||||
result = [None]
|
||||
exception = [None]
|
||||
|
||||
def target():
|
||||
try:
|
||||
result[0] = func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
exception[0] = e
|
||||
|
||||
thread = threading.Thread(target=target)
|
||||
thread.daemon = True
|
||||
|
||||
thread.start()
|
||||
thread.join(timeout)
|
||||
|
||||
if thread.is_alive():
|
||||
# 超时
|
||||
raise TimeoutError(f"代码执行超时(超过{timeout}秒)")
|
||||
|
||||
if exception[0]:
|
||||
raise exception[0]
|
||||
|
||||
return result[0]
|
||||
|
||||
def execute_rule(self, rule: PythonCodeRule, context: Dict[str, Any]) -> ValidationResult:
|
||||
"""
|
||||
执行Python代码规则
|
||||
|
||||
Args:
|
||||
rule: Python代码规则定义
|
||||
context: 验证上下文(包含规则执行所需的参数和数据)
|
||||
|
||||
Returns:
|
||||
ValidationResult: 验证结果
|
||||
"""
|
||||
# 检查是否提供了所有必需的参数
|
||||
if rule.expected_parameters:
|
||||
missing_params = [p for p in rule.expected_parameters if p not in context]
|
||||
if missing_params:
|
||||
return ValidationResult(
|
||||
is_valid=False,
|
||||
message=f"缺少必需的参数: {', '.join(missing_params)}"
|
||||
)
|
||||
|
||||
# 获取代码内容
|
||||
try:
|
||||
if rule.code:
|
||||
code = rule.code
|
||||
elif rule.code_file:
|
||||
code = self._load_code_from_file(rule)
|
||||
else:
|
||||
return ValidationResult(
|
||||
is_valid=False,
|
||||
message="规则既没有code也没有code_file属性"
|
||||
)
|
||||
except CodeExecutionError as e:
|
||||
return ValidationResult(
|
||||
is_valid=False,
|
||||
message=str(e),
|
||||
exception=e
|
||||
)
|
||||
|
||||
# 分析代码中的导入
|
||||
try:
|
||||
imports = self.analyze_code(code)
|
||||
except CodeExecutionError as e:
|
||||
return ValidationResult(
|
||||
is_valid=False,
|
||||
message=str(e),
|
||||
exception=e
|
||||
)
|
||||
|
||||
# 检查导入是否被允许
|
||||
if imports and not rule.allow_imports:
|
||||
return ValidationResult(
|
||||
is_valid=False,
|
||||
message=f"规则不允许导入模块,但代码尝试导入: {', '.join(imports)}"
|
||||
)
|
||||
|
||||
# 如果允许导入,检查是否所有导入都在允许列表中
|
||||
if imports and rule.allow_imports and rule.allowed_modules:
|
||||
unauthorized_imports = [imp for imp in imports if imp not in rule.allowed_modules]
|
||||
if unauthorized_imports:
|
||||
return ValidationResult(
|
||||
is_valid=False,
|
||||
message=f"代码尝试导入未授权的模块: {', '.join(unauthorized_imports)}"
|
||||
)
|
||||
|
||||
# 准备执行环境
|
||||
# 创建一个隔离的命名空间
|
||||
namespace = {'__builtins__': __builtins__}
|
||||
|
||||
# 添加上下文变量
|
||||
namespace.update(context)
|
||||
|
||||
# 如果允许导入,预先导入允许的模块
|
||||
if rule.allow_imports and imports:
|
||||
for module_name in imports:
|
||||
if not rule.allowed_modules or module_name in rule.allowed_modules:
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
namespace[module_name] = module
|
||||
except Exception as e:
|
||||
return ValidationResult(
|
||||
is_valid=False,
|
||||
message=f"导入模块 '{module_name}' 失败: {e}",
|
||||
exception=e
|
||||
)
|
||||
|
||||
# 执行代码
|
||||
try:
|
||||
# 编译代码
|
||||
compiled_code = compile(code, f"<rule:{rule.id}>", 'exec')
|
||||
|
||||
# 执行代码
|
||||
self._execute_with_timeout(
|
||||
exec,
|
||||
(compiled_code, namespace),
|
||||
{},
|
||||
rule.timeout
|
||||
)
|
||||
|
||||
# 获取入口函数
|
||||
if rule.entry_function not in namespace:
|
||||
return ValidationResult(
|
||||
is_valid=False,
|
||||
message=f"代码未定义指定的入口函数 '{rule.entry_function}'"
|
||||
)
|
||||
|
||||
entry_func = namespace[rule.entry_function]
|
||||
if not callable(entry_func):
|
||||
return ValidationResult(
|
||||
is_valid=False,
|
||||
message=f"'{rule.entry_function}' 不是一个可调用的函数"
|
||||
)
|
||||
|
||||
# 调用入口函数
|
||||
result = self._execute_with_timeout(
|
||||
entry_func,
|
||||
tuple(),
|
||||
{},
|
||||
rule.timeout
|
||||
)
|
||||
|
||||
# 处理结果
|
||||
if isinstance(result, dict):
|
||||
# 如果函数返回一个字典,将它转换为ValidationResult
|
||||
return ValidationResult(
|
||||
is_valid=bool(result.get('is_valid', False)),
|
||||
message=result.get('message'),
|
||||
details=result.get('details', {})
|
||||
)
|
||||
else:
|
||||
# 如果返回其他类型,将其解释为布尔值
|
||||
return ValidationResult(
|
||||
is_valid=bool(result),
|
||||
message=str(result) if result is not True else "验证通过"
|
||||
)
|
||||
|
||||
except TimeoutError as e:
|
||||
return ValidationResult(
|
||||
is_valid=False,
|
||||
message=str(e),
|
||||
exception=e
|
||||
)
|
||||
except Exception as e:
|
||||
return ValidationResult(
|
||||
is_valid=False,
|
||||
message=f"执行代码时发生错误: {e}",
|
||||
exception=e
|
||||
)
|
||||
@@ -0,0 +1,366 @@
|
||||
"""规则库核心模块"""
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Type, Union, Any
|
||||
|
||||
from ..models.rule_models import AnyRule, BaseRule, RuleQuery, RuleCategory, TargetType, RuleLifecycle, RuleScope
|
||||
from ..models.config_models import RuleRepositoryConfig
|
||||
from .adapters.base_adapter import BaseRuleStorageAdapter
|
||||
from .adapters.filesystem_adapter import FilesystemAdapter
|
||||
from .yaml_adapter import YAMLAdapter
|
||||
# 未来可能添加的其他适配器
|
||||
# from .adapters.db_adapter import DatabaseAdapter
|
||||
# from .adapters.in_memory_adapter import InMemoryAdapter
|
||||
|
||||
class RuleRepository:
|
||||
"""
|
||||
规则库模块的核心类。
|
||||
负责通过合适的存储适配器管理和提供规则。
|
||||
"""
|
||||
|
||||
def __init__(self, config: RuleRepositoryConfig):
|
||||
"""
|
||||
初始化规则库。
|
||||
|
||||
Args:
|
||||
config: 规则库配置
|
||||
"""
|
||||
self.config = config
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# 创建适当的存储适配器
|
||||
self.adapters = self._create_adapters()
|
||||
|
||||
# 用于在内存中缓存规则 (如果启用了preload_rules)
|
||||
self.rule_cache: Dict[str, Dict[str, AnyRule]] = {} # {rule_id: {version: rule}}
|
||||
|
||||
# 初始化适配器
|
||||
for adapter in self.adapters:
|
||||
adapter.initialize()
|
||||
|
||||
# 如果配置了预加载规则,则加载所有规则到内存
|
||||
if self.config.preload_rules:
|
||||
self._preload_rules()
|
||||
|
||||
def _create_adapters(self) -> List[BaseRuleStorageAdapter]:
|
||||
"""根据配置创建适当的存储适配器。"""
|
||||
adapters = []
|
||||
storage_type = self.config.storage.type.lower()
|
||||
|
||||
if storage_type == "filesystem":
|
||||
# 添加JSON规则适配器
|
||||
adapters.append(FilesystemAdapter(
|
||||
base_path=self.config.storage.path or "./rules"
|
||||
))
|
||||
|
||||
# 添加YAML规则适配器
|
||||
adapters.append(YAMLAdapter(
|
||||
base_path=self.config.storage.path or "./rules"
|
||||
))
|
||||
|
||||
# 未来可能添加的其他适配器类型
|
||||
# elif storage_type == "database":
|
||||
# adapters.append(DatabaseAdapter(
|
||||
# connection_string=self.config.storage.connection_string
|
||||
# ))
|
||||
# elif storage_type == "in_memory":
|
||||
# adapters.append(InMemoryAdapter())
|
||||
else:
|
||||
raise ValueError(f"Unsupported rule storage type: {storage_type}")
|
||||
|
||||
return adapters
|
||||
|
||||
def _preload_rules(self) -> None:
|
||||
"""预加载所有规则到内存缓存。"""
|
||||
self.logger.info("Preloading rules from storage...")
|
||||
all_rule_ids = set()
|
||||
|
||||
# 从所有适配器收集规则ID
|
||||
for adapter in self.adapters:
|
||||
rule_ids = adapter.list_all_rule_ids()
|
||||
all_rule_ids.update(rule_ids)
|
||||
|
||||
loaded_count = 0
|
||||
|
||||
for rule_id in all_rule_ids:
|
||||
versions_by_adapter = []
|
||||
|
||||
# 从所有适配器收集规则版本
|
||||
for adapter in self.adapters:
|
||||
versions = adapter.get_rule_versions(rule_id)
|
||||
if versions:
|
||||
versions_by_adapter.append((adapter, versions))
|
||||
|
||||
if not versions_by_adapter:
|
||||
continue
|
||||
|
||||
if rule_id not in self.rule_cache:
|
||||
self.rule_cache[rule_id] = {}
|
||||
|
||||
# 对于每个适配器的每个版本,尝试加载规则
|
||||
for adapter, versions in versions_by_adapter:
|
||||
for version in versions:
|
||||
rule = adapter.load_rule_by_id(rule_id, version)
|
||||
if rule:
|
||||
self.rule_cache[rule_id][version] = rule
|
||||
loaded_count += 1
|
||||
|
||||
self.logger.info(f"Preloaded {loaded_count} rules from {len(all_rule_ids)} rule IDs")
|
||||
|
||||
def get_rule(self, rule_id: str, version: Optional[str] = None) -> Optional[AnyRule]:
|
||||
"""
|
||||
获取指定ID和版本的规则。
|
||||
|
||||
Args:
|
||||
rule_id: 规则ID
|
||||
version: 规则版本(如果未指定,则使用配置的默认版本策略)
|
||||
|
||||
Returns:
|
||||
规则对象,如果未找到则返回None
|
||||
"""
|
||||
# 优先从缓存中获取,如果启用了预加载
|
||||
if self.config.preload_rules and rule_id in self.rule_cache:
|
||||
if version and version in self.rule_cache[rule_id]:
|
||||
return self.rule_cache[rule_id][version]
|
||||
elif not version and self.rule_cache[rule_id]:
|
||||
# 获取最新版本
|
||||
latest_version = self._get_latest_version(list(self.rule_cache[rule_id].keys()))
|
||||
return self.rule_cache[rule_id].get(latest_version)
|
||||
|
||||
# 从适配器加载
|
||||
for adapter in self.adapters:
|
||||
rule = adapter.load_rule_by_id(rule_id, version)
|
||||
if rule:
|
||||
return rule
|
||||
|
||||
return None
|
||||
|
||||
def _get_latest_version(self, versions: List[str]) -> str:
|
||||
"""简单地按字符串排序获取最新版本。"""
|
||||
if not versions:
|
||||
return ""
|
||||
versions.sort()
|
||||
return versions[-1]
|
||||
|
||||
def query_rules(self, query: Optional[RuleQuery] = None) -> List[AnyRule]:
|
||||
"""
|
||||
根据查询条件查询规则。
|
||||
|
||||
Args:
|
||||
query: 规则查询条件,如果为None则使用默认查询
|
||||
|
||||
Returns:
|
||||
匹配规则的列表
|
||||
"""
|
||||
query = query or RuleQuery()
|
||||
|
||||
# 从所有适配器查询规则
|
||||
results = []
|
||||
for adapter in self.adapters:
|
||||
adapter_results = adapter.query_rules(query)
|
||||
if adapter_results:
|
||||
results.extend(adapter_results)
|
||||
|
||||
# 去重(可能不同适配器返回相同ID和版本的规则)
|
||||
deduplicated = {}
|
||||
for rule in results:
|
||||
key = f"{rule.id}:{rule.version}"
|
||||
if key not in deduplicated:
|
||||
deduplicated[key] = rule
|
||||
|
||||
return list(deduplicated.values())
|
||||
|
||||
def get_rules_by_tags(self, tags: List[str], match_all: bool = False) -> List[AnyRule]:
|
||||
"""
|
||||
根据标签查询规则。
|
||||
|
||||
Args:
|
||||
tags: 要匹配的标签列表。
|
||||
match_all: 如果为True,则规则必须包含所有指定的标签;
|
||||
如果为False(默认),则规则包含任何一个指定标签即可匹配。
|
||||
|
||||
Returns:
|
||||
匹配标签的规则列表。
|
||||
"""
|
||||
if not tags:
|
||||
return [] # 如果没有提供标签,返回空列表
|
||||
|
||||
# 获取所有规则进行过滤。可以考虑优化,如果规则量非常大,
|
||||
# 且适配器支持基于标签的查询,则直接调用适配器。
|
||||
# 目前,我们先在查询所有规则后进行内存过滤。
|
||||
all_rules = self.query_rules(RuleQuery(is_enabled=True)) # 通常只查询启用的规则
|
||||
|
||||
matched_rules = []
|
||||
tag_set_query = set(tag.lower() for tag in tags) # 查询标签转换为小写集合以进行不区分大小写的比较
|
||||
|
||||
for rule in all_rules:
|
||||
if not rule.tags: # 如果规则没有标签,则跳过
|
||||
continue
|
||||
|
||||
rule_tags_set = set(t.lower() for t in rule.tags) # 规则的标签也转换为小写集合
|
||||
|
||||
if match_all:
|
||||
# 需要匹配所有查询标签
|
||||
if tag_set_query.issubset(rule_tags_set):
|
||||
matched_rules.append(rule)
|
||||
else:
|
||||
# 只需要匹配任何一个查询标签
|
||||
if not tag_set_query.isdisjoint(rule_tags_set): # 如果交集不为空
|
||||
matched_rules.append(rule)
|
||||
|
||||
return matched_rules
|
||||
|
||||
def save_rule(self, rule: BaseRule) -> bool:
|
||||
"""
|
||||
保存规则到存储。
|
||||
|
||||
Args:
|
||||
rule: 要保存的规则
|
||||
|
||||
Returns:
|
||||
操作是否成功
|
||||
"""
|
||||
# 根据规则类别选择合适的适配器
|
||||
adapter_to_use = self.adapters[0] # 默认使用第一个适配器
|
||||
|
||||
# 如果是YAML格式的规则,使用YAML适配器
|
||||
if hasattr(rule, 'code') and rule.code:
|
||||
for adapter in self.adapters:
|
||||
if isinstance(adapter, YAMLAdapter):
|
||||
adapter_to_use = adapter
|
||||
break
|
||||
|
||||
result = adapter_to_use.save_rule(rule)
|
||||
|
||||
# 如果成功保存且启用了预加载,更新缓存
|
||||
if result and self.config.preload_rules:
|
||||
if rule.id not in self.rule_cache:
|
||||
self.rule_cache[rule.id] = {}
|
||||
self.rule_cache[rule.id][rule.version] = rule
|
||||
|
||||
return result
|
||||
|
||||
def delete_rule(self, rule_id: str, version: Optional[str] = None) -> bool:
|
||||
"""
|
||||
从存储中删除规则。
|
||||
|
||||
Args:
|
||||
rule_id: 规则ID
|
||||
version: 如果指定,仅删除该版本;否则删除所有版本
|
||||
|
||||
Returns:
|
||||
操作是否成功
|
||||
"""
|
||||
# 从所有适配器删除规则
|
||||
overall_result = True
|
||||
for adapter in self.adapters:
|
||||
try:
|
||||
result = adapter.delete_rule(rule_id, version)
|
||||
if not result:
|
||||
overall_result = False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error deleting rule {rule_id} (version={version}) from adapter {adapter.__class__.__name__}: {e}")
|
||||
overall_result = False
|
||||
|
||||
# 如果启用了预加载,更新缓存
|
||||
if self.config.preload_rules:
|
||||
if version and rule_id in self.rule_cache:
|
||||
# 删除特定版本
|
||||
if version in self.rule_cache[rule_id]:
|
||||
del self.rule_cache[rule_id][version]
|
||||
# 如果该规则没有更多版本,删除整个规则条目
|
||||
if not self.rule_cache[rule_id]:
|
||||
del self.rule_cache[rule_id]
|
||||
elif rule_id in self.rule_cache:
|
||||
# 删除所有版本
|
||||
del self.rule_cache[rule_id]
|
||||
|
||||
return overall_result
|
||||
|
||||
def get_rules_for_target(self, target_type: TargetType, target_id: str) -> List[AnyRule]:
|
||||
"""
|
||||
获取适用于特定目标的规则。
|
||||
这是一个便捷方法,用于当TestExecutor或JSONSchemaValidator需要找到适用于特定API操作或数据对象的规则。
|
||||
|
||||
Args:
|
||||
target_type: 目标类型(如APIRequest, APIResponse, DataObject)
|
||||
target_id: 目标标识符(如API操作ID, 数据对象名称)
|
||||
|
||||
Returns:
|
||||
适用于该目标的规则列表
|
||||
"""
|
||||
query = RuleQuery(
|
||||
target_type=target_type,
|
||||
target_identifier=target_id,
|
||||
is_enabled=True
|
||||
)
|
||||
return self.query_rules(query)
|
||||
|
||||
def get_rules_by_lifecycle(self, lifecycle: RuleLifecycle, target_type: Optional[TargetType] = None) -> List[AnyRule]:
|
||||
"""
|
||||
获取适用于特定生命周期阶段的规则。
|
||||
|
||||
Args:
|
||||
lifecycle: 规则适用的生命周期阶段
|
||||
target_type: 可选的目标类型过滤
|
||||
|
||||
Returns:
|
||||
适用于该生命周期阶段的规则列表
|
||||
"""
|
||||
query = RuleQuery(
|
||||
lifecycle=lifecycle,
|
||||
target_type=target_type,
|
||||
is_enabled=True
|
||||
)
|
||||
return self.query_rules(query)
|
||||
|
||||
def get_rules_by_scope(self, scope: RuleScope, target_type: Optional[TargetType] = None) -> List[AnyRule]:
|
||||
"""
|
||||
获取适用于特定作用域的规则。
|
||||
|
||||
Args:
|
||||
scope: 规则的作用域
|
||||
target_type: 可选的目标类型过滤
|
||||
|
||||
Returns:
|
||||
适用于该作用域的规则列表
|
||||
"""
|
||||
query = RuleQuery(
|
||||
scope=scope,
|
||||
target_type=target_type,
|
||||
is_enabled=True
|
||||
)
|
||||
return self.query_rules(query)
|
||||
|
||||
def get_schema_for_target(self, target_type: TargetType, target_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
获取适用于特定目标的JSON Schema。
|
||||
这是一个便捷方法,用于当JSONSchemaValidator需要找到适用于特定API操作或数据对象的JSON Schema。
|
||||
|
||||
Args:
|
||||
target_type: 目标类型(如APIRequest, APIResponse, DataObject)
|
||||
target_id: 目标标识符(如API操作ID, 数据对象名称)
|
||||
|
||||
Returns:
|
||||
JSON Schema字典,如果未找到则返回None
|
||||
"""
|
||||
query = RuleQuery(
|
||||
category=RuleCategory.JSON_SCHEMA,
|
||||
target_type=target_type,
|
||||
target_identifier=target_id,
|
||||
is_enabled=True
|
||||
)
|
||||
|
||||
schemas = self.query_rules(query)
|
||||
if not schemas:
|
||||
return None
|
||||
|
||||
# 如果有多个匹配的Schema规则,使用最新版本的
|
||||
# 注意:这里可以根据需要实现更复杂的选择逻辑
|
||||
schemas.sort(key=lambda x: x.version)
|
||||
latest_schema = schemas[-1]
|
||||
|
||||
# 假设是JSONSchemaDefinition类型
|
||||
if hasattr(latest_schema, 'schema_content'):
|
||||
return latest_schema.schema_content
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,333 @@
|
||||
"""YAML adapter for rule storage using YAML files."""
|
||||
import os
|
||||
import yaml
|
||||
import glob
|
||||
from typing import List, Dict, Optional, Any, Tuple, Type, Union, cast
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from .adapters.base_adapter import BaseRuleStorageAdapter
|
||||
from ..models.rule_models import (
|
||||
AnyRule, RuleQuery, BaseRule, RuleCategory, TargetType, RuleLifecycle, RuleScope
|
||||
)
|
||||
from .adapters.rule_adapter_utils import parse_rule_data
|
||||
|
||||
class YAMLAdapter(BaseRuleStorageAdapter):
|
||||
"""
|
||||
基于YAML文件的规则存储适配器,使用YAML文件保存规则。
|
||||
|
||||
文件结构约定:
|
||||
规则文件将按以下方式组织:
|
||||
rules/
|
||||
yaml_rules/
|
||||
json_schemas/
|
||||
rule_id1/
|
||||
1.0.0.yaml
|
||||
1.1.0.yaml
|
||||
rule_id2/
|
||||
1.0.0.yaml
|
||||
business_logic/
|
||||
rule_id3/
|
||||
1.0.0.yaml
|
||||
...
|
||||
"""
|
||||
|
||||
def __init__(self, base_path: str = "./rules", file_pattern: str = "*.yaml"):
|
||||
"""
|
||||
初始化适配器。
|
||||
|
||||
Args:
|
||||
base_path: 存储规则的基本目录路径。
|
||||
file_pattern: 匹配规则文件的glob模式。
|
||||
"""
|
||||
self.base_path = os.path.abspath(base_path)
|
||||
self.file_pattern = file_pattern
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.yaml_dir = os.path.join(self.base_path, "yaml_rules")
|
||||
|
||||
def initialize(self) -> None:
|
||||
"""确保基本目录存在,并验证其可访问性。"""
|
||||
if not os.path.exists(self.yaml_dir):
|
||||
try:
|
||||
os.makedirs(self.yaml_dir)
|
||||
self.logger.info(f"Created YAML rules directory at {self.yaml_dir}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to create YAML rules directory at {self.yaml_dir}: {e}")
|
||||
raise ValueError(f"YAML rules directory {self.yaml_dir} does not exist and could not be created")
|
||||
|
||||
if not os.access(self.yaml_dir, os.R_OK | os.W_OK):
|
||||
self.logger.error(f"YAML rules directory {self.yaml_dir} is not readable and writable")
|
||||
raise ValueError(f"YAML rules directory {self.yaml_dir} is not readable and writable")
|
||||
|
||||
# 确保每个规则类别的子目录存在
|
||||
for category in RuleCategory:
|
||||
category_dir = self._get_category_dir(category)
|
||||
if not os.path.exists(category_dir):
|
||||
try:
|
||||
os.makedirs(category_dir)
|
||||
self.logger.debug(f"Created category directory at {category_dir}")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to create category directory at {category_dir}: {e}")
|
||||
|
||||
def _get_category_dir(self, category: RuleCategory) -> str:
|
||||
"""获取给定规则类别的目录路径。"""
|
||||
return os.path.join(self.yaml_dir, category.value.lower())
|
||||
|
||||
def _get_rule_dir(self, rule_id: str, category: Optional[RuleCategory] = None) -> str:
|
||||
"""
|
||||
获取给定规则ID的目录路径。
|
||||
如果指定了类别,则直接使用该类别的目录;否则尝试在所有类别中查找。
|
||||
"""
|
||||
if category:
|
||||
return os.path.join(self._get_category_dir(category), rule_id)
|
||||
|
||||
# 如果未指定类别,检查所有类别目录
|
||||
for cat in RuleCategory:
|
||||
rule_dir = os.path.join(self._get_category_dir(cat), rule_id)
|
||||
if os.path.exists(rule_dir):
|
||||
return rule_dir
|
||||
|
||||
# 如果未找到任何匹配项,默认返回通用类别目录
|
||||
return os.path.join(self._get_category_dir(RuleCategory.GENERIC), rule_id)
|
||||
|
||||
def _get_rule_file_path(self, rule_id: str, version: str, category: Optional[RuleCategory] = None) -> str:
|
||||
"""获取给定规则ID和版本的文件路径。"""
|
||||
rule_dir = self._get_rule_dir(rule_id, category)
|
||||
return os.path.join(rule_dir, f"{version}.yaml")
|
||||
|
||||
def _get_rule_from_file(self, file_path: str) -> Optional[AnyRule]:
|
||||
"""从YAML文件加载规则。"""
|
||||
if not os.path.exists(file_path):
|
||||
self.logger.debug(f"Rule file {file_path} does not exist")
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
raw_data = yaml.safe_load(f)
|
||||
|
||||
# 使用工具函数解析规则数据
|
||||
return parse_rule_data(raw_data)
|
||||
|
||||
except yaml.YAMLError as e:
|
||||
self.logger.error(f"Failed to parse YAML from {file_path}: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading rule from {file_path}: {e}")
|
||||
return None
|
||||
|
||||
def _save_rule_to_file(self, rule: BaseRule, file_path: str) -> bool:
|
||||
"""将规则保存到YAML文件。"""
|
||||
try:
|
||||
# 确保目录存在
|
||||
dir_path = os.path.dirname(file_path)
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
|
||||
# 将规则序列化为YAML并写入文件
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(rule.model_dump(), f, default_flow_style=False, sort_keys=False)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to save rule to {file_path}: {e}")
|
||||
return False
|
||||
|
||||
def _get_latest_version(self, rule_id: str, category: Optional[RuleCategory] = None) -> Optional[str]:
|
||||
"""获取给定规则ID的最新版本。"""
|
||||
versions = self.get_rule_versions(rule_id, category)
|
||||
if not versions:
|
||||
return None
|
||||
|
||||
# 简单地按字符串排序,假设版本格式是类似于"1.0.0"的语义化版本
|
||||
versions.sort()
|
||||
return versions[-1]
|
||||
|
||||
def get_rule_versions(self, rule_id: str, category: Optional[RuleCategory] = None) -> List[str]:
|
||||
"""获取给定规则ID的所有版本。"""
|
||||
rule_dir = self._get_rule_dir(rule_id, category)
|
||||
if not os.path.exists(rule_dir):
|
||||
return []
|
||||
|
||||
# 查找目录中所有匹配的YAML文件,并提取版本号(文件名)
|
||||
pattern = os.path.join(rule_dir, self.file_pattern)
|
||||
version_files = glob.glob(pattern)
|
||||
|
||||
versions = []
|
||||
for vf in version_files:
|
||||
version = os.path.splitext(os.path.basename(vf))[0] # 移除.yaml扩展名
|
||||
versions.append(version)
|
||||
|
||||
return versions
|
||||
|
||||
def load_rule_by_id(self, rule_id: str, version: Optional[str] = None) -> Optional[AnyRule]:
|
||||
"""
|
||||
按ID加载单个规则。
|
||||
|
||||
Args:
|
||||
rule_id: 规则的唯一标识符。
|
||||
version: 可选的版本标识符。如果未提供,则加载最新版本。
|
||||
|
||||
Returns:
|
||||
找到的规则对象,或者如果未找到则返回None。
|
||||
"""
|
||||
if not version:
|
||||
version = self._get_latest_version(rule_id)
|
||||
if not version:
|
||||
self.logger.debug(f"No versions found for rule ID {rule_id}")
|
||||
return None
|
||||
|
||||
file_path = self._get_rule_file_path(rule_id, version)
|
||||
return self._get_rule_from_file(file_path)
|
||||
|
||||
def query_rules(self, query: RuleQuery) -> List[AnyRule]:
|
||||
"""
|
||||
根据查询条件查询规则。
|
||||
|
||||
Args:
|
||||
query: 包含筛选条件的查询对象。
|
||||
|
||||
Returns:
|
||||
匹配查询条件的规则列表。
|
||||
"""
|
||||
results = []
|
||||
|
||||
# 如果指定了规则ID,直接加载该规则
|
||||
if query.rule_id:
|
||||
rule = self.load_rule_by_id(query.rule_id, query.version if query.version != "latest" else None)
|
||||
if rule and self._rule_matches_query(rule, query):
|
||||
results.append(rule)
|
||||
return results
|
||||
|
||||
# 否则,根据查询条件扫描规则文件
|
||||
categories_to_search = [query.category] if query.category else list(RuleCategory)
|
||||
|
||||
for category in categories_to_search:
|
||||
category_dir = self._get_category_dir(category)
|
||||
if not os.path.exists(category_dir):
|
||||
continue
|
||||
|
||||
# 获取该类别下的所有规则ID(子目录)
|
||||
rule_dirs = [d for d in os.listdir(category_dir)
|
||||
if os.path.isdir(os.path.join(category_dir, d))]
|
||||
|
||||
for rule_id in rule_dirs:
|
||||
# 对于每个规则ID,加载指定版本或最新版本
|
||||
if query.version and query.version != "latest":
|
||||
file_path = self._get_rule_file_path(rule_id, query.version, category)
|
||||
rule = self._get_rule_from_file(file_path)
|
||||
if rule and self._rule_matches_query(rule, query):
|
||||
results.append(rule)
|
||||
else:
|
||||
latest_version = self._get_latest_version(rule_id, category)
|
||||
if latest_version:
|
||||
file_path = self._get_rule_file_path(rule_id, latest_version, category)
|
||||
rule = self._get_rule_from_file(file_path)
|
||||
if rule and self._rule_matches_query(rule, query):
|
||||
results.append(rule)
|
||||
|
||||
return results
|
||||
|
||||
def _rule_matches_query(self, rule: BaseRule, query: RuleQuery) -> bool:
|
||||
"""检查规则是否匹配查询条件。"""
|
||||
# 检查是否启用
|
||||
if query.is_enabled is not None and rule.is_enabled != query.is_enabled:
|
||||
return False
|
||||
|
||||
# 检查目标类型
|
||||
if query.target_type and rule.target_type != query.target_type:
|
||||
return False
|
||||
|
||||
# 检查目标标识符
|
||||
if query.target_identifier and rule.target_identifier != query.target_identifier:
|
||||
return False
|
||||
|
||||
# 检查标签
|
||||
if query.tags:
|
||||
if not rule.tags:
|
||||
return False
|
||||
# 检查是否所有查询标签都在规则标签中
|
||||
if not all(tag in rule.tags for tag in query.tags):
|
||||
return False
|
||||
|
||||
# 检查生命周期
|
||||
if query.lifecycle and rule.lifecycle != query.lifecycle:
|
||||
return False
|
||||
|
||||
# 检查作用域
|
||||
if query.scope and rule.scope != query.scope:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def save_rule(self, rule: BaseRule) -> bool:
|
||||
"""
|
||||
保存规则到文件系统。
|
||||
|
||||
Args:
|
||||
rule: 要保存的规则对象。
|
||||
|
||||
Returns:
|
||||
操作是否成功。
|
||||
"""
|
||||
file_path = self._get_rule_file_path(rule.id, rule.version, rule.category)
|
||||
return self._save_rule_to_file(rule, file_path)
|
||||
|
||||
def delete_rule(self, rule_id: str, version: Optional[str] = None) -> bool:
|
||||
"""
|
||||
删除规则。
|
||||
|
||||
Args:
|
||||
rule_id: 规则的唯一标识符。
|
||||
version: 如果提供,仅删除该版本;否则删除所有版本。
|
||||
|
||||
Returns:
|
||||
操作是否成功。
|
||||
"""
|
||||
try:
|
||||
if version:
|
||||
# 删除特定版本
|
||||
file_path = self._get_rule_file_path(rule_id, version)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
self.logger.info(f"Deleted rule file: {file_path}")
|
||||
else:
|
||||
self.logger.warning(f"Rule file not found for deletion: {file_path}")
|
||||
return False
|
||||
else:
|
||||
# 删除所有版本(整个规则目录)
|
||||
rule_dir = self._get_rule_dir(rule_id)
|
||||
if os.path.exists(rule_dir):
|
||||
# 递归删除目录及其内容
|
||||
for root, dirs, files in os.walk(rule_dir, topdown=False):
|
||||
for file in files:
|
||||
os.remove(os.path.join(root, file))
|
||||
for dir in dirs:
|
||||
os.rmdir(os.path.join(root, dir))
|
||||
os.rmdir(rule_dir)
|
||||
self.logger.info(f"Deleted rule directory: {rule_dir}")
|
||||
else:
|
||||
self.logger.warning(f"Rule directory not found for deletion: {rule_dir}")
|
||||
return False
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error deleting rule {rule_id} (version={version}): {e}")
|
||||
return False
|
||||
|
||||
def list_all_rule_ids(self) -> List[str]:
|
||||
"""列出所有规则ID。"""
|
||||
all_rule_ids = set()
|
||||
|
||||
# 扫描所有类别目录
|
||||
for category in RuleCategory:
|
||||
category_dir = self._get_category_dir(category)
|
||||
if not os.path.exists(category_dir):
|
||||
continue
|
||||
|
||||
# 获取该类别下的所有规则ID(子目录)
|
||||
rule_dirs = [d for d in os.listdir(category_dir)
|
||||
if os.path.isdir(os.path.join(category_dir, d))]
|
||||
|
||||
all_rule_ids.update(rule_dirs)
|
||||
|
||||
return list(all_rule_ids)
|
||||
@@ -0,0 +1,100 @@
|
||||
# ddms_compliance_suite/test_loader/loader.py
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import List, Union, Dict, Any
|
||||
import yaml # PyYAML
|
||||
import json
|
||||
|
||||
from ..models.test_models import TestSuite # Ensure correct path
|
||||
# from ..models.test_models import TestCase # If TestCase can be loaded standalone
|
||||
|
||||
class LoadError(Exception):
|
||||
"""Custom exception for errors during test case loading."""
|
||||
pass
|
||||
|
||||
class BaseTestCaseLoader(ABC):
|
||||
@abstractmethod
|
||||
def load_suites_from_file(self, file_path: Union[str, Path]) -> List[TestSuite]:
|
||||
"""Loads a list of TestSuite objects from a single file."""
|
||||
pass
|
||||
|
||||
def load_suites_from_directory(self, directory_path: Union[str, Path], recursive: bool = False, pattern: str = "*.yaml") -> List[TestSuite]:
|
||||
"""Loads TestSuite objects from all matching files in a directory."""
|
||||
suites: List[TestSuite] = []
|
||||
p = Path(directory_path)
|
||||
|
||||
if not p.is_dir():
|
||||
raise LoadError(f"Directory not found: {directory_path}")
|
||||
|
||||
file_paths = list(p.rglob(pattern)) if recursive else list(p.glob(pattern))
|
||||
|
||||
for file_path in file_paths:
|
||||
if file_path.is_file():
|
||||
try:
|
||||
# Ensure that the loader instance calls its own method
|
||||
# This part might need adjustment if called directly on BaseTestCaseLoader
|
||||
# For now, assuming it's called from a concrete instance like YAMLTestCaseLoader
|
||||
suites.extend(self.load_suites_from_file(file_path))
|
||||
except LoadError as e:
|
||||
# Consider using a logger for warnings/errors
|
||||
print(f"Warning: Could not load test suites from {file_path}: {e}")
|
||||
except Exception as e:
|
||||
print(f"Warning: An unexpected error occurred loading {file_path}: {e}")
|
||||
return suites
|
||||
|
||||
class YAMLTestCaseLoader(BaseTestCaseLoader):
|
||||
def load_suites_from_file(self, file_path: Union[str, Path]) -> List[TestSuite]:
|
||||
file_p = Path(file_path)
|
||||
if not file_p.exists() or not file_p.is_file():
|
||||
raise LoadError(f"Test case file not found or is not a file: {file_path}")
|
||||
|
||||
try:
|
||||
with open(file_p, 'r', encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f)
|
||||
except yaml.YAMLError as e:
|
||||
raise LoadError(f"Error parsing YAML file {file_p}: {e}")
|
||||
except Exception as e:
|
||||
raise LoadError(f"An unexpected error occurred while reading {file_p}: {e}")
|
||||
|
||||
if data is None: # Handle empty YAML file
|
||||
return []
|
||||
|
||||
if not isinstance(data, dict) or "test_suites" not in data:
|
||||
raise LoadError(f"Invalid format in {file_p}: Missing 'test_suites' top-level key or not a dictionary.")
|
||||
|
||||
suite_data_list = data.get("test_suites") # Use .get for safer access
|
||||
if not isinstance(suite_data_list, list):
|
||||
raise LoadError(f"Invalid format in {file_p}: 'test_suites' must be a list.")
|
||||
|
||||
loaded_suites: List[TestSuite] = []
|
||||
for i, suite_data in enumerate(suite_data_list):
|
||||
if not isinstance(suite_data, dict):
|
||||
print(f"Warning: Suite data #{i+1} in {file_p} is not a dictionary, skipping.")
|
||||
continue
|
||||
try:
|
||||
suite = TestSuite.model_validate(suite_data) # For Pydantic v2+
|
||||
# For Pydantic v1, use: suite = TestSuite.parse_obj(suite_data)
|
||||
loaded_suites.append(suite)
|
||||
except Exception as e: # Catch Pydantic validation errors and others
|
||||
# It's often better to log this and continue, or collect all errors
|
||||
raise LoadError(f"Error validating test suite #{i+1} (ID: {suite_data.get('id', 'N/A')}) in {file_p}: {e}")
|
||||
|
||||
return loaded_suites
|
||||
|
||||
# Example Usage (conceptual):
|
||||
# if __name__ == '__main__':
|
||||
# yaml_loader = YAMLTestCaseLoader()
|
||||
# try:
|
||||
# # suites_from_single_file = yaml_loader.load_suites_from_file('path/to/your/single_test_suite_file.yaml')
|
||||
# # for suite in suites_from_single_file:
|
||||
# # print(f"Loaded Suite: {suite.name}")
|
||||
|
||||
# all_suites_in_dir = yaml_loader.load_suites_from_directory('path/to/test_suites_directory', recursive=True, pattern="*.test_suite.yaml")
|
||||
# for suite in all_suites_in_dir:
|
||||
# print(f"Loaded Suite from Dir: {suite.name}")
|
||||
# for tc in suite.test_cases:
|
||||
# print(f" - TestCase: {tc.name}")
|
||||
|
||||
# except LoadError as e:
|
||||
# print(f"Loading Error: {e}")
|
||||
@@ -0,0 +1,802 @@
|
||||
"""
|
||||
测试编排器模块
|
||||
|
||||
负责组合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
|
||||
Reference in New Issue
Block a user