This commit is contained in:
gongwenxin
2025-05-28 15:55:46 +08:00
parent f0cc525141
commit 936714242f
313 changed files with 345685 additions and 7847 deletions
+53
View File
@@ -0,0 +1,53 @@
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
import logging
from typing import Dict, Any, Optional # 确保引入 Optional
class StatusCode200Check(BaseAPITestCase):
# 1. 元数据
id = "TC-STATUS-001"
name = "基本状态码 200 检查"
description = "验证 API 响应状态码是否为 200 OK。"
severity = TestSeverity.CRITICAL
tags = ["status_code", "smoke_test"]
# 适用于所有方法和路径 (默认)
# applicable_methods = None
# applicable_paths_regex = None
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
# use_llm_for_headers: bool = True
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None):
super().__init__(endpoint_spec, global_api_spec, json_schema_validator=json_schema_validator, llm_service=llm_service)
self.logger.info(f"测试用例 {self.id} ({self.name}) 已针对端点 '{self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')}' 初始化。")
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> list[ValidationResult]:
results = []
expected_status_code = 200
actual_status_code = response_context.status_code
if actual_status_code == expected_status_code:
results.append(
ValidationResult(
passed=True,
message=f"响应状态码为 {actual_status_code},符合预期 {expected_status_code}。"
)
)
self.logger.info(f"状态码验证通过: {actual_status_code} == {expected_status_code} for {request_context.url}")
else:
results.append(
ValidationResult(
passed=False,
message=f"期望状态码 {expected_status_code},但收到 {actual_status_code}。",
details={
"expected_status": expected_status_code,
"actual_status": actual_status_code,
"request_url": request_context.url,
"response_body_sample": (response_context.text_content or "")[:200] # 包含部分响应体以帮助诊断
}
)
)
self.logger.warning(f"状态码验证失败: 期望 {expected_status_code}, 实际 {actual_status_code} for {request_context.url}")
return results
@@ -0,0 +1 @@
# This file marks the compliance_catalog directory as a Python package.
@@ -0,0 +1 @@
# This file marks the core_functionality directory as a Python package.
@@ -0,0 +1,81 @@
from typing import Dict, Any, Optional, List
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
import json
class ResponseSchemaValidationCase(BaseAPITestCase):
id = "TC-CORE-FUNC-001"
name = "Response Body JSON Schema Validation"
description = "验证API响应体是否符合API规范中定义的JSON Schema。"
severity = TestSeverity.CRITICAL
tags = ["core-functionality", "schema-validation", "output-format"]
execution_order = 100 # Default, can be adjusted
# This test is generally applicable, especially for GET requests or successful POST/PUT.
# It might need refinement based on specific endpoint characteristics (e.g., no response body for DELETE)
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None):
super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service=llm_service)
self.logger.info(f"测试用例 '{self.id}' 已为端点 '{self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')}' 初始化。")
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
results = []
method = request_context.method.upper()
status_code = response_context.status_code
# Determine the expected response schema based on method and status code
# This logic might need to be more sophisticated depending on how schemas are structured in your API spec (YAPI/Swagger)
expected_schema = None
response_spec_key = None
if 'responses' in self.endpoint_spec: # OpenAPI/Swagger style
if str(status_code) in self.endpoint_spec['responses']:
response_def = self.endpoint_spec['responses'][str(status_code)]
if 'content' in response_def and 'application/json' in response_def['content']:
expected_schema = response_def['content']['application/json'].get('schema')
response_spec_key = f"responses.{status_code}.content.application/json.schema"
elif 'default' in self.endpoint_spec['responses']: # Fallback to default response
response_def = self.endpoint_spec['responses']['default']
if 'content' in response_def and 'application/json' in response_def['content']:
expected_schema = response_def['content']['application/json'].get('schema')
response_spec_key = f"responses.default.content.application/json.schema"
elif 'res_body_type' in self.endpoint_spec and self.endpoint_spec['res_body_type'] == 'json': # YAPI style (simplified)
if 'res_body_is_json_schema' in self.endpoint_spec and self.endpoint_spec['res_body_is_json_schema']:
if self.endpoint_spec.get('res_body'):
try:
# YAPI often stores schema as a JSON string
expected_schema = json.loads(self.endpoint_spec['res_body'])
response_spec_key = "res_body (从JSON字符串解析)"
except json.JSONDecodeError as e:
self.logger.error(f"从YAPI res_body解析JSON schema失败: {e}")
results.append(self.failed(f"无法从YAPI规范解析响应schema: {e}"))
return results
# Only proceed with schema validation if we have a schema and a JSON response body
if expected_schema and response_context.json_content is not None:
self.logger.info(f"将根据路径 '{response_spec_key or '未知位置'}' 的schema验证响应体。")
schema_validation_results = self.validate_data_against_schema(
data_to_validate=response_context.json_content,
schema_definition=expected_schema,
context_message_prefix=f"针对 {method} {request_context.url} (状态码 {status_code}) 的响应体"
)
results.extend(schema_validation_results)
elif response_context.json_content is None and method not in ["DELETE", "HEAD", "OPTIONS"] and status_code in [200, 201, 202]:
# If we expected a JSON body (e.g. for successful GET/POST) but got none
if expected_schema: # and if a schema was defined
results.append(self.failed(
message=f"根据schema期望一个JSON响应体,但未收到可解析的JSON内容。",
details={"status_code": status_code, "response_text_sample": (response_context.text_content or "")[:200]}
))
self.logger.warning(f"期望 {method} {request_context.url} 返回JSON响应体,但未收到或非JSON格式。")
elif not expected_schema and response_context.json_content is not None and status_code // 100 == 2:
# If there is a JSON body but no schema was found for successful responses
self.logger.info(f"响应包含JSON体,但在API规范中未找到针对状态码 {status_code} 的JSON schema。跳过schema验证。")
# Optionally, add an informational validation result:
# results.append(ValidationResult(passed=True, message="Response has JSON body, but no schema defined for validation.", details={"status_code": status_code}))
elif not expected_schema and response_context.json_content is None:
self.logger.info(f"状态码 {status_code} 的响应无JSON体也无定义的schema。跳过schema验证。")
if not results: # If no specific validation was added (e.g. schema not found but not an error)
results.append(self.passed("Schema验证步骤完成(未发现问题,或schema不适用/未为此响应定义)。"))
return results
@@ -0,0 +1 @@
# This file marks the error_handling directory as a Python package.
@@ -0,0 +1,122 @@
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 # Keep this import for util_remove_value_at_path
class MissingRequiredFieldBodyCase(BaseAPITestCase):
id = "TC-ERROR-4003-BODY"
name = "Error Code 4003 - Missing Required Request Body Field Validation"
description = "测试当请求体中缺少API规范定义的必填字段时,API是否按预期返回类似4003的错误(或通用400错误)。"
severity = TestSeverity.HIGH
tags = ["error-handling", "appendix-b", "4003", "required-fields", "request-body"]
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)
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
# 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 not self.removed_field_path:
self.logger.info('在请求体 schema 中未找到可用于测试 "必填字段缺失" 的字段(通过基类方法)。')
else:
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.")
return current_query_params
def generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]:
if not self.removed_field_path:
self.logger.debug("No field path identified for removal in request body. Returning original body.")
return current_body
modified_body, self.original_value_at_path, success = schema_utils.util_remove_value_at_path(
data_container=current_body,
path=self.removed_field_path
)
if success:
self.logger.info(f"为进行必填字段缺失测试,已通过工具方法从请求体中移除字段路径 '{'.'.join(map(str, self.removed_field_path))}'。")
return modified_body
else:
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
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
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" # 响应体中业务错误码的字段名
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.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."
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
@@ -0,0 +1,106 @@
from typing import Dict, Any, Optional, List
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
import copy
class MissingRequiredFieldQueryCase(BaseAPITestCase):
id = "TC-ERROR-4003-QUERY"
name = "Error Code 4003 - Missing Required Query Parameter Validation"
description = "测试当请求中缺少API规范定义的必填查询参数时,API是否按预期返回类似4003的错误(或通用400错误)。"
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
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.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):
"""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.
self.logger.debug(f"{self.id} is focused on query parameters, generate_request_body will not modify the request body.")
return current_body
def generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]:
if self.target_param_name and isinstance(current_query_params, dict):
if self.target_param_name in current_query_params:
new_params = copy.deepcopy(current_query_params)
original_value = new_params.pop(self.target_param_name) # 移除参数
self.logger.info(f"为进行必填查询参数缺失测试,已从查询参数中移除 '{self.target_param_name}' (原值: '{original_value}')。")
return new_params
else:
self.logger.warning(f"计划移除的查询参数 '{self.target_param_name}' 在当前查询参数中未找到。")
return current_query_params
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
results = []
if not self.target_param_name:
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]
specific_business_error_code = "4003"
error_code_field_in_body = "code"
context_msg_prefix = f"当移除必填查询参数 '{self.target_param_name}' 时,"
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
@@ -0,0 +1,144 @@
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"
name = "Error Code 4001 - Request Body Type Mismatch Validation"
description = "测试当发送的请求体中字段的数据类型与API规范定义不符时,API是否按预期返回类似4001的错误(或通用400错误)。"
severity = TestSeverity.MEDIUM
tags = ["error-handling", "appendix-b", "4001", "request-body"]
execution_order = 202 # Slightly after query param one
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None):
super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service=llm_service)
self.logger.setLevel(logging.DEBUG)
self.target_field_path: Optional[List[str]] = None
self.original_field_type: Optional[str] = None
# Location is always 'body' for this class
self.target_field_location: str = "body"
self.target_field_schema: Optional[Dict[str, Any]] = None
self.json_schema_validator = json_schema_validator
self.original_value_at_path: Any = None
self.mismatched_value: Any = None
self._try_find_mismatch_target_in_body()
def _try_find_mismatch_target_in_body(self):
self.logger.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:
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.info(f"[{self.id}] No suitable simple type field found in request body schema for type mismatch test.")
else:
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.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 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.id}] Preparing to modify request body for type mismatch. Target path: {self.target_field_path}, Original type: {self.original_field_type}")
# 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
)
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))}'.")
modified_body, success = schema_utils.util_set_value_at_path(
data_container=current_body,
path=self.target_field_path,
new_value=mismatched_value
)
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
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" # 响应体中业务错误码的字段名
field_path_str = '.'.join(map(str, self.target_field_path))
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 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"{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."
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
@@ -0,0 +1,147 @@
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"
name = "Error Code 4001 - Query Parameter Type Mismatch Validation"
description = "测试当发送的查询参数数据类型与API规范定义不符时,API是否按预期返回类似4001的错误(或通用400错误)。"
severity = TestSeverity.MEDIUM
tags = ["error-handling", "appendix-b", "4001", "query-parameters"]
execution_order = 201 # Slightly after the combined one might have been
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None):
super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service=llm_service)
self.logger.setLevel(logging.DEBUG)
self.target_field_path: Optional[List[str]] = None
self.original_field_type: Optional[str] = None
# Location is always 'query' for this class
self.target_field_location: str = "query"
self.target_field_schema: Optional[Dict[str, Any]] = None
self.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()
def _try_find_mismatch_target_in_query(self):
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")
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.")
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 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.id}] Preparing to modify query params for type mismatch. Target path: {self.target_field_path}, Original type: {self.original_field_type}")
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
)
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))}'.")
# 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"
# 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))}') 类型不匹配时,"
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. (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."
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
@@ -0,0 +1 @@
# This file marks the normative_spec directory as a Python package.
@@ -0,0 +1,67 @@
from typing import Dict, Any, Optional, List
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
# class HTTPMethodUsageCase(BaseAPITestCase):
# id = "TC-NORMATIVE-001"
# name = "HTTP Method Usage Verification"
# description = "验证API是否恰当使用HTTP方法(例如,GET用于检索,POST用于创建)。目前不测试对不支持方法的405响应。"
# severity = TestSeverity.MEDIUM
# tags = ["normative-spec", "http", "restful"]
# execution_order = 110
# # 此测试通常适用。
# # 检查对不支持方法的405响应会比较复杂,需要知道哪些方法对于每个路径是明确不支持的,
# # 或者尝试所有其他方法,这在API规范中并不总是明确的。
# def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None):
# super().__init__(endpoint_spec, global_api_spec, json_schema_validator)
# self.logger.info(f"测试用例 '{self.id}' 已为端点 '{self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')}' 初始化。")
# def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
# results = []
# method = request_context.method.upper()
# status_code = response_context.status_code
# # 基于常见RESTful约定的基本检查
# # 这些是通用指南,可能需要根据具体的API设计进行调整。
# if method == "GET":
# if status_code // 100 == 2: # 成功的GET
# results.append(self.passed(f"GET请求 {request_context.url} 返回了成功的状态码 {status_code}。"))
# elif status_code == 404:
# results.append(self.passed(f"GET请求 {request_context.url} 返回404,如果资源不存在则这是有效的响应。"))
# # GET请求的其他状态码可能是错误或此处未覆盖的特定条件。
# elif method == "POST":
# if status_code == 201: # 已创建
# results.append(self.passed(f"POST请求 {request_context.url} 返回201 Created,符合资源创建的预期。"))
# elif status_code == 200 or status_code == 202: # OK或已接受(例如,用于异步任务)
# results.append(self.passed(f"POST请求 {request_context.url} 返回{status_code},这可以是有效的响应。"))
# # 可添加对400(错误请求,例如payload无效)等的检查。
# elif method == "PUT":
# if status_code == 200: # OK(已更新)
# results.append(self.passed(f"PUT请求 {request_context.url} 返回200 OK,符合资源更新的预期。"))
# elif status_code == 201: # 已创建(如果PUT在资源不存在时创建资源)
# results.append(self.passed(f"PUT请求 {request_context.url} 返回201 Created,这可以是有效的响应。"))
# elif status_code == 204: # 无内容(已更新,不返回响应体)
# results.append(self.passed(f"PUT请求 {request_context.url} 返回204 No Content,这可以是有效的响应。"))
# # 可添加对404(未找到,如果要更新的资源不存在,除非PUT会创建)的检查。
# elif method == "DELETE":
# if status_code == 200 or status_code == 202 or status_code == 204: # OK、已接受或无内容
# results.append(self.passed(f"DELETE请求 {request_context.url} 返回{status_code},表示成功删除。"))
# # 可添加对404(未找到,如果要删除的资源不存在)的检查。
# # 405(方法不允许)检查的占位符 - 这比较复杂
# # 要测试405,通常需要:
# # 1. 知道哪些方法是此路径明确不允许的。
# # 2. 或者,尝试使用其他常见方法(OPTIONS, PATCH等)发送请求,
# # 如果这些方法未在此路径的规范中定义,则期望405。
# # 这通常需要更具针对性的测试用例或不同的方法。
# # self.logger.info("此通用测试用例未实现405(方法不允许)检查。")
# if not results: # 如果没有为该方法触发特定的验证
# results.append(self.passed(f"针对 {method} {request_context.url} 的HTTP方法使用检查完成(基于通用约定未发现特定问题)。"))
# return results
@@ -0,0 +1,247 @@
# import re
# import json # 确保导入json
# from typing import Dict, Any, Optional, List
# from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext
# # LLMService的导入路径需要根据您的项目结构确认
# # 假设 LLMService 在 ddms_compliance_suite.llm_utils.llm_service
# try:
# from ddms_compliance_suite.llm_utils.llm_service import LLMService
# except ImportError:
# LLMService = None
# # print("LLMService not found, PathVerbNounCheckCase will be skipped or limited.")
# class ComprehensiveURLCheckLLMCase(BaseAPITestCase):
# id = "TC-NORMATIVE-URL-LLM-COMPREHENSIVE-001"
# name = "综合URL规范与RESTful风格检查 (LLM)"
# description = (
# "使用LLM统一评估API路径是否符合以下规范:\n"
# "1. 路径参数命名 (例如,全小写蛇形命名法)。\n"
# "2. URL路径结构 (例如,/{领域}/{版本号}/资源类型)。\n"
# "3. URL版本号嵌入 (例如,包含 /v1/)。\n"
# "4. RESTful风格与可读性 (名词表示资源,HTTP方法表示动作,易理解性)。"
# )
# severity = TestSeverity.MEDIUM # 综合性检查,可能包含不同严重级别的问题
# tags = ["normative-spec", "url", "restful", "llm", "readability", "naming-convention", "structure", "versioning", "static-check"]
# execution_order = 60 # 更新执行顺序
# # 此测试用例可以覆盖所有路径,但其有效性依赖LLM
# # applicable_methods = None
# # applicable_paths_regex = None
# # 这个标志可以用来在测试用例级别控制是否实际调用LLM,即使全局LLM服务可用
# # 如果您希望总是尝试(只要LLMService能初始化),可以不设置这个,或者在逻辑中检查 self.llm_service 是否存在
# # use_llm_for_path_analysis: bool = True
# def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[LLMService] = None):
# super().__init__(endpoint_spec, global_api_spec, json_schema_validator)
# self.llm_service = llm_service # 存储注入的 LLMService 实例
# if not self.llm_service:
# self.logger.warning(f"LLMService 未注入或初始化失败,测试用例 {self.id} 将无法执行LLM路径分析。")
# def _get_llm_service_from_orchestrator(self) -> Optional[Any]:
# # 在实际框架中,测试用例可能无法直接访问编排器来获取LLM服务。
# # 这种依赖注入通常在测试用例实例化时或方法调用时处理。
# # 此处为一个占位符,理想情况下APITestOrchestrator会将llm_service实例传给需要它的测试用例,
# # 或测试用例通过某种服务定位器获取。
# # 暂时我们假设,如果全局配置了LLM,它就能用。
# # 真实的实现需要APITestOrchestrator在执行此测试用例前,将llm_service实例注入。
# # 为了能运行,我们先返回None,并在下面逻辑中处理。
# # 或者,修改 Orchestrator 将其注入到 self.global_api_spec 或 self.endpoint_spec (不推荐)
# # 最好的方式是在 __init__ 中接收一个 llm_service: Optional[LLMService] 参数。
# # 但这需要修改 BaseAPITestCase 和 APITestOrchestrator 的 __init__ 和调用逻辑。
# # 临时的解决方法:依赖 APITestOrchestrator 初始化时是否成功创建了 LLMService。
# # 这仍然是一个间接的检查。一个更直接的方式是在Orchestrator执行此测试用例时传入。
# if hasattr(self, '_orchestrator_llm_service_instance') and self._orchestrator_llm_service_instance:
# return self._orchestrator_llm_service_instance
# # 如果没有明确注入,我们只能依赖全局LLMService是否被加载
# if LLMService is not None:
# # 这里不能直接实例化一个新的LLMService,因为它需要API Key等配置,这些配置在Orchestrator那里。
# # 这个测试用例需要依赖Orchestrator来提供一个已经配置好的LLMService实例。
# # 此处返回一个指示:如果LLM功能应该被使用,则需要Orchestrator提供服务。
# return "NEEDS_INJECTION"
# return None
# def _extract_path_param_names(self, path_template: str) -> List[str]:
# """从路径模板中提取路径参数名称。例如 /users/{user_id}/items/{item_id} -> ['user_id', 'item_id']"""
# return re.findall(r'\{([^}]+)\}', path_template)
# def validate_request_url(self, url: str, request_context: APIRequestContext) -> List[ValidationResult]:
# results: List[ValidationResult] = []
# path_template = self.endpoint_spec.get('path', '')
# http_method = request_context.method.upper()
# operation_id = self.endpoint_spec.get('operationId', self.endpoint_spec.get('title', '')) # 获取operationId或title
# if not self.llm_service:
# results.append(ValidationResult(
# passed=True, # 标记为通过以避免阻塞,但消息表明跳过
# message=f"路径 '{path_template}' 的LLM综合URL检查已跳过:LLM服务不可用。",
# details={"path_template": path_template, "http_method": http_method, "reason": "LLM Service not available or not injected."}
# ))
# self.logger.warning(f"LLM综合URL检查已跳过对路径 '{path_template}' 的检查:LLM服务不可用。")
# return results
# path_param_names = self._extract_path_param_names(path_template)
# path_params_str = ", ".join(path_param_names) if path_param_names else "无"
# # - 接口名称 (OperationId 或 Title): {operation_id if operation_id else '请你自己从路径模板中提取'}
# # 构建给LLM的Prompt,要求JSON输出
# prompt_instruction = f"""
# 请扮演一位资深的API设计评审员。我将提供一个API端点的路径模板、HTTP方法以及可能的接口名称。
# 请根据以下石油行业API设计规范评估此API端点,并以严格的JSON格式返回您的评估结果。
# JSON对象应包含一个名为 "assessments" 的键,其值为一个对象列表,每个对象代表对一个标准的评估,包含 "standard_name" (字符串), "is_compliant" (布尔值), 和 "reason" (字符串) 三个键。
# API端点信息:
# - HTTP方法: {http_method}
# - 路径模板: {path_template}
# - 路径中提取的参数名: [{path_params_str}]
# 评估标准:
# 1. **接口名称规范 (接口名称需要你从路径模板中提取,一般是路径中除了参数名以外的最后的一个单词)**:
# - 规则: 采用'动词+名词'结构,明确业务语义 (例如: GetWellLog, SubmitSeismicJob)。
# - standard_name: "interface_naming_convention"
# 2. **HTTP方法使用规范**:
# - 规则: 遵循RESTful规范:GET用于数据检索, POST用于创建资源, PUT用于更新资源, DELETE用于删除资源。
# - standard_name: "http_method_usage"
# 3. **URL路径结构规范**:
# - 规则: 格式为 `<前缀>/<专业领域>/v<版本号>/<资源类型>` (例如: /logging/v1.2/wells, /seismicprospecting/v1.0/datasets)。
# - 前缀: 示例: /api/dms
# - 专业领域: 专业领域示例: seismicprospecting, welllogging, reservoirevaluation
# - 版本号: 语义化版本,例如 v1, v1.0, v2.1.3。
# - 资源类型: 通常为名词复数。
# - standard_name: "url_path_structure"
# 4. **URL路径参数命名规范**:
# - 规则: 路径参数(如果存在)必须使用全小写字母(可以是一个单词)或小写字母加下划线命名(这是多个单词的情况),并能反映资源的唯一标识 (例如: {{well_id}},还有{{version}},{{schema}}也是合规的,比一定非要{{version_id}})。
# - standard_name: "url_path_parameter_naming"
# 5. **资源命名规范 (在路径中)**:
# - 规则: 资源集合应使用名词的复数形式表示 (例如 `/wells`, `/logs`);应优先使用石油行业的标准术语 (例如用 `trajectory` 而非 `path` 来表示井轨迹)。
# - standard_name: "resource_naming_in_path"
# 请确保您的输出是一个可以被 `json.loads()` 直接解析的JSON对象。
# 例如:
# {{
# "assessments": [
# {{
# "standard_name": "interface_naming_convention",
# "is_compliant": true,
# "reason": "接口名称 'GetWellboreTrajectory' 符合动词+名词结构。"
# }},
# {{
# "standard_name": "http_method_usage",
# "is_compliant": true,
# "reason": "GET方法用于检索资源,符合规范。"
# }}
# // ... 其他标准的评估 ...
# ]
# }}
# """
# # 6. **路径可读性与整体RESTful风格**:
# # - 规则: 路径整体是否具有良好的可读性、易于理解其功能,并且符合RESTful设计原则?(综合评估,可参考前面几点)
# # - standard_name: "general_readability_and_restfulness"
# messages = [
# {"role": "system", "content": "你是一位API设计评审专家,专注于评估API的URL规范性和RESTful风格。你的输出必须是严格的JSON格式。"},
# {"role": "user", "content": prompt_instruction}
# ]
# self.logger.info(f"向LLM发送请求,评估路径: {path_template} ({http_method})")
# # 假设 _execute_chat_completion_request 支持 response_format={"type": "json_object"} (如果LLM API支持)
# # 否则,我们需要解析文本输出。为简化,这里假设LLM会遵循JSON格式指令。
# llm_response_str = self.llm_service._execute_chat_completion_request(
# messages=messages,
# max_tokens=1024, # 根据评估结果的复杂度调整
# temperature=0.2 # 低温以获得更确定的、结构化的输出
# )
# if not llm_response_str:
# results.append(ValidationResult(
# passed=False, # 执行失败
# message=f"未能从LLM获取对路径 '{path_template}' 的评估。",
# details={"path_template": path_template, "http_method": http_method, "reason": "LLM did not return a response."}
# ))
# self.logger.error(f"LLM对路径 '{path_template}' 的评估请求未返回任何内容。")
# return results
# self.logger.debug(f"LLM对路径 '{path_template}' 的原始响应: {llm_response_str}")
# try:
# # 尝试清理并解析LLM响应
# # 有时LLM可能在JSON前后添加 "```json" 和 "```"
# cleaned_response_str = llm_response_str.strip()
# if cleaned_response_str.startswith("```json"):
# cleaned_response_str = cleaned_response_str[7:]
# if cleaned_response_str.endswith("```"):
# cleaned_response_str = cleaned_response_str[:-3]
# llm_assessment_data = json.loads(cleaned_response_str)
# if "assessments" not in llm_assessment_data or not isinstance(llm_assessment_data["assessments"], list):
# raise ValueError("LLM响应JSON中缺少 'assessments' 列表或格式不正确。")
# found_assessments = False
# for assessment in llm_assessment_data["assessments"]:
# standard_name = assessment.get("standard_name", "未知标准")
# is_compliant = assessment.get("is_compliant", False)
# reason = assessment.get("reason", "LLM未提供原因。")
# found_assessments = True
# results.append(ValidationResult(
# passed=is_compliant,
# message=f"LLM评估 - {standard_name}: {reason}",
# details={
# "standard_name": standard_name,
# "is_compliant_by_llm": is_compliant,
# "llm_reason": reason,
# "path_template": path_template,
# "http_method": http_method
# }
# ))
# log_level = self.logger.info if is_compliant else self.logger.warning
# log_level(f"LLM评估 - 标准 '{standard_name}' for '{path_template}': {'符合' if is_compliant else '不符合'}。原因: {reason}")
# if not found_assessments:
# results.append(ValidationResult(
# passed=False,
# message=f"LLM返回的评估结果中不包含任何有效的评估项。",
# details={"path_template": path_template, "http_method": http_method, "raw_llm_response": llm_response_str}
# ))
# except json.JSONDecodeError as e_json:
# results.append(ValidationResult(
# passed=False, # 执行失败
# message=f"无法将LLM对路径 '{path_template}' 的评估响应解析为JSON: {e_json}",
# details={"path_template": path_template, "http_method": http_method, "raw_llm_response": llm_response_str, "error": str(e_json)}
# ))
# self.logger.error(f"LLM对路径 '{path_template}' 的响应JSON解析失败: {e_json}. Raw response: {llm_response_str}")
# except ValueError as e_val: # 自定义错误,如缺少 'assessments'
# results.append(ValidationResult(
# passed=False, # 执行失败
# message=f"LLM对路径 '{path_template}' 的评估响应JSON结构不符合预期: {e_val}",
# details={"path_template": path_template, "http_method": http_method, "raw_llm_response": llm_response_str, "error": str(e_val)}
# ))
# self.logger.error(f"LLM对路径 '{path_template}' 的响应JSON结构错误: {e_val}. Raw response: {llm_response_str}")
# except Exception as e_generic:
# results.append(ValidationResult(
# passed=False, # 执行失败
# message=f"处理LLM对路径 '{path_template}' 的评估响应时发生未知错误: {e_generic}",
# details={"path_template": path_template, "http_method": http_method, "raw_llm_response": llm_response_str, "error": str(e_generic)}
# ))
# self.logger.error(f"处理LLM对路径 '{path_template}' 的响应时发生未知错误: {e_generic}", exc_info=True)
# return results
@@ -0,0 +1 @@
# This file marks the security directory as a Python package.
@@ -0,0 +1,84 @@
from typing import Dict, Any, Optional, List
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
import urllib.parse
class HTTPSMandatoryCase(BaseAPITestCase):
id = "TC-SECURITY-001"
name = "HTTPS Protocol Mandatory Verification"
description = "验证API端点是否通过HTTPS提供服务,以及HTTP请求是否被拒绝或重定向到HTTPS。"
severity = TestSeverity.CRITICAL
tags = ["security", "https", "transport-security"]
execution_order = 120
# 此测试会修改URL为HTTP,应适用于大多数端点。
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None):
super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service=llm_service)
self.logger.info(f"测试用例 '{self.id}' 已为端点 '{self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')}' 初始化。")
def modify_request_url(self, current_url: str) -> str:
parsed_url = urllib.parse.urlparse(current_url)
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}'")
return modified_url
else:
self.logger.warning(f"原始URL '{current_url}' 不是HTTPS。跳过此测试用例的URL修改。")
# 如果原始URL不是HTTPS,此测试可能无效,
# 或者暗示基础URL本身可能未正确配置以进行HTTPS测试。
return current_url # 如果不是HTTPS则返回原始URL
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
results = []
status_code = response_context.status_code
# request_context.url 是调用 modify_request_url 之后,APICaller实际发送的URL
request_url_scheme = urllib.parse.urlparse(request_context.url).scheme
# 检查URL是否确实被此测试的钩子修改为了HTTP
if request_url_scheme.lower() != "http":
results.append(self.passed(
message=f"测试已跳过,因为发送的URL已经是 {request_url_scheme.upper()}(可能是由于原始基础URL非HTTPS或测试设置问题)。"
))
self.logger.info("HTTPS强制性检查已跳过,因为有效URL不是HTTP。")
return results
# 如果请求是通过HTTP发出的,我们期望几种结果:
# 1. 拒绝(例如400、403,或连接被拒绝 - 尽管APICaller可能在此之前处理连接拒绝)
# 2. 重定向到HTTPS(例如301、302、307、308,并带有指向HTTPS的Location头)
if status_code in [301, 302, 307, 308]: # 重定向状态码
location_header = response_context.headers.get("Location")
if location_header and urllib.parse.urlparse(location_header).scheme.lower() == "httpss":
results.append(self.passed(
message=f"对 {request_context.url} 的HTTP请求被正确重定向到HTTPS ({location_header}),状态码 {status_code}。"
))
self.logger.info(f"HTTP被正确重定向到HTTPS: {location_header}")
else:
results.append(self.failed(
message=f"对 {request_context.url} 的HTTP请求被重定向(状态码 {status_code}),但Location头 '{location_header}' 未指向HTTPS URL。",
details={"status_code": status_code, "location_header": location_header}
))
self.logger.warning(f"HTTP被重定向,状态码 {status_code},但Location '{location_header}' 不是HTTPS。")
elif status_code // 100 == 4: # 客户端错误(例如400错误请求,403禁止访问)
results.append(self.passed(
message=f"对 {request_context.url} 的HTTP请求被客户端错误(状态码 {status_code})拒绝,表明不允许HTTP访问。"
))
self.logger.info(f"HTTP请求被客户端错误 {status_code} 拒绝。")
elif status_code // 100 == 2: # 通过HTTP成功返回2xx响应
results.append(self.failed(
message=f"API通过HTTP ({request_context.url}) 响应了成功的状态码 {status_code},这违反了HTTPS强制策略。",
details={"status_code": status_code}
))
self.logger.error(f"安全漏洞:API允许通过HTTP成功响应 ({status_code})。")
else:
# 其他状态码(例如5xx)可能表示与HTTPS强制执行无关的服务器错误,
# 或者连接在更底层被拒绝(APICaller可能会抛出异常)。
results.append(ValidationResult(
passed=False, # 或True,取决于严格程度 - 5xx不是通过,但不一定是HTTPS失败
message=f"对 {request_context.url} 的HTTP请求返回了意外的状态码 {status_code}。需要手动调查。",
details={"status_code": status_code, "response_text_sample": (response_context.text_content or "")[:200]}
))
self.logger.warning(f"HTTP请求返回意外状态码 {status_code}。潜在问题或服务器错误。")
return results
@@ -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