fix:yapi
This commit is contained in:
Binary file not shown.
@@ -13,13 +13,13 @@ class StatusCode200Check(BaseAPITestCase):
|
||||
# applicable_methods = None
|
||||
# applicable_paths_regex = None
|
||||
execution_order = 10 # 示例执行顺序
|
||||
use_llm_for_body: bool = True
|
||||
use_llm_for_path_params: bool = True
|
||||
use_llm_for_query_params: bool = True
|
||||
use_llm_for_headers: bool = True
|
||||
# use_llm_for_body: bool = True
|
||||
# use_llm_for_path_params: bool = True
|
||||
# use_llm_for_query_params: bool = True
|
||||
# use_llm_for_headers: bool = True
|
||||
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator=json_schema_validator)
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator=json_schema_validator, llm_service=llm_service)
|
||||
self.logger.info(f"测试用例 {self.id} ({self.name}) 已针对端点 '{self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')}' 初始化。")
|
||||
|
||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> list[ValidationResult]:
|
||||
@@ -50,42 +50,3 @@ class StatusCode200Check(BaseAPITestCase):
|
||||
)
|
||||
self.logger.warning(f"状态码验证失败: 期望 {expected_status_code}, 实际 {actual_status_code} for {request_context.url}")
|
||||
return results
|
||||
|
||||
class HeaderExistenceCheck(BaseAPITestCase):
|
||||
id = "TC-HEADER-001"
|
||||
name = "检查响应中是否存在 'X-Request-ID' 头"
|
||||
description = "验证 API 响应是否包含 'X-Request-ID' 头。"
|
||||
severity = TestSeverity.MEDIUM
|
||||
tags = ["header", "observability"]
|
||||
execution_order = 10 # 示例执行顺序
|
||||
use_llm_for_body = False
|
||||
|
||||
EXPECTED_HEADER = "X-Request-ID" # 示例,可以根据实际需要修改
|
||||
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator=json_schema_validator)
|
||||
self.logger.info(f"测试用例 {self.id} ({self.name}) 已初始化 for endpoint {self.endpoint_spec.get('path')}")
|
||||
|
||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> list[ValidationResult]:
|
||||
results = []
|
||||
if self.EXPECTED_HEADER in response_context.headers:
|
||||
results.append(
|
||||
ValidationResult(
|
||||
passed=True,
|
||||
message=f"响应头中找到了期望的 '{self.EXPECTED_HEADER}'。"
|
||||
)
|
||||
)
|
||||
self.logger.info(f"请求头 '{self.EXPECTED_HEADER}' 存在于 {request_context.url} 的响应中。")
|
||||
else:
|
||||
results.append(
|
||||
ValidationResult(
|
||||
passed=False,
|
||||
message=f"响应头中未找到期望的 '{self.EXPECTED_HEADER}'。",
|
||||
details={
|
||||
"expected_header": self.EXPECTED_HEADER,
|
||||
"actual_headers": list(response_context.headers.keys())
|
||||
}
|
||||
)
|
||||
)
|
||||
self.logger.warning(f"请求头 '{self.EXPECTED_HEADER}' 未在 {request_context.url} 的响应中找到。")
|
||||
return results
|
||||
BIN
Binary file not shown.
@@ -13,8 +13,8 @@ class ResponseSchemaValidationCase(BaseAPITestCase):
|
||||
# This test is generally applicable, especially for GET requests or successful POST/PUT.
|
||||
# It might need refinement based on specific endpoint characteristics (e.g., no response body for DELETE)
|
||||
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator)
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service=llm_service)
|
||||
self.logger.info(f"测试用例 '{self.id}' 已为端点 '{self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')}' 初始化。")
|
||||
|
||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+8
-57
@@ -11,68 +11,19 @@ class MissingRequiredFieldBodyCase(BaseAPITestCase):
|
||||
tags = ["error-handling", "appendix-b", "4003", "required-fields", "request-body"]
|
||||
execution_order = 210 # Before query, same as original combined
|
||||
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator)
|
||||
self.logger.setLevel(logging.DEBUG) # Ensure detailed logging for this class
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service=llm_service)
|
||||
self.logger = logging.getLogger(f"testcase.{self.id}")
|
||||
self.target_field_path: Optional[List[str]] = None
|
||||
self.original_value_at_path: Any = None
|
||||
self.removed_field_path: Optional[List[str]] = None # Path to the removed field, e.g., ['level1', 'level2_field']
|
||||
self.original_body_schema: Optional[Dict[str, Any]] = None
|
||||
self._try_find_removable_body_field()
|
||||
|
||||
def _resolve_ref_if_present(self, schema_to_resolve: Dict[str, Any]) -> Dict[str, Any]:
|
||||
ref_value = None
|
||||
if isinstance(schema_to_resolve, dict):
|
||||
if "$ref" in schema_to_resolve:
|
||||
ref_value = schema_to_resolve["$ref"]
|
||||
elif "$$ref" in schema_to_resolve:
|
||||
ref_value = schema_to_resolve["$$ref"]
|
||||
|
||||
if ref_value:
|
||||
self.logger.debug(f"发现引用 '{ref_value}',尝试解析...")
|
||||
try:
|
||||
actual_global_spec_dict = None
|
||||
if hasattr(self.global_api_spec, 'spec') and isinstance(self.global_api_spec.spec, dict):
|
||||
actual_global_spec_dict = self.global_api_spec.spec
|
||||
elif isinstance(self.global_api_spec, dict):
|
||||
actual_global_spec_dict = self.global_api_spec
|
||||
|
||||
if not actual_global_spec_dict:
|
||||
self.logger.warning(f"无法从 self.global_api_spec (类型: {type(self.global_api_spec)}) 获取用于解析引用的字典。")
|
||||
return schema_to_resolve
|
||||
|
||||
resolved_schema = None
|
||||
if ref_value.startswith("#/components/schemas/"):
|
||||
schema_name = ref_value.split("/")[-1]
|
||||
components = actual_global_spec_dict.get("components")
|
||||
if components and isinstance(components.get("schemas"), dict):
|
||||
resolved_schema = components["schemas"].get(schema_name)
|
||||
if resolved_schema and isinstance(resolved_schema, dict):
|
||||
self.logger.info(f"成功从 #/components/schemas/ 解析引用 '{ref_value}'。")
|
||||
return resolved_schema
|
||||
else:
|
||||
self.logger.warning(f"解析引用 '{ref_value}' (路径: #/components/schemas/) 失败:未找到或找到的不是字典: {schema_name}")
|
||||
else:
|
||||
self.logger.warning(f"尝试从 #/components/schemas/ 解析引用 '{ref_value}' 失败:无法找到 'components.schemas' 结构。")
|
||||
|
||||
# 如果从 #/components/schemas/ 未成功解析,尝试 #/definitions/
|
||||
if not resolved_schema and ref_value.startswith("#/definitions/"):
|
||||
schema_name = ref_value.split("/")[-1]
|
||||
definitions = actual_global_spec_dict.get("definitions")
|
||||
if definitions and isinstance(definitions, dict):
|
||||
resolved_schema = definitions.get(schema_name)
|
||||
if resolved_schema and isinstance(resolved_schema, dict):
|
||||
self.logger.info(f"成功从 #/definitions/ 解析引用 '{ref_value}'。")
|
||||
return resolved_schema
|
||||
else:
|
||||
self.logger.warning(f"解析引用 '{ref_value}' (路径: #/definitions/) 失败:未找到或找到的不是字典: {schema_name}")
|
||||
else:
|
||||
self.logger.warning(f"尝试从 #/definitions/ 解析引用 '{ref_value}' 失败:无法找到 'definitions' 结构。")
|
||||
|
||||
if not resolved_schema:
|
||||
self.logger.warning(f"最终未能通过任一已知路径 (#/components/schemas/ 或 #/definitions/) 解析引用 '{ref_value}'。")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"解析引用 '{ref_value}' 时发生错误: {e}", exc_info=True)
|
||||
return schema_to_resolve # 返回原始 schema 如果不是 ref 或者所有解析尝试都失败
|
||||
# 根据用户进一步要求,方法体简化为直接返回,不进行任何 $ref/$ $$ref 的检查。
|
||||
# self.logger.debug(f"_resolve_ref_if_present called. Returning schema as-is per new configuration.")
|
||||
return schema_to_resolve
|
||||
|
||||
def _find_required_field_in_schema_recursive(self, current_schema: Dict[str, Any], current_path: List[str]) -> Optional[List[str]]:
|
||||
"""递归查找第一个可移除的必填字段的路径。
|
||||
|
||||
+15
-14
@@ -10,10 +10,11 @@ class MissingRequiredFieldQueryCase(BaseAPITestCase):
|
||||
tags = ["error-handling", "appendix-b", "4003", "required-fields", "query-parameters"]
|
||||
execution_order = 211 # After body, before original combined one might have been
|
||||
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator)
|
||||
self.removed_field_name: Optional[str] = None
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator=json_schema_validator, llm_service=llm_service)
|
||||
self.target_param_name: Optional[str] = None
|
||||
self._try_find_removable_query_param()
|
||||
self.logger.info(f"测试用例 {self.id} ({self.name}) 已针对端点 '{self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')}' 初始化。Target param to remove: {self.target_param_name}")
|
||||
|
||||
def _try_find_removable_query_param(self):
|
||||
query_params_spec_list = self.endpoint_spec.get("parameters", [])
|
||||
@@ -23,8 +24,8 @@ class MissingRequiredFieldQueryCase(BaseAPITestCase):
|
||||
if isinstance(param_spec, dict) and param_spec.get("in") == "query" and param_spec.get("required") is True:
|
||||
field_name = param_spec.get("name")
|
||||
if field_name:
|
||||
self.removed_field_name = field_name
|
||||
self.logger.info(f"必填字段缺失测试的目标字段 (查询参数): '{self.removed_field_name}'")
|
||||
self.target_param_name = field_name
|
||||
self.logger.info(f"必填字段缺失测试的目标字段 (查询参数): '{self.target_param_name}'")
|
||||
return
|
||||
self.logger.info('在此端点规范中未找到可用于测试 "必填查询参数缺失" 的字段。')
|
||||
|
||||
@@ -34,20 +35,20 @@ class MissingRequiredFieldQueryCase(BaseAPITestCase):
|
||||
return current_body
|
||||
|
||||
def generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if self.removed_field_name and isinstance(current_query_params, dict):
|
||||
if self.removed_field_name in current_query_params:
|
||||
if self.target_param_name and isinstance(current_query_params, dict):
|
||||
if self.target_param_name in current_query_params:
|
||||
new_params = copy.deepcopy(current_query_params)
|
||||
original_value = new_params.pop(self.removed_field_name) # 移除参数
|
||||
self.logger.info(f"为进行必填查询参数缺失测试,已从查询参数中移除 '{self.removed_field_name}' (原值: '{original_value}')。")
|
||||
original_value = new_params.pop(self.target_param_name) # 移除参数
|
||||
self.logger.info(f"为进行必填查询参数缺失测试,已从查询参数中移除 '{self.target_param_name}' (原值: '{original_value}')。")
|
||||
return new_params
|
||||
else:
|
||||
self.logger.warning(f"计划移除的查询参数 '{self.removed_field_name}' 在当前查询参数中未找到。")
|
||||
self.logger.warning(f"计划移除的查询参数 '{self.target_param_name}' 在当前查询参数中未找到。")
|
||||
return current_query_params
|
||||
|
||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
results = []
|
||||
|
||||
if not self.removed_field_name:
|
||||
if not self.target_param_name:
|
||||
results.append(self.passed("跳过测试:在API规范中未找到合适的必填查询参数用于移除测试。"))
|
||||
self.logger.info("由于未识别到可移除的必填查询参数,跳过此测试用例。")
|
||||
return results
|
||||
@@ -58,7 +59,7 @@ class MissingRequiredFieldQueryCase(BaseAPITestCase):
|
||||
expected_status_codes = [400, 422]
|
||||
specific_error_code_from_appendix_b = "4003"
|
||||
|
||||
msg_prefix = f"当移除必填查询参数 '{self.removed_field_name}' 时,"
|
||||
msg_prefix = f"当移除必填查询参数 '{self.target_param_name}' 时,"
|
||||
|
||||
if status_code in expected_status_codes:
|
||||
status_msg = f"{msg_prefix}API响应了预期的错误状态码 {status_code}。"
|
||||
@@ -77,8 +78,8 @@ class MissingRequiredFieldQueryCase(BaseAPITestCase):
|
||||
else:
|
||||
results.append(self.failed(
|
||||
message=f"{msg_prefix}期望API返回状态码 {expected_status_codes} 中的一个,但实际收到 {status_code}。",
|
||||
details={"status_code": status_code, "response_body": json_content, "removed_field": f"query.{self.removed_field_name}"}
|
||||
details={"status_code": status_code, "response_body": json_content, "removed_field": f"query.{self.target_param_name}"}
|
||||
))
|
||||
self.logger.warning(f"必填查询参数缺失测试失败:期望状态码 {expected_status_codes},实际为 {status_code}。移除的参数:'{self.removed_field_name}'")
|
||||
self.logger.warning(f"必填查询参数缺失测试失败:期望状态码 {expected_status_codes},实际为 {status_code}。移除的参数:'{self.target_param_name}'")
|
||||
|
||||
return results
|
||||
@@ -11,15 +11,20 @@ class TypeMismatchBodyCase(BaseAPITestCase):
|
||||
tags = ["error-handling", "appendix-b", "4001", "request-body"]
|
||||
execution_order = 202 # Slightly after query param one
|
||||
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator)
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service=llm_service)
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
self.target_field_path: Optional[List[str]] = None
|
||||
self.original_field_type: Optional[str] = None
|
||||
# Location is always 'body' for this class
|
||||
self.target_field_location: str = "body"
|
||||
self.target_field_schema: Optional[Dict[str, Any]] = None
|
||||
self.json_schema_validator = json_schema_validator
|
||||
self.original_value_at_path: Any = None
|
||||
self.mismatched_value: Any = None
|
||||
self._try_find_mismatch_target_in_body()
|
||||
|
||||
def _try_find_mismatch_target_in_body(self):
|
||||
self.logger.critical(f"{self.id} __INIT__ >>> STARTED")
|
||||
self.logger.debug(f"开始为端点 {self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')} 初始化请求体类型不匹配测试的目标字段查找。")
|
||||
|
||||
@@ -64,58 +69,8 @@ class TypeMismatchBodyCase(BaseAPITestCase):
|
||||
self.logger.info(f"最终,在端点 {self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')} 的请求体中,均未找到可用于测试类型不匹配的字段。")
|
||||
|
||||
def _resolve_ref_if_present(self, schema_to_resolve: Dict[str, Any]) -> Dict[str, Any]:
|
||||
ref_value = None
|
||||
if isinstance(schema_to_resolve, dict):
|
||||
if "$ref" in schema_to_resolve:
|
||||
ref_value = schema_to_resolve["$ref"]
|
||||
elif "$$ref" in schema_to_resolve:
|
||||
ref_value = schema_to_resolve["$$ref"]
|
||||
|
||||
if ref_value:
|
||||
self.logger.debug(f"发现引用 '{ref_value}',尝试解析...")
|
||||
try:
|
||||
actual_global_spec_dict = None
|
||||
if hasattr(self.global_api_spec, 'spec') and isinstance(self.global_api_spec.spec, dict):
|
||||
actual_global_spec_dict = self.global_api_spec.spec
|
||||
elif isinstance(self.global_api_spec, dict):
|
||||
actual_global_spec_dict = self.global_api_spec
|
||||
|
||||
if not actual_global_spec_dict:
|
||||
self.logger.warning(f"无法从 self.global_api_spec (类型: {type(self.global_api_spec)}) 获取用于解析引用的字典。")
|
||||
return schema_to_resolve
|
||||
|
||||
resolved_schema = None
|
||||
if ref_value.startswith("#/components/schemas/"):
|
||||
schema_name = ref_value.split("/")[-1]
|
||||
components = actual_global_spec_dict.get("components")
|
||||
if components and isinstance(components.get("schemas"), dict):
|
||||
resolved_schema = components["schemas"].get(schema_name)
|
||||
if resolved_schema and isinstance(resolved_schema, dict):
|
||||
self.logger.info(f"成功从 #/components/schemas/ 解析引用 '{ref_value}'。")
|
||||
return resolved_schema
|
||||
else:
|
||||
self.logger.warning(f"解析引用 '{ref_value}' (路径: #/components/schemas/) 失败:未找到或找到的不是字典: {schema_name}")
|
||||
else:
|
||||
self.logger.warning(f"尝试从 #/components/schemas/ 解析引用 '{ref_value}' 失败:无法找到 'components.schemas' 结构。")
|
||||
|
||||
if not resolved_schema and ref_value.startswith("#/definitions/"):
|
||||
schema_name = ref_value.split("/")[-1]
|
||||
definitions = actual_global_spec_dict.get("definitions")
|
||||
if definitions and isinstance(definitions, dict):
|
||||
resolved_schema = definitions.get(schema_name)
|
||||
if resolved_schema and isinstance(resolved_schema, dict):
|
||||
self.logger.info(f"成功从 #/definitions/ 解析引用 '{ref_value}'。")
|
||||
return resolved_schema
|
||||
else:
|
||||
self.logger.warning(f"解析引用 '{ref_value}' (路径: #/definitions/) 失败:未找到或找到的不是字典: {schema_name}")
|
||||
else:
|
||||
self.logger.warning(f"尝试从 #/definitions/ 解析引用 '{ref_value}' 失败:无法找到 'definitions' 结构。")
|
||||
|
||||
if not resolved_schema:
|
||||
self.logger.warning(f"最终未能通过任一已知路径 (#/components/schemas/ 或 #/definitions/) 解析引用 '{ref_value}'。")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"解析引用 '{ref_value}' 时发生错误: {e}", exc_info=True)
|
||||
# 根据用户进一步要求,方法体简化为直接返回,不进行任何 $ref/$ $$ref 的检查。
|
||||
# self.logger.debug(f"_resolve_ref_if_present called. Returning schema as-is per new configuration.")
|
||||
return schema_to_resolve
|
||||
|
||||
def _find_target_field_in_schema(self, schema_to_search: Dict[str, Any], base_path_for_log: str) -> bool:
|
||||
|
||||
+63
-54
@@ -11,14 +11,19 @@ class TypeMismatchQueryParamCase(BaseAPITestCase):
|
||||
tags = ["error-handling", "appendix-b", "4001", "query-parameters"]
|
||||
execution_order = 201 # Slightly after the combined one might have been
|
||||
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator)
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service=llm_service)
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
self.target_field_path: Optional[List[str]] = None
|
||||
self.original_field_type: Optional[str] = None
|
||||
# Location is always 'query' for this class
|
||||
self.target_field_location: str = "query"
|
||||
self.target_field_schema: Optional[Dict[str, Any]] = None
|
||||
self.original_value_at_path: Any = None
|
||||
self.mismatched_value: Any = None
|
||||
|
||||
# 调用新方法来查找目标字段
|
||||
self._try_find_mismatch_target_in_query()
|
||||
|
||||
self.logger.critical(f"{self.id} __INIT__ >>> STARTED")
|
||||
self.logger.debug(f"开始为端点 {self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')} 初始化查询参数类型不匹配测试的目标字段查找。")
|
||||
@@ -78,59 +83,63 @@ class TypeMismatchQueryParamCase(BaseAPITestCase):
|
||||
if not self.target_field_path:
|
||||
self.logger.info(f"最终,在端点 {self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')} 的查询参数中,均未找到可用于测试类型不匹配的字段。")
|
||||
|
||||
def _try_find_mismatch_target_in_query(self):
|
||||
self.logger.critical(f"{self.id} _try_find_mismatch_target_in_query >>> STARTED")
|
||||
self.logger.debug(f"开始为端点 {self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')} 初始化查询参数类型不匹配测试的目标字段查找。")
|
||||
|
||||
parameters = self.endpoint_spec.get("parameters", [])
|
||||
self.logger.critical(f"{self.id} _try_find_mismatch_target_in_query >>> Parameters to be processed: {parameters}")
|
||||
self.logger.debug(f"传入的参数列表 (在 {self.id}中): {parameters}")
|
||||
|
||||
for param_spec in parameters:
|
||||
if param_spec.get("in") == "query":
|
||||
param_name = param_spec.get("name")
|
||||
if not param_name:
|
||||
self.logger.warning("发现一个没有名称的查询参数定义,已跳过。")
|
||||
continue
|
||||
|
||||
self.logger.debug(f"检查查询参数: '{param_name}'")
|
||||
|
||||
param_type = param_spec.get("type")
|
||||
param_schema = param_spec.get("schema")
|
||||
|
||||
# Scenario 1: Simple type directly in param_spec (e.g., type: string)
|
||||
if param_type in ["string", "number", "integer", "boolean"]:
|
||||
self.target_field_path = [param_name]
|
||||
self.original_field_type = param_type
|
||||
self.target_field_schema = param_spec
|
||||
self.logger.info(f"目标字段(查询参数 - 简单类型): {param_name},原始类型: {self.original_field_type}")
|
||||
break
|
||||
# Scenario 2: Schema defined for the query parameter (OpenAPI 3.0 style, or complex objects in query)
|
||||
elif isinstance(param_schema, dict):
|
||||
self.logger.debug(f"查询参数 '{param_name}' 包含嵌套 schema,尝试在其内部查找简单类型字段。")
|
||||
resolved_param_schema = self._resolve_ref_if_present(param_schema)
|
||||
if resolved_param_schema.get("type") == "object":
|
||||
properties = resolved_param_schema.get("properties", {})
|
||||
for prop_name, prop_details_orig in properties.items():
|
||||
prop_details = self._resolve_ref_if_present(prop_details_orig)
|
||||
if prop_details.get("type") in ["string", "number", "integer", "boolean"]:
|
||||
self.target_field_path = [param_name, prop_name]
|
||||
self.original_field_type = prop_details.get("type")
|
||||
self.target_field_schema = prop_details
|
||||
self.logger.info(f"目标字段(查询参数 - 对象属性): {param_name}.{prop_name},原始类型: {self.original_field_type}")
|
||||
break
|
||||
if self.target_field_path: break
|
||||
elif resolved_param_schema.get("type") in ["string", "number", "integer", "boolean"]:
|
||||
self.target_field_path = [param_name]
|
||||
self.original_field_type = resolved_param_schema.get("type")
|
||||
self.target_field_schema = resolved_param_schema
|
||||
self.logger.info(f"目标字段(查询参数 - schema为简单类型): {param_name},原始类型: {self.original_field_type}")
|
||||
break
|
||||
else:
|
||||
self.logger.debug(f"查询参数 '{param_name}' (type: {param_type}, schema: {param_schema}) 不是直接的简单类型,也无直接可用的对象型 schema 属性。")
|
||||
|
||||
if not self.target_field_path:
|
||||
self.logger.info(f"最终,在端点 {self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')} 的查询参数中,均未找到可用于测试类型不匹配的字段。")
|
||||
|
||||
def _resolve_ref_if_present(self, schema_to_resolve: Dict[str, Any]) -> Dict[str, Any]:
|
||||
ref_value = None
|
||||
if isinstance(schema_to_resolve, dict):
|
||||
if "$ref" in schema_to_resolve:
|
||||
ref_value = schema_to_resolve["$ref"]
|
||||
elif "$$ref" in schema_to_resolve:
|
||||
ref_value = schema_to_resolve["$$ref"]
|
||||
|
||||
if ref_value:
|
||||
self.logger.debug(f"发现引用 '{ref_value}',尝试解析...")
|
||||
try:
|
||||
actual_global_spec_dict = None
|
||||
if hasattr(self.global_api_spec, 'spec') and isinstance(self.global_api_spec.spec, dict):
|
||||
actual_global_spec_dict = self.global_api_spec.spec
|
||||
elif isinstance(self.global_api_spec, dict):
|
||||
actual_global_spec_dict = self.global_api_spec
|
||||
|
||||
if not actual_global_spec_dict:
|
||||
self.logger.warning(f"无法从 self.global_api_spec (类型: {type(self.global_api_spec)}) 获取用于解析引用的字典。")
|
||||
return schema_to_resolve
|
||||
|
||||
resolved_schema = None
|
||||
if ref_value.startswith("#/components/schemas/"):
|
||||
schema_name = ref_value.split("/")[-1]
|
||||
components = actual_global_spec_dict.get("components")
|
||||
if components and isinstance(components.get("schemas"), dict):
|
||||
resolved_schema = components["schemas"].get(schema_name)
|
||||
if resolved_schema and isinstance(resolved_schema, dict):
|
||||
self.logger.info(f"成功从 #/components/schemas/ 解析引用 '{ref_value}'。")
|
||||
return resolved_schema
|
||||
else:
|
||||
self.logger.warning(f"解析引用 '{ref_value}' (路径: #/components/schemas/) 失败:未找到或找到的不是字典: {schema_name}")
|
||||
else:
|
||||
self.logger.warning(f"尝试从 #/components/schemas/ 解析引用 '{ref_value}' 失败:无法找到 'components.schemas' 结构。")
|
||||
|
||||
if not resolved_schema and ref_value.startswith("#/definitions/"):
|
||||
schema_name = ref_value.split("/")[-1]
|
||||
definitions = actual_global_spec_dict.get("definitions")
|
||||
if definitions and isinstance(definitions, dict):
|
||||
resolved_schema = definitions.get(schema_name)
|
||||
if resolved_schema and isinstance(resolved_schema, dict):
|
||||
self.logger.info(f"成功从 #/definitions/ 解析引用 '{ref_value}'。")
|
||||
return resolved_schema
|
||||
else:
|
||||
self.logger.warning(f"解析引用 '{ref_value}' (路径: #/definitions/) 失败:未找到或找到的不是字典: {schema_name}")
|
||||
else:
|
||||
self.logger.warning(f"尝试从 #/definitions/ 解析引用 '{ref_value}' 失败:无法找到 'definitions' 结构。")
|
||||
|
||||
if not resolved_schema:
|
||||
self.logger.warning(f"最终未能通过任一已知路径 (#/components/schemas/ 或 #/definitions/) 解析引用 '{ref_value}'。")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"解析引用 '{ref_value}' 时发生错误: {e}", exc_info=True)
|
||||
# 根据用户进一步要求,方法体简化为直接返回,不进行任何 $ref/$ $$ref 的检查。
|
||||
# self.logger.debug(f"_resolve_ref_if_present called. Returning schema as-is per new configuration.")
|
||||
return schema_to_resolve
|
||||
|
||||
# No generate_request_body, or it simply returns current_body
|
||||
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,247 @@
|
||||
import re
|
||||
import json # 确保导入json
|
||||
from typing import Dict, Any, Optional, List
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext
|
||||
# LLMService的导入路径需要根据您的项目结构确认
|
||||
# 假设 LLMService 在 ddms_compliance_suite.llm_utils.llm_service
|
||||
try:
|
||||
from ddms_compliance_suite.llm_utils.llm_service import LLMService
|
||||
except ImportError:
|
||||
LLMService = None
|
||||
# print("LLMService not found, PathVerbNounCheckCase will be skipped or limited.")
|
||||
|
||||
class ComprehensiveURLCheckLLMCase(BaseAPITestCase):
|
||||
id = "TC-NORMATIVE-URL-LLM-COMPREHENSIVE-001"
|
||||
name = "综合URL规范与RESTful风格检查 (LLM)"
|
||||
description = (
|
||||
"使用LLM统一评估API路径是否符合以下规范:\n"
|
||||
"1. 路径参数命名 (例如,全小写蛇形命名法)。\n"
|
||||
"2. URL路径结构 (例如,/{领域}/{版本号}/资源类型)。\n"
|
||||
"3. URL版本号嵌入 (例如,包含 /v1/)。\n"
|
||||
"4. RESTful风格与可读性 (名词表示资源,HTTP方法表示动作,易理解性)。"
|
||||
)
|
||||
severity = TestSeverity.MEDIUM # 综合性检查,可能包含不同严重级别的问题
|
||||
tags = ["normative-spec", "url", "restful", "llm", "readability", "naming-convention", "structure", "versioning", "static-check"]
|
||||
execution_order = 60 # 更新执行顺序
|
||||
|
||||
# 此测试用例可以覆盖所有路径,但其有效性依赖LLM
|
||||
# applicable_methods = None
|
||||
# applicable_paths_regex = None
|
||||
|
||||
# 这个标志可以用来在测试用例级别控制是否实际调用LLM,即使全局LLM服务可用
|
||||
# 如果您希望总是尝试(只要LLMService能初始化),可以不设置这个,或者在逻辑中检查 self.llm_service 是否存在
|
||||
# use_llm_for_path_analysis: bool = True
|
||||
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[LLMService] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator)
|
||||
self.llm_service = llm_service # 存储注入的 LLMService 实例
|
||||
if not self.llm_service:
|
||||
self.logger.warning(f"LLMService 未注入或初始化失败,测试用例 {self.id} 将无法执行LLM路径分析。")
|
||||
|
||||
def _get_llm_service_from_orchestrator(self) -> Optional[Any]:
|
||||
# 在实际框架中,测试用例可能无法直接访问编排器来获取LLM服务。
|
||||
# 这种依赖注入通常在测试用例实例化时或方法调用时处理。
|
||||
# 此处为一个占位符,理想情况下APITestOrchestrator会将llm_service实例传给需要它的测试用例,
|
||||
# 或测试用例通过某种服务定位器获取。
|
||||
# 暂时我们假设,如果全局配置了LLM,它就能用。
|
||||
# 真实的实现需要APITestOrchestrator在执行此测试用例前,将llm_service实例注入。
|
||||
# 为了能运行,我们先返回None,并在下面逻辑中处理。
|
||||
# 或者,修改 Orchestrator 将其注入到 self.global_api_spec 或 self.endpoint_spec (不推荐)
|
||||
# 最好的方式是在 __init__ 中接收一个 llm_service: Optional[LLMService] 参数。
|
||||
# 但这需要修改 BaseAPITestCase 和 APITestOrchestrator 的 __init__ 和调用逻辑。
|
||||
|
||||
# 临时的解决方法:依赖 APITestOrchestrator 初始化时是否成功创建了 LLMService。
|
||||
# 这仍然是一个间接的检查。一个更直接的方式是在Orchestrator执行此测试用例时传入。
|
||||
if hasattr(self, '_orchestrator_llm_service_instance') and self._orchestrator_llm_service_instance:
|
||||
return self._orchestrator_llm_service_instance
|
||||
|
||||
# 如果没有明确注入,我们只能依赖全局LLMService是否被加载
|
||||
if LLMService is not None:
|
||||
# 这里不能直接实例化一个新的LLMService,因为它需要API Key等配置,这些配置在Orchestrator那里。
|
||||
# 这个测试用例需要依赖Orchestrator来提供一个已经配置好的LLMService实例。
|
||||
# 此处返回一个指示:如果LLM功能应该被使用,则需要Orchestrator提供服务。
|
||||
return "NEEDS_INJECTION"
|
||||
return None
|
||||
|
||||
def _extract_path_param_names(self, path_template: str) -> List[str]:
|
||||
"""从路径模板中提取路径参数名称。例如 /users/{user_id}/items/{item_id} -> ['user_id', 'item_id']"""
|
||||
return re.findall(r'\{([^}]+)\}', path_template)
|
||||
|
||||
def validate_request_url(self, url: str, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
results: List[ValidationResult] = []
|
||||
path_template = self.endpoint_spec.get('path', '')
|
||||
http_method = request_context.method.upper()
|
||||
operation_id = self.endpoint_spec.get('operationId', self.endpoint_spec.get('title', '')) # 获取operationId或title
|
||||
|
||||
if not self.llm_service:
|
||||
results.append(ValidationResult(
|
||||
passed=True, # 标记为通过以避免阻塞,但消息表明跳过
|
||||
message=f"路径 '{path_template}' 的LLM综合URL检查已跳过:LLM服务不可用。",
|
||||
details={"path_template": path_template, "http_method": http_method, "reason": "LLM Service not available or not injected."}
|
||||
))
|
||||
self.logger.warning(f"LLM综合URL检查已跳过对路径 '{path_template}' 的检查:LLM服务不可用。")
|
||||
return results
|
||||
|
||||
path_param_names = self._extract_path_param_names(path_template)
|
||||
path_params_str = ", ".join(path_param_names) if path_param_names else "无"
|
||||
|
||||
# - 接口名称 (OperationId 或 Title): {operation_id if operation_id else '请你自己从路径模板中提取'}
|
||||
|
||||
# 构建给LLM的Prompt,要求JSON输出
|
||||
prompt_instruction = f"""
|
||||
请扮演一位资深的API设计评审员。我将提供一个API端点的路径模板、HTTP方法以及可能的接口名称。
|
||||
请根据以下石油行业API设计规范评估此API端点,并以严格的JSON格式返回您的评估结果。
|
||||
JSON对象应包含一个名为 "assessments" 的键,其值为一个对象列表,每个对象代表对一个标准的评估,包含 "standard_name" (字符串), "is_compliant" (布尔值), 和 "reason" (字符串) 三个键。
|
||||
|
||||
API端点信息:
|
||||
- HTTP方法: {http_method}
|
||||
- 路径模板: {path_template}
|
||||
- 路径中提取的参数名: [{path_params_str}]
|
||||
|
||||
评估标准:
|
||||
|
||||
1. **接口名称规范 (接口名称需要你从路径模板中提取,一般是路径中除了参数名以外的最后的一个单词)**:
|
||||
- 规则: 采用'动词+名词'结构,明确业务语义 (例如: GetWellLog, SubmitSeismicJob)。
|
||||
- standard_name: "interface_naming_convention"
|
||||
|
||||
2. **HTTP方法使用规范**:
|
||||
- 规则: 遵循RESTful规范:GET用于数据检索, POST用于创建资源, PUT用于更新资源, DELETE用于删除资源。
|
||||
- standard_name: "http_method_usage"
|
||||
|
||||
3. **URL路径结构规范**:
|
||||
- 规则: 格式为 `<前缀>/<专业领域>/v<版本号>/<资源类型>` (例如: /logging/v1.2/wells, /seismicprospecting/v1.0/datasets)。
|
||||
- 前缀: 示例: /api/dms
|
||||
- 专业领域: 专业领域示例: seismicprospecting, welllogging, reservoirevaluation
|
||||
- 版本号: 语义化版本,例如 v1, v1.0, v2.1.3。
|
||||
- 资源类型: 通常为名词复数。
|
||||
- standard_name: "url_path_structure"
|
||||
|
||||
4. **URL路径参数命名规范**:
|
||||
- 规则: 路径参数(如果存在)必须使用全小写字母(可以是一个单词)或小写字母加下划线命名(这是多个单词的情况),并能反映资源的唯一标识 (例如: {{well_id}},{{version}},{{schema}})。
|
||||
- standard_name: "url_path_parameter_naming"
|
||||
|
||||
5. **资源命名规范 (在路径中)**:
|
||||
- 规则: 资源集合应使用名词的复数形式表示 (例如 `/wells`, `/logs`);应优先使用石油行业的标准术语 (例如用 `trajectory` 而非 `path` 来表示井轨迹)。
|
||||
- standard_name: "resource_naming_in_path"
|
||||
- standard_name: "resource"
|
||||
- standard_name: "schema"
|
||||
- standard_name: "version"
|
||||
|
||||
|
||||
|
||||
请确保您的输出是一个可以被 `json.loads()` 直接解析的JSON对象。
|
||||
例如:
|
||||
{{
|
||||
"assessments": [
|
||||
{{
|
||||
"standard_name": "interface_naming_convention",
|
||||
"is_compliant": true,
|
||||
"reason": "接口名称 'GetWellboreTrajectory' 符合动词+名词结构。"
|
||||
}},
|
||||
{{
|
||||
"standard_name": "http_method_usage",
|
||||
"is_compliant": true,
|
||||
"reason": "GET方法用于检索资源,符合规范。"
|
||||
}}
|
||||
// ... 其他标准的评估 ...
|
||||
]
|
||||
}}
|
||||
"""
|
||||
|
||||
# 6. **路径可读性与整体RESTful风格**:
|
||||
# - 规则: 路径整体是否具有良好的可读性、易于理解其功能,并且符合RESTful设计原则?(综合评估,可参考前面几点)
|
||||
# - standard_name: "general_readability_and_restfulness"
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "你是一位API设计评审专家,专注于评估API的URL规范性和RESTful风格。你的输出必须是严格的JSON格式。"},
|
||||
{"role": "user", "content": prompt_instruction}
|
||||
]
|
||||
|
||||
self.logger.info(f"向LLM发送请求,评估路径: {path_template} ({http_method})")
|
||||
# 假设 _execute_chat_completion_request 支持 response_format={"type": "json_object"} (如果LLM API支持)
|
||||
# 否则,我们需要解析文本输出。为简化,这里假设LLM会遵循JSON格式指令。
|
||||
llm_response_str = self.llm_service._execute_chat_completion_request(
|
||||
messages=messages,
|
||||
max_tokens=1024, # 根据评估结果的复杂度调整
|
||||
temperature=0.2 # 低温以获得更确定的、结构化的输出
|
||||
)
|
||||
|
||||
if not llm_response_str:
|
||||
results.append(ValidationResult(
|
||||
passed=False, # 执行失败
|
||||
message=f"未能从LLM获取对路径 '{path_template}' 的评估。",
|
||||
details={"path_template": path_template, "http_method": http_method, "reason": "LLM did not return a response."}
|
||||
))
|
||||
self.logger.error(f"LLM对路径 '{path_template}' 的评估请求未返回任何内容。")
|
||||
return results
|
||||
|
||||
self.logger.debug(f"LLM对路径 '{path_template}' 的原始响应: {llm_response_str}")
|
||||
|
||||
try:
|
||||
# 尝试清理并解析LLM响应
|
||||
# 有时LLM可能在JSON前后添加 "```json" 和 "```"
|
||||
cleaned_response_str = llm_response_str.strip()
|
||||
if cleaned_response_str.startswith("```json"):
|
||||
cleaned_response_str = cleaned_response_str[7:]
|
||||
if cleaned_response_str.endswith("```"):
|
||||
cleaned_response_str = cleaned_response_str[:-3]
|
||||
|
||||
llm_assessment_data = json.loads(cleaned_response_str)
|
||||
|
||||
if "assessments" not in llm_assessment_data or not isinstance(llm_assessment_data["assessments"], list):
|
||||
raise ValueError("LLM响应JSON中缺少 'assessments' 列表或格式不正确。")
|
||||
|
||||
found_assessments = False
|
||||
for assessment in llm_assessment_data["assessments"]:
|
||||
standard_name = assessment.get("standard_name", "未知标准")
|
||||
is_compliant = assessment.get("is_compliant", False)
|
||||
reason = assessment.get("reason", "LLM未提供原因。")
|
||||
found_assessments = True
|
||||
|
||||
results.append(ValidationResult(
|
||||
passed=is_compliant,
|
||||
message=f"LLM评估 - {standard_name}: {reason}",
|
||||
details={
|
||||
"standard_name": standard_name,
|
||||
"is_compliant_by_llm": is_compliant,
|
||||
"llm_reason": reason,
|
||||
"path_template": path_template,
|
||||
"http_method": http_method
|
||||
}
|
||||
))
|
||||
log_level = self.logger.info if is_compliant else self.logger.warning
|
||||
log_level(f"LLM评估 - 标准 '{standard_name}' for '{path_template}': {'符合' if is_compliant else '不符合'}。原因: {reason}")
|
||||
|
||||
if not found_assessments:
|
||||
results.append(ValidationResult(
|
||||
passed=False,
|
||||
message=f"LLM返回的评估结果中不包含任何有效的评估项。",
|
||||
details={"path_template": path_template, "http_method": http_method, "raw_llm_response": llm_response_str}
|
||||
))
|
||||
|
||||
|
||||
except json.JSONDecodeError as e_json:
|
||||
results.append(ValidationResult(
|
||||
passed=False, # 执行失败
|
||||
message=f"无法将LLM对路径 '{path_template}' 的评估响应解析为JSON: {e_json}",
|
||||
details={"path_template": path_template, "http_method": http_method, "raw_llm_response": llm_response_str, "error": str(e_json)}
|
||||
))
|
||||
self.logger.error(f"LLM对路径 '{path_template}' 的响应JSON解析失败: {e_json}. Raw response: {llm_response_str}")
|
||||
except ValueError as e_val: # 自定义错误,如缺少 'assessments'
|
||||
results.append(ValidationResult(
|
||||
passed=False, # 执行失败
|
||||
message=f"LLM对路径 '{path_template}' 的评估响应JSON结构不符合预期: {e_val}",
|
||||
details={"path_template": path_template, "http_method": http_method, "raw_llm_response": llm_response_str, "error": str(e_val)}
|
||||
))
|
||||
self.logger.error(f"LLM对路径 '{path_template}' 的响应JSON结构错误: {e_val}. Raw response: {llm_response_str}")
|
||||
except Exception as e_generic:
|
||||
results.append(ValidationResult(
|
||||
passed=False, # 执行失败
|
||||
message=f"处理LLM对路径 '{path_template}' 的评估响应时发生未知错误: {e_generic}",
|
||||
details={"path_template": path_template, "http_method": http_method, "raw_llm_response": llm_response_str, "error": str(e_generic)}
|
||||
))
|
||||
self.logger.error(f"处理LLM对路径 '{path_template}' 的响应时发生未知错误: {e_generic}", exc_info=True)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
BIN
Binary file not shown.
@@ -12,8 +12,8 @@ class HTTPSMandatoryCase(BaseAPITestCase):
|
||||
|
||||
# 此测试会修改URL为HTTP,应适用于大多数端点。
|
||||
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator)
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service=llm_service)
|
||||
self.logger.info(f"测试用例 '{self.id}' 已为端点 '{self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')}' 初始化。")
|
||||
|
||||
def modify_request_url(self, current_url: str) -> str:
|
||||
|
||||
Reference in New Issue
Block a user