This commit is contained in:
gongwenxin 2025-05-27 03:18:14 +08:00
parent 1138668a72
commit f0cc525141
33 changed files with 4088 additions and 3619 deletions

View File

@ -11,6 +11,7 @@
* **核心控制器**:是整个测试流程的指挥中心。 * **核心控制器**:是整个测试流程的指挥中心。
* **组件初始化**:负责初始化和管理其他关键组件,如 `InputParser`API 规范解析器)、`APICaller`API 请求调用器)、`TestCaseRegistry`(测试用例注册表)以及可选的 `LLMService`(大模型服务)。 * **组件初始化**:负责初始化和管理其他关键组件,如 `InputParser`API 规范解析器)、`APICaller`API 请求调用器)、`TestCaseRegistry`(测试用例注册表)以及可选的 `LLMService`(大模型服务)。
* **`$ref` 解析**: 在将API端点规范 (`endpoint_spec_dict`) 传递给测试用例构造函数之前,会使用 `schema_utils.resolve_json_schema_references` 解析其中的 schemas (requestBody, parameters, responses),默认会丢弃原始的 `$ref``$$` 前缀的键。
* **测试流程管理** * **测试流程管理**
* 调用 `InputParser` 解析指定的 API 规范文件,获取所有端点的定义。 * 调用 `InputParser` 解析指定的 API 规范文件,获取所有端点的定义。
* 根据用户指定的过滤器(如 YAPI 分类或 Swagger 标签)筛选需要测试的 API 端点。 * 根据用户指定的过滤器(如 YAPI 分类或 Swagger 标签)筛选需要测试的 API 端点。
@ -31,7 +32,7 @@
* **适用性筛选**:提供 `get_applicable_test_cases` 方法,根据 API 端点的 HTTP 方法和路径(通过正则表达式匹配)筛选出适用的、已排序的测试用例类列表给编排器。 * **适用性筛选**:提供 `get_applicable_test_cases` 方法,根据 API 端点的 HTTP 方法和路径(通过正则表达式匹配)筛选出适用的、已排序的测试用例类列表给编排器。
4. **测试框架核心 (`test_framework_core.py`)**: 4. **测试框架核心 (`test_framework_core.py`)**:
* **`BaseAPITestCase`**:所有自定义测试用例的基类。它定义了测试用例应具备的元数据(如 `id`, `name`, `description`, `severity`, `tags`, `execution_order`, `applicable_methods`, `applicable_paths_regex` 以及 LLM 使用标志位)和一系列生命周期钩子方法(如 `generate_*`, `validate_*`)。 * **`BaseAPITestCase`**:所有自定义测试用例的基类。它定义了测试用例应具备的元数据(如 `id`, `name`, `description`, `severity`, `tags`, `execution_order`, `applicable_methods`, `applicable_paths_regex` 以及 LLM 使用标志位)和一系列生命周期钩子方法(如 `generate_*`, `validate_*`,以及众多辅助方法(见下文)
* **`APIRequestContext` / `APIResponseContext`**:数据类,分别用于封装 API 请求和响应的上下文信息,在测试用例的钩子方法间传递。 * **`APIRequestContext` / `APIResponseContext`**:数据类,分别用于封装 API 请求和响应的上下文信息,在测试用例的钩子方法间传递。
* **`ValidationResult`**:数据类,用于表示单个验证点的结果(通过/失败、消息、详细信息)。 * **`ValidationResult`**:数据类,用于表示单个验证点的结果(通过/失败、消息、详细信息)。
* **`TestSeverity`**:枚举类型,定义测试用例的严重级别。 * **`TestSeverity`**:枚举类型,定义测试用例的严重级别。
@ -47,35 +48,37 @@
* 如果配置了 LLM 服务(如通义千问的兼容 OpenAI 模式的 API此组件负责与 LLM API 交互。 * 如果配置了 LLM 服务(如通义千问的兼容 OpenAI 模式的 API此组件负责与 LLM API 交互。
* 主要用于根据 Pydantic 模型(从 JSON Schema 动态创建)智能生成复杂的请求参数或请求体。 * 主要用于根据 Pydantic 模型(从 JSON Schema 动态创建)智能生成复杂的请求参数或请求体。
8. **工具模块 (`ddms_compliance_suite/utils/`)**:
* **`schema_utils.py`**: 包含一系列用于处理 JSON Schema 和 API 参数的实用函数。
* **`common_utils.py`**: 包含通用的辅助函数。
这个架构旨在提供一个灵活、可扩展的 API 测试框架,允许用户通过编写自定义的 Python 测试用例来定义复杂的验证逻辑。 这个架构旨在提供一个灵活、可扩展的 API 测试框架,允许用户通过编写自定义的 Python 测试用例来定义复杂的验证逻辑。
## 自定义 `APITestCase` 编写指南 (更新版) ## 自定义 `APITestCase` 编写指南
此指南帮助您创建自定义的 `APITestCase` 类,以扩展 DDMS 合规性验证软件的测试能力。核心理念是 **代码即测试** 此指南帮助您创建自定义的 `APITestCase` 类,以扩展 DDMS 合规性验证软件的测试能力。核心理念是 **代码即测试**,并充分利用框架提供的工具函数和基类辅助方法来简化测试用例的编写。
(您可以参考项目中的 `docs/APITestCase_Development_Guide.md` 文件获取更详尽的原始指南,以下内容基于该指南并加入了新特性。)
### 1. 创建自定义测试用例 ### 1. 创建自定义测试用例
1. **创建 Python 文件**:在您的自定义测试用例目录(例如 `custom_testcases/`)下创建一个新的 `.py` 文件。 1. **创建 Python 文件**:在您的自定义测试用例目录(例如 `custom_testcases/`)下创建一个新的 `.py` 文件。
2. **继承 `BaseAPITestCase`**:定义一个或多个类,使其继承自 `ddms_compliance_suite.test_framework_core.BaseAPITestCase` 2. **导入必要模块**
3. **定义元数据 (类属性)**
```python
from typing import Dict, Any, Optional, List
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
from ddms_compliance_suite.utils import schema_utils, common_utils # 导入工具模块
import logging # 可选,用于自定义日志
```
3. **继承 `BaseAPITestCase`**:定义一个或多个类,使其继承自 `BaseAPITestCase`
4. **定义元数据 (类属性)**
* `id: str`: 测试用例的全局唯一标识符 (例如 `"TC-MYFEATURE-001"`)。 * `id: str`: 测试用例的全局唯一标识符 (例如 `"TC-MYFEATURE-001"`)。
* `name: str`: 人类可读的名称。 * `name: str`: 人类可读的名称。
* `description: str`: 详细描述。 * `description: str`: 详细描述。
* `severity: TestSeverity`: 严重程度 (例如 `TestSeverity.CRITICAL`, `TestSeverity.HIGH`, 等)。 * `severity: TestSeverity`: 严重程度 (例如 `TestSeverity.CRITICAL`, `TestSeverity.HIGH`, 等)。
* `tags: List[str]`: 分类标签 (例如 `["smoke", "regression"]`)。 * `tags: List[str]`: 分类标签 (例如 `["smoke", "regression"]`)。
* **`execution_order: int` (新增)**: 控制测试用例的执行顺序。**数值较小的会比较大的先执行**。如果多个测试用例此值相同,则它们会再根据类名的字母顺序排序。默认值为 `100` * `execution_order: int`: 控制测试用例的执行顺序。**数值较小的会比较大的先执行**。默认值为 `100`
```python
class MyFirstCheck(BaseAPITestCase):
execution_order = 10
# ... other metadata
class MySecondCheck(BaseAPITestCase):
execution_order = 20
# ... other metadata
```
* `applicable_methods: Optional[List[str]]`: 限制适用的 HTTP 方法 (例如 `["POST", "PUT"]`)。`None` 表示所有方法。 * `applicable_methods: Optional[List[str]]`: 限制适用的 HTTP 方法 (例如 `["POST", "PUT"]`)。`None` 表示所有方法。
* `applicable_paths_regex: Optional[str]`: 限制适用的 API 路径 (Python 正则表达式)。`None` 表示所有路径。 * `applicable_paths_regex: Optional[str]`: 限制适用的 API 路径 (Python 正则表达式)。`None` 表示所有路径。
* **LLM 使用标志 (可选)**: 这些标志允许测试用例覆盖全局 LLM 配置。 * **LLM 使用标志 (可选)**: 这些标志允许测试用例覆盖全局 LLM 配置。
@ -83,20 +86,42 @@
* `use_llm_for_path_params: bool = False` * `use_llm_for_path_params: bool = False`
* `use_llm_for_query_params: bool = False` * `use_llm_for_query_params: bool = False`
* `use_llm_for_headers: bool = False` * `use_llm_for_headers: bool = False`
(如果测试用例中不设置这些,则遵循 `run_api_tests.py` 传入的全局 LLM 开关。) 5. **实现 `__init__` (如果需要自定义初始化逻辑)**:
4. **实现验证逻辑**:重写 `BaseAPITestCase` 中一个或多个 `generate_*``validate_*` 方法。
### 2. `BaseAPITestCase` 核心方法 * 通常,您会在这里调用基类的 `__init__`
* 许多测试用例会在这里查找并设置测试目标字段/参数,可以利用 `BaseAPITestCase` 提供的辅助方法(如 `_get_resolved_request_body_schema`, `_find_simple_type_field_in_schema`, `_find_first_simple_type_parameter`, `_find_removable_field_path`)来完成。
* **`__init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any])`**: ```python
class MissingRequiredFieldBodyCase(BaseAPITestCase):
# ...元数据...
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.removable_field_path: Optional[List[Union[str, int]]] = None
* 构造函数。`endpoint_spec` 包含当前测试端点的 API 定义,`global_api_spec` 包含完整的 API 规范。 body_schema = self._get_resolved_request_body_schema()
* 基类会初始化 `self.logger`,可用于记录日志。 if body_schema:
self.removable_field_path = self._find_removable_field_path(body_schema, "request body")
if not self.removable_field_path:
self.logger.info(f"[{self.id}] No removable required field found in request body.")
```
6. **实现验证逻辑**:重写 `BaseAPITestCase` 中一个或多个 `generate_*``validate_*` 方法。在这些方法中,积极使用框架提供的工具函数和基类辅助方法。
### 2. `BaseAPITestCase` 核心生命周期方法
这些方法由测试编排器在测试执行的不同阶段调用。
* **`__init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any], llm_service: Optional[Any])`**:
* 构造函数。`endpoint_spec` 包含当前测试端点的 API 定义(已经过 `$ref` 解析),`global_api_spec` 包含完整的 API 规范。
* 基类会初始化 `self.logger`,可用于记录日志。`json_schema_validator``llm_service` 也会被存储。
* **请求生成与修改方法**: 在 API 请求发送前调用,用于修改或生成请求数据。 * **请求生成与修改方法**: 在 API 请求发送前调用,用于修改或生成请求数据。
* `generate_path_params(self, current_path_params: Dict[str, Any]) -> Dict[str, Any]`
* `generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]` * `generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]`
* `generate_headers(self, current_headers: Dict[str, str]) -> Dict[str, str]` * `generate_headers(self, current_headers: Dict[str, str]) -> Dict[str, str]`
* `generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]` * `generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]`
* `modify_request_url(self, current_url: str) -> str`
* **请求预校验方法**: 在请求数据完全构建后、发送前调用,用于静态检查。返回 `List[ValidationResult]` * **请求预校验方法**: 在请求数据完全构建后、发送前调用,用于静态检查。返回 `List[ValidationResult]`
* `validate_request_url(self, url: str, request_context: APIRequestContext) -> List[ValidationResult]` * `validate_request_url(self, url: str, request_context: APIRequestContext) -> List[ValidationResult]`
@ -112,60 +137,181 @@
* `check_performance(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]` * `check_performance(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]`
* 通常用于检查响应时间 `response_context.elapsed_time` * 通常用于检查响应时间 `response_context.elapsed_time`
### 3. 核心辅助类 ### 3. `BaseAPITestCase` 提供的辅助方法
这些方法旨在简化测试用例中常见的任务:
* **`_get_resolved_request_body_schema(self) -> Optional[Dict[str, Any]]`**:
* 从 `self.endpoint_spec` 中提取请求体的 schema。它会自动处理常见的 content-type (如 `application/json`) 并考虑 OpenAPI 2.0 的 `in: body` 参数风格。
* 返回的 schema 已经由编排器进行过 `$ref` 解析。
* **`_find_removable_field_path(self, schema_to_search: Optional[Dict[str, Any]], schema_name_for_log: str) -> Optional[List[Union[str, int]]]`**:
* 在给定的 `schema_to_search` 中查找第一个可移除的必填字段的路径。
* 内部调用 `schema_utils.util_find_removable_field_path_recursive`
* 适用于构造"缺失必填字段"之类的测试场景。
* `schema_name_for_log` 用于日志记录,例如 "request body"。
* **`_find_simple_type_field_in_schema(self, schema_to_search: Optional[Dict[str, Any]], schema_name_for_log: str) -> Optional[Tuple[List[Union[str, int]], str, Dict[str, Any]]]`**:
* 在给定的 `schema_to_search` (应为已解析的字典) 中查找第一个简单类型 (string, integer, number, boolean) 字段。
* 内部调用 `schema_utils.find_first_simple_type_field_recursive`
* 返回一个元组 `(field_path, field_type, field_schema)``None`
* 适用于构造"字段类型不匹配"之类的测试场景。
* **`_find_first_simple_type_parameter(self, param_location: str) -> Optional[Tuple[List[Union[str, int]], str, Dict[str, Any], str]]`**:
* 在指定的参数位置 (`param_location`,如 "query", "header") 查找第一个简单类型的参数或参数内部的简单类型字段。
* 它会检查参数是否直接是简单类型,或者其 `schema` 是否为简单类型或包含简单类型的对象。
* 如果参数的 schema 是对象,它会调用 `_find_simple_type_field_in_schema` 来查找嵌套字段。
* 返回 `(full_path, param_type, param_schema, top_level_param_name)``None`
* `full_path` 可能是 `['paramName']``['paramName', 'nestedField']`
* **`_find_required_parameter_name(self, param_in: str) -> Optional[str]`**:
* 查找指定位置 (`param_in`,如 "query", "header", "path") 的第一个必填参数的名称。
* **`expect_error_response(self, response_context: APIResponseContext, expected_status_codes: List[int], expected_error_code_in_body: Optional[Union[str, int]] = None, error_code_field_name: str = "code", context_message_prefix: str = "Error response validation") -> List[ValidationResult]` (新增)**:
* 一个标准化的方法来验证错误响应。
* 检查 `response_context.status_code` 是否在 `expected_status_codes` 列表中。
* 可选地,检查响应体 (如果是JSON对象) 中 `error_code_field_name` 字段的值是否等于 `expected_error_code_in_body`
* 返回包含详细信息的 `ValidationResult` 列表。
* 示例:
```python
# 在某个测试用例的 validate_response 中
if not self.target_field_path: # 假设此用例需要一个目标字段
return [self.passed("Skipped: No target field identified.")]
field_desc = f"body field '{'.'.join(map(str, self.target_field_path))}'"
return self.expect_error_response(
response_context=response_context,
expected_status_codes=[400, 422],
expected_error_code_in_body="4001",
context_message_prefix=f"Missing required {field_desc}"
)
```
* **`validate_data_against_schema(self, data_to_validate: Any, schema_definition: Dict[str, Any], context_message_prefix: str = "Data") -> List[ValidationResult]`**:
* 使用注入的 `JSONSchemaValidator` (如果可用) 来验证数据是否符合给定的 schema。
* **`passed(message: str, details: Optional[Dict[str, Any]] = None) -> ValidationResult` (静态方法)**:
* 快速创建表示"通过"的 `ValidationResult`
* **`failed(message: str, details: Optional[Dict[str, Any]] = None) -> ValidationResult` (静态方法)**:
* 快速创建表示"失败"的 `ValidationResult`
### 4. `ddms_compliance_suite.utils` 工具模块
#### 4.1 `schema_utils.py`
这个模块包含处理 JSON Schema、API 参数和数据结构的函数。
* **`resolve_json_schema_references(schema_to_resolve: Any, full_api_spec: Dict[str, Any], ..., discard_refs: bool = True) -> Any`**:
* 递归解析 JSON Schema 中的 `$ref` 引用。
* `discard_refs=True` (默认) 会在解析前移除 `$ref` 和以 `$$` 开头的键。如果为 `False`,则尝试解析 `$ref` 并保留其他键。
* 此函数主要由测试编排器在实例化测试用例前调用。测试用例通常不需要直接使用它,因为 `endpoint_spec` 已经预处理过了。
* **`util_find_removable_field_path_recursive(current_schema: Dict[str, Any], current_path: List[Union[str, int]], full_api_spec_for_refs: Dict[str, Any]) -> Optional[List[Union[str, int]]]`**:
* 递归地在(可能包含 `$ref` 的)`current_schema` 中查找第一个可移除的必填字段的路径。
* 主要被 `BaseAPITestCase._find_removable_field_path` 调用。
* **`util_remove_value_at_path(data_container: Any, path: List[Union[str, int]]) -> Tuple[Any, Any, bool]`**:
* 从嵌套的字典/列表中移除指定路径的值。
* 返回 `(修改后的容器, 被移除的值, 是否成功)`
* 示例:
```python
# 在某个测试用例的 generate_request_body 中
if self.removable_field_path:
modified_body, removed_value, success = schema_utils.util_remove_value_at_path(
current_body, self.removable_field_path
)
if success:
self.logger.info(f"Removed value '{removed_value}' from path '{'.'.join(map(str, self.removable_field_path))}'.")
return modified_body
return current_body
```
* **`util_set_value_at_path(data_container: Any, path: List[Union[str, int]], new_value: Any) -> Tuple[Any, bool]` (新增)**:
* 在嵌套的字典/列表中为指定路径设置或修改 `new_value`
* 如果路径中的父级结构不存在,会尝试创建它们(字典会被创建,列表会用 `None` 填充到所需索引)。
* 返回 `(修改后的容器, 是否成功)`
* 示例:
```python
# 在某个测试用例的 generate_request_body 中
if self.target_field_path and self.mismatched_value is not None:
modified_body, success = schema_utils.util_set_value_at_path(
current_body, self.target_field_path, self.mismatched_value
)
if success:
return modified_body
return current_body
```
* **`generate_mismatched_value(original_type: Optional[str], original_value: Any, field_schema: Optional[Dict[str, Any]], logger_param: Optional[logging.Logger] = None) -> Any` (新增)**:
* 根据字段的 `original_type``original_value`(当前未使用)和 `field_schema`(用于检查 `enum` 等约束)生成一个类型不匹配的值。
* 适用于类型不匹配的测试场景。
* 示例:
```python
# 在某个测试用例的 __init__ 或 generate_ 方法中
if self.original_field_type: # 假设 self.original_field_type 已被正确设置
self.mismatched_val = schema_utils.generate_mismatched_value(
self.original_field_type,
None, # original_value, 暂时可以为 None
self.target_field_schema, # 字段的 schema
self.logger
)
```
* **`build_object_schema_for_params(params_spec_list: List[Dict[str, Any]], model_name_base: str, ...) -> Tuple[Optional[Dict[str, Any]], str]`**:
* 从参数规范列表(例如 OpenAPI 参数对象列表)构建一个聚合的 JSON 对象 schema。
* 可用于将多个查询参数或头部参数统一表示为一个对象 schema方便进行 schema 校验或数据生成。
* **`find_first_simple_type_field_recursive(current_schema: Dict[str, Any], ...) -> Optional[Tuple[List[Union[str, int]], str, Dict[str, Any]]]`**:
* 递归地在已解析的 `current_schema` 中查找第一个简单类型的字段 (string, integer, number, boolean),包括嵌套在对象或数组中的。
* 主要被 `BaseAPITestCase._find_simple_type_field_in_schema` 调用。
#### 4.2 `common_utils.py`
* **`format_url_with_path_params(base_url: str, path_template: str, path_params: Dict[str, Any]) -> str`**:
* 将路径模板中的占位符 (如 `{userId}`) 替换为 `path_params` 中提供的值,并与 `base_url` 组合成完整的 URL。
### 5. 核心数据类
* **`ValidationResult(passed: bool, message: str, details: Optional[Dict[str, Any]] = None)`**: * **`ValidationResult(passed: bool, message: str, details: Optional[Dict[str, Any]] = None)`**:
* 封装单个验证点的结果。所有 `validate_*` 和 `check_*` 方法都应返回此对象的列表。 * 封装单个验证点的结果。所有 `validate_*` 和 `check_*` 方法都应返回此对象的列表。
* **`APIRequestContext`**: 包含当前请求的详细信息方法、URL、参数、头、体、端点规范 * **`APIRequestContext`**: 包含当前请求的详细信息方法、URL、参数、头、体、端点规范
* **`APIResponseContext`**: 包含 API 响应的详细信息状态码、头、JSON 内容、文本内容、耗时、原始响应对象、关联的请求上下文)。 * **`APIResponseContext`**: 包含 API 响应的详细信息状态码、头、JSON 内容、文本内容、耗时、原始响应对象、关联的请求上下文)。
### 4. 示例 (展示 `execution_order`) ### 6. 编写测试用例的推荐流程
参考您项目中的 `custom_testcases/basic_checks.py`,您可以像这样添加 `execution_order` 1. **明确测试目标**:这个测试用例要验证什么?是参数处理、错误响应、数据格式还是业务逻辑?
2. **选择合适的基类方法**
```python * 如果需要修改请求数据(参数、头部、请求体),重写相应的 `generate_*` 方法。
# In custom_testcases/status_and_header_checks.py * 如果需要在发送前对请求的静态结构进行校验,重写 `validate_request_*` 方法。
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext * 核心的响应验证逻辑通常在 `validate_response` 中实现。
* 性能相关的检查在 `check_performance` 中。
3. **初始化 (在 `__init__` 中)**:
class StatusCodeCheck(BaseAPITestCase): * 调用 `super().__init__(...)`
id = "TC-STATUS-001" * 如果测试依赖于特定的请求体字段或参数:
name = "状态码检查" * 获取请求体 schema: `body_schema = self._get_resolved_request_body_schema()`
description = "验证API响应状态码。" * 查找必填字段进行移除测试: `path = self._find_removable_field_path(body_schema, "request body")`
severity = TestSeverity.CRITICAL * 查找简单类型字段进行类型不匹配测试 (请求体): `target = self._find_simple_type_field_in_schema(body_schema, "request body")`
tags = ["status", "smoke"] * 查找简单类型参数进行类型不匹配测试 (查询/头部): `target = self._find_first_simple_type_parameter("query")`
execution_order = 10 # 希望这个检查先于下面的 HeaderCheck 执行 * 查找必填参数名称: `name = self._find_required_parameter_name("query")`
* 将找到的目标路径、类型、schema 等信息存储在 `self` 的属性中,供后续方法使用。
4. **生成/修改请求数据 (在 `generate_*` 方法中)**:
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> list[ValidationResult]: * 如果需要移除字段,使用 `schema_utils.util_remove_value_at_path()`
results = [] * 如果需要修改字段值(例如进行类型不匹配测试):
if response_context.status_code == 200: 1. 使用 `schema_utils.generate_mismatched_value()` 生成不匹配的值。
results.append(ValidationResult(passed=True, message="响应状态码为 200 OK。")) 2. 使用 `schema_utils.util_set_value_at_path()` 将该值设置到请求数据中。
else: 5. **验证响应 (在 `validate_response` 方法中)**:
results.append(ValidationResult(passed=False, message=f"期望状态码 200实际为 {response_context.status_code}。"))
return results
class EssentialHeaderCheck(BaseAPITestCase): * 如果测试期望一个错误响应,优先使用 `self.expect_error_response()`
id = "TC-HEADER-ESSENTIAL-001" * 如果需要验证响应体数据是否符合特定的 JSON Schema使用 `self.validate_data_against_schema()`
name = "必要请求头 X-Trace-ID 存在性检查" * 对于其他自定义的断言,直接比较 `response_context` 中的属性(如 `status_code`, `json_content`)并使用 `self.passed()``self.failed()` 创建 `ValidationResult`
description = "验证响应中是否包含 X-Trace-ID。" 6. **日志记录**: 在关键步骤使用 `self.logger.info()`, `self.logger.debug()`, `self.logger.warning()` 等记录信息,便于调试。
severity = TestSeverity.HIGH
tags = ["header"]
execution_order = 20 # 在状态码检查之后执行
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> list[ValidationResult]: 通过遵循这些指南并善用框架提供的工具,您可以更高效地编写出简洁、健壮且易于维护的 API 合规性测试用例。
results = []
if "X-Trace-ID" in response_context.headers:
results.append(ValidationResult(passed=True, message="响应头中包含 X-Trace-ID。"))
else:
results.append(ValidationResult(passed=False, message="响应头中缺少 X-Trace-ID。"))
return results
```
### 5. 最佳实践
* **单一职责**:让每个 `APITestCase` 专注于特定的验证目标。
* **清晰命名**为类、ID、名称使用描述性文字。
* **善用 `endpoint_spec`**:参考 API 定义进行精确测试。
* **详细的 `ValidationResult`**:失败时提供充足的上下文信息。
* **日志记录**:使用 `self.logger` 记录测试过程中的重要信息和问题。
希望这份更新的架构概览和编写指南对您有所帮助!通过 `execution_order`,您可以更好地控制复杂场景下测试用例的执行流程。

View File

@ -1,230 +0,0 @@
# API测试框架
这是一个全面的API测试框架专为API合规性测试而设计。该框架能够基于Swagger和YAPI文档自动生成和执行API测试验证API响应是否符合预期的格式和结构。
## 主要功能
- **多格式支持**支持从Swagger和YAPI文件导入API定义。
- **自动测试生成**根据API文档自动生成测试用例。
- **请求参数生成**基于API文档中的schema定义自动生成合理的测试数据。
- **响应验证**
- 状态码验证
- JSON格式验证
- JSON Schema验证
- 未来可扩展自定义规则验证
- **测试编排**管理多个API测试的执行和结果收集。
- **命令行工具**便于集成到CI/CD流程中。
- **详细报告**:生成详细的测试报告,包括失败原因和建议。
## 框架架构
该框架由以下主要组件组成:
1. **输入解析器Input Parser**
- 解析Swagger和YAPI格式的API文档
- 提取API路径、方法、参数和响应schema等信息
2. **API调用器API Caller**
- 负责构建和发送API请求
- 处理不同类型的请求参数和请求体
3. **JSON验证器JSON Validator**
- 验证API响应的格式和结构
- 基于JSON Schema进行响应验证
4. **测试编排器Test Orchestrator**
- 协调上述组件的工作
- 管理测试执行流程
- 收集和汇总测试结果
5. **命令行工具CLI**
- 提供命令行接口,便于使用和集成
## 安装与依赖
该框架依赖以下库:
```
pytest
requests
jsonschema
prance (可选用于高级Swagger解析)
```
可以通过以下命令安装依赖:
```bash
pip install pytest requests jsonschema
```
## 使用方法
### 生成API测试
使用提供的命令行工具生成API测试
```bash
# 从YAPI文件生成测试
python generate_api_tests.py --yapi 路径/到/yapi导出文件.json --base-url http://api服务器:端口
# 从Swagger文件生成测试
python generate_api_tests.py --swagger 路径/到/swagger文件.json --base-url http://api服务器:端口
# 生成特定分类的API测试
python generate_api_tests.py --yapi 路径/到/yapi文件.json --base-url http://api服务器:端口 --categories 分类1,分类2
# 查看所有可用分类
python generate_api_tests.py --yapi 路径/到/yapi文件.json --list-categories
```
### 运行API测试
使用测试编排器运行API测试
```bash
# 运行所有测试
python run_api_tests.py --yapi 路径/到/yapi文件.json --base-url http://api服务器:端口
# 运行特定分类的测试
python run_api_tests.py --yapi 路径/到/yapi文件.json --base-url http://api服务器:端口 --categories 分类1,分类2
# 保存测试结果到文件
python run_api_tests.py --yapi 路径/到/yapi文件.json --base-url http://api服务器:端口 --output 测试报告.json
```
### 运行生成的测试文件
使用pytest运行生成的测试文件
```bash
pytest tests/test_generated_yapi_apis.py -v
# 或
pytest tests/test_generated_swagger_apis.py -v
```
## 测试数据生成
该框架根据API文档中定义的JSON Schema自动生成测试数据
- 对于基本类型(字符串、数字、布尔值等),生成合理的随机值
- 对于对象类型,根据属性定义生成嵌套对象
- 对于数组类型,生成包含适当元素的数组
- 优先使用文档中提供的示例值和默认值
例如对于以下Schema
```json
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "number"},
"tags": {
"type": "array",
"items": {"type": "string"}
}
}
}
```
框架会生成类似这样的测试数据:
```json
{
"name": "测试名称",
"age": 25,
"tags": ["标签1", "标签2"]
}
```
## 响应验证
该框架提供多层次的响应验证:
1. **状态码验证**检查HTTP状态码是否在预期范围内默认为2xx
2. **JSON格式验证**确保响应是有效的JSON格式
3. **Schema验证**根据API文档中定义的响应Schema验证响应内容
4. **自定义规则验证**:(未来扩展)支持基于规则库的自定义验证
验证结果包含详细信息,有助于快速定位和修复问题。
## 示例
### 直接使用验证器
```python
from ddms_compliance_suite.json_schema_validator.validator import JSONSchemaValidator
# 创建验证器
validator = JSONSchemaValidator()
# 定义schema
schema = {
"type": "object",
"properties": {
"code": {"type": "number"},
"message": {"type": "string"},
"data": {"type": "object"}
},
"required": ["code", "message"]
}
# 验证响应
response_data = {"code": 200, "message": "success", "data": {"id": 1}}
result = validator.validate(response_data, schema)
if result.is_valid:
print("验证通过")
else:
print("验证失败")
for error in result.errors:
print(f"错误: {error}")
```
### 使用测试编排器
```python
from ddms_compliance_suite.test_orchestrator import APITestOrchestrator
from ddms_compliance_suite.input_parser.parser import InputParser
# 解析API文档
parser = InputParser()
parsed_api = parser.parse_yapi_spec("path/to/yapi.json")
# 创建测试编排器
orchestrator = APITestOrchestrator(base_url="http://api-server:8080")
# 运行单个API测试
endpoint = parsed_api.endpoints[0]
result = orchestrator.run_test_for_endpoint(endpoint)
# 检查结果
print(f"测试结果: {result.status}")
print(f"测试消息: {result.message}")
# 打印验证详情
if result.validation_details:
print("验证详情:")
for key, value in result.validation_details.items():
if isinstance(value, dict) and 'is_valid' in value:
valid_str = "通过" if value['is_valid'] else "失败"
print(f" {key}: {valid_str}")
if not value['is_valid'] and 'errors' in value:
for error in value['errors']:
print(f" 错误: {error}")
```
## 未来扩展
该框架计划在未来增加以下功能:
1. **规则库**支持自定义验证规则更灵活地定义API响应的合规性要求
2. **测试覆盖率分析**提供API测试覆盖率的统计和分析
3. **历史记录**保存测试历史记录支持比较不同版本的API测试结果
4. **Web界面**提供Web界面方便查看和管理测试结果
5. **性能测试**集成性能测试功能评估API的性能指标
## 贡献与反馈
欢迎提供反馈和建议,帮助我们改进这个框架。如有问题或建议,请联系项目维护者

View File

@ -220,7 +220,7 @@
"req_body_type": "json", "req_body_type": "json",
"res_body_type": "json", "res_body_type": "json",
"res_body": "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"number\"},\"message\":{\"type\":\"string\"},\"data\":{\"type\":\"boolean\"}}}", "res_body": "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"number\"},\"message\":{\"type\":\"string\"},\"data\":{\"type\":\"boolean\"}}}",
"req_body_other": "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"type\":\"object\",\"properties\":{\"version\":{\"type\":\"string\"},\"data\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"bsflag\":{\"type\":\"number\"},\"wellCommonName\":{\"type\":\"string\"},\"wellId\":{\"type\":\"string\"},\"dataRegion\":{\"type\":\"string\"}},\"required\":[\"bsflag\",\"wellCommonName\",\"wellId\",\"dataRegion\"]}}}}", "req_body_other": "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"required\":true},\"version\":{\"type\":\"string\"}},\"required\":[\"id\",\"version\"]}",
"project_id": 1193, "project_id": 1193,
"catid": 18705, "catid": 18705,
"markdown": "", "markdown": "",

View File

@ -12,7 +12,8 @@ class StatusCode200Check(BaseAPITestCase):
# 适用于所有方法和路径 (默认) # 适用于所有方法和路径 (默认)
# applicable_methods = None # applicable_methods = None
# applicable_paths_regex = None # applicable_paths_regex = None
execution_order = 10 # 示例执行顺序 execution_order = 1 # 执行顺序
is_critical_setup_test = True
# use_llm_for_body: bool = True # use_llm_for_body: bool = True
# use_llm_for_path_params: bool = True # use_llm_for_path_params: bool = True
# use_llm_for_query_params: bool = True # use_llm_for_query_params: bool = True

View File

@ -1,7 +1,7 @@
from typing import Dict, Any, Optional, List, Union from typing import Dict, Any, Optional, List, Union
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
import logging import logging
from ddms_compliance_suite.utils import schema_utils # 导入新的工具模块 from ddms_compliance_suite.utils import schema_utils # Keep this import for util_remove_value_at_path
class MissingRequiredFieldBodyCase(BaseAPITestCase): class MissingRequiredFieldBodyCase(BaseAPITestCase):
id = "TC-ERROR-4003-BODY" id = "TC-ERROR-4003-BODY"
@ -12,57 +12,22 @@ class MissingRequiredFieldBodyCase(BaseAPITestCase):
execution_order = 210 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): 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) super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service)
self.logger = logging.getLogger(f"testcase.{self.id}") 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.removed_field_path: Optional[List[Union[str, int]]] = None # Path can contain int for array indices
self.original_value_at_path: Any = None self.original_value_at_path: Any = None
# --- Framework Utility Access --- # Use new helper methods from BaseAPITestCase
# orchestrator_placeholder 及其检查逻辑现在可以移除 body_schema = self._get_resolved_request_body_schema()
# --- End Framework Utility Access --- if body_schema:
self.removed_field_path = self._find_removable_field_path(
self._try_find_removable_body_field() schema_to_search=body_schema,
schema_name_for_log="request body"
def _get_request_body_schema(self) -> Optional[Dict[str, Any]]:
"""
Helper to get the (already $ref-resolved) request body schema from self.endpoint_spec.
"""
request_body_spec = self.endpoint_spec.get("requestBody")
if request_body_spec and isinstance(request_body_spec, dict):
content = request_body_spec.get("content", {})
# Iterate through common JSON content types or prioritize application/json
for ct in ["application/json", "application/merge-patch+json", "*/*"]:
if ct in content:
media_type_obj = content[ct]
if isinstance(media_type_obj, dict) and isinstance(media_type_obj.get("schema"), dict):
return media_type_obj["schema"]
# Fallback for OpenAPI 2.0 (Swagger) style 'in: body' parameter
parameters = self.endpoint_spec.get("parameters", [])
if isinstance(parameters, list):
for param in parameters:
if isinstance(param, dict) and param.get("in") == "body":
if isinstance(param.get("schema"), dict):
# Schema for 'in: body' parameter is directly usable
return param["schema"]
self.logger.debug("No suitable request body schema found in endpoint_spec.")
return None
def _try_find_removable_body_field(self):
body_schema_to_check = self._get_request_body_schema()
if body_schema_to_check:
self.removed_field_path = schema_utils.util_find_removable_field_path_recursive(
current_schema=body_schema_to_check,
current_path=[],
full_api_spec_for_refs=self.global_api_spec
) )
if self.removed_field_path: if not self.removed_field_path:
self.logger.info(f"必填字段缺失测试的目标字段 (请求体): '{'.'.join(map(str, self.removed_field_path))}'") self.logger.info('在请求体 schema 中未找到可用于测试 "必填字段缺失" 的字段(通过基类方法)。')
else:
self.logger.info('在请求体 schema 中未找到可用于测试 "必填字段缺失" 的字段。')
else: else:
self.logger.info('此端点规范中未定义或找到请求体 schema。') self.logger.info('此端点规范中未定义或找到请求体 schema通过基类方法')
self.removed_field_path = None
def generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]: 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.") self.logger.debug(f"{self.id} is focused on request body, generate_query_params will not modify query parameters.")
@ -85,59 +50,73 @@ class MissingRequiredFieldBodyCase(BaseAPITestCase):
self.logger.error(f"使用工具方法移除请求体字段路径 '{'.'.join(map(str, self.removed_field_path))}' 失败。将返回原始请求体。") self.logger.error(f"使用工具方法移除请求体字段路径 '{'.'.join(map(str, self.removed_field_path))}' 失败。将返回原始请求体。")
# Restore original_value_at_path to None since removal failed # Restore original_value_at_path to None since removal failed
self.original_value_at_path = None self.original_value_at_path = None
return current_body return current_body
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]: def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
results = [] results = []
if not self.removed_field_path: if not self.removed_field_path:
# This case should ideally be caught by _try_find_removable_body_field and the test case might be skipped
# by the orchestrator if it has a mechanism to check if a test case is applicable/configurable.
# For now, the test case itself handles this.
results.append(self.passed("跳过测试在API规范中未找到合适的必填请求体字段用于移除测试。")) results.append(self.passed("跳过测试在API规范中未找到合适的必填请求体字段用于移除测试。"))
self.logger.info("由于未识别到可移除的必填请求体字段,跳过此测试用例的验证。") self.logger.info("由于未识别到可移除的必填请求体字段,跳过此测试用例的验证。")
return results return results
# If original_value_at_path is None AND removed_field_path is set, it might mean the removal operation
# in generate_request_body failed or the field wasn't in the provided current_body.
# This check can make the test more robust.
if self.original_value_at_path is None and self.removed_field_path:
# Check if the field was simply not present in the input `current_body` to `generate_request_body`
# This logic is tricky because `_util_remove_value_at_path` might return success=False if path DNE.
# For now, we rely on the success flag from `_util_remove_value_at_path`.
# If generate_request_body returned original_body due to failure, this validation might be misleading.
# The logger in generate_request_body should indicate the failure.
pass
status_code = response_context.status_code status_code = response_context.status_code
json_content = response_context.json_content json_content = response_context.json_content
expected_status_codes = [400, 422] # As per many API guidelines for client errors expected_http_status_codes = [400, 422] # Common client error codes
specific_error_code_from_appendix_b = "4003" # Or a similar code indicating missing required field # specific_business_error_code 是此测试用例期望的特定业务错误码,例如 "4003"
# 这个值可以根据实际 API 的错误码约定在子类中调整或作为参数传入
removed_field_str = '.'.join(map(str, self.removed_field_path)) specific_business_error_code = "4003"
msg_prefix = f"当移除必填请求体字段 '{removed_field_str}' 时," error_code_field_in_body = "code" # 响应体中业务错误码的字段名
if status_code in expected_status_codes: removed_field_str = '.'.join(map(str, self.removed_field_path))
status_msg = f"{msg_prefix}API响应了预期的错误状态码 {status_code}" context_msg_prefix = f"当移除必填请求体字段 '{removed_field_str}' 时,"
# Check for specific error code in response body if available
if json_content and isinstance(json_content, dict) and str(json_content.get("code")) == specific_error_code_from_appendix_b: http_status_ok = status_code in expected_http_status_codes
results.append(self.passed(f"{status_msg} 且响应体中包含特定的错误码 '{specific_error_code_from_appendix_b}'")) business_code_ok = False
self.logger.info(f"正确接收到状态码 {status_code} 和错误码 '{specific_error_code_from_appendix_b}' (移除字段: body.{removed_field_str})。") is_4xx_error = 400 <= status_code <= 499
elif json_content and isinstance(json_content, dict) and "code" in json_content:
# Still pass because the HTTP status code is correct, but log a warning about the specific code if json_content and isinstance(json_content, dict):
results.append(ValidationResult(passed=True, body_code = json_content.get(error_code_field_in_body)
message=f"{status_msg} 响应体中的错误码为 '{json_content.get('code')}' (期望或类似 '{specific_error_code_from_appendix_b}')。", if body_code is not None and str(body_code) == specific_business_error_code:
details={"expected_code_detail": specific_error_code_from_appendix_b, "response_body": json_content} business_code_ok = True
if http_status_ok:
if business_code_ok:
results.append(self.passed(
f"{context_msg_prefix}API响应了预期的错误状态码 {status_code} 并且响应体中包含预期的业务错误码 '{specific_business_error_code}' (字段: '{error_code_field_in_body}')."
)) ))
self.logger.warning(f"接收到状态码 {status_code},但内部错误码是 '{json_content.get('code')}' 而不是期望的 '{specific_error_code_from_appendix_b}' (移除字段: body.{removed_field_str})。此结果仍标记为通过,因状态码正确。") self.logger.info(f"{self.id}: Passed. HTTP status {status_code} and business code '{specific_business_error_code}' match. (Removed field: body.{removed_field_str})")
else: else:
results.append(self.passed(f"{status_msg} 但响应体中未找到特定的错误码字段 ('code') 或响应体结构不符合预期。")) # HTTP status is OK, but business code is not what we specifically hoped for (or not present).
self.logger.info(f"正确接收到状态码 {status_code},但在响应体中未找到错误码字段或预期结构 (移除字段: body.{removed_field_str})。") # Still considered a pass because the primary condition (HTTP status) is met.
else: results.append(self.passed(
results.append(self.failed( f"{context_msg_prefix}API响应了预期的错误状态码 {status_code}. "
message=f"{msg_prefix}期望API返回状态码 {expected_status_codes} 中的一个,但实际收到 {status_code}", f"响应体中的业务错误码 (\'{error_code_field_in_body}\': \'{json_content.get(error_code_field_in_body)}\') 与特定期望 \'{specific_business_error_code}\' 不符或未找到但HTTP状态码正确。"
details={"status_code": status_code, "response_body": json_content, "removed_field": f"body.{removed_field_str}"} ))
self.logger.info(f"{self.id}: Passed. HTTP status {status_code} is correct. Business code mismatch/missing (Expected: \'{specific_business_error_code}\', Got: \'{json_content.get(error_code_field_in_body)}\'). (Removed field: body.{removed_field_str})")
elif business_code_ok:
# HTTP status was not in the primary list, but it's a 4xx and the business code matches.
results.append(self.passed(
f"{context_msg_prefix}API响应了状态码 {status_code} (非主要预期HTTP状态 {expected_http_status_codes}但为4xx客户端错误), "
f"且响应体中包含预期的业务错误码 '{specific_business_error_code}' (字段: '{error_code_field_in_body}')."
)) ))
self.logger.warning(f"必填请求体字段缺失测试失败:期望状态码 {expected_status_codes},实际为 {status_code} (移除字段: body.{removed_field_str})。") self.logger.info(f"{self.id}: Passed (Fallback). HTTP status {status_code} (4xx) with matching business code '{specific_business_error_code}'. (Removed field: body.{removed_field_str})")
else:
# Neither condition for passing was met.
fail_message = f"{context_msg_prefix}期望API返回状态码在 {expected_http_status_codes}或返回4xx客户端错误且业务码为 '{specific_business_error_code}'."
fail_message += f" 实际收到状态码 {status_code}."
if json_content and isinstance(json_content, dict):
fail_message += f" 响应体中的业务码 (\'{error_code_field_in_body}\') 为 \'{json_content.get(error_code_field_in_body)}\'."
elif json_content:
fail_message += " 响应体不是一个JSON对象."
else:
fail_message += " 响应体为空或非JSON."
results.append(self.failed(
message=fail_message,
details={"status_code": status_code, "response_body": json_content, "expected_http_status_codes": expected_http_status_codes, "expected_business_code": specific_business_error_code, "removed_field": f"body.{removed_field_str}"}
))
self.logger.warning(f"{self.id}: Failed. {fail_message} (Removed field: body.{removed_field_str})")
return results return results

View File

@ -11,23 +11,20 @@ class MissingRequiredFieldQueryCase(BaseAPITestCase):
execution_order = 211 # After body, before original combined one might have been 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): def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None):
super().__init__(endpoint_spec, global_api_spec, json_schema_validator=json_schema_validator, llm_service=llm_service) super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service=llm_service)
self.target_param_name: Optional[str] = None 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._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}") 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): def _try_find_removable_query_param(self):
query_params_spec_list = self.endpoint_spec.get("parameters", []) """Uses the base class helper to find a required query parameter."""
if query_params_spec_list: self.target_param_name = self._find_required_parameter_name("query")
self.logger.debug(f"检查查询参数的必填字段,总共 {len(query_params_spec_list)} 个参数定义。") # Logging about success/failure is handled by the base class method and the __init__ method.
for param_spec in query_params_spec_list:
if isinstance(param_spec, dict) and param_spec.get("in") == "query" and param_spec.get("required") is True:
field_name = param_spec.get("name")
if field_name:
self.target_param_name = field_name
self.logger.info(f"必填字段缺失测试的目标字段 (查询参数): '{self.target_param_name}'")
return
self.logger.info('在此端点规范中未找到可用于测试 "必填查询参数缺失" 的字段。')
def generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]: 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. # This test case focuses on query parameters, so it does not modify the request body.
@ -55,31 +52,55 @@ class MissingRequiredFieldQueryCase(BaseAPITestCase):
status_code = response_context.status_code status_code = response_context.status_code
json_content = response_context.json_content json_content = response_context.json_content
expected_http_status_codes = [400, 422]
expected_status_codes = [400, 422] specific_business_error_code = "4003"
specific_error_code_from_appendix_b = "4003" error_code_field_in_body = "code"
msg_prefix = f"当移除必填查询参数 '{self.target_param_name}' 时," context_msg_prefix = f"当移除必填查询参数 '{self.target_param_name}' 时,"
if status_code in expected_status_codes: http_status_ok = status_code in expected_http_status_codes
status_msg = f"{msg_prefix}API响应了预期的错误状态码 {status_code}" business_code_ok = False
if json_content and isinstance(json_content, dict) and str(json_content.get("code")) == specific_error_code_from_appendix_b: is_4xx_error = 400 <= status_code <= 499
results.append(self.passed(f"{status_msg} 且响应体中包含特定的错误码 '{specific_error_code_from_appendix_b}'"))
self.logger.info(f"正确接收到状态码 {status_code} 和错误码 '{specific_error_code_from_appendix_b}'") if json_content and isinstance(json_content, dict):
elif json_content and isinstance(json_content, dict) and "code" in json_content: body_code = json_content.get(error_code_field_in_body)
results.append(ValidationResult(passed=True, if body_code is not None and str(body_code) == specific_business_error_code:
message=f"{status_msg} 响应体中的错误码为 '{json_content.get('code')}' (期望或类似 '{specific_error_code_from_appendix_b}')。", business_code_ok = True
details=json_content
if http_status_ok:
if business_code_ok:
results.append(self.passed(
f"{context_msg_prefix}API响应了预期的错误状态码 {status_code} 并且响应体中包含预期的业务错误码 '{specific_business_error_code}' (字段: '{error_code_field_in_body}')."
)) ))
self.logger.warning(f"接收到状态码 {status_code},但错误码是 '{json_content.get('code')}' 而不是期望的 '{specific_error_code_from_appendix_b}'。此结果仍标记为通过,因状态码正确。") self.logger.info(f"{self.id}: Passed. HTTP status {status_code} and business code '{specific_business_error_code}' match. (Removed query param: {self.target_param_name})")
else: else:
results.append(self.passed(f"{status_msg} 但响应体中未找到特定的错误码字段或响应体结构不符合预期。")) results.append(self.passed(
self.logger.info(f"正确接收到状态码 {status_code},但在响应体中未找到错误码字段或预期结构。") f"{context_msg_prefix}API响应了预期的错误状态码 {status_code}. "
else: f"响应体中的业务错误码 (\'{error_code_field_in_body}\': \'{json_content.get(error_code_field_in_body)}\') 与特定期望 \'{specific_business_error_code}\' 不符或未找到但HTTP状态码正确。"
results.append(self.failed( ))
message=f"{msg_prefix}期望API返回状态码 {expected_status_codes} 中的一个,但实际收到 {status_code}", 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})")
details={"status_code": status_code, "response_body": json_content, "removed_field": f"query.{self.target_param_name}"}
elif business_code_ok:
results.append(self.passed(
f"{context_msg_prefix}API响应了状态码 {status_code} (非主要预期HTTP状态 {expected_http_status_codes}但为4xx客户端错误), "
f"且响应体中包含预期的业务错误码 '{specific_business_error_code}' (字段: '{error_code_field_in_body}')."
)) ))
self.logger.warning(f"必填查询参数缺失测试失败:期望状态码 {expected_status_codes},实际为 {status_code}。移除的参数:'{self.target_param_name}'") self.logger.info(f"{self.id}: Passed (Fallback). HTTP status {status_code} (4xx) with matching business code '{specific_business_error_code}'. (Removed query param: {self.target_param_name})")
else:
fail_message = f"{context_msg_prefix}期望API返回状态码在 {expected_http_status_codes}或返回4xx客户端错误且业务码为 '{specific_business_error_code}'."
fail_message += f" 实际收到状态码 {status_code}."
if json_content and isinstance(json_content, dict):
fail_message += f" 响应体中的业务码 (\'{error_code_field_in_body}\') 为 \'{json_content.get(error_code_field_in_body)}\'."
elif json_content:
fail_message += " 响应体不是一个JSON对象."
else:
fail_message += " 响应体为空或非JSON."
results.append(self.failed(
message=fail_message,
details={"status_code": status_code, "response_body": json_content, "expected_http_status_codes": expected_http_status_codes, "expected_business_code": specific_business_error_code, "removed_param": f"query.{self.target_param_name}"}
))
self.logger.warning(f"{self.id}: Failed. {fail_message} (Removed query param: {self.target_param_name})")
return results return results

View File

@ -1,7 +1,8 @@
from typing import Dict, Any, Optional, List from typing import Dict, Any, Optional, List, Union
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
import copy import copy
import logging import logging
from ddms_compliance_suite.utils import schema_utils
class TypeMismatchBodyCase(BaseAPITestCase): class TypeMismatchBodyCase(BaseAPITestCase):
id = "TC-ERROR-4001-BODY" id = "TC-ERROR-4001-BODY"
@ -25,317 +26,119 @@ class TypeMismatchBodyCase(BaseAPITestCase):
self._try_find_mismatch_target_in_body() self._try_find_mismatch_target_in_body()
def _try_find_mismatch_target_in_body(self): def _try_find_mismatch_target_in_body(self):
self.logger.critical(f"{self.id} __INIT__ >>> STARTED") self.logger.info(f"[{self.id}] Initializing: Looking for a simple type field in request body for type mismatch test.")
self.logger.debug(f"开始为端点 {self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')} 初始化请求体类型不匹配测试的目标字段查找。")
body_schema_to_check: Optional[Dict[str, Any]] = None
# 优先尝试从顶层 'requestBody' (OpenAPI 3.0 style) 获取 schema
request_body_spec = self.endpoint_spec.get("requestBody")
if request_body_spec and isinstance(request_body_spec, dict):
content = request_body_spec.get("content", {})
json_schema_entry = content.get("application/json") # 或者其他相关mime-type
if json_schema_entry and isinstance(json_schema_entry, dict) and isinstance(json_schema_entry.get("schema"), dict):
body_schema_to_check = json_schema_entry["schema"]
self.logger.debug(f"从顶层 'requestBody' 中获取到 schema: {list(body_schema_to_check.keys())}")
# 如果顶层 'requestBody' 未提供有效 schema则尝试从 'parameters' 列表 (Swagger 2.0 style for 'in: body') 查找
if not body_schema_to_check:
self.logger.debug(f"未从顶层 'requestBody' 找到 schema尝试从 'parameters' 列表查找 'in: body' 参数。")
parameters = self.endpoint_spec.get("parameters", [])
if isinstance(parameters, list):
for param in parameters:
if isinstance(param, dict) and param.get("in") == "body":
if isinstance(param.get("schema"), dict):
body_schema_to_check = param["schema"]
self.logger.debug(f"'parameters' 列表中找到 'in: body' 参数的 schema: {list(body_schema_to_check.keys())}")
break # 找到第一个 'in: body' 参数即可
else:
self.logger.warning(f"找到 'in: body' 参数 '{param.get('name', 'N/A')}',但其 'schema' 字段无效或缺失。")
else:
self.logger.warning("'parameters' 字段不是列表或不存在。")
body_schema_to_check = self._get_resolved_request_body_schema()
if body_schema_to_check: if body_schema_to_check:
self.logger.debug(f"最终用于检查的请求体 schema: {list(body_schema_to_check.keys())}") found_target = self._find_simple_type_field_in_schema(body_schema_to_check, "request body")
if self._find_target_field_in_schema(body_schema_to_check, base_path_for_log=""): # base_path_for_log 为空字符串代表 schema 的根 if found_target:
self.logger.info(f"类型不匹配测试的目标字段(请求体): {'.'.join(str(p) for p in self.target_field_path) if self.target_field_path else 'N/A'},原始类型: {self.original_field_type}") 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: else:
self.logger.debug(f"在提供的请求体 schema ({list(body_schema_to_check.keys())}) 中未找到适合类型不匹配测试的字段。") self.logger.info(f"[{self.id}] No suitable simple type field found in request body schema for type mismatch test.")
else: else:
self.logger.debug("在此端点规范中未找到有效的请求体 schema 定义 (无论是通过 'requestBody' 还是 'parameters' in:body)。") self.logger.info(f"[{self.id}] No request body schema found for endpoint. Skipping type mismatch target search.")
if not self.target_field_path: if not self.target_field_path:
self.logger.info(f"最终,在端点 {self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')} 的请求体中,均未找到可用于测试类型不匹配的字段。") self.logger.info(f"[{self.id}] Conclusion: No target field identified for request body type mismatch test.")
def _resolve_ref_if_present(self, schema_to_resolve: Dict[str, Any]) -> Dict[str, Any]:
# 根据用户进一步要求,方法体简化为直接返回,不进行任何 $ref/$ $$ref 的检查。
# self.logger.debug(f"_resolve_ref_if_present called. Returning schema as-is per new configuration.")
return schema_to_resolve
def _find_target_field_in_schema(self, schema_to_search: Dict[str, Any], base_path_for_log: str) -> bool:
"""
Recursively searches for a simple type field (string, integer, number, boolean) within a schema.
Sets self.target_field_path, self.original_field_type, and self.target_field_schema if found.
base_path_for_log is used to build the full path for logging.
Returns True if a field is found, False otherwise.
"""
self.logger.debug(f"Enter _find_target_field_in_schema for base_path: '{base_path_for_log}', schema_to_search keys: {list(schema_to_search.keys()) if isinstance(schema_to_search, dict) else 'Not a dict'}")
resolved_schema = self._resolve_ref_if_present(schema_to_search)
if not isinstance(resolved_schema, dict):
self.logger.debug(f"_find_target_field_in_schema: Schema at '{base_path_for_log}' is not a dict after resolution. Schema: {resolved_schema}")
return False
schema_type = resolved_schema.get("type")
self.logger.debug(f"Path: '{base_path_for_log}', Resolved Schema Type: '{schema_type}', Keys: {list(resolved_schema.keys())}")
if schema_type == "object":
properties = resolved_schema.get("properties", {})
self.logger.debug(f"Path: '{base_path_for_log}', Type is 'object'. Checking properties: {list(properties.keys())}")
for name, prop_schema_orig in properties.items():
current_path_str = f"{base_path_for_log}.{name}" if base_path_for_log else name
self.logger.debug(f"Path: '{current_path_str}', Property Schema (Original): {prop_schema_orig}")
prop_schema_resolved = self._resolve_ref_if_present(prop_schema_orig)
self.logger.debug(f"Path: '{current_path_str}', Property Schema (Resolved): {prop_schema_resolved}")
if not isinstance(prop_schema_resolved, dict):
self.logger.debug(f"Path: '{current_path_str}', Resolved schema is not a dict. Skipping.")
continue
prop_type = prop_schema_resolved.get("type")
self.logger.debug(f"Path: '{current_path_str}', Resolved Property Type: '{prop_type}'")
if prop_type in ["string", "integer", "number", "boolean"]:
# Construct path relative to the initial body schema
path_parts = base_path_for_log.split('.') if base_path_for_log else []
if path_parts == ['']: path_parts = [] # Handle initial empty base_path
self.target_field_path = path_parts + [name]
self.original_field_type = prop_type
self.target_field_schema = prop_schema_resolved
self.logger.info(f"目标字段(请求体): '{current_path_str}' (原始类型: '{prop_type}') FOUND!")
return True
elif prop_type == "object":
self.logger.debug(f"Path: '{current_path_str}', Type is 'object'. Recursing...")
if self._find_target_field_in_schema(prop_schema_resolved, current_path_str):
return True
self.logger.debug(f"Path: '{current_path_str}', Recursion for object did not find target.")
elif prop_type == "array":
self.logger.debug(f"Path: '{current_path_str}', Type is 'array'. Inspecting items...")
items_schema = prop_schema_resolved.get("items")
if isinstance(items_schema, dict):
self.logger.debug(f"Path: '{current_path_str}', Array items schema is a dict. Resolving and checking item type.")
items_schema_resolved = self._resolve_ref_if_present(items_schema)
item_type = items_schema_resolved.get("type")
self.logger.debug(f"Path: '{current_path_str}[*]', Resolved Item Type: '{item_type}'")
if item_type in ["string", "integer", "number", "boolean"]:
path_parts = base_path_for_log.split('.') if base_path_for_log else []
if path_parts == ['']: path_parts = []
self.target_field_path = path_parts + [name, 0] # Path like field.array_field.0
self.original_field_type = item_type
self.target_field_schema = items_schema_resolved # schema for the item, not the array
self.logger.info(f"目标字段(请求体 - 数组内简单类型): '{current_path_str}[0]' (原始类型: '{item_type}') FOUND!")
return True
elif item_type == "object":
self.logger.debug(f"Path: '{current_path_str}[*]', Item type is 'object'. Recursing into array item schema...")
# Path for recursion: current_path_str + ".0" (representing first item)
if self._find_target_field_in_schema(items_schema_resolved, f"{current_path_str}.0"):
# self.target_field_path would be set by recursive call.
# The path logic in _find_target_field_in_schema needs to correctly prepend array index if it comes from array item recursion.
# Let's ensure the path construction at "FOUND!" handles this.
# If current_path_str was "field.array.0" and recursion found "nested_prop",
# the path should become "field.array.0.nested_prop".
# The recursive call sets target_field_path starting from its base_path_for_log.
# So if base_path_for_log was "field.array.0", and it found "item_prop",
# self.target_field_path will be ["field", "array", 0, "item_prop"]. This seems correct.
self.logger.info(f"目标字段(请求体 - 数组内对象属性) found via recursion from '{current_path_str}.0'")
return True
self.logger.debug(f"Path: '{current_path_str}[*]', Recursion for array item object did not find target.")
else:
self.logger.debug(f"Path: '{current_path_str}[*]', Item type '{item_type}' is not simple or object.")
else:
self.logger.debug(f"Path: '{current_path_str}', Array items schema is not a dict or missing. Items: {items_schema}")
else:
self.logger.debug(f"Path: '{current_path_str}', Property type '{prop_type}' is not a simple type, object, or array. Skipping further processing for this property.")
elif schema_type == "array":
self.logger.debug(f"Path: '{base_path_for_log}', Top-level schema type is 'array'. Inspecting items...")
items_schema = resolved_schema.get("items")
if isinstance(items_schema, dict):
items_schema_resolved = self._resolve_ref_if_present(items_schema)
item_type = items_schema_resolved.get("type")
self.logger.debug(f"Path: '{base_path_for_log}[*]', Resolved Item Type: '{item_type}'")
if item_type in ["string", "integer", "number", "boolean"]:
# This means the body itself is an array of simple types.
# We target the first item. Path will be [0] if base_path_for_log is empty.
path_parts = base_path_for_log.split('.') if base_path_for_log else []
if path_parts == ['']: path_parts = []
# If base_path_for_log is empty (root schema is array), path is just [0]
# If base_path_for_log is "field.array_prop", this case shouldn't be hit here, but in object prop loop.
# This branch is for when the *entire request body schema* is an array.
self.target_field_path = path_parts + [0] # if root is array, path_parts is [], so path is [0]
self.original_field_type = item_type
self.target_field_schema = items_schema_resolved
self.logger.info(f"目标字段(请求体 - 根为简单类型数组): '{base_path_for_log}[0]' (原始类型: '{item_type}') FOUND!")
return True
elif item_type == "object":
self.logger.debug(f"Path: '{base_path_for_log}[*]', Item type is 'object'. Recursing into root array item schema...")
# Path for recursion: base_path_for_log + ".0" or just "0" if base_path is empty
new_base_path = f"{base_path_for_log}.0" if base_path_for_log else "0"
if self._find_target_field_in_schema(items_schema_resolved, new_base_path):
self.logger.info(f"目标字段(请求体 - 根为对象数组,属性在对象内) found via recursion from '{new_base_path}'")
return True
self.logger.debug(f"Path: '{base_path_for_log}[*]', Recursion for root array item object did not find target.")
else:
self.logger.debug(f"Path: '{base_path_for_log}[*]', Item type '{item_type}' is not simple or object.")
else:
self.logger.debug(f"Path: '{base_path_for_log}', Root array items schema is not a dict or missing. Items: {items_schema}")
else:
self.logger.debug(f"Path: '{base_path_for_log}', Schema type is '{schema_type}', not 'object' or 'array'. Cannot find properties here.")
self.logger.debug(f"Exit _find_target_field_in_schema for base_path: '{base_path_for_log if base_path_for_log else 'root'}'. Target NOT found in this path.")
return False
def generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]: 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.") self.logger.debug(f"{self.id} is focused on request body, generate_query_params will not modify query parameters.")
return current_query_params return current_query_params
def generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]: def generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]:
if not self.target_field_path: # target_field_location is always "body" if not self.target_field_path or not self.original_field_type:
self.logger.info(f"[{self.id}] No target field or original type identified for body type mismatch. Skipping body modification.")
return current_body return current_body
self.logger.debug(f"准备修改请求体以测试类型不匹配。目标路径: {self.target_field_path}, 原始类型: {self.original_field_type}") self.logger.debug(f"[{self.id}] Preparing to modify request body for type mismatch. Target path: {self.target_field_path}, Original type: {self.original_field_type}")
modified_body = copy.deepcopy(current_body) if current_body is not None else {} # 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
# Ensure body is a dict if path is not empty, or if it's empty and body is None, init to {} # For now, we don't strictly need original_value for generate_mismatched_value, but it takes it as an arg.
if self.target_field_path and not isinstance(modified_body, dict):
if not modified_body and not self.target_field_path[0]: # Path is effectively root, and body is None/empty
modified_body = {} # Initialize if targeting root of an empty body
else:
self.logger.warning(f"请求体不是字典类型 (is {type(modified_body)}),但目标字段路径为 {self.target_field_path}。无法安全应用修改。")
return current_body
elif not self.target_field_path and not modified_body: # No path (targeting root) and body is None
self.logger.warning(f"目标字段路径为空 (表示根对象) 但当前请求体也为空,无法确定如何修改。")
return current_body
mismatched_value = schema_utils.generate_mismatched_value(
original_type=self.original_field_type,
original_value=None, # Placeholder, as current generate_mismatched_value doesn't use it heavily yet
field_schema=self.target_field_schema,
logger_param=self.logger
)
temp_obj_ref = modified_body 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))}'.")
try:
for i, key_or_index in enumerate(self.target_field_path):
is_last_part = (i == len(self.target_field_path) - 1)
if isinstance(key_or_index, int): # Array index
if not isinstance(temp_obj_ref, list) or key_or_index >= len(temp_obj_ref):
self.logger.warning(f"路径 {self.target_field_path[:i+1]} 指向数组索引,但当前对象不是列表或索引 ({key_or_index}) 越界 (len: {len(temp_obj_ref) if isinstance(temp_obj_ref, list) else 'N/A'})。")
# Attempt to create list/elements if they don't exist up to this point (for safety, only if current is None or empty list)
if isinstance(temp_obj_ref, list) and key_or_index == 0 and not temp_obj_ref: # Empty list, trying to set first element
temp_obj_ref.append({}) # Add a dict placeholder for the first element
elif temp_obj_ref is None and key_or_index == 0: # If parent was None, can't proceed here unless path logic is very robust for creation
return current_body # Cannot proceed
else:
return current_body # Cannot proceed
if is_last_part: modified_body, success = schema_utils.util_set_value_at_path(
original_value = temp_obj_ref[key_or_index] data_container=current_body,
new_value = self._get_mismatched_value(self.original_field_type, original_value, self.target_field_schema) path=self.target_field_path,
self.logger.info(f"在路径 {self.target_field_path} (数组索引 {key_or_index}) 处,将值从 '{original_value}' 修改为 '{new_value}' (原始类型: {self.original_field_type})") new_value=mismatched_value
temp_obj_ref[key_or_index] = new_value )
else:
temp_obj_ref = temp_obj_ref[key_or_index]
elif isinstance(temp_obj_ref, dict): # Dictionary key
if key_or_index not in temp_obj_ref and not is_last_part:
self.logger.debug(f"路径 {self.target_field_path[:i+1]} 中的键 '{key_or_index}' 在当前对象中不存在,将创建它。")
temp_obj_ref[key_or_index] = {} # Create path if not exists
if is_last_part:
original_value = temp_obj_ref.get(key_or_index)
new_value = self._get_mismatched_value(self.original_field_type, original_value, self.target_field_schema)
self.logger.info(f"在路径 {self.target_field_path} (键 '{key_or_index}') 处,将值从 '{original_value}' 修改为 '{new_value}' (原始类型: {self.original_field_type})")
temp_obj_ref[key_or_index] = new_value
else:
temp_obj_ref = temp_obj_ref[key_or_index]
if temp_obj_ref is None and not is_last_part:
self.logger.warning(f"路径 {self.target_field_path[:i+1]} 的值在深入时变为None。创建空字典继续。")
# This part is tricky, if temp_obj_ref was a key in parent, parent[key_or_index] is None.
# We need to set parent[key_or_index] = {} and then temp_obj_ref = parent[key_or_index]
# This requires knowing the parent. Let's simplify: if it becomes None, we might not be able to proceed unless it's the dict itself.
# The current logic `temp_obj_ref = temp_obj_ref[key_or_index]` means if `temp_obj_ref` was `obj[key]`, now `temp_obj_ref` IS `obj[key]`s value.
# If this value is None, and we are not at the end, we should create a dict there if the next part of path is a string key.
# This modification is done in the check `if key_or_index not in temp_obj_ref and not is_last_part:`
# If it's None AFTER that, it means the schema might be complex (e.g. anyOf, oneOf) or data is unexpectedly null.
# For robustness, if it's None and not the last part, we can assume we need a dict for the next key.
# The path creation `temp_obj_ref[key_or_index] = {}` for the *next* key happens at the start of the loop for that next key.
pass # Already handled by creation logic at the start of the loop iteration for the next key
else:
self.logger.warning(f"尝试访问路径 {self.target_field_path[:i+1]} 时,当前对象 ({type(temp_obj_ref)}) 不是字典或列表。")
return current_body
except Exception as e: if success:
self.logger.error(f"在根据路径 {self.target_field_path} 修改请求体时发生错误: {e}", exc_info=True) self.logger.debug(f"[{self.id}] Successfully set mismatched value at path using util_set_value_at_path.")
return modified_body
else:
self.logger.error(f"[{self.id}] Failed to set mismatched value at path using util_set_value_at_path. Returning original body.")
return current_body return current_body
return modified_body
def _get_mismatched_value(self, original_type: Optional[str], original_value: Any, field_schema: Optional[Dict[str, Any]]) -> Any:
if original_type == "string":
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list):
if 123 not in field_schema["enum"]: return 123
if False not in field_schema["enum"]: return False
return 12345
elif original_type == "integer":
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list):
if "not-an-integer" not in field_schema["enum"]: return "not-an-integer"
if 3.14 not in field_schema["enum"]: return 3.14
return "not-an-integer"
elif original_type == "number":
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list):
if "not-a-number" not in field_schema["enum"]: return "not-a-number"
return "not-a-number"
elif original_type == "boolean":
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list):
if "not-a-boolean" not in field_schema["enum"]: return "not-a-boolean"
if 1 not in field_schema["enum"]: return 1
return "not-a-boolean"
elif original_type == "array":
return {"value": "not-an-array"}
elif original_type == "object":
return ["not", "an", "object"]
self.logger.warning(f"类型不匹配测试(请求体):原始类型 '{original_type}' 未知或无法生成不匹配值,将返回固定字符串 'mismatch_test'")
return "mismatch_test" # Fallback
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]: def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
results = [] 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 status_code = response_context.status_code
json_content = response_context.json_content json_content = response_context.json_content
expected_http_status_codes = [400, 422] # Common client error codes for type issues
# specific_business_error_code 是此测试用例期望的特定业务错误码,例如 "4001"
specific_business_error_code = "4001"
error_code_field_in_body = "code" # 响应体中业务错误码的字段名
if not self.target_field_path: field_path_str = '.'.join(map(str, self.target_field_path))
results.append(self.passed("跳过测试:在请求体中未找到合适的字段来测试类型不匹配。")) context_msg_prefix = f"当请求体字段 '{field_path_str}' 类型不匹配时,"
self.logger.info(f"{self.id}: 由于未识别到目标请求体字段,跳过类型不匹配测试。")
return results
expected_status_codes = [400, 422] http_status_ok = status_code in expected_http_status_codes
specific_error_code_from_appendix_b = "4001" # Example business_code_ok = False
is_4xx_error = 400 <= status_code <= 499
if status_code in expected_status_codes: if json_content and isinstance(json_content, dict):
msg = f"API对请求体字段 '{'.'.join(str(p) for p in self.target_field_path)}' 的类型不匹配响应了 {status_code},符合预期。" body_code = json_content.get(error_code_field_in_body)
error_code_in_response = json_content.get("code") if isinstance(json_content, dict) else None if body_code is not None and str(body_code) == specific_business_error_code:
if error_code_in_response == specific_error_code_from_appendix_b: business_code_ok = True
results.append(self.passed(f"{msg} 并成功接收到特定错误码 '{specific_error_code_from_appendix_b}'"))
elif error_code_in_response: if http_status_ok:
results.append(ValidationResult(passed=True, if business_code_ok:
message=f"{msg} 但响应体中的错误码是 '{error_code_in_response}' (期望类似 '{specific_error_code_from_appendix_b}')。", results.append(self.passed(
details=json_content if isinstance(json_content, dict) else {"raw_response": str(json_content)} 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: else:
results.append(self.passed(f"{msg} 响应体中未找到错误码或结构不符合预期。")) results.append(self.passed(
else: f"{context_msg_prefix}API响应了预期的错误状态码 {status_code}. "
results.append(self.failed( f"响应体中的业务错误码 (\'{error_code_field_in_body}\': \'{json_content.get(error_code_field_in_body)}\') 与特定期望 \'{specific_business_error_code}\' 不符或未找到但HTTP状态码正确。"
message=f"对请求体字段 '{'.'.join(str(p) for p in self.target_field_path)}' 的类型不匹配测试期望状态码为 {expected_status_codes} 之一,但收到 {status_code}", ))
details={"status_code": status_code, "response_body": json_content} self.logger.info(f"{self.id}: Passed. HTTP status {status_code} is correct. Business code mismatch/missing (Expected: \'{specific_business_error_code}\', Got: \'{json_content.get(error_code_field_in_body)}\'). (Field: body.{field_path_str})")
elif business_code_ok:
results.append(self.passed(
f"{context_msg_prefix}API响应了状态码 {status_code} (非主要预期HTTP状态 {expected_http_status_codes}但为4xx客户端错误), "
f"且响应体中包含预期的业务错误码 '{specific_business_error_code}' (字段: '{error_code_field_in_body}')."
)) ))
self.logger.warning(f"{self.id}: 类型不匹配测试失败。字段: body.{'.'.join(str(p) for p in self.target_field_path)}, 期望状态码: {expected_status_codes}, 实际: {status_code}") self.logger.info(f"{self.id}: Passed (Fallback). HTTP status {status_code} (4xx) with matching business code '{specific_business_error_code}'. (Field: body.{field_path_str})")
else:
fail_message = f"{context_msg_prefix}期望API返回状态码在 {expected_http_status_codes}或返回4xx客户端错误且业务码为 '{specific_business_error_code}'."
fail_message += f" 实际收到状态码 {status_code}."
if json_content and isinstance(json_content, dict):
fail_message += f" 响应体中的业务码 (\'{error_code_field_in_body}\') 为 \'{json_content.get(error_code_field_in_body)}\'."
elif json_content:
fail_message += " 响应体不是一个JSON对象."
else:
fail_message += " 响应体为空或非JSON."
results.append(self.failed(
message=fail_message,
details={"status_code": status_code, "response_body": json_content, "expected_http_status_codes": expected_http_status_codes, "expected_business_code": specific_business_error_code, "mismatched_field": f"body.{field_path_str}"}
))
self.logger.warning(f"{self.id}: Failed. {fail_message} (Field: body.{field_path_str})")
return results return results

View File

@ -1,7 +1,8 @@
from typing import Dict, Any, Optional, List from typing import Dict, Any, Optional, List, Tuple, Union
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
import copy import copy
import logging import logging
from ddms_compliance_suite.utils import schema_utils
class TypeMismatchQueryParamCase(BaseAPITestCase): class TypeMismatchQueryParamCase(BaseAPITestCase):
id = "TC-ERROR-4001-QUERY" id = "TC-ERROR-4001-QUERY"
@ -19,220 +20,128 @@ class TypeMismatchQueryParamCase(BaseAPITestCase):
# Location is always 'query' for this class # Location is always 'query' for this class
self.target_field_location: str = "query" self.target_field_location: str = "query"
self.target_field_schema: Optional[Dict[str, Any]] = None 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.original_value_at_path: Any = None
self.mismatched_value: Any = None self.mismatched_value: Any = None
# 调用新方法来查找目标字段 # 调用新方法来查找目标字段
self._try_find_mismatch_target_in_query() self._try_find_mismatch_target_in_query()
self.logger.critical(f"{self.id} __INIT__ >>> STARTED")
self.logger.debug(f"开始为端点 {self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')} 初始化查询参数类型不匹配测试的目标字段查找。")
parameters = self.endpoint_spec.get("parameters", [])
self.logger.critical(f"{self.id} __INIT__ >>> Parameters to be processed: {parameters}")
self.logger.debug(f"传入的参数列表 (在 {self.id}中): {parameters}")
for param_spec in parameters:
if param_spec.get("in") == "query":
param_name = param_spec.get("name")
if not param_name:
self.logger.warning("发现一个没有名称的查询参数定义,已跳过。")
continue
self.logger.debug(f"检查查询参数: '{param_name}'")
param_type = param_spec.get("type")
param_schema = param_spec.get("schema")
# Scenario 1: Simple type directly in param_spec (e.g., type: string)
if param_type in ["string", "number", "integer", "boolean"]:
self.target_field_path = [param_name]
self.original_field_type = param_type
self.target_field_schema = param_spec
self.logger.info(f"目标字段(查询参数 - 简单类型): {param_name},原始类型: {self.original_field_type}")
break
# Scenario 2: Schema defined for the query parameter (OpenAPI 3.0 style, or complex objects in query)
elif isinstance(param_schema, dict):
self.logger.debug(f"查询参数 '{param_name}' 包含嵌套 schema尝试在其内部查找简单类型字段。")
# We need to find a simple type *within* this schema.
# _find_target_field_in_schema is designed for requestBody, let's adapt or simplify.
# For query parameters, complex objects are less common or might be flattened.
# Let's try to find a simple type property directly within this schema if it's an object.
resolved_param_schema = self._resolve_ref_if_present(param_schema)
if resolved_param_schema.get("type") == "object":
properties = resolved_param_schema.get("properties", {})
for prop_name, prop_details_orig in properties.items():
prop_details = self._resolve_ref_if_present(prop_details_orig)
if prop_details.get("type") in ["string", "number", "integer", "boolean"]:
self.target_field_path = [param_name, prop_name] # Path will be param_name.prop_name
self.original_field_type = prop_details.get("type")
self.target_field_schema = prop_details
self.logger.info(f"目标字段(查询参数 - 对象属性): {param_name}.{prop_name},原始类型: {self.original_field_type}")
break # Found a suitable property
if self.target_field_path: break # Break outer loop if found
elif resolved_param_schema.get("type") in ["string", "number", "integer", "boolean"]: # Schema itself is simple after ref resolution
self.target_field_path = [param_name]
self.original_field_type = resolved_param_schema.get("type")
self.target_field_schema = resolved_param_schema
self.logger.info(f"目标字段(查询参数 - schema为简单类型): {param_name},原始类型: {self.original_field_type}")
break
else:
self.logger.debug(f"查询参数 '{param_name}' (type: {param_type}, schema: {param_schema}) 不是直接的简单类型,也无直接可用的对象型 schema 属性。")
if not self.target_field_path:
self.logger.info(f"最终,在端点 {self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')} 的查询参数中,均未找到可用于测试类型不匹配的字段。")
def _try_find_mismatch_target_in_query(self): def _try_find_mismatch_target_in_query(self):
self.logger.critical(f"{self.id} _try_find_mismatch_target_in_query >>> STARTED") self.logger.info(f"[{self.id}] Initializing: Looking for a simple type query parameter for type mismatch test.")
self.logger.debug(f"开始为端点 {self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')} 初始化查询参数类型不匹配测试的目标字段查找。")
found_target_param = self._find_first_simple_type_parameter(param_location="query")
parameters = self.endpoint_spec.get("parameters", []) if found_target_param:
self.logger.critical(f"{self.id} _try_find_mismatch_target_in_query >>> Parameters to be processed: {parameters}") full_path, param_type, param_schema, top_level_param_name = found_target_param
self.logger.debug(f"传入的参数列表 (在 {self.id}中): {parameters}") self.target_field_path = full_path
self.original_field_type = param_type
self.target_field_schema = param_schema
self.target_param_name = top_level_param_name # Store the top-level parameter name
self.logger.info(f"[{self.id}] Target for type mismatch (query): Param='{self.target_param_name}', Path='{'.'.join(map(str,self.target_field_path))}', Type='{self.original_field_type}'")
else:
self.logger.info(f"[{self.id}] No suitable simple type query parameter found for type mismatch test.")
for param_spec in parameters:
if param_spec.get("in") == "query":
param_name = param_spec.get("name")
if not param_name:
self.logger.warning("发现一个没有名称的查询参数定义,已跳过。")
continue
self.logger.debug(f"检查查询参数: '{param_name}'")
param_type = param_spec.get("type")
param_schema = param_spec.get("schema")
# Scenario 1: Simple type directly in param_spec (e.g., type: string)
if param_type in ["string", "number", "integer", "boolean"]:
self.target_field_path = [param_name]
self.original_field_type = param_type
self.target_field_schema = param_spec
self.logger.info(f"目标字段(查询参数 - 简单类型): {param_name},原始类型: {self.original_field_type}")
break
# Scenario 2: Schema defined for the query parameter (OpenAPI 3.0 style, or complex objects in query)
elif isinstance(param_schema, dict):
self.logger.debug(f"查询参数 '{param_name}' 包含嵌套 schema尝试在其内部查找简单类型字段。")
resolved_param_schema = self._resolve_ref_if_present(param_schema)
if resolved_param_schema.get("type") == "object":
properties = resolved_param_schema.get("properties", {})
for prop_name, prop_details_orig in properties.items():
prop_details = self._resolve_ref_if_present(prop_details_orig)
if prop_details.get("type") in ["string", "number", "integer", "boolean"]:
self.target_field_path = [param_name, prop_name]
self.original_field_type = prop_details.get("type")
self.target_field_schema = prop_details
self.logger.info(f"目标字段(查询参数 - 对象属性): {param_name}.{prop_name},原始类型: {self.original_field_type}")
break
if self.target_field_path: break
elif resolved_param_schema.get("type") in ["string", "number", "integer", "boolean"]:
self.target_field_path = [param_name]
self.original_field_type = resolved_param_schema.get("type")
self.target_field_schema = resolved_param_schema
self.logger.info(f"目标字段(查询参数 - schema为简单类型): {param_name},原始类型: {self.original_field_type}")
break
else:
self.logger.debug(f"查询参数 '{param_name}' (type: {param_type}, schema: {param_schema}) 不是直接的简单类型,也无直接可用的对象型 schema 属性。")
if not self.target_field_path:
self.logger.info(f"最终,在端点 {self.endpoint_spec.get('method')} {self.endpoint_spec.get('path')} 的查询参数中,均未找到可用于测试类型不匹配的字段。")
def _resolve_ref_if_present(self, schema_to_resolve: Dict[str, Any]) -> Dict[str, Any]:
# 根据用户进一步要求,方法体简化为直接返回,不进行任何 $ref/$ $$ref 的检查。
# self.logger.debug(f"_resolve_ref_if_present called. Returning schema as-is per new configuration.")
return schema_to_resolve
# No generate_request_body, or it simply returns current_body
def generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]: 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.") self.logger.debug(f"{self.id} is focused on query parameters, generate_request_body will not modify the body.")
return current_body return current_body
def generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]: def generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]:
if not self.target_field_path: # target_field_location is always "query" if not self.target_field_path or not self.original_field_type:
self.logger.info(f"[{self.id}] No target field or original type identified for query param type mismatch. Skipping query param modification.")
return current_query_params return current_query_params
self.logger.debug(f"准备修改查询参数以测试类型不匹配。目标路径: {self.target_field_path}, 原始类型: {self.original_field_type}") self.logger.debug(f"[{self.id}] Preparing to modify query params for type mismatch. Target path: {self.target_field_path}, Original type: {self.original_field_type}")
modified_params = copy.deepcopy(current_query_params) if current_query_params is not None else {} mismatched_value = schema_utils.generate_mismatched_value(
original_type=self.original_field_type,
temp_obj_ref = modified_params original_value=None, # Placeholder
try: field_schema=self.target_field_schema,
for i, key in enumerate(self.target_field_path): logger_param=self.logger
is_last_part = (i == len(self.target_field_path) - 1) )
if is_last_part:
original_value = temp_obj_ref.get(key)
new_value = self._get_mismatched_value(self.original_field_type, original_value, self.target_field_schema)
self.logger.info(f"在查询参数路径 {self.target_field_path} (键 '{key}') 处,将值从 '{original_value}' 修改为 '{new_value}' (原始类型: {self.original_field_type})")
temp_obj_ref[key] = new_value
else: # Navigating a nested structure within a query param (e.g. filter[field]=value)
if key not in temp_obj_ref or not isinstance(temp_obj_ref[key], dict):
# If path expects a dict but it's not there, create it.
# This is crucial for structured query params like "filter[name]=value"
# where target_field_path might be ["filter", "name"].
temp_obj_ref[key] = {}
temp_obj_ref = temp_obj_ref[key]
except Exception as e:
self.logger.error(f"在根据路径 {self.target_field_path} 修改查询参数时发生错误: {e}", exc_info=True)
return current_query_params
return modified_params self.logger.info(f"[{self.id}] Generated mismatched value '{mismatched_value}' for original type '{self.original_field_type}' at query path '{'.'.join(map(str, self.target_field_path))}'.")
def _get_mismatched_value(self, original_type: Optional[str], original_value: Any, field_schema: Optional[Dict[str, Any]]) -> Any: # 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)
if original_type == "string": modified_params, success = schema_utils.util_set_value_at_path(
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list): data_container=current_query_params,
if 123 not in field_schema["enum"]: return 123 path=self.target_field_path, # Path might be like ['paramName'] or ['paramName', 'nestedKey']
if False not in field_schema["enum"]: return False new_value=mismatched_value
return 12345 )
elif original_type == "integer":
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list): if success:
if "not-an-integer" not in field_schema["enum"]: return "not-an-integer" self.logger.debug(f"[{self.id}] Successfully set mismatched value in query params using util_set_value_at_path.")
if 3.14 not in field_schema["enum"]: return 3.14 return modified_params
return "not-an-integer" else:
elif original_type == "number": self.logger.error(f"[{self.id}] Failed to set mismatched value in query params using util_set_value_at_path. Returning original params.")
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list): return current_query_params
if "not-a-number" not in field_schema["enum"]: return "not-a-number"
return "not-a-number"
elif original_type == "boolean":
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list):
if "not-a-boolean" not in field_schema["enum"]: return "not-a-boolean"
if 1 not in field_schema["enum"]: return 1
return "not-a-boolean"
self.logger.warning(f"类型不匹配测试(查询参数):原始类型 '{original_type}' 未知或无法生成不匹配值,将返回固定字符串 'mismatch_test'")
return "mismatch_test" # Fallback for other types or if logic is incomplete
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]: def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
results = [] 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 status_code = response_context.status_code
json_content = response_context.json_content json_content = response_context.json_content
expected_http_status_codes = [400, 422]
specific_business_error_code = "4001"
error_code_field_in_body = "code"
if not self.target_field_path: # Use self.target_param_name for a clearer context message if a top-level param was identified
results.append(self.passed("跳过测试:在查询参数中未找到合适的字段来测试类型不匹配。")) context_param_identifier = self.target_param_name or '.'.join(map(str, self.target_field_path))
self.logger.info(f"{self.id}: 由于未识别到目标查询参数字段,跳过类型不匹配测试。") context_msg_prefix = f"当查询参数 '{context_param_identifier}' (路径: '{'.'.join(map(str, self.target_field_path))}') 类型不匹配时,"
return results
expected_status_codes = [400, 422] http_status_ok = status_code in expected_http_status_codes
specific_error_code_from_appendix_b = "4001" # Example business_code_ok = False
is_4xx_error = 400 <= status_code <= 499
if status_code in expected_status_codes: if json_content and isinstance(json_content, dict):
msg = f"API对查询参数 '{'.'.join(self.target_field_path)}' 的类型不匹配响应了 {status_code},符合预期。" body_code = json_content.get(error_code_field_in_body)
# Further check for specific error code in body if applicable if body_code is not None and str(body_code) == specific_business_error_code:
error_code_in_response = json_content.get("code") if isinstance(json_content, dict) else None business_code_ok = True
if error_code_in_response == specific_error_code_from_appendix_b:
results.append(self.passed(f"{msg} 并成功接收到特定错误码 '{specific_error_code_from_appendix_b}'")) if http_status_ok:
elif error_code_in_response: if business_code_ok:
results.append(ValidationResult(passed=True, results.append(self.passed(
message=f"{msg} 但响应体中的错误码是 '{error_code_in_response}' (期望类似 '{specific_error_code_from_appendix_b}')。", f"{context_msg_prefix}API响应了预期的错误状态码 {status_code} 并且响应体中包含预期的业务错误码 '{specific_business_error_code}' (字段: '{error_code_field_in_body}')."
details=json_content if isinstance(json_content, dict) else {"raw_response": str(json_content)}
)) ))
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: else:
results.append(self.passed(f"{msg} 响应体中未找到错误码或结构不符合预期。")) results.append(self.passed(
else: f"{context_msg_prefix}API响应了预期的错误状态码 {status_code}. "
results.append(self.failed( f"响应体中的业务错误码 (\'{error_code_field_in_body}\': \'{json_content.get(error_code_field_in_body)}\') 与特定期望 \'{specific_business_error_code}\' 不符或未找到但HTTP状态码正确。"
message=f"对查询参数 '{'.'.join(self.target_field_path)}' 的类型不匹配测试期望状态码为 {expected_status_codes} 之一,但收到 {status_code}", ))
details={"status_code": status_code, "response_body": json_content} 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})")
))
self.logger.warning(f"{self.id}: 类型不匹配测试失败。字段: query.{'.'.join(self.target_field_path)}, 期望状态码: {expected_status_codes}, 实际: {status_code}")
return results elif business_code_ok:
results.append(self.passed(
f"{context_msg_prefix}API响应了状态码 {status_code} (非主要预期HTTP状态 {expected_http_status_codes}但为4xx客户端错误), "
f"且响应体中包含预期的业务错误码 '{specific_business_error_code}' (字段: '{error_code_field_in_body}')."
))
self.logger.info(f"{self.id}: Passed (Fallback). HTTP status {status_code} (4xx) with matching business code '{specific_business_error_code}'. (Query param: {context_param_identifier})")
else:
fail_message = f"{context_msg_prefix}期望API返回状态码在 {expected_http_status_codes}或返回4xx客户端错误且业务码为 '{specific_business_error_code}'."
fail_message += f" 实际收到状态码 {status_code}."
if json_content and isinstance(json_content, dict):
fail_message += f" 响应体中的业务码 (\'{error_code_field_in_body}\') 为 \'{json_content.get(error_code_field_in_body)}\'."
elif json_content:
fail_message += " 响应体不是一个JSON对象."
else:
fail_message += " 响应体为空或非JSON."
results.append(self.failed(
message=fail_message,
details={"status_code": status_code, "response_body": json_content, "expected_http_status_codes": expected_http_status_codes, "expected_business_code": specific_business_error_code, "mismatched_param": context_param_identifier}
))
self.logger.warning(f"{self.id}: Failed. {fail_message} (Query param: {context_param_identifier})")
return results
def generate_path_params(self, current_path_params: Dict[str, Any]) -> Dict[str, Any]:
# ... existing code ...
pass
return current_path_params

View File

@ -115,14 +115,13 @@
# - 版本号: 语义化版本,例如 v1, v1.0, v2.1.3。 # - 版本号: 语义化版本,例如 v1, v1.0, v2.1.3。
# - 资源类型: 通常为名词复数。 # - 资源类型: 通常为名词复数。
# - standard_name: "url_path_structure" # - standard_name: "url_path_structure"
# - standard_name: "resource"
# - standard_name: "schema"
# - standard_name: "version"
# 4. **URL路径参数命名规范**: # 4. **URL路径参数命名规范**:
# - 规则: 路径参数(如果存在)必须使用全小写字母(可以是一个单词)或小写字母加下划线命名(这是多个单词的情况),并能反映资源的唯一标识 (例如: {{well_id}},{{version}},{{schema}})。 # - 规则: 路径参数(如果存在)必须使用全小写字母(可以是一个单词)或小写字母加下划线命名(这是多个单词的情况),并能反映资源的唯一标识 (例如: {{well_id}},还有{{version}},{{schema}}也是合规的,比一定非要{{version_id}})。
# - standard_name: "url_path_parameter_naming" # - standard_name: "url_path_parameter_naming"
# 5. **资源命名规范 (在路径中)**: # 5. **资源命名规范 (在路径中)**:
# - 规则: 资源集合应使用名词的复数形式表示 (例如 `/wells`, `/logs`);应优先使用石油行业的标准术语 (例如用 `trajectory` 而非 `path` 来表示井轨迹)。 # - 规则: 资源集合应使用名词的复数形式表示 (例如 `/wells`, `/logs`);应优先使用石油行业的标准术语 (例如用 `trajectory` 而非 `path` 来表示井轨迹)。
# - standard_name: "resource_naming_in_path" # - standard_name: "resource_naming_in_path"

View File

@ -18,7 +18,7 @@ class HTTPSMandatoryCase(BaseAPITestCase):
def modify_request_url(self, current_url: str) -> str: def modify_request_url(self, current_url: str) -> str:
parsed_url = urllib.parse.urlparse(current_url) parsed_url = urllib.parse.urlparse(current_url)
if parsed_url.scheme.lower() == "httpss": if parsed_url.scheme.lower() == "https":
# 将 https 替换为 http # 将 https 替换为 http
modified_url = parsed_url._replace(scheme="http").geturl() modified_url = parsed_url._replace(scheme="http").geturl()
self.logger.info(f"为进行HTTPS检查修改URL原始 '{current_url}', 修改为 '{modified_url}'") self.logger.info(f"为进行HTTPS检查修改URL原始 '{current_url}', 修改为 '{modified_url}'")

View File

@ -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

View File

@ -1,61 +0,0 @@
"""Assertion Engine Module"""
from typing import Any, Dict, List
# from ..models.rule_models import BusinessAssertionTemplate # Assuming rule_models.py will exist
class AssertionEngine:
"""
Responsible for verifying test step results based on predefined rules.
This is a placeholder and will need significant development based on
how assertion rules are defined and evaluated (e.g., Python expressions, JSONPath, etc.).
"""
def __init__(self):
# Initialization, potentially loading common assertion helpers or context
pass
def evaluate_assertion(self, assertion_rule: Any, context_data: Dict[str, Any]) -> bool:
"""
Evaluates a single assertion rule against the given context data.
Args:
assertion_rule: The rule definition (e.g., a Pydantic model like BusinessAssertionTemplate).
The structure of this will depend on your rule design.
context_data: Data from the test execution context (e.g., API response, extracted variables).
Returns:
True if the assertion passes, False otherwise.
"""
# Placeholder logic - this needs to be implemented based on rule type
# Example: if rule is a python expression
# if assertion_rule.template_language == "python_expression":
# try:
# # Ensure the expression is safe to eval!
# # Consider using ast.literal_eval for simple cases or a safer evaluation library.
# # For complex expressions, a dedicated DSL or restricted environment is better.
# # The context_data would be made available to the expression.
# return bool(eval(assertion_rule.template_expression, {}, context_data))
# except Exception as e:
# print(f"Error evaluating Python expression assertion: {e}")
# return False
# Example: if rule is a simple equality check (defined differently)
# if "expected_value" in assertion_rule and "actual_value_path" in assertion_rule:
# actual_value = get_value_from_path(context_data, assertion_rule.actual_value_path) # Needs helper
# return actual_value == assertion_rule.expected_value
print(f"[AssertionEngine] Placeholder: Evaluating rule '{getattr(assertion_rule, "name", "Unnamed Rule")}'. Context: {context_data}")
# This is a very basic placeholder. Real implementation depends heavily on rule definition.
return True # Default to True for now
# Helper function example (would likely be more complex or use a library like jsonpath-ng)
# def get_value_from_path(data: Dict[str, Any], path: str) -> Any:
# """Retrieves a value from a nested dict using a simple dot-separated path."""
# keys = path.split('.')
# value = data
# for key in keys:
# if isinstance(value, dict) and key in value:
# value = value[key]
# else:
# return None # Or raise an error
# return value

View File

@ -1,6 +1,7 @@
from enum import Enum from enum import Enum
from typing import Any, Dict, Optional, List, Tuple, Type from typing import Any, Dict, Optional, List, Tuple, Type, Union
import logging import logging
from .utils import schema_utils
class TestSeverity(Enum): class TestSeverity(Enum):
"""测试用例的严重程度""" """测试用例的严重程度"""
@ -87,6 +88,10 @@ class BaseAPITestCase:
# 新增:测试用例执行顺序 (数值越小越先执行) # 新增:测试用例执行顺序 (数值越小越先执行)
execution_order: int = 100 execution_order: int = 100
# 新增:标记此测试用例是否为关键的前置设置测试
# 如果此用例失败,后续针对该端点的其他测试用例将被跳过
is_critical_setup_test: bool = False
# LLM 生成控制属性 (默认为 False表示不使用LLM除非显式开启) # LLM 生成控制属性 (默认为 False表示不使用LLM除非显式开启)
use_llm_for_body: bool = False use_llm_for_body: bool = False
use_llm_for_path_params: bool = False use_llm_for_path_params: bool = False
@ -238,4 +243,259 @@ class BaseAPITestCase:
# --- Helper to easily create a failed ValidationResult --- # --- Helper to easily create a failed ValidationResult ---
@staticmethod @staticmethod
def failed(message: str, details: Optional[Dict[str, Any]] = None) -> ValidationResult: def failed(message: str, details: Optional[Dict[str, Any]] = None) -> ValidationResult:
return ValidationResult(passed=False, message=message, details=details) return ValidationResult(passed=False, message=message, details=details)
# --- New helper methods for schema and field finding ---
def _get_resolved_request_body_schema(self) -> Optional[Dict[str, Any]]:
"""
Helper to get the (potentially $ref-resolved by orchestrator) request body schema
from self.endpoint_spec.
The orchestrator is expected to have handled $ref resolution before test case instantiation.
"""
request_body_spec = self.endpoint_spec.get("requestBody")
if request_body_spec and isinstance(request_body_spec, dict):
content = request_body_spec.get("content", {})
# Iterate through common JSON content types or prioritize application/json
# Order matters: more specific first
for ct in ["application/json", "application/merge-patch+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.logger.debug(f"Found request body schema under content type: {ct}")
return media_type_obj["schema"]
# Fallback for OpenAPI 2.0 (Swagger) style 'in: body' parameter
# This might also be present in OpenAPI 3.0 for compatibility or by mistake
parameters = self.endpoint_spec.get("parameters", [])
if isinstance(parameters, list):
for param in parameters:
if isinstance(param, dict) and param.get("in") == "body":
param_schema = param.get("schema")
if isinstance(param_schema, dict):
self.logger.debug("Found request body schema under 'in: body' parameter (Swagger 2.0 style).")
# Schema for 'in: body' parameter is directly usable
return param_schema
self.logger.debug("No suitable request body schema found in endpoint_spec.")
return None
def _find_removable_field_path(self, schema_to_search: Optional[Dict[str, Any]], schema_name_for_log: str) -> Optional[List[Union[str, int]]]:
"""
Uses schema_utils to find a removable (required) field path within the given schema.
Args:
schema_to_search: The schema dictionary to search within.
schema_name_for_log: A string name for the schema (e.g., "request body", "response header") for logging.
Returns:
A list representing the path to a removable field, or None if not found.
"""
if not schema_to_search:
self.logger.info(f"Schema for '{schema_name_for_log}' is missing or empty. Cannot find removable field.")
return None
removable_path = schema_utils.util_find_removable_field_path_recursive(
current_schema=schema_to_search,
current_path=[],
full_api_spec_for_refs=self.global_api_spec
# schema_utils.resolve_json_schema_references will use discard_refs=True by default
)
if removable_path:
self.logger.info(f"Found a removable field path in '{schema_name_for_log}' schema: '{'.'.join(map(str, removable_path))}'")
else:
self.logger.info(f"No removable (required) field path found in '{schema_name_for_log}' schema.")
return removable_path
def _find_simple_type_field_in_schema(
self,
schema_to_search: Optional[Dict[str, Any]],
schema_name_for_log: str
) -> Optional[Tuple[List[Union[str, int]], str, Dict[str, Any]]]:
"""
(Helper for test cases) Finds the first simple type field (string, integer, number, boolean)
in the given schema using schema_utils.
Args:
schema_to_search: The schema dictionary to search within.
Expected to be already resolved (e.g., from _get_resolved_request_body_schema).
schema_name_for_log: A name for the schema (e.g., "request body", "response body") for logging.
Returns:
A tuple (field_path, field_type, field_schema) if found, otherwise None.
"""
if not schema_to_search:
self.logger.debug(f"_find_simple_type_field_in_schema: Schema for '{schema_name_for_log}' is None or empty, cannot search.")
return None
if not isinstance(schema_to_search, dict):
self.logger.warning(f"_find_simple_type_field_in_schema: Expected schema for '{schema_name_for_log}' to be a dict, got {type(schema_to_search)}.")
return None
self.logger.debug(f"_find_simple_type_field_in_schema: Searching for simple type field in '{schema_name_for_log}' schema...")
found_target = schema_utils.find_first_simple_type_field_recursive(
current_schema=schema_to_search,
logger_param=self.logger # Pass the test case's logger
)
if found_target:
field_path, field_type, field_prop_schema = found_target
self.logger.info(f"_find_simple_type_field_in_schema: Found simple type field in '{schema_name_for_log}': Path={'.'.join(map(str, field_path))}, Type={field_type}")
return field_path, field_type, field_prop_schema
else:
self.logger.debug(f"_find_simple_type_field_in_schema: No simple type field found in '{schema_name_for_log}' schema.")
return None
def _find_first_simple_type_parameter(
self,
param_location: str
) -> Optional[Tuple[List[Union[str, int]], str, Dict[str, Any], str]]:
"""
Finds the first parameter in the specified location (e.g., 'query', 'header')
that is a simple type or contains a simple type if it's an object schema.
Args:
param_location: The location of the parameter ('query', 'header').
Returns:
A tuple (full_path, param_type, param_schema, param_name) if found, otherwise None.
- full_path: Path to the simple type (e.g., ['paramName'] or ['paramName', 'nestedField']).
- param_type: The original type of the simple field.
- param_schema: The schema definition of the simple field.
- param_name: The name of the top-level parameter.
"""
parameters = self.endpoint_spec.get("parameters", [])
if not isinstance(parameters, list):
self.logger.warning(f"_find_first_simple_type_parameter: 'parameters' in endpoint_spec is not a list. Cannot find {param_location} parameter.")
return None
for param_spec in parameters:
if not isinstance(param_spec, dict) or param_spec.get("in") != param_location:
continue
param_name = param_spec.get("name")
if not param_name:
self.logger.warning(f"_find_first_simple_type_parameter: Found a {param_location} parameter without a name. Skipping: {param_spec}")
continue
self.logger.debug(f"_find_first_simple_type_parameter: Checking {param_location} parameter '{param_name}'.")
# Case 1: Parameter schema is directly defined at the top level of param_spec (OpenAPI 3.0)
param_actual_schema = param_spec.get("schema")
if isinstance(param_actual_schema, dict):
schema_type = param_actual_schema.get("type")
if schema_type in ["string", "integer", "number", "boolean"]:
self.logger.info(f"_find_first_simple_type_parameter: Found simple type {param_location} parameter '{param_name}' (type: {schema_type}) via its 'schema'.")
return [param_name], schema_type, param_actual_schema, param_name
elif schema_type == "object":
self.logger.debug(f"_find_first_simple_type_parameter: {param_location} parameter '{param_name}' has an object schema. Searching within...")
# Schema is already resolved by orchestrator, so no need to call resolve_ref here.
found_in_object = self._find_simple_type_field_in_schema(param_actual_schema, f"{param_location} parameter '{param_name}'")
if found_in_object:
nested_path, nested_type, nested_schema = found_in_object
full_path = [param_name] + nested_path
self.logger.info(f"_find_first_simple_type_parameter: Found simple type field within object {param_location} parameter '{param_name}'. Path: {'.'.join(map(str,full_path))}, Type: {nested_type}")
return full_path, nested_type, nested_schema, param_name
# Add other cases if necessary, e.g. array of simple types for query params (though less common for type mismatch target)
# Case 2: Type is defined directly in param_spec (OpenAPI 2.0 / Swagger or simple OpenAPI 3.0 params)
# This is checked after 'schema' as 'schema' is more explicit in OpenAPI 3+
direct_param_type = param_spec.get("type")
if direct_param_type in ["string", "integer", "number", "boolean"]:
# This param_spec itself is the schema for the simple type
self.logger.info(f"_find_first_simple_type_parameter: Found simple type {param_location} parameter '{param_name}' (type: {direct_param_type}) via direct 'type'.")
return [param_name], direct_param_type, param_spec, param_name
self.logger.info(f"_find_first_simple_type_parameter: No suitable simple type field found for {param_location} parameters.")
return None
def _find_required_parameter_name(self, param_in: str) -> Optional[str]:
"""
Finds the name of the first required parameter in the specified location ('query', 'header', 'path').
Args:
param_in: The location of the parameter (e.g., "query", "header", "path").
Returns:
The name of the first required parameter found, or None.
"""
parameters = self.endpoint_spec.get("parameters", [])
if not isinstance(parameters, list):
self.logger.warning(f"'parameters' in endpoint_spec is not a list, cannot find required {param_in} parameter.")
return None
for param_spec in parameters:
if (isinstance(param_spec, dict) and
param_spec.get("in") == param_in and
param_spec.get("required") is True):
param_name = param_spec.get("name")
if param_name:
self.logger.info(f"Found required '{param_in}' parameter: '{param_name}'.")
return param_name
self.logger.info(f"No required '{param_in}' parameter found in endpoint_spec.")
return None
def expect_error_response(
self,
response_context: APIResponseContext,
expected_status_codes: List[int],
expected_error_code_in_body: Optional[Union[str, int]] = None,
error_code_field_name: str = "code",
context_message_prefix: str = "Error response validation"
) -> List[ValidationResult]:
"""
Validates if the response matches expected error conditions.
Args:
response_context: The API response context.
expected_status_codes: A list of expected HTTP status codes (e.g., [400, 422]).
expected_error_code_in_body: Optional. A specific error code expected in the response body.
error_code_field_name: The name of the field in the JSON response body that contains the error code.
context_message_prefix: Prefix for logging and result messages.
Returns:
A list of ValidationResult objects.
"""
results = []
status_code = response_context.status_code
json_content = response_context.json_content
if status_code in expected_status_codes:
msg = f"{context_message_prefix}: Received expected status code {status_code}."
if expected_error_code_in_body is not None:
if isinstance(json_content, dict):
error_code_in_response = json_content.get(error_code_field_name)
if error_code_in_response == expected_error_code_in_body:
results.append(self.passed(f"{msg} Specific error code '{expected_error_code_in_body}' (field: '{error_code_field_name}') found in response body."))
elif error_code_in_response is not None:
results.append(ValidationResult(passed=True, # Still counts as a valid error status, but code mismatch is noted
message=f"{msg} Status code is as expected, but error code in body ('{error_code_field_name}': '{error_code_in_response}') does not match expected '{expected_error_code_in_body}'.",
details={"response_body": json_content}
))
else: # Error code field not found
results.append(ValidationResult(passed=True, # Status is good, but code presence is an issue
message=f"{msg} Status code is as expected, but did not find error code field '{error_code_field_name}' in response body.",
details={"response_body": json_content}
))
else: # JSON content is not a dict
# Add a check for text_content before slicing
raw_text_detail = response_context.text_content[:500] if response_context.text_content else "(No text content)"
results.append(ValidationResult(passed=True, # Status is good, but body isn't inspectable for code
message=f"{msg} Status code is as expected, but response body is not a JSON object, so cannot check for error code '{error_code_field_name}'.",
details={"raw_response_text": raw_text_detail}
))
else: # No specific error code in body to check, status code match is enough
results.append(self.passed(msg))
else:
details = {"received_status_code": status_code, "expected_status_codes": expected_status_codes}
if isinstance(json_content, dict):
details["response_body"] = json_content
else:
# Add a check for text_content before slicing
raw_text_detail = response_context.text_content[:500] if response_context.text_content else "(No text content)"
details["raw_response_text"] = raw_text_detail
results.append(self.failed(
message=f"{context_message_prefix}: Expected status code to be one of {expected_status_codes}, but received {status_code}.",
details=details
))
self.logger.warning(f"{self.id}: {context_message_prefix} failed. Expected status: {expected_status_codes}, Actual: {status_code}")
return results

View File

@ -26,6 +26,7 @@ from .test_framework_core import ValidationResult, TestSeverity, APIRequestConte
from .test_case_registry import TestCaseRegistry from .test_case_registry import TestCaseRegistry
# 尝试导入 utils.schema_utils # 尝试导入 utils.schema_utils
from .utils import schema_utils from .utils import schema_utils
from .utils.common_utils import format_url_with_path_params # 新增导入
# 尝试导入 LLMService如果失败则允许因为 LLM 功能是可选的 # 尝试导入 LLMService如果失败则允许因为 LLM 功能是可选的
try: try:
@ -65,12 +66,17 @@ class ExecutedTestCaseResult:
self.timestamp = datetime.datetime.now() self.timestamp = datetime.datetime.now()
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
message=""
if self.message:
message = self.message
else:
message= ";".join([vp.message for vp in self.validation_points])
return { return {
"test_case_id": self.test_case_id, "test_case_id": self.test_case_id,
"test_case_name": self.test_case_name, "test_case_name": self.test_case_name,
"test_case_severity": self.test_case_severity.value, # 使用枚举值 "test_case_severity": self.test_case_severity.value, # 使用枚举值
"status": self.status.value, "status": self.status.value,
"message": self.message, "message": message,
"duration_seconds": self.duration, "duration_seconds": self.duration,
"timestamp": self.timestamp.isoformat(), "timestamp": self.timestamp.isoformat(),
"validation_points": [vp.details if vp.details else {"passed": vp.passed, "message": vp.message} for vp in self.validation_points] "validation_points": [vp.details if vp.details else {"passed": vp.passed, "message": vp.message} for vp in self.validation_points]
@ -102,6 +108,7 @@ class TestResult: # 原来的 TestResult 被重构为 EndpointExecutionResult
self.start_time = start_time if start_time else datetime.datetime.now() self.start_time = start_time if start_time else datetime.datetime.now()
self.end_time: Optional[datetime.datetime] = None self.end_time: Optional[datetime.datetime] = None
self.error_message: Optional[str] = None # 如果整个端点测试出错,记录错误信息 self.error_message: Optional[str] = None # 如果整个端点测试出错,记录错误信息
self.message: Optional[str] = None
def add_executed_test_case_result(self, result: ExecutedTestCaseResult): def add_executed_test_case_result(self, result: ExecutedTestCaseResult):
self.executed_test_cases.append(result) self.executed_test_cases.append(result)
@ -264,6 +271,7 @@ class TestSummary:
"error_in_execution": self.test_cases_error, "error_in_execution": self.test_cases_error,
"skipped_during_endpoint_execution": self.test_cases_skipped_in_endpoint, "skipped_during_endpoint_execution": self.test_cases_skipped_in_endpoint,
"success_rate_percentage": f"{self.test_case_success_rate:.2f}", "success_rate_percentage": f"{self.test_case_success_rate:.2f}",
}, },
"detailed_results": [result.to_dict() for result in self.detailed_results] "detailed_results": [result.to_dict() for result in self.detailed_results]
} }
@ -445,20 +453,33 @@ class APITestOrchestrator:
recursion_depth: int = 0 recursion_depth: int = 0
) -> Optional[Type[BaseModel]]: ) -> Optional[Type[BaseModel]]:
""" """
动态地从JSON Schema字典创建一个Pydantic模型类 Dynamically creates a Pydantic model from a JSON schema.
支持嵌套对象和数组 Handles nested schemas, arrays, and various OpenAPI/JSON Schema constructs.
Uses a cache (_dynamic_model_cache) to avoid redefining identical models.
Args:
schema: JSON Schema字典
model_name: 要创建的Pydantic模型的名称
recursion_depth: 当前递归深度用于防止无限循环
Returns:
一个Pydantic BaseModel的子类如果创建失败则返回None
""" """
MAX_RECURSION_DEPTH = 10 # This cache key generation might need refinement for very complex/deep schemas
if recursion_depth > MAX_RECURSION_DEPTH: # For now, using a combination of model_name and sorted schema keys/values
self.logger.error(f"创建Pydantic模型 '{model_name}' 时达到最大递归深度 {MAX_RECURSION_DEPTH}。可能存在循环引用。") # Important: dicts are unhashable, so we convert to a sorted tuple of items for the cache key.
# This is a simplified cache key; a more robust approach might involve serializing the schema.
# schema_tuple_for_key = tuple(sorted(schema.items())) if isinstance(schema, dict) else schema
# cache_key = (model_name, schema_tuple_for_key, recursion_depth) # Might be too verbose/complex
# Simpler cache key based on model_name only if we assume model_name is sufficiently unique
# for a given schema structure within a run. If schemas can change for the same model_name,
# this needs to be more sophisticated.
# If model_name is unique per structure, this is fine.
# Let's assume model_name is carefully constructed to be unique for each distinct schema structure
# by the calling functions (e.g., _generate_data_from_schema, _build_object_schema_for_params).
# Simplified approach: if a model with this exact name was already created, reuse it.
# This relies on the caller to ensure `model_name` is unique per schema structure.
if model_name in _dynamic_model_cache:
self.logger.debug(f"Reusing cached Pydantic model: {model_name}")
return _dynamic_model_cache[model_name]
if recursion_depth > self.MAX_RECURSION_DEPTH_PYDANTIC:
self.logger.error(f"创建Pydantic模型 '{model_name}' 时达到最大递归深度 {self.MAX_RECURSION_DEPTH_PYDANTIC}。可能存在循环引用。")
return None return None
# 清理模型名称使其成为有效的Python标识符 # 清理模型名称使其成为有效的Python标识符
@ -875,13 +896,15 @@ class APITestOrchestrator:
final_url_template = endpoint_spec_dict.get('path', '') final_url_template = endpoint_spec_dict.get('path', '')
# 添加日志:打印将要用于替换的路径参数
self.logger.debug(f"Path parameters to be substituted: {current_path_params}")
final_url = self.base_url + final_url_template final_url = self.base_url + final_url_template
for p_name, p_val in current_path_params.items(): for p_name, p_val in current_path_params.items():
placeholder = f"{{{p_name}}}" placeholder = f"{{{p_name}}}"
if placeholder in final_url_template: # 替换基础路径模板中的占位符 if placeholder in final_url_template: # 检查原始模板中是否存在占位符
final_url = final_url.replace(placeholder, str(p_val)) final_url = final_url.replace(placeholder, str(p_val))
# 注意: 如果 _prepare_initial_request_data 填充的 final_url 已经包含了 base_url这里的拼接逻辑需要调整 # 添加日志打印替换后的URL (在测试用例修改之前)
# 假设 final_url_template 只是 path string e.g. /users/{id} self.logger.debug(f"URL after path parameter substitution (before TC modify_request_url hook): {final_url}")
# ---- 调用测试用例的 URL 修改钩子 ---- # ---- 调用测试用例的 URL 修改钩子 ----
effective_url = final_url # 默认使用原始构建的URL effective_url = final_url # 默认使用原始构建的URL
@ -1015,9 +1038,9 @@ class APITestOrchestrator:
根据API端点规范准备初始的请求数据包括URL模板路径参数查询参数头部和请求体 根据API端点规范准备初始的请求数据包括URL模板路径参数查询参数头部和请求体
这些数据将作为测试用例中 generate_* 方法的输入 这些数据将作为测试用例中 generate_* 方法的输入
""" """
method = endpoint_spec.get('method', 'GET').upper() method = endpoint_spec.get("method", "GET").upper()
path_template = endpoint_spec.get('path', '/') # 这是路径模板, e.g., /users/{id} path_template = endpoint_spec.get("path", "/")
operation_id = endpoint_spec.get('operationId') or f"{method}_{path_template.replace('/', '_').replace('{', '_').replace('}','')}" operation_id = endpoint_spec.get("operationId", path_template) # 使用 path 作为 operationId 的 fallback
initial_path_params: Dict[str, Any] = {} initial_path_params: Dict[str, Any] = {}
initial_query_params: Dict[str, Any] = {} initial_query_params: Dict[str, Any] = {}
@ -1317,31 +1340,70 @@ class APITestOrchestrator:
endpoint_test_result.finalize_endpoint_test() endpoint_test_result.finalize_endpoint_test()
return endpoint_test_result return endpoint_test_result
applicable_test_case_classes = self.test_case_registry.get_applicable_test_cases( applicable_test_case_classes_unordered = self.test_case_registry.get_applicable_test_cases(
endpoint_method=endpoint.method.upper(), endpoint_method=endpoint.method.upper(),
endpoint_path=endpoint.path endpoint_path=endpoint.path
) )
if not applicable_test_case_classes: if not applicable_test_case_classes_unordered:
self.logger.info(f"端点 '{endpoint_id}' 没有找到适用的自定义测试用例。") self.logger.info(f"端点 '{endpoint_id}' 没有找到适用的自定义测试用例。")
endpoint_test_result.finalize_endpoint_test() endpoint_test_result.finalize_endpoint_test() # 确保在返回前调用
return endpoint_test_result return endpoint_test_result
self.logger.info(f"端点 '{endpoint_id}' 发现了 {len(applicable_test_case_classes)} 个适用的测试用例: {[tc.id for tc in applicable_test_case_classes]}") # 根据 execution_order 排序测试用例
applicable_test_case_classes = sorted(
applicable_test_case_classes_unordered,
key=lambda tc_class: tc_class.execution_order
)
self.logger.info(f"端点 '{endpoint_id}' 发现了 {len(applicable_test_case_classes)} 个适用的测试用例 (已排序): {[tc.id for tc in applicable_test_case_classes]}")
critical_setup_test_failed = False
critical_setup_failure_reason = ""
for tc_class in applicable_test_case_classes: for tc_class in applicable_test_case_classes:
self.logger.debug(f"准备执行测试用例 '{tc_class.id}' for '{endpoint_id}'") start_single_tc_time = time.monotonic() # 用于计算跳过测试用例的持续时间
executed_case_result = self._execute_single_test_case(
test_case_class=tc_class, if critical_setup_test_failed:
endpoint_spec=endpoint, self.logger.warning(f"由于关键的前置测试用例失败,跳过测试用例 '{tc_class.id}' for '{endpoint_id}'. 原因: {critical_setup_failure_reason}")
global_api_spec=global_api_spec skipped_tc_duration = time.monotonic() - start_single_tc_time
) executed_case_result = ExecutedTestCaseResult(
endpoint_test_result.add_executed_test_case_result(executed_case_result) test_case_id=tc_class.id,
if executed_case_result.status.value == TestResult.Status.FAILED.value: test_case_name=tc_class.name,
# 红色 test_case_severity=tc_class.severity,
self.logger.debug(f"\033[91m ❌ 测试用例 '{tc_class.id}' 执行失败。\033[0m") status=ExecutedTestCaseResult.Status.SKIPPED,
validation_points=[],
message=f"由于关键的前置测试失败而被跳过: {critical_setup_failure_reason}",
duration=skipped_tc_duration
)
else: else:
self.logger.debug(f"准备执行测试用例 '{tc_class.id}' for '{endpoint_id}'")
executed_case_result = self._execute_single_test_case(
test_case_class=tc_class,
endpoint_spec=endpoint,
global_api_spec=global_api_spec
)
# 检查是否是关键测试用例以及是否失败
if hasattr(tc_class, 'is_critical_setup_test') and tc_class.is_critical_setup_test:
if executed_case_result.status in [ExecutedTestCaseResult.Status.FAILED, ExecutedTestCaseResult.Status.ERROR]:
critical_setup_test_failed = True
critical_setup_failure_reason = f"关键测试 '{tc_class.id}' 失败 (状态: {executed_case_result.status.value})。消息: {executed_case_result.message}"
self.logger.error(f"关键的前置测试用例 '{tc_class.id}' for '{endpoint_id}' 失败。后续测试将被跳过。原因: {critical_setup_failure_reason}")
endpoint_test_result.add_executed_test_case_result(executed_case_result)
# 日志部分可以保持不变或根据需要调整
if executed_case_result.status.value == ExecutedTestCaseResult.Status.FAILED.value:
self.logger.debug(f"\033[91m ❌ 测试用例 '{tc_class.id}' 执行失败。\033[0m")
elif executed_case_result.status.value == ExecutedTestCaseResult.Status.PASSED.value :
self.logger.debug(f"\033[92m ✅ 测试用例 '{tc_class.id}' 执行成功。\033[0m") self.logger.debug(f"\033[92m ✅ 测试用例 '{tc_class.id}' 执行成功。\033[0m")
# 对于SKIPPED和ERROR状态可以添加不同颜色的日志
elif executed_case_result.status.value == ExecutedTestCaseResult.Status.SKIPPED.value:
self.logger.debug(f"\033[93m ⏭️ 测试用例 '{tc_class.id}' 被跳过。\033[0m") # 黄色
elif executed_case_result.status.value == ExecutedTestCaseResult.Status.ERROR.value:
self.logger.debug(f"\033[91m 💥 测试用例 '{tc_class.id}' 执行时发生错误。\033[0m") # 红色 (与FAILED相同或不同)
self.logger.debug(f"测试用例 '{tc_class.id}' 执行完毕,状态: {executed_case_result.status.value}") self.logger.debug(f"测试用例 '{tc_class.id}' 执行完毕,状态: {executed_case_result.status.value}")
endpoint_test_result.finalize_endpoint_test() endpoint_test_result.finalize_endpoint_test()

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
import logging
import re
from typing import Dict, Any
logger = logging.getLogger(__name__)
def format_url_with_path_params(path_template: str, path_params: Dict[str, Any]) -> str:
"""
使用提供的路径参数格式化URL路径模板
例如, path_template='/users/{userId}/items/{itemId}'
path_params={'userId': 123, 'itemId': 'abc'}
-> '/users/123/items/abc'
Args:
path_template: 包含占位符的URL路径例如 /resource/{id}
path_params: 包含占位符名称及其值的字典
Returns:
格式化后的URL路径
"""
url = path_template
try:
# 优先使用 .format(**path_params) 如果所有占位符都能匹配
# 这要求 path_params 中的键与模板中的占位符完全对应
# url = path_template.format(**path_params) # 更简洁,但如果参数不完全匹配会报错
# 使用正则表达式逐个替换更安全,可以处理部分参数或额外参数的情况
for param_name, param_value in path_params.items():
placeholder = f"{{{param_name}}}"
if placeholder in url:
url = url.replace(placeholder, str(param_value))
else:
# Log if a path param was provided but not found in template. Could be optional.
logger.debug(f"Path parameter '{param_name}' provided but placeholder '{placeholder}' not found in template '{path_template}'.")
# 检查是否还有未替换的占位符 (可选,但推荐)
remaining_placeholders = re.findall(r"({[^{}]+?})", url)
if remaining_placeholders:
logger.warning(f"URL '{url}' 中仍有未替换的路径参数占位符: {remaining_placeholders}。原始模板: '{path_template}', 提供参数: {path_params}")
except KeyError as e:
logger.error(f"格式化URL路径 '{path_template}' 失败:路径参数 '{e}' 未在提供的 path_params 中找到。可用参数: {list(path_params.keys())}")
# 根据需要,这里可以选择是返回原始模板还是抛出异常
# return path_template
raise ValueError(f"Missing path parameter {e} for URL template {path_template}") from e
except Exception as e:
logger.error(f"格式化URL路径 '{path_template}' 时发生未知错误: {e}")
raise # 或者返回原始模板
return url

View File

@ -271,4 +271,342 @@ def util_remove_value_at_path(
return data_container, None, False return data_container, None, False
logger.error(f"[Util] util_remove_value_at_path 未能在循环内按预期返回。路径: {'.'.join(map(str,path))}") logger.error(f"[Util] util_remove_value_at_path 未能在循环内按预期返回。路径: {'.'.join(map(str,path))}")
return data_container, None, False return data_container, None, False
def util_set_value_at_path(
data_container: Any,
path: List[Union[str, int]],
new_value: Any,
# logger_param: Optional[logging.Logger] = None
) -> Tuple[Any, bool]:
"""
(框架辅助方法) 在嵌套的字典/列表中为指定路径设置新值
如果路径中的某些部分不存在会尝试创建它们 (字典会创建列表会尝试填充到指定索引但需谨慎)
返回 (修改后的容器, 是否成功)
"""
# effective_logger = logger_param or logger
if not path:
logger.error("[Util] util_set_value_at_path: 路径不能为空。")
# 如果路径为空,是否应该用 new_value 替换整个 data_container
# 当前行为:返回原始容器和失败状态,因为路径通常指向容器内部。
# 如果要支持替换整个容器,需要明确此行为。
if data_container is None and new_value is not None: # 特殊情况如果原始容器是None且路径为空则新值成为容器
logger.info("[Util] util_set_value_at_path: 路径为空原始容器为None新值将作为新容器返回。")
return new_value, True
elif data_container is not None and new_value is None and not path: # 路径为空新值为None则清空容器
logger.info("[Util] util_set_value_at_path: 路径为空新值为None容器将被清空 (返回None)。")
return None, True
# 对于路径为空且两者都不是None的情况目前返回失败因为通常期望有路径。
# 或者可以考虑直接返回 new_value意味着整个对象被替换。
# logger.info(f"[Util] util_set_value_at_path: Path is empty. Replacing entire container.")
# return new_value, True # 备选行为:替换整个对象
return data_container, False
# 深拷贝以避免修改原始输入除非原始输入是None
container_copy = copy.deepcopy(data_container) if data_container is not None else None
current_level = container_copy
try:
for i, key_or_index in enumerate(path):
is_last_element = (i == len(path) - 1)
if is_last_element:
if isinstance(key_or_index, str):
if not isinstance(current_level, dict):
# 如果当前层级不是字典 (例如是None或是被意外替换为其他类型),无法设置键值对
logger.error(f"[Util] util_set_value_at_path: 路径的最后一部分 '{key_or_index}' (string key) 期望父级是字典,但找到 {type(current_level)}。路径: {'.'.join(map(str,path))}")
# 尝试强制转换为字典?这可能不是预期行为。
# 如果 current_level 是 None 且它是根 (container_copy is None),则初始化 container_copy
if current_level is None and i == 0: # 路径只有一级且容器本身是None
container_copy = {}
current_level = container_copy
else: # 更深层级的None或类型错误
return data_container, False
current_level[key_or_index] = new_value
logger.info(f"[Util] 在路径 {'.'.join(map(str,path))} (键 '{key_or_index}') 处设置值为 '{new_value}'")
return container_copy, True
elif isinstance(key_or_index, int): # key_or_index is an integer (list index)
if not isinstance(current_level, list):
logger.error(f"[Util] util_set_value_at_path: 路径的最后一部分索引 '{key_or_index}' 期望父级是列表,但找到 {type(current_level)}。路径: {'.'.join(map(str,path))}")
if current_level is None and i == 0: # 路径只有一级且容器本身是None
container_copy = [None] * (key_or_index + 1) # 创建足够长度的列表
current_level = container_copy
else:
return data_container, False
elif isinstance(key_or_index, int):
# 确保列表足够长以容纳索引
while len(current_level) <= key_or_index:
current_level.append(None) # 用 None 填充直到达到所需长度
current_level[key_or_index] = new_value
logger.info(f"[Util] 在路径 {'.'.join(map(str,path))} (索引 '{key_or_index}') 处设置值为 '{new_value}'")
return container_copy, True
else:
logger.error(f"[Util] util_set_value_at_path: 路径的最后一部分 '{key_or_index}' 类型未知。路径: {'.'.join(map(str,path))}")
return data_container, False
else: # Not the last element, traverse deeper
next_key_or_index_is_int = isinstance(path[i+1], int)
if isinstance(key_or_index, str): # Current path part is a dictionary key
if not isinstance(current_level, dict):
# 如果在根级别且容器是None则初始化为字典
if current_level is None and i == 0:
container_copy = {}
current_level = container_copy
else:
logger.error(f"[Util] util_set_value_at_path: 路径期望字典,但在 '{key_or_index}' 处找到 {type(current_level)}。路径: {'.'.join(map(str,path[:i+1]))}")
return data_container, False
if key_or_index not in current_level or current_level[key_or_index] is None or \
(next_key_or_index_is_int and not isinstance(current_level[key_or_index], list)) or \
(not next_key_or_index_is_int and not isinstance(current_level[key_or_index], dict)):
# 如果键不存在或值为None或类型与下一路径部分不匹配则创建/重置
logger.debug(f"[Util] util_set_value_at_path: 在路径 '{key_or_index}' 处创建/重置结构。下一个是索引: {next_key_or_index_is_int}")
current_level[key_or_index] = [] if next_key_or_index_is_int else {}
current_level = current_level[key_or_index]
elif isinstance(key_or_index, int): # Current path part is a list index
if not isinstance(current_level, list):
if current_level is None and i == 0:
container_copy = []
current_level = container_copy
else:
logger.error(f"[Util] util_set_value_at_path: 路径期望列表以应用索引 '{key_or_index}',但找到 {type(current_level)}。路径: {'.'.join(map(str,path[:i+1]))}")
return data_container, False
elif isinstance(key_or_index, int):
# 确保列表足够长以容纳索引,并确保该索引处的元素是正确的类型 (list/dict)
while len(current_level) <= key_or_index:
current_level.append(None) # 用 None 填充
if current_level[key_or_index] is None or \
(next_key_or_index_is_int and not isinstance(current_level[key_or_index], list)) or \
(not next_key_or_index_is_int and not isinstance(current_level[key_or_index], dict)):
logger.debug(f"[Util] util_set_value_at_path: 在列表索引 '{key_or_index}' 处创建/重置结构。下一个是索引: {next_key_or_index_is_int}")
current_level[key_or_index] = [] if next_key_or_index_is_int else {}
current_level = current_level[key_or_index]
else:
logger.error(f"[Util] util_set_value_at_path: 路径部分 '{key_or_index}' 类型未知 ({type(key_or_index)})。路径: {'.'.join(map(str,path[:i+1]))}")
return data_container, False
except Exception as e:
logger.error(f"[Util] 在准备设置字段路径 {'.'.join(map(str,path))} 的值时发生错误: {e}", exc_info=True)
return data_container, False
# Should not be reached if logic is correct, path must have at least one element by initial check.
logger.error(f"[Util] util_set_value_at_path 未能在循环内按预期返回。路径: {'.'.join(map(str,path))}")
return data_container, False
def generate_mismatched_value(
original_type: Optional[str],
original_value: Any,
field_schema: Optional[Dict[str, Any]],
logger_param: Optional[logging.Logger] = None
) -> Any:
"""
(框架辅助方法) 根据原始数据类型原始值和字段 schema 生成一个类型不匹配的值
主要用于类型不匹配的测试用例
Args:
original_type: 字段的原始 OpenAPI 类型 (e.g., "string", "integer").
original_value: 字段的原始值 (当前未直接用于生成逻辑但可供未来扩展).
field_schema: 字段的 schema 定义用于检查如 "enum" 之类的约束
logger_param: 可选的 logger 实例
Returns:
一个与 original_type 不匹配的值
"""
effective_logger = logger_param or logger
# 优先考虑 schema 中的 enum选择一个不在 enum 中且类型不匹配的值
if field_schema and "enum" in field_schema and isinstance(field_schema["enum"], list):
enum_values = field_schema["enum"]
if original_type == "string":
if 123 not in enum_values: return 123
if False not in enum_values: return False
# 如果数字和布尔都在枚举中,尝试一个与已知枚举值不同的字符串
# (虽然这仍然是字符串类型,但目的是为了触发非枚举值的验证)
# 或者,如果目的是严格类型不匹配,这里应该返回非字符串。
# 当前逻辑倾向于返回一个肯定非字符串的值。
elif original_type == "integer":
if "not-an-integer" not in enum_values: return "not-an-integer"
if 3.14 not in enum_values: return 3.14
elif original_type == "number": # Includes float/double
if "not-a-number" not in enum_values: return "not-a-number"
elif original_type == "boolean":
if "not-a-boolean" not in enum_values: return "not-a-boolean"
if 1 not in enum_values: return 1
# 如果枚举覆盖了所有简单备选,则回退到下面的通用逻辑
# 通用类型不匹配逻辑 (当 enum 不存在或 enum 检查未返回时)
if original_type == "string":
return 12345 # Number instead of string
elif original_type == "integer":
return "not-an-integer" # String instead of integer
elif original_type == "number": # Includes float/double
return "not-a-number" # String instead of number
elif original_type == "boolean":
return "not-a-boolean" # String instead of boolean
elif original_type == "array":
return {"value": "not-an-array"} # Object instead of array
elif original_type == "object":
return ["not", "an", "object"] # Array instead of object
effective_logger.warning(f"generate_mismatched_value: 原始类型 '{original_type}' 未知或无法生成不匹配值。将返回固定字符串 'mismatch_test_default'")
return "mismatch_test_default" # Fallback for unknown types
def build_object_schema_for_params(params_spec_list: List[Dict[str, Any]], model_name_base: str, logger_param: Optional[logging.Logger] = None) -> Tuple[Optional[Dict[str, Any]], str]:
"""
从参数规范列表构建一个对象的JSON schema主要用于请求体查询参数或头部的聚合
Args:
params_spec_list: 参数规范的列表 (例如OpenAPI参数对象列表)
model_name_base: 用于生成动态模型名称的基础字符串
logger_param: 可选的 logger 实例
Returns:
一个元组包含 (构建的JSON object schema None, 模型名称字符串)
"""
effective_logger = logger_param or logger # Use passed logger or module logger
if not params_spec_list:
effective_logger.debug(f"参数列表为空,无需为 '{model_name_base}' 构建对象 schema。")
return None, f"{model_name_base}EmptyParams"
properties = {}
required_fields = []
for param_spec in params_spec_list:
param_name = param_spec.get("name")
if not param_name:
effective_logger.warning(f"参数规范缺少 'name' 字段,已跳过: {param_spec}")
continue
# 从参数规范中提取 schema (OpenAPI 3.x)
param_schema = param_spec.get("schema")
if not param_schema:
# 尝试兼容 OpenAPI 2.0 (Swagger) 的情况,其中类型信息直接在参数级别
# 例如: type, format, items, default, enum 等
# https://swagger.io/specification/v2/#parameterObject
# 注意: 这种兼容性可能不完整,因为很多属性需要映射
compatible_schema = {}
if "type" in param_spec:
compatible_schema["type"] = param_spec["type"]
if "format" in param_spec:
compatible_schema["format"] = param_spec["format"]
if "items" in param_spec: # for array types
compatible_schema["items"] = param_spec["items"]
if "default" in param_spec:
compatible_schema["default"] = param_spec["default"]
if "enum" in param_spec:
compatible_schema["enum"] = param_spec["enum"]
# 其他如 description, example 等也可以考虑加入
if compatible_schema: # 如果至少收集到了一些类型信息
param_schema = compatible_schema
effective_logger.debug(f"参数 '{param_name}' 没有 'schema' 字段,但从顶级字段构建了兼容 schema: {param_schema}")
else:
effective_logger.warning(f"参数 '{param_name}' 缺少 'schema' 字段且无法构建兼容schema已跳过。规范: {param_spec}")
continue
properties[param_name] = param_schema
if param_spec.get("required", False):
required_fields.append(param_name)
if not properties:
effective_logger.debug(f"未能从参数列表为 '{model_name_base}' 提取任何属性。")
return None, f"{model_name_base}NoProps"
final_schema: Dict[str, Any] = {
"type": "object",
"properties": properties
}
if required_fields:
final_schema["required"] = required_fields
# 生成一个稍微独特的名字,以防多个操作有相同的 param_type
# 例如 OperationIdQueryRequest, OperationIdHeaderRequest
model_name = f"{model_name_base.replace(' ', '')}Params"
effective_logger.debug(f"'{model_name_base}' 构建的对象 schema: {final_schema}, 模型名: {model_name}")
return final_schema, model_name
def find_first_simple_type_field_recursive(
current_schema: Dict[str, Any],
current_path: Optional[List[Union[str, int]]] = None,
# full_api_spec_for_refs: Optional[Dict[str, Any]] = None, # Schema is expected to be pre-resolved
logger_param: Optional[logging.Logger] = None
) -> Optional[Tuple[List[Union[str, int]], str, Dict[str, Any]]]:
"""
递归地在给定的 schema 中查找第一个简单类型的字段 (string, integer, number, boolean)
这包括查找嵌套在对象或数组中的简单类型字段
Args:
current_schema: 当前正在搜索的 schema 部分 (应为字典)
current_path: 到达当前 schema 的路径列表 (用于构建完整路径)
logger_param: 可选的 logger 实例
Returns:
一个元组 (field_path, field_type, field_schema) 如果找到否则为 None
field_path 是一个列表表示从根 schema 到找到的字段的路径
field_type 是字段的原始类型字符串 (e.g., "string")
field_schema 是该字段自身的 schema 定义
"""
effective_logger = logger_param or logger # Use module logger if specific one not provided
path_so_far = current_path if current_path is not None else []
if not isinstance(current_schema, dict):
effective_logger.debug(f"Schema at path {'.'.join(map(str, path_so_far))} is not a dict, cannot search further.")
return None
schema_type = current_schema.get("type")
# effective_logger.debug(f"Searching in path: {'.'.join(map(str, path_so_far))}, Schema Type: '{schema_type}'")
if schema_type == "object":
properties = current_schema.get("properties", {})
for name, prop_schema in properties.items():
if not isinstance(prop_schema, dict):
effective_logger.debug(f"Property '{name}' at path {'.'.join(map(str, path_so_far + [name]))} has non-dict schema. Skipping.")
continue
prop_type = prop_schema.get("type")
if prop_type in ["string", "integer", "number", "boolean"]:
field_path = path_so_far + [name]
effective_logger.info(f"Found simple type field: Path={'.'.join(map(str, field_path))}, Type={prop_type}")
return field_path, prop_type, prop_schema
elif prop_type == "object":
found_in_nested_object = find_first_simple_type_field_recursive(
prop_schema,
path_so_far + [name],
logger_param=effective_logger
)
if found_in_nested_object:
return found_in_nested_object
elif prop_type == "array":
items_schema = prop_schema.get("items")
if isinstance(items_schema, dict):
# Look for simple type or object within array items
item_type = items_schema.get("type")
if item_type in ["string", "integer", "number", "boolean"]:
field_path = path_so_far + [name, 0] # Target first item of the array
effective_logger.info(f"Found simple type field in array item: Path={'.'.join(map(str, field_path))}, Type={item_type}")
return field_path, item_type, items_schema
elif item_type == "object":
# Path to the first item of the array, then recurse into that item's object schema
found_in_array_item_object = find_first_simple_type_field_recursive(
items_schema,
path_so_far + [name, 0],
logger_param=effective_logger
)
if found_in_array_item_object:
return found_in_array_item_object
elif schema_type == "array": # If the current_schema itself is an array (e.g., root schema is an array)
items_schema = current_schema.get("items")
if isinstance(items_schema, dict):
item_type = items_schema.get("type")
if item_type in ["string", "integer", "number", "boolean"]:
field_path = path_so_far + [0] # Target first item of this root/current array
effective_logger.info(f"Found simple type field in root/current array item: Path={'.'.join(map(str, field_path))}, Type={item_type}")
return field_path, item_type, items_schema
elif item_type == "object":
# Path to the first item of this root/current array, then recurse
found_in_root_array_item_object = find_first_simple_type_field_recursive(
items_schema,
path_so_far + [0],
logger_param=effective_logger
)
if found_in_root_array_item_object:
return found_in_root_array_item_object
# effective_logger.debug(f"No simple type field found at path {'.'.join(map(str, path_so_far))}")
return None

4734
log.txt

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
{ {
"summary_metadata": { "summary_metadata": {
"start_time": "2025-05-26T17:10:18.937545", "start_time": "2025-05-27T03:17:32.169725",
"end_time": "2025-05-26T17:10:19.037161", "end_time": "2025-05-27T03:17:33.726074",
"duration_seconds": "0.10" "duration_seconds": "1.56"
}, },
"endpoint_stats": { "endpoint_stats": {
"total_defined": 6, "total_defined": 6,
@ -17,35 +17,33 @@
"test_case_stats": { "test_case_stats": {
"total_applicable": 42, "total_applicable": 42,
"total_executed": 42, "total_executed": 42,
"passed": 23, "passed": 24,
"failed": 19, "failed": 18,
"error_in_execution": 0, "error_in_execution": 0,
"skipped_during_endpoint_execution": 0, "skipped_during_endpoint_execution": 0,
"success_rate_percentage": "54.76" "success_rate_percentage": "57.14"
}, },
"detailed_results": [ "detailed_results": [
{ {
"endpoint_id": "POST /api/dms/{dms_instance_code}/v1/message/push/{schema}/{version}", "endpoint_id": "POST /api/dms/{dms_instance_code}/v1/message/push/{schema}/{version}",
"endpoint_name": "数据推送接口", "endpoint_name": "数据推送接口",
"overall_status": "失败", "overall_status": "失败",
"duration_seconds": 0.043781, "duration_seconds": 0.38021,
"start_time": "2025-05-26T17:10:18.937890", "start_time": "2025-05-27T03:17:32.170519",
"end_time": "2025-05-26T17:10:18.981671", "end_time": "2025-05-27T03:17:32.550729",
"executed_test_cases": [ "executed_test_cases": [
{ {
"test_case_id": "TC-STATUS-001", "test_case_id": "TC-STATUS-001",
"test_case_name": "基本状态码 200 检查", "test_case_name": "基本状态码 200 检查",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "失败", "status": "通过",
"message": "", "message": "响应状态码为 200符合预期 200。",
"duration_seconds": 0.035302374977618456, "duration_seconds": 0.10054145799949765,
"timestamp": "2025-05-26T17:10:18.973269", "timestamp": "2025-05-27T03:17:32.271233",
"validation_points": [ "validation_points": [
{ {
"expected_status": 200, "passed": true,
"actual_status": 500, "message": "响应状态码为 200符合预期 200。"
"request_url": "https://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/message/push/example_schema/example_version",
"response_body_sample": ""
} }
] ]
}, },
@ -54,9 +52,9 @@
"test_case_name": "Response Body JSON Schema Validation", "test_case_name": "Response Body JSON Schema Validation",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "通过", "status": "通过",
"message": "", "message": "Schema验证步骤完成未发现问题或schema不适用/未为此响应定义)。",
"duration_seconds": 0.001680959016084671, "duration_seconds": 0.04229112481698394,
"timestamp": "2025-05-26T17:10:18.974994", "timestamp": "2025-05-27T03:17:32.313678",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
@ -68,14 +66,13 @@
"test_case_id": "TC-SECURITY-001", "test_case_id": "TC-SECURITY-001",
"test_case_name": "HTTPS Protocol Mandatory Verification", "test_case_name": "HTTPS Protocol Mandatory Verification",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "通过", "status": "失败",
"message": "", "message": "API通过HTTP (http://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/message/push/example_schema/example_version) 响应了成功的状态码 200这违反了HTTPS强制策略。",
"duration_seconds": 0.0012912091333419085, "duration_seconds": 0.046983249951153994,
"timestamp": "2025-05-26T17:10:18.976330", "timestamp": "2025-05-27T03:17:32.360817",
"validation_points": [ "validation_points": [
{ {
"passed": true, "status_code": 200
"message": "测试已跳过因为发送的URL已经是 HTTPS可能是由于原始基础URL非HTTPS或测试设置问题。"
} }
] ]
}, },
@ -84,9 +81,9 @@
"test_case_name": "Error Code 4001 - Query Parameter Type Mismatch Validation", "test_case_name": "Error Code 4001 - Query Parameter Type Mismatch Validation",
"test_case_severity": "中", "test_case_severity": "中",
"status": "通过", "status": "通过",
"message": "", "message": "跳过测试:在查询参数中未找到合适的字段来测试类型不匹配。",
"duration_seconds": 0.0013675407972186804, "duration_seconds": 0.04334258288145065,
"timestamp": "2025-05-26T17:10:18.977732", "timestamp": "2025-05-27T03:17:32.404292",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
@ -99,13 +96,34 @@
"test_case_name": "Error Code 4001 - Request Body Type Mismatch Validation", "test_case_name": "Error Code 4001 - Request Body Type Mismatch Validation",
"test_case_severity": "中", "test_case_severity": "中",
"status": "失败", "status": "失败",
"message": "", "message": "当请求体字段 'isSearchCount' 类型不匹配时期望API返回状态码在 [400, 422] 中或返回4xx客户端错误且业务码为 '4001'. 实际收到状态码 200. 响应体中的业务码 ('code') 为 '89'.",
"duration_seconds": 0.001474499935284257, "duration_seconds": 0.04505762504413724,
"timestamp": "2025-05-26T17:10:18.979238", "timestamp": "2025-05-27T03:17:32.449477",
"validation_points": [ "validation_points": [
{ {
"status_code": 500, "status_code": 200,
"response_body": null "response_body": {
"code": 89,
"message": "non mollit",
"data": {
"total": 56,
"list": [
{
"dsid": "97",
"dataRegion": "nulla laborum ut cupidatat incididunt",
"gasReleaseMon": null,
"gasReleaseYear": null,
"releaseGasCum": null
}
]
}
},
"expected_http_status_codes": [
400,
422
],
"expected_business_code": "4001",
"mismatched_field": "body.isSearchCount"
} }
] ]
}, },
@ -114,9 +132,9 @@
"test_case_name": "Error Code 4003 - Missing Required Request Body Field Validation", "test_case_name": "Error Code 4003 - Missing Required Request Body Field Validation",
"test_case_severity": "高", "test_case_severity": "高",
"status": "通过", "status": "通过",
"message": "", "message": "跳过测试在API规范中未找到合适的必填请求体字段用于移除测试。",
"duration_seconds": 0.0012187499087303877, "duration_seconds": 0.053338083904236555,
"timestamp": "2025-05-26T17:10:18.980486", "timestamp": "2025-05-27T03:17:32.503262",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
@ -129,9 +147,9 @@
"test_case_name": "Error Code 4003 - Missing Required Query Parameter Validation", "test_case_name": "Error Code 4003 - Missing Required Query Parameter Validation",
"test_case_severity": "高", "test_case_severity": "高",
"status": "通过", "status": "通过",
"message": "", "message": "跳过测试在API规范中未找到合适的必填查询参数用于移除测试。",
"duration_seconds": 0.0011259580496698618, "duration_seconds": 0.047208999982103705,
"timestamp": "2025-05-26T17:10:18.981646", "timestamp": "2025-05-27T03:17:32.550622",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
@ -145,24 +163,22 @@
"endpoint_id": "POST /api/dms/{dms_instance_code}/v1/cd_geo_unit/{version}", "endpoint_id": "POST /api/dms/{dms_instance_code}/v1/cd_geo_unit/{version}",
"endpoint_name": "地质单元列表查询", "endpoint_name": "地质单元列表查询",
"overall_status": "失败", "overall_status": "失败",
"duration_seconds": 0.015458, "duration_seconds": 0.289961,
"start_time": "2025-05-26T17:10:18.981700", "start_time": "2025-05-27T03:17:32.550812",
"end_time": "2025-05-26T17:10:18.997158", "end_time": "2025-05-27T03:17:32.840773",
"executed_test_cases": [ "executed_test_cases": [
{ {
"test_case_id": "TC-STATUS-001", "test_case_id": "TC-STATUS-001",
"test_case_name": "基本状态码 200 检查", "test_case_name": "基本状态码 200 检查",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "失败", "status": "通过",
"message": "", "message": "响应状态码为 200符合预期 200。",
"duration_seconds": 0.0016555420588701963, "duration_seconds": 0.03967254119925201,
"timestamp": "2025-05-26T17:10:18.983441", "timestamp": "2025-05-27T03:17:32.590701",
"validation_points": [ "validation_points": [
{ {
"expected_status": 200, "passed": true,
"actual_status": 500, "message": "响应状态码为 200符合预期 200。"
"request_url": "https://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit/1.0.0",
"response_body_sample": ""
} }
] ]
}, },
@ -171,9 +187,9 @@
"test_case_name": "Response Body JSON Schema Validation", "test_case_name": "Response Body JSON Schema Validation",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "通过", "status": "通过",
"message": "", "message": "Schema验证步骤完成未发现问题或schema不适用/未为此响应定义)。",
"duration_seconds": 0.0011254160199314356, "duration_seconds": 0.05223862477578223,
"timestamp": "2025-05-26T17:10:18.984597", "timestamp": "2025-05-27T03:17:32.643082",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
@ -185,14 +201,13 @@
"test_case_id": "TC-SECURITY-001", "test_case_id": "TC-SECURITY-001",
"test_case_name": "HTTPS Protocol Mandatory Verification", "test_case_name": "HTTPS Protocol Mandatory Verification",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "通过", "status": "失败",
"message": "", "message": "API通过HTTP (http://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit/1.0.0) 响应了成功的状态码 200这违反了HTTPS强制策略。",
"duration_seconds": 0.0011057921219617128, "duration_seconds": 0.04689537500962615,
"timestamp": "2025-05-26T17:10:18.985732", "timestamp": "2025-05-27T03:17:32.690147",
"validation_points": [ "validation_points": [
{ {
"passed": true, "status_code": 200
"message": "测试已跳过因为发送的URL已经是 HTTPS可能是由于原始基础URL非HTTPS或测试设置问题。"
} }
] ]
}, },
@ -201,13 +216,48 @@
"test_case_name": "Error Code 4001 - Query Parameter Type Mismatch Validation", "test_case_name": "Error Code 4001 - Query Parameter Type Mismatch Validation",
"test_case_severity": "中", "test_case_severity": "中",
"status": "失败", "status": "失败",
"message": "", "message": "当查询参数 'pageNo' (路径: 'pageNo') 类型不匹配时期望API返回状态码在 [400, 422] 中或返回4xx客户端错误且业务码为 '4001'. 实际收到状态码 200. 响应体中的业务码 ('code') 为 '34'.",
"duration_seconds": 0.001326208934187889, "duration_seconds": 0.044708832865580916,
"timestamp": "2025-05-26T17:10:18.987090", "timestamp": "2025-05-27T03:17:32.735038",
"validation_points": [ "validation_points": [
{ {
"status_code": 500, "status_code": 200,
"response_body": null "response_body": {
"code": 34,
"message": "est",
"data": {
"total": 76,
"list": [
{
"dsid": "56",
"dataRegion": "et dolore veniam ex voluptate",
"gasReleaseMon": null,
"gasReleaseYear": null,
"releaseGasCum": null
},
{
"dsid": "8",
"dataRegion": "in",
"gasReleaseMon": null,
"gasReleaseYear": null,
"releaseGasCum": null
},
{
"dsid": "16",
"dataRegion": "consequat irure proident",
"gasReleaseMon": null,
"gasReleaseYear": null,
"releaseGasCum": null
}
]
}
},
"expected_http_status_codes": [
400,
422
],
"expected_business_code": "4001",
"mismatched_param": "pageNo"
} }
] ]
}, },
@ -216,13 +266,41 @@
"test_case_name": "Error Code 4001 - Request Body Type Mismatch Validation", "test_case_name": "Error Code 4001 - Request Body Type Mismatch Validation",
"test_case_severity": "中", "test_case_severity": "中",
"status": "失败", "status": "失败",
"message": "", "message": "当请求体字段 'isSearchCount' 类型不匹配时期望API返回状态码在 [400, 422] 中或返回4xx客户端错误且业务码为 '4001'. 实际收到状态码 200. 响应体中的业务码 ('code') 为 '23'.",
"duration_seconds": 0.0013839590828865767, "duration_seconds": 0.039677416905760765,
"timestamp": "2025-05-26T17:10:18.988501", "timestamp": "2025-05-27T03:17:32.774854",
"validation_points": [ "validation_points": [
{ {
"status_code": 500, "status_code": 200,
"response_body": null "response_body": {
"code": 23,
"message": "occaecat aute ut velit Excepteur",
"data": {
"total": 88,
"list": [
{
"dsid": "93",
"dataRegion": "officia",
"gasReleaseMon": null,
"gasReleaseYear": null,
"releaseGasCum": null
},
{
"dsid": "21",
"dataRegion": "ullamco commodo proident dolore id",
"gasReleaseMon": null,
"gasReleaseYear": null,
"releaseGasCum": null
}
]
}
},
"expected_http_status_codes": [
400,
422
],
"expected_business_code": "4001",
"mismatched_field": "body.isSearchCount"
} }
] ]
}, },
@ -231,9 +309,9 @@
"test_case_name": "Error Code 4003 - Missing Required Request Body Field Validation", "test_case_name": "Error Code 4003 - Missing Required Request Body Field Validation",
"test_case_severity": "高", "test_case_severity": "高",
"status": "通过", "status": "通过",
"message": "", "message": "跳过测试在API规范中未找到合适的必填请求体字段用于移除测试。",
"duration_seconds": 0.0022551249712705612, "duration_seconds": 0.033955666003748775,
"timestamp": "2025-05-26T17:10:18.990784", "timestamp": "2025-05-27T03:17:32.808929",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
@ -246,9 +324,9 @@
"test_case_name": "Error Code 4003 - Missing Required Query Parameter Validation", "test_case_name": "Error Code 4003 - Missing Required Query Parameter Validation",
"test_case_severity": "高", "test_case_severity": "高",
"status": "通过", "status": "通过",
"message": "", "message": "跳过测试在API规范中未找到合适的必填查询参数用于移除测试。",
"duration_seconds": 0.0063235408160835505, "duration_seconds": 0.0316302499268204,
"timestamp": "2025-05-26T17:10:18.997136", "timestamp": "2025-05-27T03:17:32.840677",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
@ -262,24 +340,22 @@
"endpoint_id": "PUT /api/dms/{dms_instance_code}/v1/cd_geo_unit", "endpoint_id": "PUT /api/dms/{dms_instance_code}/v1/cd_geo_unit",
"endpoint_name": "地质单元数据修改", "endpoint_name": "地质单元数据修改",
"overall_status": "失败", "overall_status": "失败",
"duration_seconds": 0.007749, "duration_seconds": 0.194852,
"start_time": "2025-05-26T17:10:18.997186", "start_time": "2025-05-27T03:17:32.840839",
"end_time": "2025-05-26T17:10:19.004935", "end_time": "2025-05-27T03:17:33.035691",
"executed_test_cases": [ "executed_test_cases": [
{ {
"test_case_id": "TC-STATUS-001", "test_case_id": "TC-STATUS-001",
"test_case_name": "基本状态码 200 检查", "test_case_name": "基本状态码 200 检查",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "失败", "status": "通过",
"message": "", "message": "响应状态码为 200符合预期 200。",
"duration_seconds": 0.0011879999656230211, "duration_seconds": 0.0246629579924047,
"timestamp": "2025-05-26T17:10:18.998449", "timestamp": "2025-05-27T03:17:32.865694",
"validation_points": [ "validation_points": [
{ {
"expected_status": 200, "passed": true,
"actual_status": 500, "message": "响应状态码为 200符合预期 200。"
"request_url": "https://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit",
"response_body_sample": ""
} }
] ]
}, },
@ -288,13 +364,13 @@
"test_case_name": "Response Body JSON Schema Validation", "test_case_name": "Response Body JSON Schema Validation",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "通过", "status": "通过",
"message": "", "message": "针对 PUT http://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit (状态码 200) 的响应体 conforms to the JSON schema.",
"duration_seconds": 0.0008167079649865627, "duration_seconds": 0.028576500015333295,
"timestamp": "2025-05-26T17:10:18.999302", "timestamp": "2025-05-27T03:17:32.894368",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
"message": "Schema验证步骤完成未发现问题或schema不适用/未为此响应定义)。" "message": "针对 PUT http://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit (状态码 200) 的响应体 conforms to the JSON schema."
} }
] ]
}, },
@ -302,14 +378,13 @@
"test_case_id": "TC-SECURITY-001", "test_case_id": "TC-SECURITY-001",
"test_case_name": "HTTPS Protocol Mandatory Verification", "test_case_name": "HTTPS Protocol Mandatory Verification",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "通过", "status": "失败",
"message": "", "message": "API通过HTTP (http://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit) 响应了成功的状态码 200这违反了HTTPS强制策略。",
"duration_seconds": 0.0009453750681132078, "duration_seconds": 0.023044000146910548,
"timestamp": "2025-05-26T17:10:19.000282", "timestamp": "2025-05-27T03:17:32.917541",
"validation_points": [ "validation_points": [
{ {
"passed": true, "status_code": 200
"message": "测试已跳过因为发送的URL已经是 HTTPS可能是由于原始基础URL非HTTPS或测试设置问题。"
} }
] ]
}, },
@ -318,13 +393,23 @@
"test_case_name": "Error Code 4001 - Query Parameter Type Mismatch Validation", "test_case_name": "Error Code 4001 - Query Parameter Type Mismatch Validation",
"test_case_severity": "中", "test_case_severity": "中",
"status": "失败", "status": "失败",
"message": "", "message": "当查询参数 'id' (路径: 'id') 类型不匹配时期望API返回状态码在 [400, 422] 中或返回4xx客户端错误且业务码为 '4001'. 实际收到状态码 200. 响应体中的业务码 ('code') 为 '36'.",
"duration_seconds": 0.001086625037714839, "duration_seconds": 0.027355958009138703,
"timestamp": "2025-05-26T17:10:19.001401", "timestamp": "2025-05-27T03:17:32.944990",
"validation_points": [ "validation_points": [
{ {
"status_code": 500, "status_code": 200,
"response_body": null "response_body": {
"code": 36,
"message": "velit proident nisi",
"data": false
},
"expected_http_status_codes": [
400,
422
],
"expected_business_code": "4001",
"mismatched_param": "id"
} }
] ]
}, },
@ -333,13 +418,23 @@
"test_case_name": "Error Code 4001 - Request Body Type Mismatch Validation", "test_case_name": "Error Code 4001 - Request Body Type Mismatch Validation",
"test_case_severity": "中", "test_case_severity": "中",
"status": "失败", "status": "失败",
"message": "", "message": "当请求体字段 'id' 类型不匹配时期望API返回状态码在 [400, 422] 中或返回4xx客户端错误且业务码为 '4001'. 实际收到状态码 200. 响应体中的业务码 ('code') 为 '38'.",
"duration_seconds": 0.0013515420723706484, "duration_seconds": 0.02183720818720758,
"timestamp": "2025-05-26T17:10:19.002791", "timestamp": "2025-05-27T03:17:32.966910",
"validation_points": [ "validation_points": [
{ {
"status_code": 500, "status_code": 200,
"response_body": null "response_body": {
"code": 38,
"message": "Excepteur",
"data": false
},
"expected_http_status_codes": [
400,
422
],
"expected_business_code": "4001",
"mismatched_field": "body.id"
} }
] ]
}, },
@ -347,15 +442,14 @@
"test_case_id": "TC-ERROR-4003-BODY", "test_case_id": "TC-ERROR-4003-BODY",
"test_case_name": "Error Code 4003 - Missing Required Request Body Field Validation", "test_case_name": "Error Code 4003 - Missing Required Request Body Field Validation",
"test_case_severity": "高", "test_case_severity": "高",
"status": "失败", "status": "通过",
"message": "", "message": "当移除必填请求体字段 'id' 时API响应了状态码 200 (非主要预期HTTP状态 [400, 422]但为4xx客户端错误), 且响应体中包含预期的业务错误码 '4003' (字段: 'code').",
"duration_seconds": 0.0010457078460603952, "duration_seconds": 0.02861408400349319,
"timestamp": "2025-05-26T17:10:19.003869", "timestamp": "2025-05-27T03:17:32.995596",
"validation_points": [ "validation_points": [
{ {
"status_code": 500, "passed": true,
"response_body": null, "message": "当移除必填请求体字段 'id' 时API响应了状态码 200 (非主要预期HTTP状态 [400, 422]但为4xx客户端错误), 且响应体中包含预期的业务错误码 '4003' (字段: 'code')."
"removed_field": "body.data.0.bsflag"
} }
] ]
}, },
@ -364,14 +458,23 @@
"test_case_name": "Error Code 4003 - Missing Required Query Parameter Validation", "test_case_name": "Error Code 4003 - Missing Required Query Parameter Validation",
"test_case_severity": "高", "test_case_severity": "高",
"status": "失败", "status": "失败",
"message": "", "message": "当移除必填查询参数 'id' 时期望API返回状态码在 [400, 422] 中或返回4xx客户端错误且业务码为 '4003'. 实际收到状态码 200. 响应体中的业务码 ('code') 为 '37'.",
"duration_seconds": 0.0010109590366482735, "duration_seconds": 0.039827125146985054,
"timestamp": "2025-05-26T17:10:19.004911", "timestamp": "2025-05-27T03:17:33.035557",
"validation_points": [ "validation_points": [
{ {
"status_code": 500, "status_code": 200,
"response_body": null, "response_body": {
"removed_field": "query.id" "code": 37,
"message": "est sint non magna",
"data": false
},
"expected_http_status_codes": [
400,
422
],
"expected_business_code": "4003",
"removed_param": "query.id"
} }
] ]
} }
@ -381,24 +484,22 @@
"endpoint_id": "DELETE /api/dms/{dms_instance_code}/v1/cd_geo_unit", "endpoint_id": "DELETE /api/dms/{dms_instance_code}/v1/cd_geo_unit",
"endpoint_name": "地质单元数据删除", "endpoint_name": "地质单元数据删除",
"overall_status": "失败", "overall_status": "失败",
"duration_seconds": 0.008712, "duration_seconds": 0.214498,
"start_time": "2025-05-26T17:10:19.004961", "start_time": "2025-05-27T03:17:33.035783",
"end_time": "2025-05-26T17:10:19.013673", "end_time": "2025-05-27T03:17:33.250281",
"executed_test_cases": [ "executed_test_cases": [
{ {
"test_case_id": "TC-STATUS-001", "test_case_id": "TC-STATUS-001",
"test_case_name": "基本状态码 200 检查", "test_case_name": "基本状态码 200 检查",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "失败", "status": "通过",
"message": "", "message": "响应状态码为 200符合预期 200。",
"duration_seconds": 0.0009155420120805502, "duration_seconds": 0.024680749978870153,
"timestamp": "2025-05-26T17:10:19.005958", "timestamp": "2025-05-27T03:17:33.060754",
"validation_points": [ "validation_points": [
{ {
"expected_status": 200, "passed": true,
"actual_status": 500, "message": "响应状态码为 200符合预期 200。"
"request_url": "https://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit",
"response_body_sample": ""
} }
] ]
}, },
@ -407,13 +508,13 @@
"test_case_name": "Response Body JSON Schema Validation", "test_case_name": "Response Body JSON Schema Validation",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "通过", "status": "通过",
"message": "", "message": "针对 DELETE http://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit (状态码 200) 的响应体 conforms to the JSON schema.",
"duration_seconds": 0.0012859171256422997, "duration_seconds": 0.02513625007122755,
"timestamp": "2025-05-26T17:10:19.007307", "timestamp": "2025-05-27T03:17:33.086215",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
"message": "Schema验证步骤完成未发现问题或schema不适用/未为此响应定义)。" "message": "针对 DELETE http://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit (状态码 200) 的响应体 conforms to the JSON schema."
} }
] ]
}, },
@ -421,14 +522,13 @@
"test_case_id": "TC-SECURITY-001", "test_case_id": "TC-SECURITY-001",
"test_case_name": "HTTPS Protocol Mandatory Verification", "test_case_name": "HTTPS Protocol Mandatory Verification",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "通过", "status": "失败",
"message": "", "message": "API通过HTTP (http://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit) 响应了成功的状态码 200这违反了HTTPS强制策略。",
"duration_seconds": 0.0010566250421106815, "duration_seconds": 0.023132374975830317,
"timestamp": "2025-05-26T17:10:19.008399", "timestamp": "2025-05-27T03:17:33.109590",
"validation_points": [ "validation_points": [
{ {
"passed": true, "status_code": 200
"message": "测试已跳过因为发送的URL已经是 HTTPS可能是由于原始基础URL非HTTPS或测试设置问题。"
} }
] ]
}, },
@ -437,13 +537,23 @@
"test_case_name": "Error Code 4001 - Query Parameter Type Mismatch Validation", "test_case_name": "Error Code 4001 - Query Parameter Type Mismatch Validation",
"test_case_severity": "中", "test_case_severity": "中",
"status": "失败", "status": "失败",
"message": "", "message": "当查询参数 'id' (路径: 'id') 类型不匹配时期望API返回状态码在 [400, 422] 中或返回4xx客户端错误且业务码为 '4001'. 实际收到状态码 200. 响应体中的业务码 ('code') 为 '57'.",
"duration_seconds": 0.0011866658460348845, "duration_seconds": 0.02844312507659197,
"timestamp": "2025-05-26T17:10:19.009618", "timestamp": "2025-05-27T03:17:33.138175",
"validation_points": [ "validation_points": [
{ {
"status_code": 500, "status_code": 200,
"response_body": null "response_body": {
"code": 57,
"message": "culpa reprehenderit amet pariatur",
"data": false
},
"expected_http_status_codes": [
400,
422
],
"expected_business_code": "4001",
"mismatched_param": "id"
} }
] ]
}, },
@ -452,13 +562,23 @@
"test_case_name": "Error Code 4001 - Request Body Type Mismatch Validation", "test_case_name": "Error Code 4001 - Request Body Type Mismatch Validation",
"test_case_severity": "中", "test_case_severity": "中",
"status": "失败", "status": "失败",
"message": "", "message": "当请求体字段 'version' 类型不匹配时期望API返回状态码在 [400, 422] 中或返回4xx客户端错误且业务码为 '4001'. 实际收到状态码 200. 响应体中的业务码 ('code') 为 '2'.",
"duration_seconds": 0.0016809171065688133, "duration_seconds": 0.05025433306582272,
"timestamp": "2025-05-26T17:10:19.011357", "timestamp": "2025-05-27T03:17:33.188548",
"validation_points": [ "validation_points": [
{ {
"status_code": 500, "status_code": 200,
"response_body": null "response_body": {
"code": 2,
"message": "tempor ullamco consequat",
"data": false
},
"expected_http_status_codes": [
400,
422
],
"expected_business_code": "4001",
"mismatched_field": "body.version"
} }
] ]
}, },
@ -467,9 +587,9 @@
"test_case_name": "Error Code 4003 - Missing Required Request Body Field Validation", "test_case_name": "Error Code 4003 - Missing Required Request Body Field Validation",
"test_case_severity": "高", "test_case_severity": "高",
"status": "通过", "status": "通过",
"message": "", "message": "跳过测试在API规范中未找到合适的必填请求体字段用于移除测试。",
"duration_seconds": 0.0014303750358521938, "duration_seconds": 0.03652524994686246,
"timestamp": "2025-05-26T17:10:19.012824", "timestamp": "2025-05-27T03:17:33.225187",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
@ -482,14 +602,23 @@
"test_case_name": "Error Code 4003 - Missing Required Query Parameter Validation", "test_case_name": "Error Code 4003 - Missing Required Query Parameter Validation",
"test_case_severity": "高", "test_case_severity": "高",
"status": "失败", "status": "失败",
"message": "", "message": "当移除必填查询参数 'id' 时期望API返回状态码在 [400, 422] 中或返回4xx客户端错误且业务码为 '4003'. 实际收到状态码 200. 响应体中的业务码 ('code') 为 '49'.",
"duration_seconds": 0.000795166939496994, "duration_seconds": 0.024812292074784636,
"timestamp": "2025-05-26T17:10:19.013652", "timestamp": "2025-05-27T03:17:33.250181",
"validation_points": [ "validation_points": [
{ {
"status_code": 500, "status_code": 200,
"response_body": null, "response_body": {
"removed_field": "query.id" "code": 49,
"message": "culpa cillum",
"data": true
},
"expected_http_status_codes": [
400,
422
],
"expected_business_code": "4003",
"removed_param": "query.id"
} }
] ]
} }
@ -499,24 +628,22 @@
"endpoint_id": "POST /api/dms/{dms_instance_code}/v1/cd_geo_unit", "endpoint_id": "POST /api/dms/{dms_instance_code}/v1/cd_geo_unit",
"endpoint_name": "地质单元数据添加", "endpoint_name": "地质单元数据添加",
"overall_status": "失败", "overall_status": "失败",
"duration_seconds": 0.007254, "duration_seconds": 0.200211,
"start_time": "2025-05-26T17:10:19.013697", "start_time": "2025-05-27T03:17:33.250350",
"end_time": "2025-05-26T17:10:19.020951", "end_time": "2025-05-27T03:17:33.450561",
"executed_test_cases": [ "executed_test_cases": [
{ {
"test_case_id": "TC-STATUS-001", "test_case_id": "TC-STATUS-001",
"test_case_name": "基本状态码 200 检查", "test_case_name": "基本状态码 200 检查",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "失败", "status": "通过",
"message": "", "message": "响应状态码为 200符合预期 200。",
"duration_seconds": 0.0007979590445756912, "duration_seconds": 0.028625000035390258,
"timestamp": "2025-05-26T17:10:19.014569", "timestamp": "2025-05-27T03:17:33.279196",
"validation_points": [ "validation_points": [
{ {
"expected_status": 200, "passed": true,
"actual_status": 500, "message": "响应状态码为 200符合预期 200。"
"request_url": "https://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit",
"response_body_sample": ""
} }
] ]
}, },
@ -525,9 +652,9 @@
"test_case_name": "Response Body JSON Schema Validation", "test_case_name": "Response Body JSON Schema Validation",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "通过", "status": "通过",
"message": "", "message": "Schema验证步骤完成未发现问题或schema不适用/未为此响应定义)。",
"duration_seconds": 0.0007929170969873667, "duration_seconds": 0.02822887501679361,
"timestamp": "2025-05-26T17:10:19.015392", "timestamp": "2025-05-27T03:17:33.307693",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
@ -539,14 +666,13 @@
"test_case_id": "TC-SECURITY-001", "test_case_id": "TC-SECURITY-001",
"test_case_name": "HTTPS Protocol Mandatory Verification", "test_case_name": "HTTPS Protocol Mandatory Verification",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "通过", "status": "失败",
"message": "", "message": "API通过HTTP (http://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit) 响应了成功的状态码 200这违反了HTTPS强制策略。",
"duration_seconds": 0.001027249963954091, "duration_seconds": 0.02737983292900026,
"timestamp": "2025-05-26T17:10:19.016455", "timestamp": "2025-05-27T03:17:33.335201",
"validation_points": [ "validation_points": [
{ {
"passed": true, "status_code": 200
"message": "测试已跳过因为发送的URL已经是 HTTPS可能是由于原始基础URL非HTTPS或测试设置问题。"
} }
] ]
}, },
@ -555,9 +681,9 @@
"test_case_name": "Error Code 4001 - Query Parameter Type Mismatch Validation", "test_case_name": "Error Code 4001 - Query Parameter Type Mismatch Validation",
"test_case_severity": "中", "test_case_severity": "中",
"status": "通过", "status": "通过",
"message": "", "message": "跳过测试:在查询参数中未找到合适的字段来测试类型不匹配。",
"duration_seconds": 0.001004124991595745, "duration_seconds": 0.03305029193870723,
"timestamp": "2025-05-26T17:10:19.017489", "timestamp": "2025-05-27T03:17:33.368404",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
@ -570,13 +696,23 @@
"test_case_name": "Error Code 4001 - Request Body Type Mismatch Validation", "test_case_name": "Error Code 4001 - Request Body Type Mismatch Validation",
"test_case_severity": "中", "test_case_severity": "中",
"status": "失败", "status": "失败",
"message": "", "message": "当请求体字段 'version' 类型不匹配时期望API返回状态码在 [400, 422] 中或返回4xx客户端错误且业务码为 '4001'. 实际收到状态码 200. 响应体中的业务码 ('code') 为 '58'.",
"duration_seconds": 0.0013770000077784061, "duration_seconds": 0.03271925006993115,
"timestamp": "2025-05-26T17:10:19.018896", "timestamp": "2025-05-27T03:17:33.401426",
"validation_points": [ "validation_points": [
{ {
"status_code": 500, "status_code": 200,
"response_body": null "response_body": {
"code": 58,
"message": "enim",
"data": false
},
"expected_http_status_codes": [
400,
422
],
"expected_business_code": "4001",
"mismatched_field": "body.version"
} }
] ]
}, },
@ -585,13 +721,22 @@
"test_case_name": "Error Code 4003 - Missing Required Request Body Field Validation", "test_case_name": "Error Code 4003 - Missing Required Request Body Field Validation",
"test_case_severity": "高", "test_case_severity": "高",
"status": "失败", "status": "失败",
"message": "", "message": "当移除必填请求体字段 'data.0.bsflag' 时期望API返回状态码在 [400, 422] 中或返回4xx客户端错误且业务码为 '4003'. 实际收到状态码 200. 响应体中的业务码 ('code') 为 '97'.",
"duration_seconds": 0.0009045829065144062, "duration_seconds": 0.025021000066772103,
"timestamp": "2025-05-26T17:10:19.019847", "timestamp": "2025-05-27T03:17:33.426714",
"validation_points": [ "validation_points": [
{ {
"status_code": 500, "status_code": 200,
"response_body": null, "response_body": {
"code": 97,
"message": "sit enim est anim",
"data": false
},
"expected_http_status_codes": [
400,
422
],
"expected_business_code": "4003",
"removed_field": "body.data.0.bsflag" "removed_field": "body.data.0.bsflag"
} }
] ]
@ -601,9 +746,9 @@
"test_case_name": "Error Code 4003 - Missing Required Query Parameter Validation", "test_case_name": "Error Code 4003 - Missing Required Query Parameter Validation",
"test_case_severity": "高", "test_case_severity": "高",
"status": "通过", "status": "通过",
"message": "", "message": "跳过测试在API规范中未找到合适的必填查询参数用于移除测试。",
"duration_seconds": 0.0010490419808775187, "duration_seconds": 0.023357625119388103,
"timestamp": "2025-05-26T17:10:19.020929", "timestamp": "2025-05-27T03:17:33.450349",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
@ -617,24 +762,22 @@
"endpoint_id": "GET /api/dms/{dms_instance_code}/v1/cd_geo_unit/{version}/{id}", "endpoint_id": "GET /api/dms/{dms_instance_code}/v1/cd_geo_unit/{version}/{id}",
"endpoint_name": "地质单元查询详情", "endpoint_name": "地质单元查询详情",
"overall_status": "失败", "overall_status": "失败",
"duration_seconds": 0.016169, "duration_seconds": 0.275309,
"start_time": "2025-05-26T17:10:19.020976", "start_time": "2025-05-27T03:17:33.450733",
"end_time": "2025-05-26T17:10:19.037145", "end_time": "2025-05-27T03:17:33.726042",
"executed_test_cases": [ "executed_test_cases": [
{ {
"test_case_id": "TC-STATUS-001", "test_case_id": "TC-STATUS-001",
"test_case_name": "基本状态码 200 检查", "test_case_name": "基本状态码 200 检查",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "失败", "status": "通过",
"message": "", "message": "响应状态码为 200符合预期 200。",
"duration_seconds": 0.0014472499024122953, "duration_seconds": 0.026520791929215193,
"timestamp": "2025-05-26T17:10:19.022501", "timestamp": "2025-05-27T03:17:33.477855",
"validation_points": [ "validation_points": [
{ {
"expected_status": 200, "passed": true,
"actual_status": 500, "message": "响应状态码为 200符合预期 200。"
"request_url": "https://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit/1.0.0/example_id",
"response_body_sample": ""
} }
] ]
}, },
@ -643,13 +786,13 @@
"test_case_name": "Response Body JSON Schema Validation", "test_case_name": "Response Body JSON Schema Validation",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "通过", "status": "通过",
"message": "", "message": "针对 GET http://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit/1.0.0/example_id (状态码 200) 的响应体 conforms to the JSON schema.",
"duration_seconds": 0.0011265419889241457, "duration_seconds": 0.0279082499910146,
"timestamp": "2025-05-26T17:10:19.023676", "timestamp": "2025-05-27T03:17:33.506072",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
"message": "Schema验证步骤完成未发现问题或schema不适用/未为此响应定义)。" "message": "针对 GET http://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit/1.0.0/example_id (状态码 200) 的响应体 conforms to the JSON schema."
} }
] ]
}, },
@ -657,14 +800,13 @@
"test_case_id": "TC-SECURITY-001", "test_case_id": "TC-SECURITY-001",
"test_case_name": "HTTPS Protocol Mandatory Verification", "test_case_name": "HTTPS Protocol Mandatory Verification",
"test_case_severity": "严重", "test_case_severity": "严重",
"status": "通过", "status": "失败",
"message": "", "message": "API通过HTTP (http://127.0.0.1:4523/m1/6389742-6086420-default/api/dms/example_dms_instance_code/v1/cd_geo_unit/1.0.0/example_id) 响应了成功的状态码 200这违反了HTTPS强制策略。",
"duration_seconds": 0.0011152499355375767, "duration_seconds": 0.029651165939867496,
"timestamp": "2025-05-26T17:10:19.024819", "timestamp": "2025-05-27T03:17:33.536036",
"validation_points": [ "validation_points": [
{ {
"passed": true, "status_code": 200
"message": "测试已跳过因为发送的URL已经是 HTTPS可能是由于原始基础URL非HTTPS或测试设置问题。"
} }
] ]
}, },
@ -673,9 +815,9 @@
"test_case_name": "Error Code 4001 - Query Parameter Type Mismatch Validation", "test_case_name": "Error Code 4001 - Query Parameter Type Mismatch Validation",
"test_case_severity": "中", "test_case_severity": "中",
"status": "通过", "status": "通过",
"message": "", "message": "跳过测试:在查询参数中未找到合适的字段来测试类型不匹配。",
"duration_seconds": 0.008005708921700716, "duration_seconds": 0.035403457935899496,
"timestamp": "2025-05-26T17:10:19.032866", "timestamp": "2025-05-27T03:17:33.571723",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
@ -688,13 +830,41 @@
"test_case_name": "Error Code 4001 - Request Body Type Mismatch Validation", "test_case_name": "Error Code 4001 - Request Body Type Mismatch Validation",
"test_case_severity": "中", "test_case_severity": "中",
"status": "失败", "status": "失败",
"message": "", "message": "当请求体字段 'isSearchCount' 类型不匹配时期望API返回状态码在 [400, 422] 中或返回4xx客户端错误且业务码为 '4001'. 实际收到状态码 200. 响应体中的业务码 ('code') 为 '96'.",
"duration_seconds": 0.0014979999978095293, "duration_seconds": 0.0588684999383986,
"timestamp": "2025-05-26T17:10:19.034398", "timestamp": "2025-05-27T03:17:33.630695",
"validation_points": [ "validation_points": [
{ {
"status_code": 500, "status_code": 200,
"response_body": null "response_body": {
"code": 96,
"message": "qui eu sit",
"data": {
"total": 88,
"list": [
{
"dsid": "25",
"dataRegion": "veniam dolor",
"gasReleaseMon": null,
"gasReleaseYear": null,
"releaseGasCum": null
},
{
"dsid": "1",
"dataRegion": "aute do",
"gasReleaseMon": null,
"gasReleaseYear": null,
"releaseGasCum": null
}
]
}
},
"expected_http_status_codes": [
400,
422
],
"expected_business_code": "4001",
"mismatched_field": "body.isSearchCount"
} }
] ]
}, },
@ -703,9 +873,9 @@
"test_case_name": "Error Code 4003 - Missing Required Request Body Field Validation", "test_case_name": "Error Code 4003 - Missing Required Request Body Field Validation",
"test_case_severity": "高", "test_case_severity": "高",
"status": "通过", "status": "通过",
"message": "", "message": "跳过测试在API规范中未找到合适的必填请求体字段用于移除测试。",
"duration_seconds": 0.0013297090772539377, "duration_seconds": 0.06513866619206965,
"timestamp": "2025-05-26T17:10:19.035759", "timestamp": "2025-05-27T03:17:33.696569",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,
@ -718,9 +888,9 @@
"test_case_name": "Error Code 4003 - Missing Required Query Parameter Validation", "test_case_name": "Error Code 4003 - Missing Required Query Parameter Validation",
"test_case_severity": "高", "test_case_severity": "高",
"status": "通过", "status": "通过",
"message": "", "message": "跳过测试在API规范中未找到合适的必填查询参数用于移除测试。",
"duration_seconds": 0.0013257910031825304, "duration_seconds": 0.02917162491939962,
"timestamp": "2025-05-26T17:10:19.037121", "timestamp": "2025-05-27T03:17:33.725963",
"validation_points": [ "validation_points": [
{ {
"passed": true, "passed": true,