mvp
This commit is contained in:
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,164 @@
|
||||
from typing import Dict, Any, Optional, List, Union, Tuple
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
|
||||
import logging
|
||||
from ddms_compliance_suite.utils import schema_utils
|
||||
import random
|
||||
import string
|
||||
|
||||
class InvalidEnumValueCase(BaseAPITestCase):
|
||||
id = "TC-ERROR-4006"
|
||||
name = "Error Code 4006 - Invalid Enum Value Validation"
|
||||
description = "测试当发送的参数值不在指定的枚举范围内时,API是否按预期返回code=4006的错误(状态码应为200)。"
|
||||
severity = TestSeverity.MEDIUM
|
||||
tags = ["error-handling", "appendix-b", "4006", "invalid-enum"]
|
||||
execution_order = 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=llm_service)
|
||||
self.logger.setLevel(logging.INFO)
|
||||
|
||||
self.target_field_location: Optional[str] = None
|
||||
self.target_field_path: Optional[List[Union[str, int]]] = None
|
||||
self.target_field_name: Optional[str] = None
|
||||
self.valid_enums: Optional[List[str]] = None
|
||||
self.invalid_enum_value: Optional[str] = None
|
||||
|
||||
self._initialize_test_case()
|
||||
|
||||
def _find_first_enum_field(self) -> Optional[Tuple[str, List[Union[str, int]], str, List[str], str]]:
|
||||
"""
|
||||
按优先级(body -> query -> path -> header)查找第一个具有枚举值限制的字段。
|
||||
"""
|
||||
self.logger.info(f"[{self.id}] Searching for a field with enum constraints...")
|
||||
|
||||
# 1. 检查请求体 (Body)
|
||||
body_schema = self._get_resolved_request_body_schema()
|
||||
if body_schema:
|
||||
self.logger.debug(f"[{self.id}] Checking for enum field in request body.")
|
||||
result = schema_utils.util_find_enum_field_recursive(body_schema, [], self.logger)
|
||||
if result:
|
||||
path, valid_enums, desc = result
|
||||
name = path[-1] if path else "body_root"
|
||||
self.logger.info(f"[{self.id}] Found enum field in BODY at path: {'.'.join(map(str, path))}")
|
||||
return "body", path, name, valid_enums, desc
|
||||
|
||||
# 2. 检查查询参数, 路径参数, 请求头
|
||||
for location in ["query", "path", "header"]:
|
||||
if 'parameters' in self.endpoint_spec:
|
||||
self.logger.debug(f"[{self.id}] Checking for enum field in {location} parameters.")
|
||||
for param in self.endpoint_spec['parameters']:
|
||||
if param.get('in') == location:
|
||||
param_desc = param.get('description', '')
|
||||
param_name = param.get('name')
|
||||
|
||||
valid_enums = schema_utils.util_extract_enum_from_description(param_desc, self.logger)
|
||||
|
||||
if valid_enums:
|
||||
self.logger.info(f"[{self.id}] Found enum field in {location.upper()}: '{param_name}'")
|
||||
return location, [param_name], param_name, valid_enums, param_desc
|
||||
|
||||
self.logger.info(f"[{self.id}] No field with enum constraints found in any location.")
|
||||
return None
|
||||
|
||||
def _initialize_test_case(self):
|
||||
"""初始化测试用例。"""
|
||||
found_field = self._find_first_enum_field()
|
||||
|
||||
if found_field:
|
||||
location, path, name, valid_enums, description = found_field
|
||||
|
||||
self.target_field_location = location
|
||||
self.target_field_path = path
|
||||
self.target_field_name = name
|
||||
self.valid_enums = valid_enums
|
||||
|
||||
self.logger.info(
|
||||
f"[{self.id}] Target found in '{location}' at path '{'.'.join(map(str, path))}'. "
|
||||
f"Valid enums: {valid_enums}, Desc: '{description}'"
|
||||
)
|
||||
|
||||
self._set_invalid_enum_value()
|
||||
else:
|
||||
self.logger.info(f"[{self.id}] No suitable enum field found for this endpoint.")
|
||||
|
||||
def _set_invalid_enum_value(self):
|
||||
"""生成一个非法的枚举值。"""
|
||||
random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||
self.invalid_enum_value = f"invalid_enum_{random_suffix}"
|
||||
self.logger.info(f"[{self.id}] Setting invalid enum value to '{self.invalid_enum_value}' (valid are {self.valid_enums}).")
|
||||
|
||||
def _modify_params(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""通用函数,用于修改参数字典中的值。"""
|
||||
if self.invalid_enum_value is None or not self.target_field_path:
|
||||
return params
|
||||
|
||||
modified_params, success = schema_utils.util_set_value_at_path(
|
||||
data_container=params,
|
||||
path=self.target_field_path,
|
||||
new_value=self.invalid_enum_value
|
||||
)
|
||||
|
||||
if success:
|
||||
self.logger.debug(f"[{self.id}] Successfully injected invalid enum value into parameters.")
|
||||
return modified_params
|
||||
|
||||
self.logger.error(f"[{self.id}] Failed to set invalid enum value in parameters.")
|
||||
return params
|
||||
|
||||
def generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if self.target_field_location == "query":
|
||||
return self._modify_params(current_query_params)
|
||||
return current_query_params
|
||||
|
||||
def generate_path_params(self, current_path_params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if self.target_field_location == "path":
|
||||
return self._modify_params(current_path_params)
|
||||
return current_path_params
|
||||
|
||||
def generate_headers(self, current_headers: Dict[str, str]) -> Dict[str, str]:
|
||||
if self.target_field_location == "header":
|
||||
return self._modify_params(current_headers)
|
||||
return current_headers
|
||||
|
||||
def generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]:
|
||||
if self.target_field_location == "body":
|
||||
return self._modify_params(current_body)
|
||||
return current_body
|
||||
|
||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
"""验证API是否正确返回code=4006错误(状态码为200)。"""
|
||||
results = []
|
||||
|
||||
if self.invalid_enum_value is None:
|
||||
return [self.passed("跳过测试:未找到具有明确枚举值限制的字段。")]
|
||||
|
||||
expected_http_status_code = 200
|
||||
expected_business_error_code = "4006"
|
||||
|
||||
context_msg_prefix = (
|
||||
f"当 {self.target_field_location} 字段 '{self.target_field_name}' "
|
||||
f"值为 '{self.invalid_enum_value}' (合法值为: {self.valid_enums}) 时, "
|
||||
)
|
||||
|
||||
# 检查状态码
|
||||
if response_context.status_code != expected_http_status_code:
|
||||
return [self.failed(
|
||||
message=f"{context_msg_prefix}API应返回状态码 {expected_http_status_code},但实际为 {response_context.status_code}。",
|
||||
details={"expected_status": expected_http_status_code, "actual_status": response_context.status_code}
|
||||
)]
|
||||
|
||||
# 检查业务错误码
|
||||
json_content = response_context.json_content
|
||||
if not isinstance(json_content, dict):
|
||||
return [self.failed(f"{context_msg_prefix}API响应体不是一个有效的JSON对象。")]
|
||||
|
||||
actual_business_code = json_content.get("code")
|
||||
if str(actual_business_code) != expected_business_error_code:
|
||||
return [self.failed(
|
||||
message=f"{context_msg_prefix}业务错误码应为 '{expected_business_error_code}',但实际为 '{actual_business_code}'。",
|
||||
details={"expected_code": expected_business_error_code, "actual_code": actual_business_code, "response_body": json_content}
|
||||
)]
|
||||
|
||||
return [self.passed(
|
||||
f"{context_msg_prefix}API正确返回了状态码 {expected_http_status_code} 和业务错误码 '{expected_business_error_code}'。"
|
||||
)]
|
||||
+24
-56
@@ -6,7 +6,7 @@ from ddms_compliance_suite.utils import schema_utils # Keep this import for util
|
||||
class MissingRequiredFieldBodyCase(BaseAPITestCase):
|
||||
id = "TC-ERROR-4003-BODY"
|
||||
name = "Error Code 4003 - Missing Required Request Body Field Validation"
|
||||
description = "测试当请求体中缺少API规范定义的必填字段时,API是否按预期返回类似4003的错误(或通用400错误)。"
|
||||
description = "测试当请求体中缺少API规范定义的必填字段时,API是否按预期返回类似4003的错误。"
|
||||
severity = TestSeverity.HIGH
|
||||
tags = ["error-handling", "appendix-b", "4003", "required-fields", "request-body"]
|
||||
execution_order = 210
|
||||
@@ -53,70 +53,38 @@ class MissingRequiredFieldBodyCase(BaseAPITestCase):
|
||||
return current_body
|
||||
|
||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
"""验证API是否正确返回code=4003错误(状态码为200)。"""
|
||||
results = []
|
||||
if not self.removed_field_path:
|
||||
results.append(self.passed("跳过测试:在API规范中未找到合适的必填请求体字段用于移除测试。"))
|
||||
self.logger.info("由于未识别到可移除的必填请求体字段,跳过此测试用例的验证。")
|
||||
return results
|
||||
|
||||
status_code = response_context.status_code
|
||||
json_content = response_context.json_content
|
||||
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" # 响应体中业务错误码的字段名
|
||||
expected_http_status_code = 200
|
||||
expected_business_error_code = "4003"
|
||||
|
||||
removed_field_str = '.'.join(map(str, self.removed_field_path))
|
||||
context_msg_prefix = f"当移除必填请求体字段 '{removed_field_str}' 时,"
|
||||
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 response_context.status_code != expected_http_status_code:
|
||||
return [self.failed(
|
||||
message=f"{context_msg_prefix}API应返回状态码 {expected_http_status_code},但实际为 {response_context.status_code}。",
|
||||
details={"expected_status": expected_http_status_code, "actual_status": response_context.status_code}
|
||||
)]
|
||||
|
||||
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
|
||||
# 检查业务错误码
|
||||
json_content = response_context.json_content
|
||||
if not isinstance(json_content, dict):
|
||||
return [self.failed(f"{context_msg_prefix}API响应体不是一个有效的JSON对象。")]
|
||||
|
||||
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. (Removed field: body.{removed_field_str})")
|
||||
else:
|
||||
# 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.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."
|
||||
actual_business_code = json_content.get("code")
|
||||
if str(actual_business_code) != expected_business_error_code:
|
||||
return [self.failed(
|
||||
message=f"{context_msg_prefix}业务错误码应为 '{expected_business_error_code}',但实际为 '{actual_business_code}'。",
|
||||
details={"expected_code": expected_business_error_code, "actual_code": actual_business_code, "response_body": json_content}
|
||||
)]
|
||||
|
||||
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
|
||||
return [self.passed(
|
||||
f"{context_msg_prefix}API正确返回了状态码 {expected_http_status_code} 和业务错误码 '{expected_business_error_code}'。"
|
||||
)]
|
||||
+26
-52
@@ -5,7 +5,7 @@ import copy
|
||||
class MissingRequiredFieldQueryCase(BaseAPITestCase):
|
||||
id = "TC-ERROR-4003-QUERY"
|
||||
name = "Error Code 4003 - Missing Required Query Parameter Validation"
|
||||
description = "测试当请求中缺少API规范定义的必填查询参数时,API是否按预期返回类似4003的错误(或通用400错误)。"
|
||||
description = "测试当请求中缺少API规范定义的必填查询参数时,API是否按预期返回类似4003的错误。"
|
||||
severity = TestSeverity.HIGH
|
||||
tags = ["error-handling", "appendix-b", "4003", "required-fields", "query-parameters"]
|
||||
execution_order = 211 # After body, before original combined one might have been
|
||||
@@ -43,6 +43,7 @@ class MissingRequiredFieldQueryCase(BaseAPITestCase):
|
||||
return current_query_params
|
||||
|
||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
"""验证API是否正确返回code=4003错误(状态码为200)。"""
|
||||
results = []
|
||||
|
||||
if not self.target_param_name:
|
||||
@@ -50,57 +51,30 @@ class MissingRequiredFieldQueryCase(BaseAPITestCase):
|
||||
self.logger.info("由于未识别到可移除的必填查询参数,跳过此测试用例。")
|
||||
return results
|
||||
|
||||
status_code = response_context.status_code
|
||||
expected_http_status_code = 200
|
||||
expected_business_error_code = "4003"
|
||||
|
||||
context_msg_prefix = f"当移除必填查询参数 '{self.target_param_name}' 时, "
|
||||
|
||||
# 检查状态码
|
||||
if response_context.status_code != expected_http_status_code:
|
||||
return [self.failed(
|
||||
message=f"{context_msg_prefix}API应返回状态码 {expected_http_status_code},但实际为 {response_context.status_code}。",
|
||||
details={"expected_status": expected_http_status_code, "actual_status": response_context.status_code}
|
||||
)]
|
||||
|
||||
# 检查业务错误码
|
||||
json_content = response_context.json_content
|
||||
expected_http_status_codes = [400, 422]
|
||||
specific_business_error_code = "4003"
|
||||
error_code_field_in_body = "code"
|
||||
if not isinstance(json_content, dict):
|
||||
return [self.failed(f"{context_msg_prefix}API响应体不是一个有效的JSON对象。")]
|
||||
|
||||
context_msg_prefix = f"当移除必填查询参数 '{self.target_param_name}' 时,"
|
||||
actual_business_code = json_content.get("code")
|
||||
if str(actual_business_code) != expected_business_error_code:
|
||||
return [self.failed(
|
||||
message=f"{context_msg_prefix}业务错误码应为 '{expected_business_error_code}',但实际为 '{actual_business_code}'。",
|
||||
details={"expected_code": expected_business_error_code, "actual_code": actual_business_code, "response_body": 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.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"{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.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
|
||||
return [self.passed(
|
||||
f"{context_msg_prefix}API正确返回了状态码 {expected_http_status_code} 和业务错误码 '{expected_business_error_code}'。"
|
||||
)]
|
||||
@@ -0,0 +1,193 @@
|
||||
from typing import Dict, Any, Optional, List, Union, Tuple
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
|
||||
import logging
|
||||
from ddms_compliance_suite.utils import schema_utils
|
||||
|
||||
class NumberOutOfRangeCase(BaseAPITestCase):
|
||||
id = "TC-ERROR-4002"
|
||||
name = "Error Code 4002 - Number Value Out of Range Validation"
|
||||
description = "测试当发送的数值参数超出范围限制时,API是否按预期返回code=4002的错误(状态码应为200)。"
|
||||
severity = TestSeverity.MEDIUM
|
||||
tags = ["error-handling", "appendix-b", "4002", "out-of-range"]
|
||||
execution_order = 203 # 在类型不匹配测试之后执行
|
||||
|
||||
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_location: Optional[str] = None
|
||||
self.target_field_path: Optional[List[Union[str, int]]] = None
|
||||
self.target_field_schema: Optional[Dict[str, Any]] = None
|
||||
self.target_field_name: Optional[str] = None
|
||||
self.target_field_type: Optional[str] = None
|
||||
self.min_value: Optional[float] = None
|
||||
self.max_value: Optional[float] = None
|
||||
self.out_of_range_value: Optional[Union[int, float]] = None
|
||||
|
||||
self._initialize_test_case()
|
||||
|
||||
def _find_first_ranged_numeric_field(self) -> Optional[Tuple[str, List[Union[str, int]], Dict[str, Any], str, Optional[str], Optional[float], Optional[float], str]]:
|
||||
"""
|
||||
按优先级(body -> query -> path -> header)查找第一个具有范围限制的数值字段。
|
||||
"""
|
||||
self.logger.info(f"[{self.id}] Searching for a ranged numeric field...")
|
||||
|
||||
# 1. 检查请求体 (Body)
|
||||
body_schema = self._get_resolved_request_body_schema()
|
||||
if body_schema:
|
||||
self.logger.debug(f"[{self.id}] Checking for ranged numeric field in request body.")
|
||||
result = schema_utils.util_find_ranged_field_recursive(body_schema, [], self.logger)
|
||||
if result:
|
||||
path, schema, type, min_val, max_val, desc = result
|
||||
name = path[-1] if path else "body_root"
|
||||
self.logger.info(f"[{self.id}] Found ranged numeric field in BODY at path: {'.'.join(map(str, path))}")
|
||||
return "body", path, schema, name, type, min_val, max_val, desc
|
||||
|
||||
# 2. 检查查询参数, 路径参数, 请求头
|
||||
for location in ["query", "path", "header"]:
|
||||
if 'parameters' in self.endpoint_spec:
|
||||
self.logger.debug(f"[{self.id}] Checking for ranged numeric field in {location} parameters.")
|
||||
for param in self.endpoint_spec['parameters']:
|
||||
if param.get('in') == location:
|
||||
param_schema = param.get('schema', {})
|
||||
param_desc = param.get('description', '')
|
||||
param_name = param.get('name')
|
||||
|
||||
min_val, max_val = schema_utils.util_extract_range_from_description(param_desc, self.logger)
|
||||
|
||||
if min_val is None and max_val is None:
|
||||
min_val = param_schema.get('minimum')
|
||||
max_val = param_schema.get('maximum')
|
||||
|
||||
if min_val is not None or max_val is not None:
|
||||
param_type = param_schema.get('type')
|
||||
self.logger.info(f"[{self.id}] Found ranged numeric field in {location.upper()}: '{param_name}'")
|
||||
return location, [param_name], param_schema, param_name, param_type, min_val, max_val, param_desc
|
||||
|
||||
self.logger.info(f"[{self.id}] No ranged numeric field found in any location.")
|
||||
return None
|
||||
|
||||
def _initialize_test_case(self):
|
||||
"""使用框架辅助方法初始化测试用例。"""
|
||||
found_field = self._find_first_ranged_numeric_field()
|
||||
|
||||
if found_field:
|
||||
location, path, schema, name, field_type, min_val, max_val, description = found_field
|
||||
|
||||
self.target_field_location = location
|
||||
self.target_field_path = path
|
||||
self.target_field_schema = schema
|
||||
self.target_field_name = name
|
||||
# 如果'type'未定义,我们根据是否存在小数来推断
|
||||
self.target_field_type = field_type if field_type else ('number' if isinstance(min_val, float) or isinstance(max_val, float) else 'integer')
|
||||
self.min_value = min_val
|
||||
self.max_value = max_val
|
||||
|
||||
self.logger.info(
|
||||
f"[{self.id}] Target found in '{location}' at path '{'.'.join(map(str, path))}'. "
|
||||
f"Type: {self.target_field_type}, Min: {min_val}, Max: {max_val}, Desc: '{description}'"
|
||||
)
|
||||
|
||||
self._set_out_of_range_value()
|
||||
else:
|
||||
self.logger.info(f"[{self.id}] No suitable ranged numeric field found for this endpoint.")
|
||||
|
||||
def _set_out_of_range_value(self):
|
||||
"""根据提取的范围信息设置越界值。"""
|
||||
# 优先选择超过最大值
|
||||
if self.max_value is not None:
|
||||
if self.target_field_type == 'integer':
|
||||
self.out_of_range_value = int(self.max_value) + 1
|
||||
else:
|
||||
self.out_of_range_value = self.max_value + 1.0
|
||||
self.logger.info(f"[{self.id}] Setting out-of-range value to {self.out_of_range_value} (max is {self.max_value}).")
|
||||
return
|
||||
|
||||
# 其次选择小于最小值
|
||||
if self.min_value is not None:
|
||||
if self.target_field_type == 'integer':
|
||||
self.out_of_range_value = int(self.min_value) - 1
|
||||
else:
|
||||
self.out_of_range_value = self.min_value - 1.0
|
||||
self.logger.info(f"[{self.id}] Setting out-of-range value to {self.out_of_range_value} (min is {self.min_value}).")
|
||||
|
||||
def _modify_params(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""通用函数,用于修改参数字典中的值。"""
|
||||
if self.out_of_range_value is None or not self.target_field_path:
|
||||
return params
|
||||
|
||||
modified_params, success = schema_utils.util_set_value_at_path(
|
||||
data_container=params,
|
||||
path=self.target_field_path,
|
||||
new_value=self.out_of_range_value
|
||||
)
|
||||
|
||||
if success:
|
||||
self.logger.debug(f"[{self.id}] Successfully injected out-of-range value into parameters.")
|
||||
return modified_params
|
||||
|
||||
self.logger.error(f"[{self.id}] Failed to set out-of-range value in parameters.")
|
||||
return params
|
||||
|
||||
def generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if self.target_field_location == "query":
|
||||
return self._modify_params(current_query_params)
|
||||
return current_query_params
|
||||
|
||||
def generate_path_params(self, current_path_params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if self.target_field_location == "path":
|
||||
return self._modify_params(current_path_params)
|
||||
return current_path_params
|
||||
|
||||
def generate_headers(self, current_headers: Dict[str, str]) -> Dict[str, str]:
|
||||
if self.target_field_location == "header":
|
||||
# Header values must be strings
|
||||
original_value = self.out_of_range_value
|
||||
self.out_of_range_value = str(self.out_of_range_value)
|
||||
headers = self._modify_params(current_headers)
|
||||
self.out_of_range_value = original_value # revert for logging
|
||||
return headers
|
||||
return current_headers
|
||||
|
||||
def generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]:
|
||||
if self.target_field_location == "body":
|
||||
return self._modify_params(current_body)
|
||||
return current_body
|
||||
|
||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
"""验证API是否正确返回code=4002错误(状态码为200)。"""
|
||||
results = []
|
||||
|
||||
if self.out_of_range_value is None:
|
||||
return [self.passed("跳过测试:未找到具有明确范围限制的数值字段。")]
|
||||
|
||||
expected_http_status_code = 200
|
||||
expected_business_error_code = "4002"
|
||||
|
||||
context_msg_prefix = (
|
||||
f"当 {self.target_field_location} 字段 '{self.target_field_name}' "
|
||||
f"值为 {self.out_of_range_value} (超出范围: min={self.min_value}, max={self.max_value}) 时, "
|
||||
)
|
||||
|
||||
# 检查状态码
|
||||
if response_context.status_code != expected_http_status_code:
|
||||
return [self.failed(
|
||||
message=f"{context_msg_prefix}API应返回状态码 {expected_http_status_code},但实际为 {response_context.status_code}。",
|
||||
details={"expected_status": expected_http_status_code, "actual_status": response_context.status_code}
|
||||
)]
|
||||
|
||||
# 检查业务错误码
|
||||
json_content = response_context.json_content
|
||||
if not isinstance(json_content, dict):
|
||||
return [self.failed(f"{context_msg_prefix}API响应体不是一个有效的JSON对象。")]
|
||||
|
||||
actual_business_code = json_content.get("code")
|
||||
if str(actual_business_code) != expected_business_error_code:
|
||||
return [self.failed(
|
||||
message=f"{context_msg_prefix}业务错误码应为 '{expected_business_error_code}',但实际为 '{actual_business_code}'。",
|
||||
details={"expected_code": expected_business_error_code, "actual_code": actual_business_code, "response_body": json_content}
|
||||
)]
|
||||
|
||||
return [self.passed(
|
||||
f"{context_msg_prefix}API正确返回了状态码 {expected_http_status_code} 和业务错误码 '{expected_business_error_code}'。"
|
||||
)]
|
||||
@@ -81,64 +81,37 @@ class TypeMismatchBodyCase(BaseAPITestCase):
|
||||
return current_body
|
||||
|
||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
"""验证API是否正确返回code=4001错误(状态码为200)。"""
|
||||
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" # 响应体中业务错误码的字段名
|
||||
expected_http_status_code = 200
|
||||
expected_business_error_code = "4001"
|
||||
|
||||
field_path_str = '.'.join(map(str, self.target_field_path))
|
||||
context_msg_prefix = f"当请求体字段 '{field_path_str}' 类型不匹配时,"
|
||||
context_msg_prefix = f"当请求体字段 '{field_path_str}' 类型不匹配时, "
|
||||
|
||||
http_status_ok = status_code in expected_http_status_codes
|
||||
business_code_ok = False
|
||||
is_4xx_error = 400 <= status_code <= 499
|
||||
# 检查状态码
|
||||
if response_context.status_code != expected_http_status_code:
|
||||
return [self.failed(
|
||||
message=f"{context_msg_prefix}API应返回状态码 {expected_http_status_code},但实际为 {response_context.status_code}。",
|
||||
details={"expected_status": expected_http_status_code, "actual_status": response_context.status_code}
|
||||
)]
|
||||
|
||||
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
|
||||
# 检查业务错误码
|
||||
json_content = response_context.json_content
|
||||
if not isinstance(json_content, dict):
|
||||
return [self.failed(f"{context_msg_prefix}API响应体不是一个有效的JSON对象。")]
|
||||
|
||||
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"{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.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."
|
||||
actual_business_code = json_content.get("code")
|
||||
if str(actual_business_code) != expected_business_error_code:
|
||||
return [self.failed(
|
||||
message=f"{context_msg_prefix}业务错误码应为 '{expected_business_error_code}',但实际为 '{actual_business_code}'。",
|
||||
details={"expected_code": expected_business_error_code, "actual_code": actual_business_code, "response_body": json_content}
|
||||
)]
|
||||
|
||||
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
|
||||
return [self.passed(
|
||||
f"{context_msg_prefix}API正确返回了状态码 {expected_http_status_code} 和业务错误码 '{expected_business_error_code}'。"
|
||||
)]
|
||||
+24
-51
@@ -78,67 +78,40 @@ class TypeMismatchQueryParamCase(BaseAPITestCase):
|
||||
return current_query_params
|
||||
|
||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
"""验证API是否正确返回code=4001错误(状态码为200)。"""
|
||||
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"
|
||||
|
||||
# Use self.target_param_name for a clearer context message if a top-level param was identified
|
||||
expected_http_status_code = 200
|
||||
expected_business_error_code = "4001"
|
||||
|
||||
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))}') 类型不匹配时,"
|
||||
context_msg_prefix = f"当查询参数 '{context_param_identifier}' (路径: '{'.'.join(map(str, self.target_field_path))}') 类型不匹配时, "
|
||||
|
||||
http_status_ok = status_code in expected_http_status_codes
|
||||
business_code_ok = False
|
||||
is_4xx_error = 400 <= status_code <= 499
|
||||
# 检查状态码
|
||||
if response_context.status_code != expected_http_status_code:
|
||||
return [self.failed(
|
||||
message=f"{context_msg_prefix}API应返回状态码 {expected_http_status_code},但实际为 {response_context.status_code}。",
|
||||
details={"expected_status": expected_http_status_code, "actual_status": response_context.status_code}
|
||||
)]
|
||||
|
||||
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
|
||||
# 检查业务错误码
|
||||
json_content = response_context.json_content
|
||||
if not isinstance(json_content, dict):
|
||||
return [self.failed(f"{context_msg_prefix}API响应体不是一个有效的JSON对象。")]
|
||||
|
||||
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"{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})")
|
||||
|
||||
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."
|
||||
actual_business_code = json_content.get("code")
|
||||
if str(actual_business_code) != expected_business_error_code:
|
||||
return [self.failed(
|
||||
message=f"{context_msg_prefix}业务错误码应为 '{expected_business_error_code}',但实际为 '{actual_business_code}'。",
|
||||
details={"expected_code": expected_business_error_code, "actual_code": actual_business_code, "response_body": json_content}
|
||||
)]
|
||||
|
||||
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
|
||||
return [self.passed(
|
||||
f"{context_msg_prefix}API正确返回了状态码 {expected_http_status_code} 和业务错误码 '{expected_business_error_code}'。"
|
||||
)]
|
||||
|
||||
def generate_path_params(self, current_path_params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
# ... existing code ...
|
||||
|
||||
Reference in New Issue
Block a user