v0.2
This commit is contained in:
Binary file not shown.
@@ -12,7 +12,8 @@ class StatusCode200Check(BaseAPITestCase):
|
||||
# 适用于所有方法和路径 (默认)
|
||||
# applicable_methods = None
|
||||
# applicable_paths_regex = None
|
||||
execution_order = 10 # 示例执行顺序
|
||||
execution_order = 1 # 执行顺序
|
||||
is_critical_setup_test = True
|
||||
# use_llm_for_body: bool = True
|
||||
# use_llm_for_path_params: bool = True
|
||||
# use_llm_for_query_params: bool = True
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+67
-88
@@ -1,7 +1,7 @@
|
||||
from typing import Dict, Any, Optional, List, Union
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
|
||||
import logging
|
||||
from ddms_compliance_suite.utils import schema_utils # 导入新的工具模块
|
||||
from ddms_compliance_suite.utils import schema_utils # Keep this import for util_remove_value_at_path
|
||||
|
||||
class MissingRequiredFieldBodyCase(BaseAPITestCase):
|
||||
id = "TC-ERROR-4003-BODY"
|
||||
@@ -12,57 +12,22 @@ class MissingRequiredFieldBodyCase(BaseAPITestCase):
|
||||
execution_order = 210
|
||||
|
||||
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.removed_field_path: Optional[List[Union[str, int]]] = None # Path can contain int for array indices
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service)
|
||||
self.logger = logging.getLogger(f"testcase.{self.id}") # Already set in super, but can be re-set if specific sub-logger is needed. Better to rely on super's logger.
|
||||
self.original_value_at_path: Any = None
|
||||
|
||||
# --- Framework Utility Access ---
|
||||
# orchestrator_placeholder 及其检查逻辑现在可以移除
|
||||
# --- End Framework Utility Access ---
|
||||
|
||||
self._try_find_removable_body_field()
|
||||
|
||||
def _get_request_body_schema(self) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Helper to get the (already $ref-resolved) request body schema from self.endpoint_spec.
|
||||
"""
|
||||
request_body_spec = self.endpoint_spec.get("requestBody")
|
||||
if request_body_spec and isinstance(request_body_spec, dict):
|
||||
content = request_body_spec.get("content", {})
|
||||
# Iterate through common JSON content types or prioritize application/json
|
||||
for ct in ["application/json", "application/merge-patch+json", "*/*"]:
|
||||
if ct in content:
|
||||
media_type_obj = content[ct]
|
||||
if isinstance(media_type_obj, dict) and isinstance(media_type_obj.get("schema"), dict):
|
||||
return media_type_obj["schema"]
|
||||
|
||||
# Fallback for OpenAPI 2.0 (Swagger) style 'in: body' parameter
|
||||
parameters = self.endpoint_spec.get("parameters", [])
|
||||
if isinstance(parameters, list):
|
||||
for param in parameters:
|
||||
if isinstance(param, dict) and param.get("in") == "body":
|
||||
if isinstance(param.get("schema"), dict):
|
||||
# Schema for 'in: body' parameter is directly usable
|
||||
return param["schema"]
|
||||
self.logger.debug("No suitable request body schema found in endpoint_spec.")
|
||||
return None
|
||||
|
||||
def _try_find_removable_body_field(self):
|
||||
body_schema_to_check = self._get_request_body_schema()
|
||||
|
||||
if body_schema_to_check:
|
||||
self.removed_field_path = schema_utils.util_find_removable_field_path_recursive(
|
||||
current_schema=body_schema_to_check,
|
||||
current_path=[],
|
||||
full_api_spec_for_refs=self.global_api_spec
|
||||
# Use new helper methods from BaseAPITestCase
|
||||
body_schema = self._get_resolved_request_body_schema()
|
||||
if body_schema:
|
||||
self.removed_field_path = self._find_removable_field_path(
|
||||
schema_to_search=body_schema,
|
||||
schema_name_for_log="request body"
|
||||
)
|
||||
if self.removed_field_path:
|
||||
self.logger.info(f"必填字段缺失测试的目标字段 (请求体): '{'.'.join(map(str, self.removed_field_path))}'")
|
||||
else:
|
||||
self.logger.info('在请求体 schema 中未找到可用于测试 "必填字段缺失" 的字段。')
|
||||
if not self.removed_field_path:
|
||||
self.logger.info('在请求体 schema 中未找到可用于测试 "必填字段缺失" 的字段(通过基类方法)。')
|
||||
else:
|
||||
self.logger.info('此端点规范中未定义或找到请求体 schema。')
|
||||
self.logger.info('此端点规范中未定义或找到请求体 schema(通过基类方法)。')
|
||||
self.removed_field_path = None
|
||||
|
||||
def generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
self.logger.debug(f"{self.id} is focused on request body, generate_query_params will not modify query parameters.")
|
||||
@@ -85,59 +50,73 @@ class MissingRequiredFieldBodyCase(BaseAPITestCase):
|
||||
self.logger.error(f"使用工具方法移除请求体字段路径 '{'.'.join(map(str, self.removed_field_path))}' 失败。将返回原始请求体。")
|
||||
# Restore original_value_at_path to None since removal failed
|
||||
self.original_value_at_path = None
|
||||
return current_body
|
||||
return current_body
|
||||
|
||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
results = []
|
||||
if not self.removed_field_path:
|
||||
# This case should ideally be caught by _try_find_removable_body_field and the test case might be skipped
|
||||
# by the orchestrator if it has a mechanism to check if a test case is applicable/configurable.
|
||||
# For now, the test case itself handles this.
|
||||
results.append(self.passed("跳过测试:在API规范中未找到合适的必填请求体字段用于移除测试。"))
|
||||
self.logger.info("由于未识别到可移除的必填请求体字段,跳过此测试用例的验证。")
|
||||
return results
|
||||
|
||||
# If original_value_at_path is None AND removed_field_path is set, it might mean the removal operation
|
||||
# in generate_request_body failed or the field wasn't in the provided current_body.
|
||||
# This check can make the test more robust.
|
||||
if self.original_value_at_path is None and self.removed_field_path:
|
||||
# Check if the field was simply not present in the input `current_body` to `generate_request_body`
|
||||
# This logic is tricky because `_util_remove_value_at_path` might return success=False if path DNE.
|
||||
# For now, we rely on the success flag from `_util_remove_value_at_path`.
|
||||
# If generate_request_body returned original_body due to failure, this validation might be misleading.
|
||||
# The logger in generate_request_body should indicate the failure.
|
||||
pass
|
||||
|
||||
|
||||
status_code = response_context.status_code
|
||||
json_content = response_context.json_content
|
||||
expected_status_codes = [400, 422] # As per many API guidelines for client errors
|
||||
specific_error_code_from_appendix_b = "4003" # Or a similar code indicating missing required field
|
||||
|
||||
removed_field_str = '.'.join(map(str, self.removed_field_path))
|
||||
msg_prefix = f"当移除必填请求体字段 '{removed_field_str}' 时,"
|
||||
expected_http_status_codes = [400, 422] # Common client error codes
|
||||
# specific_business_error_code 是此测试用例期望的特定业务错误码,例如 "4003"
|
||||
# 这个值可以根据实际 API 的错误码约定在子类中调整或作为参数传入
|
||||
specific_business_error_code = "4003"
|
||||
error_code_field_in_body = "code" # 响应体中业务错误码的字段名
|
||||
|
||||
if status_code in expected_status_codes:
|
||||
status_msg = f"{msg_prefix}API响应了预期的错误状态码 {status_code}。"
|
||||
# Check for specific error code in response body if available
|
||||
if json_content and isinstance(json_content, dict) and str(json_content.get("code")) == specific_error_code_from_appendix_b:
|
||||
results.append(self.passed(f"{status_msg} 且响应体中包含特定的错误码 '{specific_error_code_from_appendix_b}'。"))
|
||||
self.logger.info(f"正确接收到状态码 {status_code} 和错误码 '{specific_error_code_from_appendix_b}' (移除字段: body.{removed_field_str})。")
|
||||
elif json_content and isinstance(json_content, dict) and "code" in json_content:
|
||||
# Still pass because the HTTP status code is correct, but log a warning about the specific code
|
||||
results.append(ValidationResult(passed=True,
|
||||
message=f"{status_msg} 响应体中的错误码为 '{json_content.get('code')}' (期望或类似 '{specific_error_code_from_appendix_b}')。",
|
||||
details={"expected_code_detail": specific_error_code_from_appendix_b, "response_body": json_content}
|
||||
removed_field_str = '.'.join(map(str, self.removed_field_path))
|
||||
context_msg_prefix = f"当移除必填请求体字段 '{removed_field_str}' 时,"
|
||||
|
||||
http_status_ok = status_code in expected_http_status_codes
|
||||
business_code_ok = False
|
||||
is_4xx_error = 400 <= status_code <= 499
|
||||
|
||||
if json_content and isinstance(json_content, dict):
|
||||
body_code = json_content.get(error_code_field_in_body)
|
||||
if body_code is not None and str(body_code) == specific_business_error_code:
|
||||
business_code_ok = True
|
||||
|
||||
if http_status_ok:
|
||||
if business_code_ok:
|
||||
results.append(self.passed(
|
||||
f"{context_msg_prefix}API响应了预期的错误状态码 {status_code} 并且响应体中包含预期的业务错误码 '{specific_business_error_code}' (字段: '{error_code_field_in_body}')."
|
||||
))
|
||||
self.logger.warning(f"接收到状态码 {status_code},但内部错误码是 '{json_content.get('code')}' 而不是期望的 '{specific_error_code_from_appendix_b}' (移除字段: body.{removed_field_str})。此结果仍标记为通过,因状态码正确。")
|
||||
self.logger.info(f"{self.id}: Passed. HTTP status {status_code} and business code '{specific_business_error_code}' match. (Removed field: body.{removed_field_str})")
|
||||
else:
|
||||
results.append(self.passed(f"{status_msg} 但响应体中未找到特定的错误码字段 ('code') 或响应体结构不符合预期。"))
|
||||
self.logger.info(f"正确接收到状态码 {status_code},但在响应体中未找到错误码字段或预期结构 (移除字段: body.{removed_field_str})。")
|
||||
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"body.{removed_field_str}"}
|
||||
# HTTP status is OK, but business code is not what we specifically hoped for (or not present).
|
||||
# Still considered a pass because the primary condition (HTTP status) is met.
|
||||
results.append(self.passed(
|
||||
f"{context_msg_prefix}API响应了预期的错误状态码 {status_code}. "
|
||||
f"响应体中的业务错误码 (\'{error_code_field_in_body}\': \'{json_content.get(error_code_field_in_body)}\') 与特定期望 \'{specific_business_error_code}\' 不符或未找到,但HTTP状态码正确。"
|
||||
))
|
||||
self.logger.info(f"{self.id}: Passed. HTTP status {status_code} is correct. Business code mismatch/missing (Expected: \'{specific_business_error_code}\', Got: \'{json_content.get(error_code_field_in_body)}\'). (Removed field: body.{removed_field_str})")
|
||||
|
||||
elif business_code_ok:
|
||||
# HTTP status was not in the primary list, but it's a 4xx and the business code matches.
|
||||
results.append(self.passed(
|
||||
f"{context_msg_prefix}API响应了状态码 {status_code} (非主要预期HTTP状态 {expected_http_status_codes},但为4xx客户端错误), "
|
||||
f"且响应体中包含预期的业务错误码 '{specific_business_error_code}' (字段: '{error_code_field_in_body}')."
|
||||
))
|
||||
self.logger.warning(f"必填请求体字段缺失测试失败:期望状态码 {expected_status_codes},实际为 {status_code} (移除字段: body.{removed_field_str})。")
|
||||
self.logger.info(f"{self.id}: Passed (Fallback). HTTP status {status_code} (4xx) with matching business code '{specific_business_error_code}'. (Removed field: body.{removed_field_str})")
|
||||
|
||||
else:
|
||||
# Neither condition for passing was met.
|
||||
fail_message = f"{context_msg_prefix}期望API返回状态码在 {expected_http_status_codes} 中,或返回4xx客户端错误且业务码为 '{specific_business_error_code}'."
|
||||
fail_message += f" 实际收到状态码 {status_code}."
|
||||
if json_content and isinstance(json_content, dict):
|
||||
fail_message += f" 响应体中的业务码 (\'{error_code_field_in_body}\') 为 \'{json_content.get(error_code_field_in_body)}\'."
|
||||
elif json_content:
|
||||
fail_message += " 响应体不是一个JSON对象."
|
||||
else:
|
||||
fail_message += " 响应体为空或非JSON."
|
||||
|
||||
results.append(self.failed(
|
||||
message=fail_message,
|
||||
details={"status_code": status_code, "response_body": json_content, "expected_http_status_codes": expected_http_status_codes, "expected_business_code": specific_business_error_code, "removed_field": f"body.{removed_field_str}"}
|
||||
))
|
||||
self.logger.warning(f"{self.id}: Failed. {fail_message} (Removed field: body.{removed_field_str})")
|
||||
|
||||
return results
|
||||
+54
-33
@@ -11,23 +11,20 @@ class MissingRequiredFieldQueryCase(BaseAPITestCase):
|
||||
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, llm_service: Optional[Any] = None):
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator=json_schema_validator, llm_service=llm_service)
|
||||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service=llm_service)
|
||||
self.target_param_name: Optional[str] = None
|
||||
# Location is always 'query' for this class
|
||||
self.target_param_location: str = "query"
|
||||
self.original_query_params: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Call the simplified method to find the target parameter
|
||||
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", [])
|
||||
if query_params_spec_list:
|
||||
self.logger.debug(f"检查查询参数的必填字段,总共 {len(query_params_spec_list)} 个参数定义。")
|
||||
for param_spec in query_params_spec_list:
|
||||
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.target_param_name = field_name
|
||||
self.logger.info(f"必填字段缺失测试的目标字段 (查询参数): '{self.target_param_name}'")
|
||||
return
|
||||
self.logger.info('在此端点规范中未找到可用于测试 "必填查询参数缺失" 的字段。')
|
||||
"""Uses the base class helper to find a required query parameter."""
|
||||
self.target_param_name = self._find_required_parameter_name("query")
|
||||
# Logging about success/failure is handled by the base class method and the __init__ method.
|
||||
|
||||
def generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]:
|
||||
# This test case focuses on query parameters, so it does not modify the request body.
|
||||
@@ -55,31 +52,55 @@ class MissingRequiredFieldQueryCase(BaseAPITestCase):
|
||||
|
||||
status_code = response_context.status_code
|
||||
json_content = response_context.json_content
|
||||
|
||||
expected_status_codes = [400, 422]
|
||||
specific_error_code_from_appendix_b = "4003"
|
||||
expected_http_status_codes = [400, 422]
|
||||
specific_business_error_code = "4003"
|
||||
error_code_field_in_body = "code"
|
||||
|
||||
msg_prefix = f"当移除必填查询参数 '{self.target_param_name}' 时,"
|
||||
context_msg_prefix = f"当移除必填查询参数 '{self.target_param_name}' 时,"
|
||||
|
||||
if status_code in expected_status_codes:
|
||||
status_msg = f"{msg_prefix}API响应了预期的错误状态码 {status_code}。"
|
||||
if json_content and isinstance(json_content, dict) and str(json_content.get("code")) == specific_error_code_from_appendix_b:
|
||||
results.append(self.passed(f"{status_msg} 且响应体中包含特定的错误码 '{specific_error_code_from_appendix_b}'。"))
|
||||
self.logger.info(f"正确接收到状态码 {status_code} 和错误码 '{specific_error_code_from_appendix_b}'。")
|
||||
elif json_content and isinstance(json_content, dict) and "code" in json_content:
|
||||
results.append(ValidationResult(passed=True,
|
||||
message=f"{status_msg} 响应体中的错误码为 '{json_content.get('code')}' (期望或类似 '{specific_error_code_from_appendix_b}')。",
|
||||
details=json_content
|
||||
http_status_ok = status_code in expected_http_status_codes
|
||||
business_code_ok = False
|
||||
is_4xx_error = 400 <= status_code <= 499
|
||||
|
||||
if json_content and isinstance(json_content, dict):
|
||||
body_code = json_content.get(error_code_field_in_body)
|
||||
if body_code is not None and str(body_code) == specific_business_error_code:
|
||||
business_code_ok = True
|
||||
|
||||
if http_status_ok:
|
||||
if business_code_ok:
|
||||
results.append(self.passed(
|
||||
f"{context_msg_prefix}API响应了预期的错误状态码 {status_code} 并且响应体中包含预期的业务错误码 '{specific_business_error_code}' (字段: '{error_code_field_in_body}')."
|
||||
))
|
||||
self.logger.warning(f"接收到状态码 {status_code},但错误码是 '{json_content.get('code')}' 而不是期望的 '{specific_error_code_from_appendix_b}'。此结果仍标记为通过,因状态码正确。")
|
||||
self.logger.info(f"{self.id}: Passed. HTTP status {status_code} and business code '{specific_business_error_code}' match. (Removed query param: {self.target_param_name})")
|
||||
else:
|
||||
results.append(self.passed(f"{status_msg} 但响应体中未找到特定的错误码字段或响应体结构不符合预期。"))
|
||||
self.logger.info(f"正确接收到状态码 {status_code},但在响应体中未找到错误码字段或预期结构。")
|
||||
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.target_param_name}"}
|
||||
results.append(self.passed(
|
||||
f"{context_msg_prefix}API响应了预期的错误状态码 {status_code}. "
|
||||
f"响应体中的业务错误码 (\'{error_code_field_in_body}\': \'{json_content.get(error_code_field_in_body)}\') 与特定期望 \'{specific_business_error_code}\' 不符或未找到,但HTTP状态码正确。"
|
||||
))
|
||||
self.logger.info(f"{self.id}: Passed. HTTP status {status_code} is correct. Business code mismatch/missing (Expected: \'{specific_business_error_code}\', Got: \'{json_content.get(error_code_field_in_body)}\'). (Removed query param: {self.target_param_name})")
|
||||
|
||||
elif business_code_ok:
|
||||
results.append(self.passed(
|
||||
f"{context_msg_prefix}API响应了状态码 {status_code} (非主要预期HTTP状态 {expected_http_status_codes},但为4xx客户端错误), "
|
||||
f"且响应体中包含预期的业务错误码 '{specific_business_error_code}' (字段: '{error_code_field_in_body}')."
|
||||
))
|
||||
self.logger.warning(f"必填查询参数缺失测试失败:期望状态码 {expected_status_codes},实际为 {status_code}。移除的参数:'{self.target_param_name}'")
|
||||
self.logger.info(f"{self.id}: Passed (Fallback). HTTP status {status_code} (4xx) with matching business code '{specific_business_error_code}'. (Removed query param: {self.target_param_name})")
|
||||
|
||||
else:
|
||||
fail_message = f"{context_msg_prefix}期望API返回状态码在 {expected_http_status_codes} 中,或返回4xx客户端错误且业务码为 '{specific_business_error_code}'."
|
||||
fail_message += f" 实际收到状态码 {status_code}."
|
||||
if json_content and isinstance(json_content, dict):
|
||||
fail_message += f" 响应体中的业务码 (\'{error_code_field_in_body}\') 为 \'{json_content.get(error_code_field_in_body)}\'."
|
||||
elif json_content:
|
||||
fail_message += " 响应体不是一个JSON对象."
|
||||
else:
|
||||
fail_message += " 响应体为空或非JSON."
|
||||
|
||||
results.append(self.failed(
|
||||
message=fail_message,
|
||||
details={"status_code": status_code, "response_body": json_content, "expected_http_status_codes": expected_http_status_codes, "expected_business_code": specific_business_error_code, "removed_param": f"query.{self.target_param_name}"}
|
||||
))
|
||||
self.logger.warning(f"{self.id}: Failed. {fail_message} (Removed query param: {self.target_param_name})")
|
||||
|
||||
return results
|
||||
@@ -1,7 +1,8 @@
|
||||
from typing import Dict, Any, Optional, List
|
||||
from typing import Dict, Any, Optional, List, Union
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
|
||||
import copy
|
||||
import logging
|
||||
from ddms_compliance_suite.utils import schema_utils
|
||||
|
||||
class TypeMismatchBodyCase(BaseAPITestCase):
|
||||
id = "TC-ERROR-4001-BODY"
|
||||
@@ -25,317 +26,119 @@ class TypeMismatchBodyCase(BaseAPITestCase):
|
||||
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')} 初始化请求体类型不匹配测试的目标字段查找。")
|
||||
|
||||
body_schema_to_check: Optional[Dict[str, Any]] = None
|
||||
|
||||
# 优先尝试从顶层 'requestBody' (OpenAPI 3.0 style) 获取 schema
|
||||
request_body_spec = self.endpoint_spec.get("requestBody")
|
||||
if request_body_spec and isinstance(request_body_spec, dict):
|
||||
content = request_body_spec.get("content", {})
|
||||
json_schema_entry = content.get("application/json") # 或者其他相关mime-type
|
||||
if json_schema_entry and isinstance(json_schema_entry, dict) and isinstance(json_schema_entry.get("schema"), dict):
|
||||
body_schema_to_check = json_schema_entry["schema"]
|
||||
self.logger.debug(f"从顶层 'requestBody' 中获取到 schema: {list(body_schema_to_check.keys())}")
|
||||
|
||||
# 如果顶层 'requestBody' 未提供有效 schema,则尝试从 'parameters' 列表 (Swagger 2.0 style for 'in: body') 查找
|
||||
if not body_schema_to_check:
|
||||
self.logger.debug(f"未从顶层 'requestBody' 找到 schema,尝试从 'parameters' 列表查找 'in: body' 参数。")
|
||||
parameters = self.endpoint_spec.get("parameters", [])
|
||||
if isinstance(parameters, list):
|
||||
for param in parameters:
|
||||
if isinstance(param, dict) and param.get("in") == "body":
|
||||
if isinstance(param.get("schema"), dict):
|
||||
body_schema_to_check = param["schema"]
|
||||
self.logger.debug(f"从 'parameters' 列表中找到 'in: body' 参数的 schema: {list(body_schema_to_check.keys())}")
|
||||
break # 找到第一个 'in: body' 参数即可
|
||||
else:
|
||||
self.logger.warning(f"找到 'in: body' 参数 '{param.get('name', 'N/A')}',但其 'schema' 字段无效或缺失。")
|
||||
else:
|
||||
self.logger.warning("'parameters' 字段不是列表或不存在。")
|
||||
self.logger.info(f"[{self.id}] Initializing: Looking for a simple type field in request body for type mismatch test.")
|
||||
|
||||
body_schema_to_check = self._get_resolved_request_body_schema()
|
||||
|
||||
if body_schema_to_check:
|
||||
self.logger.debug(f"最终用于检查的请求体 schema: {list(body_schema_to_check.keys())}")
|
||||
if self._find_target_field_in_schema(body_schema_to_check, base_path_for_log=""): # base_path_for_log 为空字符串代表 schema 的根
|
||||
self.logger.info(f"类型不匹配测试的目标字段(请求体): {'.'.join(str(p) for p in self.target_field_path) if self.target_field_path else 'N/A'},原始类型: {self.original_field_type}")
|
||||
found_target = self._find_simple_type_field_in_schema(body_schema_to_check, "request body")
|
||||
if found_target:
|
||||
self.target_field_path, self.original_field_type, self.target_field_schema = found_target
|
||||
self.logger.info(f"[{self.id}] Target field for type mismatch (body): {'.'.join(map(str, self.target_field_path))}, Original Type: {self.original_field_type}")
|
||||
else:
|
||||
self.logger.debug(f"在提供的请求体 schema ({list(body_schema_to_check.keys())}) 中未找到适合类型不匹配测试的字段。")
|
||||
self.logger.info(f"[{self.id}] No suitable simple type field found in request body schema for type mismatch test.")
|
||||
else:
|
||||
self.logger.debug("在此端点规范中未找到有效的请求体 schema 定义 (无论是通过 'requestBody' 还是 'parameters' in:body)。")
|
||||
self.logger.info(f"[{self.id}] No request body schema found for endpoint. Skipping type mismatch target search.")
|
||||
|
||||
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/$ $$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:
|
||||
"""
|
||||
Recursively searches for a simple type field (string, integer, number, boolean) within a schema.
|
||||
Sets self.target_field_path, self.original_field_type, and self.target_field_schema if found.
|
||||
base_path_for_log is used to build the full path for logging.
|
||||
Returns True if a field is found, False otherwise.
|
||||
"""
|
||||
self.logger.debug(f"Enter _find_target_field_in_schema for base_path: '{base_path_for_log}', schema_to_search keys: {list(schema_to_search.keys()) if isinstance(schema_to_search, dict) else 'Not a dict'}")
|
||||
resolved_schema = self._resolve_ref_if_present(schema_to_search)
|
||||
|
||||
if not isinstance(resolved_schema, dict):
|
||||
self.logger.debug(f"_find_target_field_in_schema: Schema at '{base_path_for_log}' is not a dict after resolution. Schema: {resolved_schema}")
|
||||
return False
|
||||
|
||||
schema_type = resolved_schema.get("type")
|
||||
self.logger.debug(f"Path: '{base_path_for_log}', Resolved Schema Type: '{schema_type}', Keys: {list(resolved_schema.keys())}")
|
||||
|
||||
if schema_type == "object":
|
||||
properties = resolved_schema.get("properties", {})
|
||||
self.logger.debug(f"Path: '{base_path_for_log}', Type is 'object'. Checking properties: {list(properties.keys())}")
|
||||
for name, prop_schema_orig in properties.items():
|
||||
current_path_str = f"{base_path_for_log}.{name}" if base_path_for_log else name
|
||||
self.logger.debug(f"Path: '{current_path_str}', Property Schema (Original): {prop_schema_orig}")
|
||||
prop_schema_resolved = self._resolve_ref_if_present(prop_schema_orig)
|
||||
self.logger.debug(f"Path: '{current_path_str}', Property Schema (Resolved): {prop_schema_resolved}")
|
||||
|
||||
if not isinstance(prop_schema_resolved, dict):
|
||||
self.logger.debug(f"Path: '{current_path_str}', Resolved schema is not a dict. Skipping.")
|
||||
continue
|
||||
|
||||
prop_type = prop_schema_resolved.get("type")
|
||||
self.logger.debug(f"Path: '{current_path_str}', Resolved Property Type: '{prop_type}'")
|
||||
|
||||
if prop_type in ["string", "integer", "number", "boolean"]:
|
||||
# Construct path relative to the initial body schema
|
||||
path_parts = base_path_for_log.split('.') if base_path_for_log else []
|
||||
if path_parts == ['']: path_parts = [] # Handle initial empty base_path
|
||||
self.target_field_path = path_parts + [name]
|
||||
self.original_field_type = prop_type
|
||||
self.target_field_schema = prop_schema_resolved
|
||||
self.logger.info(f"目标字段(请求体): '{current_path_str}' (原始类型: '{prop_type}') FOUND!")
|
||||
return True
|
||||
elif prop_type == "object":
|
||||
self.logger.debug(f"Path: '{current_path_str}', Type is 'object'. Recursing...")
|
||||
if self._find_target_field_in_schema(prop_schema_resolved, current_path_str):
|
||||
return True
|
||||
self.logger.debug(f"Path: '{current_path_str}', Recursion for object did not find target.")
|
||||
elif prop_type == "array":
|
||||
self.logger.debug(f"Path: '{current_path_str}', Type is 'array'. Inspecting items...")
|
||||
items_schema = prop_schema_resolved.get("items")
|
||||
if isinstance(items_schema, dict):
|
||||
self.logger.debug(f"Path: '{current_path_str}', Array items schema is a dict. Resolving and checking item type.")
|
||||
items_schema_resolved = self._resolve_ref_if_present(items_schema)
|
||||
item_type = items_schema_resolved.get("type")
|
||||
self.logger.debug(f"Path: '{current_path_str}[*]', Resolved Item Type: '{item_type}'")
|
||||
if item_type in ["string", "integer", "number", "boolean"]:
|
||||
path_parts = base_path_for_log.split('.') if base_path_for_log else []
|
||||
if path_parts == ['']: path_parts = []
|
||||
self.target_field_path = path_parts + [name, 0] # Path like field.array_field.0
|
||||
self.original_field_type = item_type
|
||||
self.target_field_schema = items_schema_resolved # schema for the item, not the array
|
||||
self.logger.info(f"目标字段(请求体 - 数组内简单类型): '{current_path_str}[0]' (原始类型: '{item_type}') FOUND!")
|
||||
return True
|
||||
elif item_type == "object":
|
||||
self.logger.debug(f"Path: '{current_path_str}[*]', Item type is 'object'. Recursing into array item schema...")
|
||||
# Path for recursion: current_path_str + ".0" (representing first item)
|
||||
if self._find_target_field_in_schema(items_schema_resolved, f"{current_path_str}.0"):
|
||||
# self.target_field_path would be set by recursive call.
|
||||
# The path logic in _find_target_field_in_schema needs to correctly prepend array index if it comes from array item recursion.
|
||||
# Let's ensure the path construction at "FOUND!" handles this.
|
||||
# If current_path_str was "field.array.0" and recursion found "nested_prop",
|
||||
# the path should become "field.array.0.nested_prop".
|
||||
# The recursive call sets target_field_path starting from its base_path_for_log.
|
||||
# So if base_path_for_log was "field.array.0", and it found "item_prop",
|
||||
# self.target_field_path will be ["field", "array", 0, "item_prop"]. This seems correct.
|
||||
self.logger.info(f"目标字段(请求体 - 数组内对象属性) found via recursion from '{current_path_str}.0'")
|
||||
return True
|
||||
self.logger.debug(f"Path: '{current_path_str}[*]', Recursion for array item object did not find target.")
|
||||
else:
|
||||
self.logger.debug(f"Path: '{current_path_str}[*]', Item type '{item_type}' is not simple or object.")
|
||||
else:
|
||||
self.logger.debug(f"Path: '{current_path_str}', Array items schema is not a dict or missing. Items: {items_schema}")
|
||||
else:
|
||||
self.logger.debug(f"Path: '{current_path_str}', Property type '{prop_type}' is not a simple type, object, or array. Skipping further processing for this property.")
|
||||
elif schema_type == "array":
|
||||
self.logger.debug(f"Path: '{base_path_for_log}', Top-level schema type is 'array'. Inspecting items...")
|
||||
items_schema = resolved_schema.get("items")
|
||||
if isinstance(items_schema, dict):
|
||||
items_schema_resolved = self._resolve_ref_if_present(items_schema)
|
||||
item_type = items_schema_resolved.get("type")
|
||||
self.logger.debug(f"Path: '{base_path_for_log}[*]', Resolved Item Type: '{item_type}'")
|
||||
if item_type in ["string", "integer", "number", "boolean"]:
|
||||
# This means the body itself is an array of simple types.
|
||||
# We target the first item. Path will be [0] if base_path_for_log is empty.
|
||||
path_parts = base_path_for_log.split('.') if base_path_for_log else []
|
||||
if path_parts == ['']: path_parts = []
|
||||
# If base_path_for_log is empty (root schema is array), path is just [0]
|
||||
# If base_path_for_log is "field.array_prop", this case shouldn't be hit here, but in object prop loop.
|
||||
# This branch is for when the *entire request body schema* is an array.
|
||||
self.target_field_path = path_parts + [0] # if root is array, path_parts is [], so path is [0]
|
||||
self.original_field_type = item_type
|
||||
self.target_field_schema = items_schema_resolved
|
||||
self.logger.info(f"目标字段(请求体 - 根为简单类型数组): '{base_path_for_log}[0]' (原始类型: '{item_type}') FOUND!")
|
||||
return True
|
||||
elif item_type == "object":
|
||||
self.logger.debug(f"Path: '{base_path_for_log}[*]', Item type is 'object'. Recursing into root array item schema...")
|
||||
# Path for recursion: base_path_for_log + ".0" or just "0" if base_path is empty
|
||||
new_base_path = f"{base_path_for_log}.0" if base_path_for_log else "0"
|
||||
if self._find_target_field_in_schema(items_schema_resolved, new_base_path):
|
||||
self.logger.info(f"目标字段(请求体 - 根为对象数组,属性在对象内) found via recursion from '{new_base_path}'")
|
||||
return True
|
||||
self.logger.debug(f"Path: '{base_path_for_log}[*]', Recursion for root array item object did not find target.")
|
||||
else:
|
||||
self.logger.debug(f"Path: '{base_path_for_log}[*]', Item type '{item_type}' is not simple or object.")
|
||||
else:
|
||||
self.logger.debug(f"Path: '{base_path_for_log}', Root array items schema is not a dict or missing. Items: {items_schema}")
|
||||
else:
|
||||
self.logger.debug(f"Path: '{base_path_for_log}', Schema type is '{schema_type}', not 'object' or 'array'. Cannot find properties here.")
|
||||
|
||||
|
||||
self.logger.debug(f"Exit _find_target_field_in_schema for base_path: '{base_path_for_log if base_path_for_log else 'root'}'. Target NOT found in this path.")
|
||||
return False
|
||||
self.logger.info(f"[{self.id}] Conclusion: No target field identified for request body type mismatch test.")
|
||||
|
||||
def generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
self.logger.debug(f"{self.id} is focused on request body, generate_query_params will not modify query parameters.")
|
||||
return current_query_params
|
||||
|
||||
def generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]:
|
||||
if not self.target_field_path: # target_field_location is always "body"
|
||||
if not self.target_field_path or not self.original_field_type:
|
||||
self.logger.info(f"[{self.id}] No target field or original type identified for body type mismatch. Skipping body modification.")
|
||||
return current_body
|
||||
|
||||
self.logger.debug(f"准备修改请求体以测试类型不匹配。目标路径: {self.target_field_path}, 原始类型: {self.original_field_type}")
|
||||
self.logger.debug(f"[{self.id}] Preparing to modify request body for type mismatch. Target path: {self.target_field_path}, Original type: {self.original_field_type}")
|
||||
|
||||
modified_body = copy.deepcopy(current_body) if current_body is not None else {}
|
||||
|
||||
# Ensure body is a dict if path is not empty, or if it's empty and body is None, init to {}
|
||||
if self.target_field_path and not isinstance(modified_body, dict):
|
||||
if not modified_body and not self.target_field_path[0]: # Path is effectively root, and body is None/empty
|
||||
modified_body = {} # Initialize if targeting root of an empty body
|
||||
else:
|
||||
self.logger.warning(f"请求体不是字典类型 (is {type(modified_body)}),但目标字段路径为 {self.target_field_path}。无法安全应用修改。")
|
||||
return current_body
|
||||
elif not self.target_field_path and not modified_body: # No path (targeting root) and body is None
|
||||
self.logger.warning(f"目标字段路径为空 (表示根对象) 但当前请求体也为空,无法确定如何修改。")
|
||||
return current_body
|
||||
# Get the original value at path for logging/context if needed (optional)
|
||||
# current_val_at_path, _ = schema_utils.util_get_value_at_path(current_body, self.target_field_path) # Assuming a get_value_at_path util exists or is added
|
||||
# For now, we don't strictly need original_value for generate_mismatched_value, but it takes it as an arg.
|
||||
|
||||
mismatched_value = schema_utils.generate_mismatched_value(
|
||||
original_type=self.original_field_type,
|
||||
original_value=None, # Placeholder, as current generate_mismatched_value doesn't use it heavily yet
|
||||
field_schema=self.target_field_schema,
|
||||
logger_param=self.logger
|
||||
)
|
||||
|
||||
temp_obj_ref = modified_body
|
||||
try:
|
||||
for i, key_or_index in enumerate(self.target_field_path):
|
||||
is_last_part = (i == len(self.target_field_path) - 1)
|
||||
|
||||
if isinstance(key_or_index, int): # Array index
|
||||
if not isinstance(temp_obj_ref, list) or key_or_index >= len(temp_obj_ref):
|
||||
self.logger.warning(f"路径 {self.target_field_path[:i+1]} 指向数组索引,但当前对象不是列表或索引 ({key_or_index}) 越界 (len: {len(temp_obj_ref) if isinstance(temp_obj_ref, list) else 'N/A'})。")
|
||||
# Attempt to create list/elements if they don't exist up to this point (for safety, only if current is None or empty list)
|
||||
if isinstance(temp_obj_ref, list) and key_or_index == 0 and not temp_obj_ref: # Empty list, trying to set first element
|
||||
temp_obj_ref.append({}) # Add a dict placeholder for the first element
|
||||
elif temp_obj_ref is None and key_or_index == 0: # If parent was None, can't proceed here unless path logic is very robust for creation
|
||||
return current_body # Cannot proceed
|
||||
else:
|
||||
return current_body # Cannot proceed
|
||||
self.logger.info(f"[{self.id}] Generated mismatched value '{mismatched_value}' for original type '{self.original_field_type}' at path '{'.'.join(map(str, self.target_field_path))}'.")
|
||||
|
||||
if is_last_part:
|
||||
original_value = temp_obj_ref[key_or_index]
|
||||
new_value = self._get_mismatched_value(self.original_field_type, original_value, self.target_field_schema)
|
||||
self.logger.info(f"在路径 {self.target_field_path} (数组索引 {key_or_index}) 处,将值从 '{original_value}' 修改为 '{new_value}' (原始类型: {self.original_field_type})")
|
||||
temp_obj_ref[key_or_index] = new_value
|
||||
else:
|
||||
temp_obj_ref = temp_obj_ref[key_or_index]
|
||||
|
||||
elif isinstance(temp_obj_ref, dict): # Dictionary key
|
||||
if key_or_index not in temp_obj_ref and not is_last_part:
|
||||
self.logger.debug(f"路径 {self.target_field_path[:i+1]} 中的键 '{key_or_index}' 在当前对象中不存在,将创建它。")
|
||||
temp_obj_ref[key_or_index] = {} # Create path if not exists
|
||||
|
||||
if is_last_part:
|
||||
original_value = temp_obj_ref.get(key_or_index)
|
||||
new_value = self._get_mismatched_value(self.original_field_type, original_value, self.target_field_schema)
|
||||
self.logger.info(f"在路径 {self.target_field_path} (键 '{key_or_index}') 处,将值从 '{original_value}' 修改为 '{new_value}' (原始类型: {self.original_field_type})")
|
||||
temp_obj_ref[key_or_index] = new_value
|
||||
else:
|
||||
temp_obj_ref = temp_obj_ref[key_or_index]
|
||||
if temp_obj_ref is None and not is_last_part:
|
||||
self.logger.warning(f"路径 {self.target_field_path[:i+1]} 的值在深入时变为None。创建空字典继续。")
|
||||
# This part is tricky, if temp_obj_ref was a key in parent, parent[key_or_index] is None.
|
||||
# We need to set parent[key_or_index] = {} and then temp_obj_ref = parent[key_or_index]
|
||||
# This requires knowing the parent. Let's simplify: if it becomes None, we might not be able to proceed unless it's the dict itself.
|
||||
# The current logic `temp_obj_ref = temp_obj_ref[key_or_index]` means if `temp_obj_ref` was `obj[key]`, now `temp_obj_ref` IS `obj[key]`s value.
|
||||
# If this value is None, and we are not at the end, we should create a dict there if the next part of path is a string key.
|
||||
# This modification is done in the check `if key_or_index not in temp_obj_ref and not is_last_part:`
|
||||
# If it's None AFTER that, it means the schema might be complex (e.g. anyOf, oneOf) or data is unexpectedly null.
|
||||
# For robustness, if it's None and not the last part, we can assume we need a dict for the next key.
|
||||
# The path creation `temp_obj_ref[key_or_index] = {}` for the *next* key happens at the start of the loop for that next key.
|
||||
pass # Already handled by creation logic at the start of the loop iteration for the next key
|
||||
else:
|
||||
self.logger.warning(f"尝试访问路径 {self.target_field_path[:i+1]} 时,当前对象 ({type(temp_obj_ref)}) 不是字典或列表。")
|
||||
return current_body
|
||||
modified_body, success = schema_utils.util_set_value_at_path(
|
||||
data_container=current_body,
|
||||
path=self.target_field_path,
|
||||
new_value=mismatched_value
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"在根据路径 {self.target_field_path} 修改请求体时发生错误: {e}", exc_info=True)
|
||||
if success:
|
||||
self.logger.debug(f"[{self.id}] Successfully set mismatched value at path using util_set_value_at_path.")
|
||||
return modified_body
|
||||
else:
|
||||
self.logger.error(f"[{self.id}] Failed to set mismatched value at path using util_set_value_at_path. Returning original body.")
|
||||
return current_body
|
||||
|
||||
return modified_body
|
||||
|
||||
def _get_mismatched_value(self, original_type: Optional[str], original_value: Any, field_schema: Optional[Dict[str, Any]]) -> Any:
|
||||
if original_type == "string":
|
||||
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list):
|
||||
if 123 not in field_schema["enum"]: return 123
|
||||
if False not in field_schema["enum"]: return False
|
||||
return 12345
|
||||
elif original_type == "integer":
|
||||
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list):
|
||||
if "not-an-integer" not in field_schema["enum"]: return "not-an-integer"
|
||||
if 3.14 not in field_schema["enum"]: return 3.14
|
||||
return "not-an-integer"
|
||||
elif original_type == "number":
|
||||
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list):
|
||||
if "not-a-number" not in field_schema["enum"]: return "not-a-number"
|
||||
return "not-a-number"
|
||||
elif original_type == "boolean":
|
||||
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list):
|
||||
if "not-a-boolean" not in field_schema["enum"]: return "not-a-boolean"
|
||||
if 1 not in field_schema["enum"]: return 1
|
||||
return "not-a-boolean"
|
||||
elif original_type == "array":
|
||||
return {"value": "not-an-array"}
|
||||
elif original_type == "object":
|
||||
return ["not", "an", "object"]
|
||||
|
||||
self.logger.warning(f"类型不匹配测试(请求体):原始类型 '{original_type}' 未知或无法生成不匹配值,将返回固定字符串 'mismatch_test'。")
|
||||
return "mismatch_test" # Fallback
|
||||
|
||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
results = []
|
||||
if not self.target_field_path:
|
||||
self.logger.info(f"[{self.id}] Skipped type mismatch (body) validation: No target field was identified.")
|
||||
return [self.passed("跳过测试:在请求体中未找到合适的字段来测试类型不匹配。")]
|
||||
|
||||
status_code = response_context.status_code
|
||||
json_content = response_context.json_content
|
||||
expected_http_status_codes = [400, 422] # Common client error codes for type issues
|
||||
# specific_business_error_code 是此测试用例期望的特定业务错误码,例如 "4001"
|
||||
specific_business_error_code = "4001"
|
||||
error_code_field_in_body = "code" # 响应体中业务错误码的字段名
|
||||
|
||||
if not self.target_field_path:
|
||||
results.append(self.passed("跳过测试:在请求体中未找到合适的字段来测试类型不匹配。"))
|
||||
self.logger.info(f"{self.id}: 由于未识别到目标请求体字段,跳过类型不匹配测试。")
|
||||
return results
|
||||
field_path_str = '.'.join(map(str, self.target_field_path))
|
||||
context_msg_prefix = f"当请求体字段 '{field_path_str}' 类型不匹配时,"
|
||||
|
||||
expected_status_codes = [400, 422]
|
||||
specific_error_code_from_appendix_b = "4001" # Example
|
||||
http_status_ok = status_code in expected_http_status_codes
|
||||
business_code_ok = False
|
||||
is_4xx_error = 400 <= status_code <= 499
|
||||
|
||||
if status_code in expected_status_codes:
|
||||
msg = f"API对请求体字段 '{'.'.join(str(p) for p in self.target_field_path)}' 的类型不匹配响应了 {status_code},符合预期。"
|
||||
error_code_in_response = json_content.get("code") if isinstance(json_content, dict) else None
|
||||
if error_code_in_response == specific_error_code_from_appendix_b:
|
||||
results.append(self.passed(f"{msg} 并成功接收到特定错误码 '{specific_error_code_from_appendix_b}'。"))
|
||||
elif error_code_in_response:
|
||||
results.append(ValidationResult(passed=True,
|
||||
message=f"{msg} 但响应体中的错误码是 '{error_code_in_response}' (期望类似 '{specific_error_code_from_appendix_b}')。",
|
||||
details=json_content if isinstance(json_content, dict) else {"raw_response": str(json_content)}
|
||||
if json_content and isinstance(json_content, dict):
|
||||
body_code = json_content.get(error_code_field_in_body)
|
||||
if body_code is not None and str(body_code) == specific_business_error_code:
|
||||
business_code_ok = True
|
||||
|
||||
if http_status_ok:
|
||||
if business_code_ok:
|
||||
results.append(self.passed(
|
||||
f"{context_msg_prefix}API响应了预期的错误状态码 {status_code} 并且响应体中包含预期的业务错误码 '{specific_business_error_code}' (字段: '{error_code_field_in_body}')."
|
||||
))
|
||||
self.logger.info(f"{self.id}: Passed. HTTP status {status_code} and business code '{specific_business_error_code}' match. (Field: body.{field_path_str})")
|
||||
else:
|
||||
results.append(self.passed(f"{msg} 响应体中未找到错误码或结构不符合预期。"))
|
||||
else:
|
||||
results.append(self.failed(
|
||||
message=f"对请求体字段 '{'.'.join(str(p) for p in self.target_field_path)}' 的类型不匹配测试期望状态码为 {expected_status_codes} 之一,但收到 {status_code}。",
|
||||
details={"status_code": status_code, "response_body": json_content}
|
||||
results.append(self.passed(
|
||||
f"{context_msg_prefix}API响应了预期的错误状态码 {status_code}. "
|
||||
f"响应体中的业务错误码 (\'{error_code_field_in_body}\': \'{json_content.get(error_code_field_in_body)}\') 与特定期望 \'{specific_business_error_code}\' 不符或未找到,但HTTP状态码正确。"
|
||||
))
|
||||
self.logger.info(f"{self.id}: Passed. HTTP status {status_code} is correct. Business code mismatch/missing (Expected: \'{specific_business_error_code}\', Got: \'{json_content.get(error_code_field_in_body)}\'). (Field: body.{field_path_str})")
|
||||
|
||||
elif business_code_ok:
|
||||
results.append(self.passed(
|
||||
f"{context_msg_prefix}API响应了状态码 {status_code} (非主要预期HTTP状态 {expected_http_status_codes},但为4xx客户端错误), "
|
||||
f"且响应体中包含预期的业务错误码 '{specific_business_error_code}' (字段: '{error_code_field_in_body}')."
|
||||
))
|
||||
self.logger.warning(f"{self.id}: 类型不匹配测试失败。字段: body.{'.'.join(str(p) for p in self.target_field_path)}, 期望状态码: {expected_status_codes}, 实际: {status_code}。")
|
||||
self.logger.info(f"{self.id}: Passed (Fallback). HTTP status {status_code} (4xx) with matching business code '{specific_business_error_code}'. (Field: body.{field_path_str})")
|
||||
|
||||
else:
|
||||
fail_message = f"{context_msg_prefix}期望API返回状态码在 {expected_http_status_codes} 中,或返回4xx客户端错误且业务码为 '{specific_business_error_code}'."
|
||||
fail_message += f" 实际收到状态码 {status_code}."
|
||||
if json_content and isinstance(json_content, dict):
|
||||
fail_message += f" 响应体中的业务码 (\'{error_code_field_in_body}\') 为 \'{json_content.get(error_code_field_in_body)}\'."
|
||||
elif json_content:
|
||||
fail_message += " 响应体不是一个JSON对象."
|
||||
else:
|
||||
fail_message += " 响应体为空或非JSON."
|
||||
|
||||
results.append(self.failed(
|
||||
message=fail_message,
|
||||
details={"status_code": status_code, "response_body": json_content, "expected_http_status_codes": expected_http_status_codes, "expected_business_code": specific_business_error_code, "mismatched_field": f"body.{field_path_str}"}
|
||||
))
|
||||
self.logger.warning(f"{self.id}: Failed. {fail_message} (Field: body.{field_path_str})")
|
||||
|
||||
return results
|
||||
+97
-188
@@ -1,7 +1,8 @@
|
||||
from typing import Dict, Any, Optional, List
|
||||
from typing import Dict, Any, Optional, List, Tuple, Union
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
|
||||
import copy
|
||||
import logging
|
||||
from ddms_compliance_suite.utils import schema_utils
|
||||
|
||||
class TypeMismatchQueryParamCase(BaseAPITestCase):
|
||||
id = "TC-ERROR-4001-QUERY"
|
||||
@@ -19,220 +20,128 @@ class TypeMismatchQueryParamCase(BaseAPITestCase):
|
||||
# Location is always 'query' for this class
|
||||
self.target_field_location: str = "query"
|
||||
self.target_field_schema: Optional[Dict[str, Any]] = None
|
||||
self.target_param_name: Optional[str] = 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_query()
|
||||
|
||||
self.logger.critical(f"{self.id} __INIT__ >>> 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} __INIT__ >>> 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,尝试在其内部查找简单类型字段。")
|
||||
# We need to find a simple type *within* this schema.
|
||||
# _find_target_field_in_schema is designed for requestBody, let's adapt or simplify.
|
||||
# For query parameters, complex objects are less common or might be flattened.
|
||||
# Let's try to find a simple type property directly within this schema if it's an object.
|
||||
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] # Path will be 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 # Found a suitable property
|
||||
if self.target_field_path: break # Break outer loop if found
|
||||
elif resolved_param_schema.get("type") in ["string", "number", "integer", "boolean"]: # Schema itself is simple after ref resolution
|
||||
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 _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')} 初始化查询参数类型不匹配测试的目标字段查找。")
|
||||
self.logger.info(f"[{self.id}] Initializing: Looking for a simple type query parameter for type mismatch test.")
|
||||
|
||||
found_target_param = self._find_first_simple_type_parameter(param_location="query")
|
||||
|
||||
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}")
|
||||
if found_target_param:
|
||||
full_path, param_type, param_schema, top_level_param_name = found_target_param
|
||||
self.target_field_path = full_path
|
||||
self.original_field_type = param_type
|
||||
self.target_field_schema = param_schema
|
||||
self.target_param_name = top_level_param_name # Store the top-level parameter name
|
||||
self.logger.info(f"[{self.id}] Target for type mismatch (query): Param='{self.target_param_name}', Path='{'.'.join(map(str,self.target_field_path))}', Type='{self.original_field_type}'")
|
||||
else:
|
||||
self.logger.info(f"[{self.id}] No suitable simple type query parameter found for type mismatch test.")
|
||||
|
||||
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/$ $$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
|
||||
def generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]:
|
||||
self.logger.debug(f"{self.id} is focused on query parameters, generate_request_body will not modify the body.")
|
||||
return current_body
|
||||
|
||||
def generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not self.target_field_path: # target_field_location is always "query"
|
||||
if not self.target_field_path or not self.original_field_type:
|
||||
self.logger.info(f"[{self.id}] No target field or original type identified for query param type mismatch. Skipping query param modification.")
|
||||
return current_query_params
|
||||
|
||||
self.logger.debug(f"准备修改查询参数以测试类型不匹配。目标路径: {self.target_field_path}, 原始类型: {self.original_field_type}")
|
||||
self.logger.debug(f"[{self.id}] Preparing to modify query params for type mismatch. Target path: {self.target_field_path}, Original type: {self.original_field_type}")
|
||||
|
||||
modified_params = copy.deepcopy(current_query_params) if current_query_params is not None else {}
|
||||
|
||||
temp_obj_ref = modified_params
|
||||
try:
|
||||
for i, key in enumerate(self.target_field_path):
|
||||
is_last_part = (i == len(self.target_field_path) - 1)
|
||||
if is_last_part:
|
||||
original_value = temp_obj_ref.get(key)
|
||||
new_value = self._get_mismatched_value(self.original_field_type, original_value, self.target_field_schema)
|
||||
self.logger.info(f"在查询参数路径 {self.target_field_path} (键 '{key}') 处,将值从 '{original_value}' 修改为 '{new_value}' (原始类型: {self.original_field_type})")
|
||||
temp_obj_ref[key] = new_value
|
||||
else: # Navigating a nested structure within a query param (e.g. filter[field]=value)
|
||||
if key not in temp_obj_ref or not isinstance(temp_obj_ref[key], dict):
|
||||
# If path expects a dict but it's not there, create it.
|
||||
# This is crucial for structured query params like "filter[name]=value"
|
||||
# where target_field_path might be ["filter", "name"].
|
||||
temp_obj_ref[key] = {}
|
||||
temp_obj_ref = temp_obj_ref[key]
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"在根据路径 {self.target_field_path} 修改查询参数时发生错误: {e}", exc_info=True)
|
||||
return current_query_params
|
||||
mismatched_value = schema_utils.generate_mismatched_value(
|
||||
original_type=self.original_field_type,
|
||||
original_value=None, # Placeholder
|
||||
field_schema=self.target_field_schema,
|
||||
logger_param=self.logger
|
||||
)
|
||||
|
||||
return modified_params
|
||||
self.logger.info(f"[{self.id}] Generated mismatched value '{mismatched_value}' for original type '{self.original_field_type}' at query path '{'.'.join(map(str, self.target_field_path))}'.")
|
||||
|
||||
def _get_mismatched_value(self, original_type: Optional[str], original_value: Any, field_schema: Optional[Dict[str, Any]]) -> Any:
|
||||
if original_type == "string":
|
||||
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list):
|
||||
if 123 not in field_schema["enum"]: return 123
|
||||
if False not in field_schema["enum"]: return False
|
||||
return 12345
|
||||
elif original_type == "integer":
|
||||
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list):
|
||||
if "not-an-integer" not in field_schema["enum"]: return "not-an-integer"
|
||||
if 3.14 not in field_schema["enum"]: return 3.14
|
||||
return "not-an-integer"
|
||||
elif original_type == "number":
|
||||
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list):
|
||||
if "not-a-number" not in field_schema["enum"]: return "not-a-number"
|
||||
return "not-a-number"
|
||||
elif original_type == "boolean":
|
||||
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list):
|
||||
if "not-a-boolean" not in field_schema["enum"]: return "not-a-boolean"
|
||||
if 1 not in field_schema["enum"]: return 1
|
||||
return "not-a-boolean"
|
||||
|
||||
self.logger.warning(f"类型不匹配测试(查询参数):原始类型 '{original_type}' 未知或无法生成不匹配值,将返回固定字符串 'mismatch_test'。")
|
||||
return "mismatch_test" # Fallback for other types or if logic is incomplete
|
||||
# Query parameters are typically a flat dictionary, but util_set_value_at_path can handle nested paths if needed (e.g. for object-style query params)
|
||||
modified_params, success = schema_utils.util_set_value_at_path(
|
||||
data_container=current_query_params,
|
||||
path=self.target_field_path, # Path might be like ['paramName'] or ['paramName', 'nestedKey']
|
||||
new_value=mismatched_value
|
||||
)
|
||||
|
||||
if success:
|
||||
self.logger.debug(f"[{self.id}] Successfully set mismatched value in query params using util_set_value_at_path.")
|
||||
return modified_params
|
||||
else:
|
||||
self.logger.error(f"[{self.id}] Failed to set mismatched value in query params using util_set_value_at_path. Returning original params.")
|
||||
return current_query_params
|
||||
|
||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
results = []
|
||||
if not self.target_field_path:
|
||||
self.logger.info(f"[{self.id}] Skipped type mismatch (query) validation: No target query parameter was identified.")
|
||||
return [self.passed("跳过测试:在查询参数中未找到合适的字段来测试类型不匹配。")]
|
||||
|
||||
status_code = response_context.status_code
|
||||
json_content = response_context.json_content
|
||||
expected_http_status_codes = [400, 422]
|
||||
specific_business_error_code = "4001"
|
||||
error_code_field_in_body = "code"
|
||||
|
||||
if not self.target_field_path:
|
||||
results.append(self.passed("跳过测试:在查询参数中未找到合适的字段来测试类型不匹配。"))
|
||||
self.logger.info(f"{self.id}: 由于未识别到目标查询参数字段,跳过类型不匹配测试。")
|
||||
return results
|
||||
# Use self.target_param_name for a clearer context message if a top-level param was identified
|
||||
context_param_identifier = self.target_param_name or '.'.join(map(str, self.target_field_path))
|
||||
context_msg_prefix = f"当查询参数 '{context_param_identifier}' (路径: '{'.'.join(map(str, self.target_field_path))}') 类型不匹配时,"
|
||||
|
||||
expected_status_codes = [400, 422]
|
||||
specific_error_code_from_appendix_b = "4001" # Example
|
||||
http_status_ok = status_code in expected_http_status_codes
|
||||
business_code_ok = False
|
||||
is_4xx_error = 400 <= status_code <= 499
|
||||
|
||||
if status_code in expected_status_codes:
|
||||
msg = f"API对查询参数 '{'.'.join(self.target_field_path)}' 的类型不匹配响应了 {status_code},符合预期。"
|
||||
# Further check for specific error code in body if applicable
|
||||
error_code_in_response = json_content.get("code") if isinstance(json_content, dict) else None
|
||||
if error_code_in_response == specific_error_code_from_appendix_b:
|
||||
results.append(self.passed(f"{msg} 并成功接收到特定错误码 '{specific_error_code_from_appendix_b}'。"))
|
||||
elif error_code_in_response:
|
||||
results.append(ValidationResult(passed=True,
|
||||
message=f"{msg} 但响应体中的错误码是 '{error_code_in_response}' (期望类似 '{specific_error_code_from_appendix_b}')。",
|
||||
details=json_content if isinstance(json_content, dict) else {"raw_response": str(json_content)}
|
||||
if json_content and isinstance(json_content, dict):
|
||||
body_code = json_content.get(error_code_field_in_body)
|
||||
if body_code is not None and str(body_code) == specific_business_error_code:
|
||||
business_code_ok = True
|
||||
|
||||
if http_status_ok:
|
||||
if business_code_ok:
|
||||
results.append(self.passed(
|
||||
f"{context_msg_prefix}API响应了预期的错误状态码 {status_code} 并且响应体中包含预期的业务错误码 '{specific_business_error_code}' (字段: '{error_code_field_in_body}')."
|
||||
))
|
||||
self.logger.info(f"{self.id}: Passed. HTTP status {status_code} and business code '{specific_business_error_code}' match. (Query param: {context_param_identifier})")
|
||||
else:
|
||||
results.append(self.passed(f"{msg} 响应体中未找到错误码或结构不符合预期。"))
|
||||
else:
|
||||
results.append(self.failed(
|
||||
message=f"对查询参数 '{'.'.join(self.target_field_path)}' 的类型不匹配测试期望状态码为 {expected_status_codes} 之一,但收到 {status_code}。",
|
||||
details={"status_code": status_code, "response_body": json_content}
|
||||
))
|
||||
self.logger.warning(f"{self.id}: 类型不匹配测试失败。字段: query.{'.'.join(self.target_field_path)}, 期望状态码: {expected_status_codes}, 实际: {status_code}。")
|
||||
results.append(self.passed(
|
||||
f"{context_msg_prefix}API响应了预期的错误状态码 {status_code}. "
|
||||
f"响应体中的业务错误码 (\'{error_code_field_in_body}\': \'{json_content.get(error_code_field_in_body)}\') 与特定期望 \'{specific_business_error_code}\' 不符或未找到,但HTTP状态码正确。"
|
||||
))
|
||||
self.logger.info(f"{self.id}: Passed. HTTP status {status_code} is correct. Business code mismatch/missing (Expected: \'{specific_business_error_code}\', Got: \'{json_content.get(error_code_field_in_body)}\'). (Query param: {context_param_identifier})")
|
||||
|
||||
return results
|
||||
elif business_code_ok:
|
||||
results.append(self.passed(
|
||||
f"{context_msg_prefix}API响应了状态码 {status_code} (非主要预期HTTP状态 {expected_http_status_codes},但为4xx客户端错误), "
|
||||
f"且响应体中包含预期的业务错误码 '{specific_business_error_code}' (字段: '{error_code_field_in_body}')."
|
||||
))
|
||||
self.logger.info(f"{self.id}: Passed (Fallback). HTTP status {status_code} (4xx) with matching business code '{specific_business_error_code}'. (Query param: {context_param_identifier})")
|
||||
|
||||
else:
|
||||
fail_message = f"{context_msg_prefix}期望API返回状态码在 {expected_http_status_codes} 中,或返回4xx客户端错误且业务码为 '{specific_business_error_code}'."
|
||||
fail_message += f" 实际收到状态码 {status_code}."
|
||||
if json_content and isinstance(json_content, dict):
|
||||
fail_message += f" 响应体中的业务码 (\'{error_code_field_in_body}\') 为 \'{json_content.get(error_code_field_in_body)}\'."
|
||||
elif json_content:
|
||||
fail_message += " 响应体不是一个JSON对象."
|
||||
else:
|
||||
fail_message += " 响应体为空或非JSON."
|
||||
|
||||
results.append(self.failed(
|
||||
message=fail_message,
|
||||
details={"status_code": status_code, "response_body": json_content, "expected_http_status_codes": expected_http_status_codes, "expected_business_code": specific_business_error_code, "mismatched_param": context_param_identifier}
|
||||
))
|
||||
self.logger.warning(f"{self.id}: Failed. {fail_message} (Query param: {context_param_identifier})")
|
||||
|
||||
return results
|
||||
|
||||
def generate_path_params(self, current_path_params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
# ... existing code ...
|
||||
pass
|
||||
|
||||
return current_path_params
|
||||
BIN
Binary file not shown.
@@ -115,14 +115,13 @@
|
||||
# - 版本号: 语义化版本,例如 v1, v1.0, v2.1.3。
|
||||
# - 资源类型: 通常为名词复数。
|
||||
# - standard_name: "url_path_structure"
|
||||
# - standard_name: "resource"
|
||||
# - standard_name: "schema"
|
||||
# - standard_name: "version"
|
||||
|
||||
|
||||
# 4. **URL路径参数命名规范**:
|
||||
# - 规则: 路径参数(如果存在)必须使用全小写字母(可以是一个单词)或小写字母加下划线命名(这是多个单词的情况),并能反映资源的唯一标识 (例如: {{well_id}},{{version}},{{schema}})。
|
||||
# - 规则: 路径参数(如果存在)必须使用全小写字母(可以是一个单词)或小写字母加下划线命名(这是多个单词的情况),并能反映资源的唯一标识 (例如: {{well_id}},还有{{version}},{{schema}}也是合规的,比一定非要{{version_id}})。
|
||||
# - standard_name: "url_path_parameter_naming"
|
||||
|
||||
|
||||
# 5. **资源命名规范 (在路径中)**:
|
||||
# - 规则: 资源集合应使用名词的复数形式表示 (例如 `/wells`, `/logs`);应优先使用石油行业的标准术语 (例如用 `trajectory` 而非 `path` 来表示井轨迹)。
|
||||
# - standard_name: "resource_naming_in_path"
|
||||
|
||||
BIN
Binary file not shown.
@@ -18,7 +18,7 @@ class HTTPSMandatoryCase(BaseAPITestCase):
|
||||
|
||||
def modify_request_url(self, current_url: str) -> str:
|
||||
parsed_url = urllib.parse.urlparse(current_url)
|
||||
if parsed_url.scheme.lower() == "httpss":
|
||||
if parsed_url.scheme.lower() == "https":
|
||||
# 将 https 替换为 http
|
||||
modified_url = parsed_url._replace(scheme="http").geturl()
|
||||
self.logger.info(f"为进行HTTPS检查修改URL:原始 '{current_url}', 修改为 '{modified_url}'")
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,78 @@
|
||||
# from typing import Dict, Any, Optional, List
|
||||
# from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
|
||||
|
||||
# class BasicAPISanityCheckCase(BaseAPITestCase):
|
||||
# id = "TC-FRAMEWORK-SANITY-001"
|
||||
# name = "Basic API Sanity Check"
|
||||
# description = ("Performs a basic API call with default generated data and expects a generally successful "
|
||||
# "response (e.g., 200, 201, 204). If a response schema is defined for success, "
|
||||
# "it also validates the response body against it. "
|
||||
# "If this test case fails, subsequent test cases for this endpoint may be skipped.")
|
||||
# severity = TestSeverity.CRITICAL
|
||||
# tags = ["sanity", "framework-setup"]
|
||||
|
||||
# # This flag indicates to the orchestrator that if this test fails,
|
||||
# # subsequent tests for THIS ENDPOINT should be skipped.
|
||||
# is_critical_setup_test: bool = True
|
||||
# execution_order = 1 # Ensures this runs first for an endpoint
|
||||
|
||||
# # Expected successful HTTP status codes
|
||||
# EXPECTED_SUCCESS_STATUS_CODES: List[int] = [200, 201, 202, 204]
|
||||
|
||||
# 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)
|
||||
# self.target_success_schema: Optional[Dict[str, Any]] = None
|
||||
|
||||
# # Try to find a schema for a successful response (e.g., 200 or 201)
|
||||
# responses_spec = self.endpoint_spec.get("responses", {})
|
||||
# if isinstance(responses_spec, dict):
|
||||
# for status_code_str in map(str, self.EXPECTED_SUCCESS_STATUS_CODES):
|
||||
# if status_code_str in responses_spec:
|
||||
# response_def = responses_spec[status_code_str]
|
||||
# if isinstance(response_def, dict):
|
||||
# content = response_def.get("content", {})
|
||||
# for ct in ["application/json", "application/*+json", "*/*"]:
|
||||
# if ct in content:
|
||||
# media_type_obj = content[ct]
|
||||
# if isinstance(media_type_obj, dict) and isinstance(media_type_obj.get("schema"), dict):
|
||||
# self.target_success_schema = media_type_obj["schema"]
|
||||
# self.logger.info(f"[{self.id}] Found success response schema for status {status_code_str} under content type {ct}.")
|
||||
# break # Found a schema for this content type
|
||||
# if self.target_success_schema:
|
||||
# break # Found a schema for this status code
|
||||
# if not self.target_success_schema:
|
||||
# self.logger.info(f"[{self.id}] No specific success response JSON schema found to validate against for this endpoint.")
|
||||
|
||||
# # No need to override generate_* methods, as we want the default behavior.
|
||||
|
||||
# def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
# results = []
|
||||
# status_code = response_context.status_code
|
||||
|
||||
# if status_code in self.EXPECTED_SUCCESS_STATUS_CODES:
|
||||
# msg = f"Basic sanity check: Received expected success status code {status_code}."
|
||||
# results.append(self.passed(msg))
|
||||
|
||||
# # If we have a schema for successful responses, validate the body
|
||||
# if self.target_success_schema:
|
||||
# if response_context.json_content is not None:
|
||||
# results.extend(self.validate_data_against_schema(
|
||||
# data_to_validate=response_context.json_content,
|
||||
# schema_definition=self.target_success_schema,
|
||||
# context_message_prefix="Successful response body"
|
||||
# ))
|
||||
# elif response_context.text_content and not response_context.text_content.strip() and status_code == 204:
|
||||
# # HTTP 204 No Content, body is expected to be empty, so schema validation is not applicable.
|
||||
# results.append(self.passed("Response is 204 No Content, body is correctly empty."))
|
||||
# elif status_code != 204 : # For 200, 201, 202, if schema is present, content is expected
|
||||
# results.append(self.failed(
|
||||
# message="Basic sanity check: Response body is empty or not JSON, but a success schema was defined.",
|
||||
# details={"status_code": status_code, "content_type": response_context.headers.get("Content-Type")}
|
||||
# ))
|
||||
# else:
|
||||
# results.append(self.failed(
|
||||
# message=f"Basic sanity check: Expected a success status code (one of {self.EXPECTED_SUCCESS_STATUS_CODES}), but received {status_code}.",
|
||||
# details={"status_code": status_code, "response_body": response_context.json_content if response_context.json_content else response_context.text_content}
|
||||
# ))
|
||||
|
||||
# return results
|
||||
Reference in New Issue
Block a user