step1
This commit is contained in:
parent
6d0b70fe11
commit
331e397367
BIN
__pycache__/run_tests.cpython-312.pyc
Normal file
BIN
__pycache__/run_tests.cpython-312.pyc
Normal file
Binary file not shown.
BIN
ddms_compliance_suite/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
ddms_compliance_suite/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
ddms_compliance_suite/__pycache__/api_test_cli.cpython-312.pyc
Normal file
BIN
ddms_compliance_suite/__pycache__/api_test_cli.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
210
ddms_compliance_suite/test_case_registry.py
Normal file
210
ddms_compliance_suite/test_case_registry.py
Normal file
@ -0,0 +1,210 @@
|
||||
import os
|
||||
import importlib.util
|
||||
import inspect
|
||||
import logging
|
||||
import re
|
||||
from typing import List, Type, Optional, Dict
|
||||
|
||||
# 确保可以从 sibling 模块导入
|
||||
from .test_framework_core import BaseAPITestCase
|
||||
|
||||
class TestCaseRegistry:
|
||||
"""
|
||||
负责发现、加载和管理所有自定义的APITestCase类。
|
||||
"""
|
||||
def __init__(self, test_cases_dir: str):
|
||||
"""
|
||||
初始化 TestCaseRegistry。
|
||||
Args:
|
||||
test_cases_dir: 存放自定义测试用例 (.py 文件) 的目录路径。
|
||||
"""
|
||||
self.test_cases_dir = test_cases_dir
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self._registry: Dict[str, Type[BaseAPITestCase]] = {}
|
||||
self._test_case_classes: List[Type[BaseAPITestCase]] = []
|
||||
self.discover_test_cases()
|
||||
|
||||
def discover_test_cases(self):
|
||||
"""
|
||||
扫描指定目录,动态导入模块,并注册所有继承自 BaseAPITestCase 的类。
|
||||
"""
|
||||
if not os.path.isdir(self.test_cases_dir):
|
||||
self.logger.warning(f"测试用例目录不存在或不是一个目录: {self.test_cases_dir}")
|
||||
return
|
||||
|
||||
self.logger.info(f"开始从目录 '{self.test_cases_dir}' 发现测试用例...")
|
||||
found_count = 0
|
||||
for filename in os.listdir(self.test_cases_dir):
|
||||
if filename.endswith(".py") and not filename.startswith("__"):
|
||||
module_name = filename[:-3]
|
||||
file_path = os.path.join(self.test_cases_dir, filename)
|
||||
try:
|
||||
# 动态导入模块
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||
if spec and spec.loader:
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
self.logger.debug(f"成功导入模块: {module_name} 从 {file_path}")
|
||||
|
||||
# 在模块中查找 BaseAPITestCase 的子类
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if inspect.isclass(obj) and issubclass(obj, BaseAPITestCase) and obj is not BaseAPITestCase:
|
||||
if obj.id in self._registry:
|
||||
self.logger.warning(f"发现重复的测试用例 ID: '{obj.id}' (来自类 '{obj.__name__}' in {file_path})。之前的定义将被覆盖。")
|
||||
|
||||
self._registry[obj.id] = obj
|
||||
if obj not in self._test_case_classes: # 避免重复添加同一个类对象
|
||||
self._test_case_classes.append(obj)
|
||||
found_count += 1
|
||||
self.logger.info(f"已注册测试用例: '{obj.id}' ({obj.name}) 来自类 '{obj.__name__}'")
|
||||
else:
|
||||
self.logger.error(f"无法为文件 '{file_path}' 创建模块规范。")
|
||||
except ImportError as e:
|
||||
self.logger.error(f"导入模块 '{module_name}' 从 '{file_path}' 失败: {e}", exc_info=True)
|
||||
except AttributeError as e:
|
||||
self.logger.error(f"在模块 '{module_name}' ({file_path}) 中查找测试用例时出错 (可能是缺少必要的元数据如 'id'): {e}", exc_info=True)
|
||||
except Exception as e:
|
||||
self.logger.error(f"处理文件 '{file_path}' 时发生未知错误: {e}", exc_info=True)
|
||||
|
||||
self.logger.info(f"测试用例发现完成。总共注册了 {len(self._registry)} 个独特的测试用例 (基于ID)。发现 {found_count} 个测试用例类。")
|
||||
|
||||
def get_test_case_by_id(self, case_id: str) -> Optional[Type[BaseAPITestCase]]:
|
||||
"""根据ID获取已注册的测试用例类。"""
|
||||
return self._registry.get(case_id)
|
||||
|
||||
def get_all_test_case_classes(self) -> List[Type[BaseAPITestCase]]:
|
||||
"""获取所有已注册的测试用例类列表。"""
|
||||
return list(self._test_case_classes) # 返回副本
|
||||
|
||||
def get_applicable_test_cases(self, endpoint_method: str, endpoint_path: str) -> List[Type[BaseAPITestCase]]:
|
||||
"""
|
||||
根据API端点的方法和路径,筛选出适用的测试用例类。
|
||||
|
||||
Args:
|
||||
endpoint_method: API端点的方法 (例如 "GET", "POST")。
|
||||
endpoint_path: API端点的路径 (例如 "/users/{id}")。
|
||||
|
||||
Returns:
|
||||
一个包含适用测试用例类的列表。
|
||||
"""
|
||||
applicable_cases: List[Type[BaseAPITestCase]] = []
|
||||
for tc_class in self._test_case_classes:
|
||||
# 1. 检查 applicable_methods
|
||||
if tc_class.applicable_methods is not None:
|
||||
if endpoint_method.upper() not in [m.upper() for m in tc_class.applicable_methods]:
|
||||
self.logger.debug(f"测试用例 '{tc_class.id}' 不适用于方法 '{endpoint_method}' (期望: {tc_class.applicable_methods}),已跳过。")
|
||||
continue # 方法不匹配,跳过此测试用例
|
||||
|
||||
# 2. 检查 applicable_paths_regex
|
||||
if tc_class.applicable_paths_regex is not None:
|
||||
try:
|
||||
if not re.match(tc_class.applicable_paths_regex, endpoint_path):
|
||||
self.logger.debug(f"测试用例 '{tc_class.id}' 不适用于路径 '{endpoint_path}' (正则: '{tc_class.applicable_paths_regex}'),已跳过。")
|
||||
continue # 路径正则不匹配,跳过此测试用例
|
||||
except re.error as e:
|
||||
self.logger.error(f"测试用例 '{tc_class.id}' 中的路径正则表达式 '{tc_class.applicable_paths_regex}' 无效: {e}。此测试用例将不匹配任何路径。")
|
||||
continue
|
||||
|
||||
# 如果通过了所有检查,则认为适用
|
||||
applicable_cases.append(tc_class)
|
||||
self.logger.debug(f"测试用例 '{tc_class.id}' 适用于端点 '{endpoint_method} {endpoint_path}'。")
|
||||
|
||||
return applicable_cases
|
||||
|
||||
# 示例用法 (用于测试此模块,实际使用时由编排器调用)
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 假设项目结构如下:
|
||||
# your_project_root/
|
||||
# ddms_compliance_suite/
|
||||
# test_framework_core.py
|
||||
# test_case_registry.py
|
||||
# custom_testcases/ <-- 测试用例存放目录
|
||||
# example_checks.py
|
||||
# another_set_of_checks.py
|
||||
|
||||
# 创建一个临时的 custom_testcases 目录和一些示例测试用例文件用于测试
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
custom_testcases_path = os.path.join(os.path.dirname(current_dir), "custom_testcases") # 假设custom_testcases在ddms_compliance_suite的父目录
|
||||
|
||||
if not os.path.exists(custom_testcases_path):
|
||||
os.makedirs(custom_testcases_path)
|
||||
logger.info(f"创建临时目录: {custom_testcases_path}")
|
||||
|
||||
# 示例测试用例文件1: example_checks.py
|
||||
example_checks_content = """
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
|
||||
class MyFirstTest(BaseAPITestCase):
|
||||
id = "TC-EXAMPLE-001"
|
||||
name = "我的第一个测试"
|
||||
description = "一个简单的示例测试。"
|
||||
severity = TestSeverity.INFO
|
||||
tags = ["example"]
|
||||
applicable_methods = ["GET"]
|
||||
|
||||
class MySecondTest(BaseAPITestCase):
|
||||
id = "TC-EXAMPLE-002"
|
||||
name = "我的第二个测试"
|
||||
description = "另一个示例测试,适用于所有方法和特定路径。"
|
||||
severity = TestSeverity.MEDIUM
|
||||
tags = ["example", "path_specific"]
|
||||
applicable_paths_regex = r"/api/users/.+"
|
||||
"""
|
||||
with open(os.path.join(custom_testcases_path, "example_checks.py"), "w", encoding="utf-8") as f:
|
||||
f.write(example_checks_content)
|
||||
logger.info(f"创建示例测试文件: {os.path.join(custom_testcases_path, 'example_checks.py')}")
|
||||
|
||||
# 示例测试用例文件2: specific_feature_tests.py (无适用性限制)
|
||||
specific_tests_content = """
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
|
||||
class FeatureXCheck(BaseAPITestCase):
|
||||
id = "TC-FEATUREX-001"
|
||||
name = "特性X的检查"
|
||||
description = "验证特性X的相关功能。"
|
||||
severity = TestSeverity.HIGH
|
||||
tags = ["feature-x"]
|
||||
"""
|
||||
with open(os.path.join(custom_testcases_path, "specific_feature_tests.py"), "w", encoding="utf-8") as f:
|
||||
f.write(specific_tests_content)
|
||||
logger.info(f"创建示例测试文件: {os.path.join(custom_testcases_path, 'specific_feature_tests.py')}")
|
||||
|
||||
# 测试 TestCaseRegistry
|
||||
registry = TestCaseRegistry(test_cases_dir=custom_testcases_path)
|
||||
|
||||
logger.info("\n--- 所有已注册的测试用例类 ---")
|
||||
all_cases = registry.get_all_test_case_classes()
|
||||
for tc_class in all_cases:
|
||||
logger.info(f" ID: {tc_class.id}, Name: {tc_class.name}, Methods: {tc_class.applicable_methods}, Path Regex: {tc_class.applicable_paths_regex}")
|
||||
|
||||
logger.info("\n--- 测试适用性筛选 ---")
|
||||
endpoint1_method = "GET"
|
||||
endpoint1_path = "/api/users/123"
|
||||
logger.info(f"筛选适用于 '{endpoint1_method} {endpoint1_path}':")
|
||||
applicable1 = registry.get_applicable_test_cases(endpoint1_method, endpoint1_path)
|
||||
for tc_class in applicable1:
|
||||
logger.info(f" Applicable: {tc_class.id} ({tc_class.name})")
|
||||
|
||||
endpoint2_method = "POST"
|
||||
endpoint2_path = "/api/orders"
|
||||
logger.info(f"筛选适用于 '{endpoint2_method} {endpoint2_path}':")
|
||||
applicable2 = registry.get_applicable_test_cases(endpoint2_method, endpoint2_path)
|
||||
for tc_class in applicable2:
|
||||
logger.info(f" Applicable: {tc_class.id} ({tc_class.name})")
|
||||
|
||||
endpoint3_method = "GET"
|
||||
endpoint3_path = "/api/health"
|
||||
logger.info(f"筛选适用于 '{endpoint3_method} {endpoint3_path}':")
|
||||
applicable3 = registry.get_applicable_test_cases(endpoint3_method, endpoint3_path)
|
||||
for tc_class in applicable3:
|
||||
logger.info(f" Applicable: {tc_class.id} ({tc_class.name})")
|
||||
|
||||
# 清理临时文件和目录
|
||||
# os.remove(os.path.join(custom_testcases_path, "example_checks.py"))
|
||||
# os.remove(os.path.join(custom_testcases_path, "specific_feature_tests.py"))
|
||||
# if not os.listdir(custom_testcases_path):
|
||||
# os.rmdir(custom_testcases_path)
|
||||
# logger.info("已清理临时文件和目录。")
|
||||
143
ddms_compliance_suite/test_framework_core.py
Normal file
143
ddms_compliance_suite/test_framework_core.py
Normal file
@ -0,0 +1,143 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Optional, List, Tuple, Type
|
||||
import logging
|
||||
|
||||
class TestSeverity(Enum):
|
||||
"""测试用例的严重程度"""
|
||||
CRITICAL = "严重"
|
||||
HIGH = "高"
|
||||
MEDIUM = "中"
|
||||
LOW = "低"
|
||||
INFO = "信息"
|
||||
|
||||
class ValidationResult:
|
||||
"""封装单个验证点的结果"""
|
||||
def __init__(self, passed: bool, message: str, details: Optional[Dict[str, Any]] = None):
|
||||
self.passed = passed # True 表示通过, False 表示失败
|
||||
self.message = message # 验证结果的描述信息
|
||||
self.details = details or {} # 其他详细信息,如实际值、期望值等
|
||||
|
||||
def __repr__(self):
|
||||
return f"ValidationResult(passed={self.passed}, message='{self.message}')"
|
||||
|
||||
class APIRequestContext:
|
||||
"""封装当前API请求的上下文信息"""
|
||||
def __init__(self,
|
||||
method: str,
|
||||
url: str,
|
||||
path_params: Dict[str, Any],
|
||||
query_params: Dict[str, Any],
|
||||
headers: Dict[str, str],
|
||||
body: Optional[Any],
|
||||
endpoint_spec: Dict[str, Any] # 添加 endpoint_spec 到请求上下文
|
||||
):
|
||||
self.method = method
|
||||
self.url = url
|
||||
self.path_params = path_params
|
||||
self.query_params = query_params
|
||||
self.headers = headers
|
||||
self.body = body
|
||||
self.endpoint_spec = endpoint_spec # 当前测试端点的API规范部分
|
||||
|
||||
def __repr__(self):
|
||||
return (f"APIRequestContext(method='{self.method}', url='{self.url}', "
|
||||
f"query_params={self.query_params}, headers_keys={list(self.headers.keys())}, "
|
||||
f"has_body={self.body is not None})")
|
||||
|
||||
class APIResponseContext:
|
||||
"""封装当前API响应的上下文信息"""
|
||||
def __init__(self,
|
||||
status_code: int,
|
||||
headers: Dict[str, str],
|
||||
json_content: Optional[Any],
|
||||
text_content: Optional[str],
|
||||
elapsed_time: float,
|
||||
original_response: Any, # 例如 requests.Response 对象
|
||||
request_context: APIRequestContext # 对应的请求上下文
|
||||
):
|
||||
self.status_code = status_code
|
||||
self.headers = headers
|
||||
self.json_content = json_content
|
||||
self.text_content = text_content
|
||||
self.elapsed_time = elapsed_time
|
||||
self.original_response = original_response
|
||||
self.request_context = request_context # 包含触发此响应的请求信息
|
||||
|
||||
def __repr__(self):
|
||||
return (f"APIResponseContext(status_code={self.status_code}, "
|
||||
f"headers_keys={list(self.headers.keys())}, has_json={self.json_content is not None}, "
|
||||
f"elapsed_time={self.elapsed_time:.4f}s)")
|
||||
|
||||
|
||||
class BaseAPITestCase:
|
||||
"""
|
||||
自定义API测试用例的基类。
|
||||
用户应继承此类来创建具体的测试用例。
|
||||
"""
|
||||
# --- 元数据 (由子类定义) ---
|
||||
id: str = "base_test_case"
|
||||
name: str = "基础API测试用例"
|
||||
description: str = "这是一个基础测试用例,应由具体测试用例继承。"
|
||||
severity: TestSeverity = TestSeverity.MEDIUM
|
||||
tags: List[str] = []
|
||||
|
||||
applicable_methods: Optional[List[str]] = None
|
||||
applicable_paths_regex: Optional[str] = None
|
||||
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any]):
|
||||
"""
|
||||
初始化测试用例。
|
||||
Args:
|
||||
endpoint_spec: 当前被测API端点的详细定义 (来自YAPI/Swagger解析结果)。
|
||||
global_api_spec: 完整的API规范文档 (来自YAPI/Swagger解析结果)。
|
||||
"""
|
||||
self.endpoint_spec = endpoint_spec
|
||||
self.global_api_spec = global_api_spec
|
||||
self.logger = logging.getLogger(f"testcase.{self.id}")
|
||||
self.logger.debug(f"Test case '{self.id}' initialized for endpoint: {self.endpoint_spec.get('method', '')} {self.endpoint_spec.get('path', '')}")
|
||||
|
||||
# --- 1. 请求生成与修改阶段 ---
|
||||
def generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
self.logger.debug(f"Hook: generate_query_params, current: {current_query_params}")
|
||||
return current_query_params
|
||||
|
||||
def generate_headers(self, current_headers: Dict[str, str]) -> Dict[str, str]:
|
||||
self.logger.debug(f"Hook: generate_headers, current keys: {list(current_headers.keys())}")
|
||||
return current_headers
|
||||
|
||||
def generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]:
|
||||
self.logger.debug(f"Hook: generate_request_body, current body type: {type(current_body)}")
|
||||
return current_body
|
||||
|
||||
# --- 2. 请求预校验阶段 ---
|
||||
def validate_request_url(self, url: str, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
self.logger.debug(f"Hook: validate_request_url, url: {url}")
|
||||
return []
|
||||
|
||||
def validate_request_headers(self, headers: Dict[str, str], request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
self.logger.debug(f"Hook: validate_request_headers, header keys: {list(headers.keys())}")
|
||||
return []
|
||||
|
||||
def validate_request_body(self, body: Optional[Any], request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
self.logger.debug(f"Hook: validate_request_body, body type: {type(body)}")
|
||||
return []
|
||||
|
||||
# --- 3. 响应验证阶段 ---
|
||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
self.logger.debug(f"Hook: validate_response, status: {response_context.status_code}")
|
||||
return []
|
||||
|
||||
# --- 4. 性能与附加检查阶段 (可选) ---
|
||||
def check_performance(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||||
self.logger.debug(f"Hook: check_performance, elapsed: {response_context.elapsed_time}")
|
||||
return []
|
||||
|
||||
# --- Helper to easily create a passed ValidationResult ---
|
||||
@staticmethod
|
||||
def passed(message: str, details: Optional[Dict[str, Any]] = None) -> ValidationResult:
|
||||
return ValidationResult(passed=True, message=message, details=details)
|
||||
|
||||
# --- Helper to easily create a failed ValidationResult ---
|
||||
@staticmethod
|
||||
def failed(message: str, details: Optional[Dict[str, Any]] = None) -> ValidationResult:
|
||||
return ValidationResult(passed=False, message=message, details=details)
|
||||
330
docs/APITestCase_Development_Guide.md
Normal file
330
docs/APITestCase_Development_Guide.md
Normal file
@ -0,0 +1,330 @@
|
||||
# APITestCase 开发指南
|
||||
|
||||
本文档旨在指导开发人员如何创建和使用自定义的 `APITestCase` 类,以扩展 DDMS 合规性验证软件的测试能力。通过继承 `BaseAPITestCase`,您可以编写灵活且强大的 Python 代码来定义针对 API 的各种验证逻辑。
|
||||
|
||||
## 1. `APITestCase` 概述
|
||||
|
||||
`APITestCase` 是 DDMS 合规性验证软件中定义具体测试逻辑的核心单元。每个派生自 `BaseAPITestCase` 的类代表一个独立的测试场景或一组相关的检查点,例如验证特定的请求头、检查响应状态码、或确保响应体符合特定业务规则。
|
||||
|
||||
**核心理念:**
|
||||
|
||||
* **代码即测试**:使用 Python 的全部功能来定义复杂的测试逻辑,摆脱传统基于配置或简单规则的限制。
|
||||
* **灵活性**:允许测试用例在 API 请求的各个阶段介入,包括请求数据的生成、请求发送前的预校验、以及响应接收后的深度验证。
|
||||
* **可重用性与模块化**:常见的验证逻辑可以封装在辅助函数或基类中,方便在多个测试用例间共享。
|
||||
|
||||
在测试执行期间,测试编排器(`APITestOrchestrator`)会自动发现、加载并执行适用于当前 API 端点的所有已注册的 `APITestCase` 实例。
|
||||
|
||||
## 2. 如何创建自定义测试用例
|
||||
|
||||
创建一个新的测试用例涉及以下步骤:
|
||||
|
||||
1. **创建 Python 文件**:在指定的测试用例目录(例如 `custom_testcases/`)下创建一个新的 `.py` 文件。建议文件名能反映其测试内容,例如 `header_validation_tests.py`。
|
||||
2. **继承 `BaseAPITestCase`**:在文件中定义一个或多个类,使其继承自 `your_project.test_framework_core.BaseAPITestCase` (请替换为实际路径)。
|
||||
3. **定义元数据**:在您的自定义测试用例类中,必须定义以下类属性:
|
||||
* `id: str`: 测试用例的全局唯一标识符。建议使用前缀来分类,例如 `"TC-HEADER-001"`。
|
||||
* `name: str`: 人类可读的测试用例名称,例如 `"必要请求头 X-Tenant-ID 存在性检查"`。
|
||||
* `description: str`: 对测试用例目的和范围的详细描述。
|
||||
* `severity: TestSeverity`: 测试用例的严重程度,使用 `TestSeverity` 枚举(例如 `TestSeverity.CRITICAL`, `TestSeverity.HIGH`, `TestSeverity.MEDIUM`, `TestSeverity.LOW`, `TestSeverity.INFO`)。
|
||||
* `tags: List[str]`: 一个字符串列表,用于对测试用例进行分类和过滤,例如 `["header", "security", "core-functionality"]`。
|
||||
4. **可选:控制适用范围**:您可以选择性地定义以下类属性来限制测试用例的应用范围:
|
||||
* `applicable_methods: Optional[List[str]]`: 一个 HTTP 方法字符串的列表(大写),例如 `["POST", "PUT"]`。如果定义了此属性,则该测试用例仅应用于具有这些方法的 API 端点。如果为 `None`(默认),则适用于所有方法。
|
||||
* `applicable_paths_regex: Optional[str]`: 一个 Python 正则表达式字符串。如果定义了此属性,则该测试用例仅应用于其路径与此正则表达式匹配的 API 端点。如果为 `None`(默认),则适用于所有路径。
|
||||
5. **实现验证逻辑**:重写 `BaseAPITestCase` 中一个或多个 `generate_*` 或 `validate_*` 方法来实现您的具体测试逻辑。
|
||||
|
||||
**示例骨架:**
|
||||
|
||||
```python
|
||||
# In custom_testcases/my_custom_header_check.py
|
||||
from your_project.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext # 替换为实际路径
|
||||
import logging # 推荐为每个测试用例获取 logger
|
||||
|
||||
class MySpecificHeaderCheck(BaseAPITestCase):
|
||||
# 1. 元数据
|
||||
id = "TC-MYHEADER-001"
|
||||
name = "自定义头部 My-Custom-Header 格式检查"
|
||||
description = "验证请求中 My-Custom-Header 是否存在且格式为 UUID。"
|
||||
severity = TestSeverity.MEDIUM
|
||||
tags = ["custom", "header", "format"]
|
||||
|
||||
# 2. 可选:适用范围 (例如,仅用于 POST 请求)
|
||||
applicable_methods = ["POST"]
|
||||
# applicable_paths_regex = r"/api/v1/orders/.*" # 示例:仅用于特定路径模式
|
||||
|
||||
def __init__(self, endpoint_spec: dict, global_api_spec: dict):
|
||||
super().__init__(endpoint_spec, global_api_spec)
|
||||
# self.logger 在基类中已初始化为 logging.getLogger(f"testcase.{self.id}")
|
||||
self.logger.info(f"测试用例 {self.id} 已针对端点 {self.endpoint_spec.get('path')} 初始化。")
|
||||
|
||||
# 3. 实现验证逻辑 (见下一节)
|
||||
def generate_headers(self, current_headers: dict) -> dict:
|
||||
# 示例:确保我们的自定义头存在,如果不存在则添加一个用于测试
|
||||
if "My-Custom-Header" not in current_headers:
|
||||
current_headers["My-Custom-Header"] = "default-test-uuid-value" # 实际应生成有效UUID
|
||||
return current_headers
|
||||
|
||||
def validate_request_headers(self, headers: dict, request_context: APIRequestContext) -> list[ValidationResult]:
|
||||
results = []
|
||||
custom_header_value = headers.get("My-Custom-Header")
|
||||
if not custom_header_value:
|
||||
results.append(ValidationResult(passed=False, message="请求头缺少 'My-Custom-Header'。"))
|
||||
else:
|
||||
# 假设有一个 is_valid_uuid 函数
|
||||
# if not is_valid_uuid(custom_header_value):
|
||||
# results.append(ValidationResult(passed=False, message=f"'My-Custom-Header' 的值 '{custom_header_value}' 不是有效的UUID格式。"))
|
||||
# else:
|
||||
results.append(ValidationResult(passed=True, message="'My-Custom-Header' 存在且格式初步检查通过。"))
|
||||
return results
|
||||
|
||||
# ... 其他可能需要重写的方法 ...
|
||||
```
|
||||
|
||||
## 3. `BaseAPITestCase` 详解
|
||||
|
||||
`BaseAPITestCase` 提供了一系列可以在子类中重写的方法,这些方法覆盖了 API 测试生命周期的不同阶段。
|
||||
|
||||
### 3.1 构造函数 (`__init__`)
|
||||
|
||||
```python
|
||||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any]):
|
||||
```
|
||||
|
||||
* 当测试编排器为某个 API 端点实例化您的测试用例时,会调用此构造函数。
|
||||
* **参数**:
|
||||
* `endpoint_spec: Dict[str, Any]`: 当前正在测试的 API 端点的详细定义。这些信息直接来自 YAPI/Swagger 解析器解析得到的该端点的具体规范,例如包含路径、方法、参数定义(路径参数、查询参数、请求头参数)、请求体 schema、响应 schema 等。您可以使用这些信息来指导您的测试逻辑,例如,了解哪些字段是必需的,它们的数据类型是什么等。
|
||||
* `global_api_spec: Dict[str, Any]`: 完整的 API 规范文档(例如,整个 YAPI 导出的 JSON 数组或整个 Swagger JSON 对象)。这允许测试用例在需要时访问 API 规范的全局信息,比如全局定义、标签、分类等。
|
||||
* **注意**: 基类 `__init__` 方法会初始化 `self.endpoint_spec`, `self.global_api_spec` 和 `self.logger`。如果您重写 `__init__`,请务必调用 `super().__init__(endpoint_spec, global_api_spec)`。
|
||||
|
||||
### 3.2 请求生成与修改方法
|
||||
|
||||
这些方法在测试编排器构建 API 请求之前被调用,允许您动态地修改或生成请求的各个部分。这对于构造特定的测试场景(例如,发送无效数据、测试边界条件、注入特定测试值)非常有用。
|
||||
|
||||
对于每个 API 端点,测试编排器会先尝试根据 API 规范(YAPI/Swagger)生成一个"基线"的请求(包含必要的参数、基于 schema 的请求体等)。然后,您的测试用例的 `generate_*` 方法会被调用,并传入这个基线数据作为参数,您可以对其进行修改。
|
||||
|
||||
1. **`generate_query_params(self, current_query_params: Dict[str, Any]) -> Dict[str, Any]`**
|
||||
* **何时调用**: 在确定请求的查询参数时。
|
||||
* **输入**: `current_query_params` - 一个字典,包含测试编排器根据 API 规范(例如 `endpoint_spec['req_query']`)和可能的默认值生成的当前查询参数。
|
||||
* **输出**: 您必须返回一个字典,该字典将作为最终发送请求时使用的查询参数。您可以添加、删除或修改 `current_query_params` 中的条目。
|
||||
* **用途**: 注入特定的查询参数值,测试不同的过滤条件、分页参数组合等。
|
||||
|
||||
2. **`generate_headers(self, current_headers: Dict[str, str]) -> Dict[str, str]`**
|
||||
* **何时调用**: 在确定请求头时。
|
||||
* **输入**: `current_headers` - 一个字典,包含测试编排器生成的当前请求头(可能包含如 `Content-Type`, `Accept` 等默认头,以及 API 规范中定义的请求头)。
|
||||
* **输出**: 您必须返回一个字典,作为最终的请求头。
|
||||
* **用途**: 添加/修改认证令牌 (`Authorization`)、租户ID (`X-Tenant-ID`)、自定义测试头等。
|
||||
|
||||
3. **`generate_request_body(self, current_body: Optional[Any]) -> Optional[Any]`**
|
||||
* **何时调用**: 在确定请求体时 (主要用于 `POST`, `PUT`, `PATCH` 等方法)。
|
||||
* **输入**: `current_body` - 测试编排器根据 API 规范中的请求体 schema (例如 `endpoint_spec['req_body_other']` for YAPI JSON body, 或 Swagger requestBody schema) 生成的请求体。可能是字典/列表 (对于JSON),字符串或其他类型。
|
||||
* **输出**: 您必须返回最终要发送的请求体。
|
||||
* **用途**: 构造特定的请求体数据,例如:
|
||||
* 发送缺少必填字段的数据。
|
||||
* 发送类型不匹配的数据。
|
||||
* 发送超出范围的数值。
|
||||
* 注入用于测试特定业务逻辑的数据。
|
||||
|
||||
### 3.3 请求预校验方法
|
||||
|
||||
这些方法在 API 请求的各个部分(URL、头、体)完全构建完成之后,但在实际发送到服务器之前被调用。这允许您在请求发出前对其进行最终的静态检查。
|
||||
|
||||
每个预校验方法都应返回一个 `List[ValidationResult]`。
|
||||
|
||||
1. **`validate_request_url(self, url: str, request_context: APIRequestContext) -> List[ValidationResult]`**
|
||||
* **何时调用**: 请求的完整 URL 构建完毕后。
|
||||
* **输入**:
|
||||
* `url: str`: 最终构建的、将要发送的完整请求 URL。
|
||||
* `request_context: APIRequestContext`: 包含当前请求的详细上下文信息(见 4.2 节)。
|
||||
* **用途**: 检查 URL 格式是否符合规范(例如 RESTful 路径结构 `/api/{version}/{resource}`)、路径参数是否正确编码、查询参数是否符合命名规范(如全小写+下划线)等。
|
||||
|
||||
2. **`validate_request_headers(self, headers: Dict[str, str], request_context: APIRequestContext) -> List[ValidationResult]`**
|
||||
* **何时调用**: 请求头完全确定后。
|
||||
* **输入**:
|
||||
* `headers: Dict[str, str]`: 最终将要发送的请求头。
|
||||
* `request_context: APIRequestContext`: 当前请求的上下文。
|
||||
* **用途**: 检查是否包含所有必要的请求头 (`X-Tenant-ID`, `Authorization`)、头部字段值是否符合特定格式或约定。
|
||||
|
||||
3. **`validate_request_body(self, body: Optional[Any], request_context: APIRequestContext) -> List[ValidationResult]`**
|
||||
* **何时调用**: 请求体完全确定后。
|
||||
* **输入**:
|
||||
* `body: Optional[Any]`: 最终将要发送的请求体。
|
||||
* `request_context: APIRequestContext`: 当前请求的上下文。
|
||||
* **用途**: 对最终的请求体进行静态检查,例如,检查 JSON 结构是否与预期一致(不一定是严格的 schema 验证,因为那通常在 `generate_request_body` 或由框架处理,但可以做一些更具体的业务逻辑检查)。
|
||||
|
||||
### 3.4 响应验证方法
|
||||
|
||||
这是最核心的验证阶段,在从服务器接收到 API 响应后调用。
|
||||
|
||||
1. **`validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]`**
|
||||
* **何时调用**: 收到 API 响应后。
|
||||
* **输入**:
|
||||
* `response_context: APIResponseContext`: 包含 API 响应的详细上下文信息(见 4.3 节),如状态码、响应头、响应体内容等。
|
||||
* `request_context: APIRequestContext`: 触发此响应的原始请求的上下文。
|
||||
* **输出**: 返回一个 `List[ValidationResult]`,包含对该响应的所有验证点的结果。
|
||||
* **用途**: 这是进行绝大多数验证的地方,例如:
|
||||
* 检查 HTTP 状态码是否符合预期。
|
||||
* 验证响应头是否包含特定字段及其值。
|
||||
* 对响应体内容进行 JSON Schema 验证(可以调用框架提供的 `JSONSchemaValidator`)。
|
||||
* 验证响应体中的具体数据是否符合业务规则。
|
||||
* 检查错误响应的结构和错误码是否正确。
|
||||
|
||||
### 3.5 性能与附加检查方法 (可选)
|
||||
|
||||
1. **`check_performance(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]`**
|
||||
* **何时调用**: 收到 API 响应后,通常在主要的 `validate_response` 之后。
|
||||
* **输入**: 与 `validate_response` 相同。
|
||||
* **输出**: 返回一个 `List[ValidationResult]`。
|
||||
* **用途**: 执行与性能相关的检查,最常见的是检查 API 的响应时间 (`response_context.elapsed_time`) 是否在可接受的阈值内。
|
||||
|
||||
## 4. 核心辅助类
|
||||
|
||||
这些类是 `BaseAPITestCase` 的重要组成部分,用于传递信息和报告结果。
|
||||
|
||||
### 4.1 `ValidationResult`
|
||||
|
||||
```python
|
||||
class ValidationResult:
|
||||
def __init__(self, passed: bool, message: str, details: Optional[Dict[str, Any]] = None):
|
||||
self.passed: bool # True 表示验证通过, False 表示失败
|
||||
self.message: str # 对验证结果的描述性消息
|
||||
self.details: Dict[str, Any] # 可选的字典,用于存储额外信息,如实际值、期望值、上下文等
|
||||
```
|
||||
|
||||
* **用途**: 所有 `validate_*` 和 `check_*` 方法都应返回一个此对象的列表。每个对象代表一个具体的检查点。
|
||||
* **示例**:
|
||||
```python
|
||||
results.append(ValidationResult(passed=True, message="状态码为 200 OK。"))
|
||||
results.append(ValidationResult(
|
||||
passed=False,
|
||||
message=f"用户ID不匹配。期望: '{expected_id}', 实际: '{actual_id}'",
|
||||
details={"expected": expected_id, "actual": actual_id}
|
||||
))
|
||||
```
|
||||
|
||||
### 4.2 `APIRequestContext`
|
||||
|
||||
```python
|
||||
class APIRequestContext:
|
||||
def __init__(self, method: str, url: str, path_params: Dict[str, Any],
|
||||
query_params: Dict[str, Any], headers: Dict[str, str], body: Optional[Any]):
|
||||
self.method: str # HTTP 方法 (e.g., "GET", "POST")
|
||||
self.url: str # 完整的请求 URL
|
||||
self.path_params: Dict[str, Any] # 从路径中解析出的参数及其值
|
||||
self.query_params: Dict[str, Any]# 最终使用的查询参数
|
||||
self.headers: Dict[str, str] # 最终使用的请求头
|
||||
self.body: Optional[Any] # 最终使用的请求体
|
||||
```
|
||||
* **用途**: 在请求相关的钩子方法中提供关于已构建请求的全面信息。
|
||||
|
||||
### 4.3 `APIResponseContext`
|
||||
|
||||
```python
|
||||
class APIResponseContext:
|
||||
def __init__(self, status_code: int, headers: Dict[str, str],
|
||||
json_content: Optional[Any], text_content: Optional[str],
|
||||
elapsed_time: float, original_response: Any):
|
||||
self.status_code: int # HTTP 响应状态码 (e.g., 200, 404)
|
||||
self.headers: Dict[str, str] # 响应头
|
||||
self.json_content: Optional[Any]# 如果响应是JSON且成功解析,则为解析后的对象 (字典或列表),否则为 None
|
||||
self.text_content: Optional[str]# 原始响应体文本内容
|
||||
self.elapsed_time: float # API 调用耗时 (从发送请求到收到完整响应头),单位:秒
|
||||
self.original_response: Any # 底层 HTTP 库返回的原始响应对象 (例如 `requests.Response`),供高级用例使用
|
||||
```
|
||||
* **用途**: 在响应相关的钩子方法中提供关于收到的 API 响应的全面信息。
|
||||
|
||||
### 4.4 `TestSeverity` 枚举
|
||||
|
||||
```python
|
||||
from enum import Enum
|
||||
|
||||
class TestSeverity(Enum):
|
||||
CRITICAL = "严重"
|
||||
HIGH = "高"
|
||||
MEDIUM = "中"
|
||||
LOW = "低"
|
||||
INFO = "信息"
|
||||
```
|
||||
* **用途**: 用于定义测试用例的严重级别,方便报告和结果分析。
|
||||
|
||||
## 5. 日志记录
|
||||
|
||||
`BaseAPITestCase` 在其 `__init__` 方法中为每个测试用例实例初始化了一个标准的 Python logger:
|
||||
`self.logger = logging.getLogger(f"testcase.{self.id}")`
|
||||
|
||||
您可以在测试用例的任何方法中使用 `self.logger` 来输出调试信息、执行流程或遇到的问题。
|
||||
|
||||
**示例**:
|
||||
```python
|
||||
self.logger.info(f"正在为端点 {self.endpoint_spec['path']} 生成请求体...")
|
||||
if error_condition:
|
||||
self.logger.warning(f"在为 {self.endpoint_spec['title']} 处理数据时遇到警告: {error_condition}")
|
||||
```
|
||||
这些日志将由应用程序的整体日志配置进行管理。
|
||||
|
||||
## 6. 简单示例:检查状态码和响应时间
|
||||
|
||||
```python
|
||||
# In custom_testcases/basic_response_checks.py
|
||||
from your_project.test_framework_core import BaseAPITestCase, TestSeverity, ValidationResult, APIRequestContext, APIResponseContext
|
||||
|
||||
class StatusCode200Check(BaseAPITestCase):
|
||||
id = "TC-STATUS-001"
|
||||
name = "状态码 200 OK 检查"
|
||||
description = "验证 API 是否成功响应并返回状态码 200。"
|
||||
severity = TestSeverity.CRITICAL
|
||||
tags = ["status_code", "smoke"]
|
||||
|
||||
# 此测试用例适用于所有端点,因此无需定义 applicable_methods 或 applicable_paths_regex
|
||||
|
||||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> list[ValidationResult]:
|
||||
results = []
|
||||
if response_context.status_code == 200:
|
||||
results.append(ValidationResult(passed=True, message="响应状态码为 200 OK。"))
|
||||
else:
|
||||
results.append(ValidationResult(
|
||||
passed=False,
|
||||
message=f"期望状态码 200,但收到 {response_context.status_code}。",
|
||||
details={
|
||||
"expected_status": 200,
|
||||
"actual_status": response_context.status_code,
|
||||
"response_body_sample": (response_context.text_content or "")[:200] # 包含部分响应体以帮助诊断
|
||||
}
|
||||
))
|
||||
return results
|
||||
|
||||
class ResponseTimeCheck(BaseAPITestCase):
|
||||
id = "TC-PERF-001"
|
||||
name = "API 响应时间检查 (小于1秒)"
|
||||
description = "验证 API 响应时间是否在 1000 毫秒以内。"
|
||||
severity = TestSeverity.MEDIUM
|
||||
tags = ["performance"]
|
||||
|
||||
MAX_RESPONSE_TIME_SECONDS = 1.0 # 1 秒
|
||||
|
||||
def check_performance(self, response_context: APIResponseContext, request_context: APIRequestContext) -> list[ValidationResult]:
|
||||
results = []
|
||||
elapsed_ms = response_context.elapsed_time * 1000
|
||||
if response_context.elapsed_time <= self.MAX_RESPONSE_TIME_SECONDS:
|
||||
results.append(ValidationResult(
|
||||
passed=True,
|
||||
message=f"响应时间 {elapsed_ms:.2f}ms,在阈值 {self.MAX_RESPONSE_TIME_SECONDS*1000:.0f}ms 以内。"
|
||||
))
|
||||
else:
|
||||
results.append(ValidationResult(
|
||||
passed=False,
|
||||
message=f"响应时间过长: {elapsed_ms:.2f}ms。期望小于 {self.MAX_RESPONSE_TIME_SECONDS*1000:.0f}ms。",
|
||||
details={"actual_ms": elapsed_ms, "threshold_ms": self.MAX_RESPONSE_TIME_SECONDS*1000}
|
||||
))
|
||||
return results
|
||||
```
|
||||
|
||||
## 7. 最佳实践和注意事项
|
||||
|
||||
* **保持测试用例的单一职责**:尽量让每个 `APITestCase` 类专注于一个特定的验证目标或一小组紧密相关的检查点。这使得测试用例更易于理解、维护和调试。
|
||||
* **清晰的命名**:为您的测试用例类、`id` 和 `name` 使用清晰、描述性的名称。
|
||||
* **充分利用 `endpoint_spec`**:在测试逻辑中,参考 `self.endpoint_spec` 来了解 API 的预期行为、参数、schema 等,使您的测试更加精确。
|
||||
* **详细的 `ValidationResult` 消息**:当验证失败时,提供足够详细的 `message` 和 `details`,以便快速定位问题。
|
||||
* **考虑性能**:虽然灵活性是关键,但避免在测试用例中执行过于耗时的操作,除非是专门的性能测试。
|
||||
* **错误处理**:在您的测试用例代码中妥善处理可能发生的异常,并使用 `self.logger` 记录它们。
|
||||
* **可重用逻辑**:如果多个测试用例需要相似的逻辑(例如,解析特定的响应结构、生成特定的测试数据),考虑将这些逻辑提取到共享的辅助函数或一个共同的基类中(您的测试用例可以继承自这个中间基类,而这个中间基类继承自 `BaseAPITestCase`)。
|
||||
* **逐步实现**:从简单的测试用例开始,逐步构建更复杂的验证逻辑。
|
||||
|
||||
通过遵循本指南,您将能够有效地利用 `APITestCase` 机制为您的 DDMS 合规性验证软件构建强大而灵活的自动化测试。
|
||||
87
prompt.md
Normal file
87
prompt.md
Normal file
@ -0,0 +1,87 @@
|
||||
需要改进的地方是我想实现一个机制,能够灵活地增加各种测试方法,现在的规则库我觉得不够好,我想完全重新设计,你帮我想想怎么能够实现一种强大的自定义规则库的方法(是不是完全用代码定义规则好一点?我需要你帮我设计)比如说至少要支持下面这些规则,怎么把这些验证添加到header、响应、url中,或者定义并行测试等等,我不是很清楚,你一定给我一个强大通用的设计,具体的测试功能可以先不考虑,先想想这个架构。比如域名要求是动宾结构,我就可以在这里添加大模型测试。是不是让用户自定义validate函数比较好,比如可以定义生成query的函数,定义验证url的函数,定义验证响应的函数,不同的函数自动加到不同的部分来验证,还可以定义额外的并行参数、对于响应时间等参数的测试函数等,就是提供一个testcase模板,这样也容易灵活添加,你觉得怎么样:一、 核心功能测试
|
||||
|
||||
1. 数据存取服务接口测试:
|
||||
|
||||
- 标准化查询接口验证: 测试数据查询接口,覆盖不同查询条件,验证返回结果的准确性和完整性。
|
||||
- 标准化读取接口验证: 测试数据读取接口,验证特定标识数据的准确获取。
|
||||
- 标准化写入接口验证: 测试数据写入接口,验证数据正确持久化,并考虑边界值、异常值写入。
|
||||
- 输出格式验证: 确保所有数据存取服务的输出符合定义的数据交换格式 (JSON Schema)。
|
||||
|
||||
2. 状态监控接口测试:
|
||||
|
||||
- 实时状态反馈验证: 调用状态监控接口,验证返回的系统运行状态、接口健康度信息的准确性和实时性。
|
||||
|
||||
3. 专业领域扩展接口测试:
|
||||
|
||||
- 遵循总则要求验证: 对各专业领域扩展的数据服务接口,均需重新应用本总则中的所有相关测试点。
|
||||
- 专业领域数据交换模型验证: 验证其是否按照该专业领域的特定应用要求进行设置和交互。
|
||||
二、 数据服务接口规范性测试 (对应规范第7部分)
|
||||
|
||||
1. 通用技术实现测试:
|
||||
|
||||
- OpenAPI规范符合性验证:
|
||||
- 验证实际API行为与描述是否一致(路径、参数、方法、响应码、数据结构等)。
|
||||
- JSON序列化验证: 确保所有请求和响应的数据都使用有效的JSON格式进行序列化。
|
||||
|
||||
2. 数据服务接口功能与设计原则测试:
|
||||
|
||||
- 可靠性存取验证: 进行长时间、多并发的数据操作,验证其可靠性。
|
||||
- 分页查询功能验证: 测试不同的页码、每页数量,验证返回数据子集的正确性、边界条件(首页、末页、空页)。
|
||||
- 条件过滤功能验证: 测试多种有效和无效的过滤条件组合,验证结果集的准确性。
|
||||
- RESTful API设计原则符合性验证:
|
||||
- HTTP方法使用: 验证GET(检索)、POST(创建)、PUT(更新)、DELETE(删除)的正确使用。测试对不支持的方法返回405。
|
||||
- 接口命名语义化: 评审接口名称是否为动词+名词,比如GetWellLog
|
||||
- 路径结构: 验证路径是否遵循 /{专业}/{版本号}/资源类型 格式。
|
||||
- 资源命名: 验证资源集合是否用复数,术语是否为行业标准。
|
||||
- 请求头验证:
|
||||
- 必含字段:
|
||||
- X-Tenant-ID 测试: 验证包含、缺失、无效租户标识的场景下的行为,特别是数据隔离性。
|
||||
- Authorization (Bearer令牌) 测试: 验证包含有效令牌、缺失令牌、无效/过期令牌的场景。
|
||||
- X-Data-Domain:数据域声明,比如logging
|
||||
- 路径参数测试: 全小写+下划线命名
|
||||
- 查询参数测试: 测试过滤、分页、排序参数的有效组合及无效输入的处理。
|
||||
- 错误响应结构验证: 触发错误,验证响应是否为包含code、message的结构化错误码(参照附录B)。
|
||||
|
||||
3. 模型管理接口功能与安全测试:
|
||||
|
||||
- 元信息管理功能验证: 测试数据字典、权限策略、接口版本等元信息的增删改查功能(若API提供)。
|
||||
- 动态配置与审计追踪验证: 测试动态配置的生效情况,验证相关操作是否产生审计记录(若可查)。
|
||||
- 敏感元数据访问限制: 验证不同权限用户对敏感元数据的访问是否受控。
|
||||
|
||||
4. 兼容性测试:
|
||||
|
||||
- URL版本号嵌入与多版本共存验证: 验证URL中是否包含版本号 (如 /v1/datasets)。
|
||||
|
||||
5. 服务质量测试):
|
||||
|
||||
- 分页控制有效性: 验证分页参数能有效控制响应数据量。
|
||||
四、 安全要求测试
|
||||
|
||||
1. 传输安全测试:
|
||||
|
||||
- HTTPS协议强制性验证: 验证所有接口是否仅能通过HTTPS访问,HTTP访问是否被拒绝或重定向。
|
||||
- 认证鉴权方法验证:
|
||||
- 无凭证访问: 尝试在未提供认证信息的情况下访问受保护接口,验证是否返回401或类似错误。
|
||||
- 无效/过期凭证访问: 使用无效或过期的令牌/凭证访问,验证是否被拒绝。
|
||||
|
||||
2. 敏感数据防护测试:
|
||||
|
||||
- 敏感字段加密传输验证: 抓包分析(在HTTPS解密后,若测试环境允许)或通过响应内容,验证规范中定义的敏感字段是否按要求(如SM4算法)进行了字段级加密(此项可能需要特定配合或白盒信息)。通常HTTPS已保障传输层加密。
|
||||
- 强制加密传输验证: 验证在认证鉴权后,传输内容是否仍然强制通过加密信道(HTTPS)。
|
||||
|
||||
3. 访问控制测试:
|
||||
|
||||
- 权限分离验证:
|
||||
- 使用不同角色的账户/令牌,分别测试其对数据进行增加、删除、修改、查询等操作的权限。
|
||||
- 验证用户A无法访问/操作用户B或其他租户的数据(除非明确授权)。
|
||||
- 验证低权限用户无法执行高权限操作。
|
||||
- 对未授权的操作,验证是否返回403或类似错误。
|
||||
五、 附录B (JSON关键字与错误代码) 符合性测试
|
||||
- 特定错误场景触发与代码验证:
|
||||
- 类型校验失败 (4001): 发送与Schema定义类型不符的数据 (如string代替number)。
|
||||
- 必填字段缺失 (4003): 请求中缺少Schema定义的必填字段。
|
||||
- 数值越界 (4002): 发送超出Schema定义minimum/maximum范围的数值。
|
||||
- 自定义格式校验失败 (4004): 发送不符合Schema中format定义的数据 (如无效的email、date-time)。
|
||||
- 数组元素重复 (4005): 若Schema定义了uniqueItems: true,发送包含重复元素的数组。
|
||||
- 非法枚举值 (4006): 发送不在Schema中enum定义列表内的值。
|
||||
- 对以上每种情况,验证API是否返回对应的、正确的错误码和描述。
|
||||
BIN
tests/__pycache__/test_test_case_registry.cpython-312.pyc
Normal file
BIN
tests/__pycache__/test_test_case_registry.cpython-312.pyc
Normal file
Binary file not shown.
@ -1,638 +0,0 @@
|
||||
"""
|
||||
地震体 API 模拟服务器
|
||||
|
||||
用于模拟地震体相关的 API 接口,用于测试 DDMS 合规性验证软件。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
import time
|
||||
import random
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
from flask import Flask, request, jsonify, Response, send_file
|
||||
import io
|
||||
import numpy as np
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# 内存数据存储
|
||||
seismic_files = {} # 存储地震体文件信息
|
||||
export_tasks = {} # 存储导出任务信息
|
||||
import_tasks = {} # 存储导入任务信息
|
||||
seismic_data = {} # 存储地震体数据
|
||||
|
||||
# 生成唯一ID
|
||||
def generate_id():
|
||||
current_time = int(time.time() * 1000)
|
||||
return f"{current_time}_{random.randint(1, 10)}"
|
||||
|
||||
# 初始化一些样例数据
|
||||
def init_sample_data():
|
||||
# 添加一个样例地震体
|
||||
sample_seismic_id = "20221113181927_1"
|
||||
seismic_files[sample_seismic_id] = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "Sample_Seismic",
|
||||
"dsType": 1,
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 1,
|
||||
"serviceMax": 100,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 2,
|
||||
"dimensionName": "xline",
|
||||
"serviceMin": 1,
|
||||
"serviceMax": 100,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 3,
|
||||
"dimensionName": "slice",
|
||||
"serviceMin": 3500,
|
||||
"serviceMax": 3600,
|
||||
"serviceSpan": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 初始化地震体数据 (简单的随机数据)
|
||||
seismic_data[sample_seismic_id] = {
|
||||
"traces": {}, # 存储道数据 {(inline, xline): values}
|
||||
"h3200": b"Sample 3200 Header Data",
|
||||
"h400": b"Sample 400 Header Data",
|
||||
"h240": {1: b"Sample 240 Header Data for trace 1"},
|
||||
"keywords": {
|
||||
"h400": [{"name": "Sample H400 Keyword", "value": "Sample Value"}],
|
||||
"h240": [{"name": "Sample H240 Keyword", "value": "Sample Value"}],
|
||||
"gather": [{"name": "Sample Gather Keyword", "value": "Sample Value"}]
|
||||
},
|
||||
"coordinates": {
|
||||
# 坐标映射 {(x, y): (inline, xline)}
|
||||
(606406.1281141682, 6082083.338731234): (440, 333),
|
||||
(609767.8725899048, 6080336.549935018): (366, 465),
|
||||
(615271.9052119441, 6082017.422172886): (427, 686),
|
||||
(612173.8269695987, 6084291.543435885): (521, 566)
|
||||
}
|
||||
}
|
||||
|
||||
# 5.1.1 新建地震体文件
|
||||
@app.route('/api/gsc/appmodel/api/v1/seismic/file/add', methods=['POST'])
|
||||
def add_seismic_file():
|
||||
try:
|
||||
data = request.json
|
||||
seismic_id = generate_id()
|
||||
|
||||
# 存储地震体信息
|
||||
seismic_files[seismic_id] = data
|
||||
|
||||
# 初始化地震体数据
|
||||
seismic_data[seismic_id] = {
|
||||
"traces": {},
|
||||
"h3200": b"Default 3200 Header Data",
|
||||
"h400": b"Default 400 Header Data",
|
||||
"h240": {},
|
||||
"keywords": {
|
||||
"h400": [],
|
||||
"h240": [],
|
||||
"gather": []
|
||||
},
|
||||
"coordinates": {}
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
"code": "0",
|
||||
"msg": "",
|
||||
"flag": True,
|
||||
"result": seismic_id
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding seismic file: {e}")
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": str(e),
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 500
|
||||
|
||||
# 5.1.2 Inline 写入三维地震体文件
|
||||
@app.route('/api/gsc/appmodel/api/v1/seismic/3d/inline/writer/array', methods=['POST'])
|
||||
def write_inline_array():
|
||||
try:
|
||||
data = request.json
|
||||
seismic_id = data.get('seismicId')
|
||||
inline_no = data.get('inlineNo')
|
||||
values = data.get('values', [])
|
||||
|
||||
if seismic_id not in seismic_data:
|
||||
return jsonify({
|
||||
"msg": "Seismic file not found",
|
||||
"code": 1,
|
||||
"result": ""
|
||||
}), 404
|
||||
|
||||
# 存储道数据
|
||||
for xline_idx, trace_values in enumerate(values[0]):
|
||||
xline_no = seismic_files[seismic_id]['dimensions'][1]['serviceMin'] + xline_idx
|
||||
seismic_data[seismic_id]["traces"][(inline_no, xline_no)] = trace_values
|
||||
|
||||
return jsonify({
|
||||
"msg": "success",
|
||||
"code": 0,
|
||||
"result": ""
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error writing inline array: {e}")
|
||||
return jsonify({
|
||||
"msg": str(e),
|
||||
"code": 1,
|
||||
"result": ""
|
||||
}), 500
|
||||
|
||||
# 5.1.3 3D 任意道写入三维地震体文件
|
||||
@app.route('/api/gsc/appmodel/api/v1/seismic/3d/writer/any/array', methods=['POST'])
|
||||
def write_any_array():
|
||||
try:
|
||||
data = request.json
|
||||
seismic_id = data.get('seismicId')
|
||||
traces = data.get('traces', [])
|
||||
|
||||
if seismic_id not in seismic_data:
|
||||
return jsonify({
|
||||
"msg": "Seismic file not found",
|
||||
"code": 1,
|
||||
"result": ""
|
||||
}), 404
|
||||
|
||||
# 存储道数据
|
||||
for trace in traces:
|
||||
inline_no = trace.get('inlineNo')
|
||||
xline_no = trace.get('xlineNo')
|
||||
values = trace.get('values', [])
|
||||
seismic_data[seismic_id]["traces"][(inline_no, xline_no)] = values
|
||||
|
||||
return jsonify({
|
||||
"msg": "success",
|
||||
"code": 0,
|
||||
"result": ""
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error writing any array: {e}")
|
||||
return jsonify({
|
||||
"msg": str(e),
|
||||
"code": 1,
|
||||
"result": ""
|
||||
}), 500
|
||||
|
||||
# 5.1.4 导出地震体—提交任务
|
||||
@app.route('/api/gsc/appmodel/api/v1/seismic/export/submit', methods=['POST'])
|
||||
def submit_export():
|
||||
try:
|
||||
data = request.json
|
||||
seismic_id = data.get('seismicId')
|
||||
|
||||
if seismic_id not in seismic_data:
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": "Seismic file not found",
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 404
|
||||
|
||||
# 创建导出任务
|
||||
task_id = f"export_{generate_id()}"
|
||||
export_tasks[task_id] = {
|
||||
"seismicId": seismic_id,
|
||||
"status": "submitted",
|
||||
"progress": 0,
|
||||
"saveDir": data.get('saveDir', f"/export/{seismic_id}.sgy")
|
||||
}
|
||||
|
||||
# 模拟任务开始处理
|
||||
def process_task():
|
||||
for i in range(1, 11):
|
||||
export_tasks[task_id]["progress"] = i * 10
|
||||
time.sleep(0.5) # 模拟处理时间
|
||||
export_tasks[task_id]["status"] = "completed"
|
||||
|
||||
# 启动后台线程处理任务
|
||||
import threading
|
||||
thread = threading.Thread(target=process_task)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
return jsonify({
|
||||
"code": "0",
|
||||
"msg": "操作成功!",
|
||||
"flag": True,
|
||||
"result": {
|
||||
"taskId": task_id,
|
||||
"status": "submitted",
|
||||
"progress": 0
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error submitting export task: {e}")
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": str(e),
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 500
|
||||
|
||||
# 5.1.5 导出地震体—查询进度
|
||||
@app.route('/api/gsc/appmodel/api/v1/seismic/export/progress', methods=['GET'])
|
||||
def query_export_progress():
|
||||
try:
|
||||
task_id = request.args.get('taskId')
|
||||
|
||||
if task_id not in export_tasks:
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": "Task not found",
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 404
|
||||
|
||||
task = export_tasks[task_id]
|
||||
|
||||
return jsonify({
|
||||
"code": "0",
|
||||
"msg": "操作成功!",
|
||||
"flag": True,
|
||||
"result": {
|
||||
"taskId": task_id,
|
||||
"status": task["status"],
|
||||
"progress": task["progress"]
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error querying export progress: {e}")
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": str(e),
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 500
|
||||
|
||||
# 5.1.6 导出地震体—下载
|
||||
@app.route('/api/gsc/appmodel/api/v1/seismic/export/download', methods=['GET'])
|
||||
def download_export():
|
||||
try:
|
||||
task_id = request.args.get('taskId')
|
||||
|
||||
if task_id not in export_tasks:
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": "Task not found",
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 404
|
||||
|
||||
task = export_tasks[task_id]
|
||||
|
||||
if task["status"] != "completed":
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": "Task not completed",
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 400
|
||||
|
||||
# 创建一个模拟的SEG-Y文件
|
||||
dummy_segy = io.BytesIO()
|
||||
# 添加一些随机二进制数据
|
||||
dummy_segy.write(b"FAKE SEGY FILE CONTENT")
|
||||
dummy_segy.seek(0)
|
||||
|
||||
# 如果 delFile 参数设置为 true,删除任务
|
||||
if request.args.get('delFile') == '1':
|
||||
export_tasks.pop(task_id, None)
|
||||
|
||||
return send_file(
|
||||
dummy_segy,
|
||||
mimetype='application/octet-stream',
|
||||
as_attachment=True,
|
||||
download_name=f"seismic_{task['seismicId']}.sgy"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading export: {e}")
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": str(e),
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 500
|
||||
|
||||
# 5.1.7 查询地震体 3200 头
|
||||
@app.route('/api/gsc/appmodel/api/v1/seismic/head/h3200', methods=['POST'])
|
||||
def get_h3200():
|
||||
try:
|
||||
data = request.json
|
||||
seismic_id = data.get('seismicId')
|
||||
|
||||
if seismic_id not in seismic_data:
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": "Seismic file not found",
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 404
|
||||
|
||||
# 返回二进制数据
|
||||
return Response(
|
||||
seismic_data[seismic_id]["h3200"],
|
||||
mimetype='application/octet-stream'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting h3200: {e}")
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": str(e),
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 500
|
||||
|
||||
# 5.1.8 查询地震体 400 头
|
||||
@app.route('/api/gsc/appmodel/api/v1/seismic/head/h400', methods=['POST'])
|
||||
def get_h400():
|
||||
try:
|
||||
data = request.json
|
||||
seismic_id = data.get('seismicId')
|
||||
|
||||
if seismic_id not in seismic_data:
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": "Seismic file not found",
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 404
|
||||
|
||||
# 返回二进制数据
|
||||
return Response(
|
||||
seismic_data[seismic_id]["h400"],
|
||||
mimetype='application/octet-stream'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting h400: {e}")
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": str(e),
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 500
|
||||
|
||||
# 5.1.9 查询地震体总道数
|
||||
@app.route('/api/gsc/appmodel/api/v1/seismic/traces/count', methods=['POST'])
|
||||
def get_traces_count():
|
||||
try:
|
||||
data = request.json
|
||||
seismic_id = data.get('seismicId')
|
||||
|
||||
if seismic_id not in seismic_data:
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": "Seismic file not found",
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 404
|
||||
|
||||
# 获取道数
|
||||
trace_count = len(seismic_data[seismic_id]["traces"])
|
||||
|
||||
return jsonify({
|
||||
"msg": "success",
|
||||
"code": 0,
|
||||
"result": str(trace_count)
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting traces count: {e}")
|
||||
return jsonify({
|
||||
"msg": str(e),
|
||||
"code": 1,
|
||||
"result": ""
|
||||
}), 500
|
||||
|
||||
# 5.1.10 查询地震体 240 头
|
||||
@app.route('/api/gsc/appmodel/api/v1/seismic/head/h240', methods=['POST'])
|
||||
def get_h240():
|
||||
try:
|
||||
data = request.json
|
||||
seismic_id = data.get('seismicId')
|
||||
trace_index = data.get('traceIndex')
|
||||
|
||||
if seismic_id not in seismic_data:
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": "Seismic file not found",
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 404
|
||||
|
||||
# 获取道头数据
|
||||
if trace_index not in seismic_data[seismic_id]["h240"]:
|
||||
# 如果不存在,生成一个随机的道头数据
|
||||
seismic_data[seismic_id]["h240"][trace_index] = f"Sample 240 Header Data for trace {trace_index}".encode()
|
||||
|
||||
# 返回二进制数据
|
||||
return Response(
|
||||
seismic_data[seismic_id]["h240"][trace_index],
|
||||
mimetype='application/octet-stream'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting h240: {e}")
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": str(e),
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 500
|
||||
|
||||
# 5.1.11 查询地震体卷头关键字信息
|
||||
@app.route('/api/gsc/appmodel/api/v1/seismic/h400/keyword/list', methods=['POST'])
|
||||
def get_h400_keywords():
|
||||
try:
|
||||
data = request.json
|
||||
seismic_id = data.get('seismicId')
|
||||
|
||||
if seismic_id not in seismic_data:
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": "Seismic file not found",
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 404
|
||||
|
||||
return jsonify({
|
||||
"flag": True,
|
||||
"msg": "success",
|
||||
"code": 0,
|
||||
"result": seismic_data[seismic_id]["keywords"]["h400"]
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting h400 keywords: {e}")
|
||||
return jsonify({
|
||||
"flag": False,
|
||||
"msg": str(e),
|
||||
"code": 1,
|
||||
"result": None
|
||||
}), 500
|
||||
|
||||
# 5.1.12 查询地震体道头关键字信息
|
||||
@app.route('/api/gsc/appmodel/api/v1/seismic/h240/keyword/list', methods=['POST'])
|
||||
def get_h240_keywords():
|
||||
try:
|
||||
data = request.json
|
||||
seismic_id = data.get('seismicId')
|
||||
|
||||
if seismic_id not in seismic_data:
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": "Seismic file not found",
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 404
|
||||
|
||||
return jsonify({
|
||||
"flag": True,
|
||||
"msg": "success",
|
||||
"code": 0,
|
||||
"result": seismic_data[seismic_id]["keywords"]["h240"]
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting h240 keywords: {e}")
|
||||
return jsonify({
|
||||
"flag": False,
|
||||
"msg": str(e),
|
||||
"code": 1,
|
||||
"result": None
|
||||
}), 500
|
||||
|
||||
# 5.1.13 查询地震体道集关键字信息
|
||||
@app.route('/api/gsc/appmodel/api/v1/seismic/gather/keyword/list', methods=['POST'])
|
||||
def get_gather_keywords():
|
||||
try:
|
||||
data = request.json
|
||||
seismic_id = data.get('seismicId')
|
||||
|
||||
if seismic_id not in seismic_data:
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": "Seismic file not found",
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 404
|
||||
|
||||
return jsonify({
|
||||
"flag": True,
|
||||
"msg": "success",
|
||||
"code": 0,
|
||||
"result": seismic_data[seismic_id]["keywords"]["gather"]
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting gather keywords: {e}")
|
||||
return jsonify({
|
||||
"flag": False,
|
||||
"msg": str(e),
|
||||
"code": 1,
|
||||
"result": None
|
||||
}), 500
|
||||
|
||||
# 5.1.14 导入地震体—查询进度
|
||||
@app.route('/api/gsc/appmodel/api/v1/seismic/import/progress', methods=['POST'])
|
||||
def query_import_progress():
|
||||
try:
|
||||
data = request.json
|
||||
seismic_id = data.get('seismicId')
|
||||
|
||||
if seismic_id not in seismic_data:
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": "Seismic file not found",
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 404
|
||||
|
||||
# 检查是否存在导入任务
|
||||
task_id = f"import_{seismic_id}"
|
||||
if task_id not in import_tasks:
|
||||
# 创建一个模拟的导入任务
|
||||
import_tasks[task_id] = {
|
||||
"seismicId": seismic_id,
|
||||
"status": "completed",
|
||||
"progress": 100
|
||||
}
|
||||
|
||||
task = import_tasks[task_id]
|
||||
|
||||
return jsonify({
|
||||
"msg": "success",
|
||||
"code": 0,
|
||||
"result": {
|
||||
"taskId": task_id,
|
||||
"status": task["status"],
|
||||
"progress": task["progress"]
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error querying import progress: {e}")
|
||||
return jsonify({
|
||||
"msg": str(e),
|
||||
"code": 1,
|
||||
"result": None
|
||||
}), 500
|
||||
|
||||
# 5.1.15 查询地震体点线坐标
|
||||
@app.route('/api/gsc/appmodel/api/v1/seismic/coordinate/geodetic/toline', methods=['POST'])
|
||||
def convert_coordinates():
|
||||
try:
|
||||
data = request.json
|
||||
seismic_id = data.get('seismicId')
|
||||
points = data.get('points', [])
|
||||
|
||||
if seismic_id not in seismic_data:
|
||||
return jsonify({
|
||||
"code": "1",
|
||||
"msg": "Seismic file not found",
|
||||
"flag": False,
|
||||
"result": None
|
||||
}), 404
|
||||
|
||||
# 转换坐标
|
||||
result = []
|
||||
for point in points:
|
||||
x, y = point
|
||||
# 查找最接近的已知坐标点
|
||||
if (x, y) in seismic_data[seismic_id]["coordinates"]:
|
||||
result.append(list(seismic_data[seismic_id]["coordinates"][(x, y)]))
|
||||
else:
|
||||
# 如果找不到精确匹配,返回模拟数据
|
||||
result.append([random.randint(1, 100), random.randint(1, 100)])
|
||||
|
||||
return jsonify({
|
||||
"msg": "success",
|
||||
"code": 0,
|
||||
"result": result
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error converting coordinates: {e}")
|
||||
return jsonify({
|
||||
"msg": str(e),
|
||||
"code": 1,
|
||||
"result": None
|
||||
}), 500
|
||||
|
||||
# 初始化样例数据
|
||||
init_sample_data()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5001, debug=True)
|
||||
@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
启动地震体 API 模拟服务器
|
||||
|
||||
启动 mock_seismic_api.py 中定义的 Flask 应用,
|
||||
用于在本地端口 5001 上提供模拟的地震体 API 服务。
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
# 导入模拟 API 服务器模块
|
||||
from tests.mock_seismic_api import app
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("启动地震体 API 模拟服务器在 http://localhost:5001/")
|
||||
print("使用 Ctrl+C 停止服务器")
|
||||
app.run(host='0.0.0.0', port=5001, debug=True)
|
||||
@ -1,219 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
API Caller Test Module
|
||||
|
||||
Unit tests for ddms_compliance_suite.api_caller.caller module,
|
||||
validates API request and response handling.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
import json
|
||||
from unittest import mock
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to Python path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from ddms_compliance_suite.api_caller.caller import APICaller, APIRequest, APIResponse
|
||||
|
||||
class MockResponse:
|
||||
"""Mock class for requests response"""
|
||||
def __init__(self, json_data, status_code, headers=None, content=None, elapsed_seconds=0.1):
|
||||
self.json_data = json_data
|
||||
self.status_code = status_code
|
||||
self.headers = headers or {"Content-Type": "application/json"}
|
||||
self.content = content or json.dumps(json_data).encode('utf-8')
|
||||
self.elapsed = mock.Mock()
|
||||
self.elapsed.total_seconds.return_value = elapsed_seconds
|
||||
|
||||
def json(self):
|
||||
return self.json_data
|
||||
|
||||
def raise_for_status(self):
|
||||
if self.status_code >= 400:
|
||||
from requests.exceptions import HTTPError
|
||||
raise HTTPError(f"HTTP Error: {self.status_code}")
|
||||
|
||||
class TestAPICaller(unittest.TestCase):
|
||||
"""API Caller Test Class"""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup before tests"""
|
||||
self.api_caller = APICaller(
|
||||
default_timeout=30, # Match the default timeout in the actual implementation
|
||||
default_headers={"X-Test": "test-value"}
|
||||
)
|
||||
|
||||
@mock.patch('requests.request')
|
||||
def test_successful_get_request(self, mock_request):
|
||||
"""Test successful GET request"""
|
||||
# Set mock return value
|
||||
mock_response = MockResponse(
|
||||
json_data={"message": "success", "data": {"id": 1, "name": "Test Project"}},
|
||||
status_code=200
|
||||
)
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
# Create request object
|
||||
request = APIRequest(
|
||||
method="GET",
|
||||
url="https://api.example.com/test",
|
||||
params={"id": 1}
|
||||
)
|
||||
|
||||
# Execute request
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# Validate
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json_content["message"], "success")
|
||||
self.assertEqual(response.json_content["data"]["name"], "Test Project")
|
||||
|
||||
# Validate mock call with proper timeout
|
||||
mock_request.assert_called_once_with(
|
||||
method="GET",
|
||||
url="https://api.example.com/test",
|
||||
headers={"X-Test": "test-value"},
|
||||
params={"id": 1},
|
||||
json=None,
|
||||
data=None,
|
||||
timeout=30 # Default timeout from the implementation
|
||||
)
|
||||
|
||||
@mock.patch('requests.request')
|
||||
def test_successful_post_request(self, mock_request):
|
||||
"""Test successful POST request"""
|
||||
# Set mock return value
|
||||
mock_response = MockResponse(
|
||||
json_data={"message": "created", "id": 123},
|
||||
status_code=201
|
||||
)
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
# Create request object
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url="https://api.example.com/create",
|
||||
json_data={"name": "New Test Project", "description": "Test Description"},
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
# Execute request
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# Validate
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.json_content["message"], "created")
|
||||
self.assertEqual(response.json_content["id"], 123)
|
||||
|
||||
# Validate mock call with proper timeout
|
||||
mock_request.assert_called_once_with(
|
||||
method="POST",
|
||||
url="https://api.example.com/create",
|
||||
headers={"X-Test": "test-value", "Content-Type": "application/json"},
|
||||
params=None,
|
||||
json={"name": "New Test Project", "description": "Test Description"},
|
||||
data=None,
|
||||
timeout=30 # Default timeout from the implementation
|
||||
)
|
||||
|
||||
@mock.patch('requests.request')
|
||||
def test_error_request(self, mock_request):
|
||||
"""Test request failure case"""
|
||||
# Set mock to raise exception
|
||||
from requests.exceptions import HTTPError
|
||||
mock_response = mock.Mock()
|
||||
mock_response.status_code = 404
|
||||
mock_response.headers = {"Content-Type": "application/json"}
|
||||
mock_response.content = b"Not Found"
|
||||
|
||||
# Create a proper HTTPError with response attached
|
||||
http_error = HTTPError("404 Client Error")
|
||||
http_error.response = mock_response
|
||||
mock_request.side_effect = http_error
|
||||
|
||||
# Create request object
|
||||
request = APIRequest(
|
||||
method="GET",
|
||||
url="https://api.example.com/nonexistent"
|
||||
)
|
||||
|
||||
# Execute request
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# Validate
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertIsNone(response.json_content)
|
||||
|
||||
@mock.patch('requests.request')
|
||||
def test_non_json_response(self, mock_request):
|
||||
"""Test non-JSON response"""
|
||||
# Create a real mock for requests.request
|
||||
content = b"Non-JSON content"
|
||||
mock_response = mock.Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.headers = {"Content-Type": "text/plain"}
|
||||
mock_response.content = content
|
||||
mock_response.elapsed.total_seconds.return_value = 0.1
|
||||
|
||||
# Setup the JSON decode error when json() is called
|
||||
from requests.exceptions import JSONDecodeError
|
||||
mock_response.json.side_effect = JSONDecodeError("Invalid JSON", "", 0)
|
||||
|
||||
# Set the mock response for the request
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
# Create request object
|
||||
request = APIRequest(
|
||||
method="GET",
|
||||
url="https://api.example.com/text"
|
||||
)
|
||||
|
||||
# Execute request
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# Validate
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.content, b"Non-JSON content")
|
||||
self.assertIsNone(response.json_content)
|
||||
|
||||
@mock.patch('requests.request')
|
||||
def test_custom_timeout(self, mock_request):
|
||||
"""Test custom timeout setting"""
|
||||
# Set mock return value
|
||||
mock_response = MockResponse(
|
||||
json_data={"message": "success"},
|
||||
status_code=200
|
||||
)
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
# Create request object with custom timeout
|
||||
request = APIRequest(
|
||||
method="GET",
|
||||
url="https://api.example.com/slow",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# Execute request
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# Validate
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Validate mock call used custom timeout
|
||||
mock_request.assert_called_once_with(
|
||||
method="GET",
|
||||
url="https://api.example.com/slow",
|
||||
headers={"X-Test": "test-value"},
|
||||
params=None,
|
||||
json=None,
|
||||
data=None,
|
||||
timeout=10 # Custom timeout specified in the request
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -1,443 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
API 调用器和 JSON Schema 验证器的集成测试
|
||||
|
||||
本测试模块验证 API 调用器获取的数据是否能够被 JSON Schema 验证器正确验证,
|
||||
测试两个组件能否协同工作。
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
import json
|
||||
from unittest import mock
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from ddms_compliance_suite.api_caller.caller import APICaller, APIRequest
|
||||
from ddms_compliance_suite.json_schema_validator.validator import JSONSchemaValidator
|
||||
from ddms_compliance_suite.models.rule_models import JSONSchemaDefinition, RuleCategory, TargetType
|
||||
|
||||
class MockResponse:
|
||||
"""Mock 类用于模拟 requests 的响应"""
|
||||
def __init__(self, json_data, status_code, headers=None, content=None, elapsed_seconds=0.1):
|
||||
self.json_data = json_data
|
||||
self.status_code = status_code
|
||||
self.headers = headers or {"Content-Type": "application/json"}
|
||||
self.content = content or json.dumps(json_data).encode('utf-8')
|
||||
self.elapsed = mock.Mock()
|
||||
self.elapsed.total_seconds.return_value = elapsed_seconds
|
||||
|
||||
def json(self):
|
||||
return self.json_data
|
||||
|
||||
def raise_for_status(self):
|
||||
if self.status_code >= 400:
|
||||
from requests.exceptions import HTTPError
|
||||
raise HTTPError(f"HTTP Error: {self.status_code}")
|
||||
|
||||
class TestAPISchemaIntegration(unittest.TestCase):
|
||||
"""API 调用器和 JSON Schema 验证器的集成测试类"""
|
||||
|
||||
def setUp(self):
|
||||
"""测试前的设置"""
|
||||
# 创建 API 调用器实例
|
||||
self.api_caller = APICaller(
|
||||
default_timeout=30,
|
||||
default_headers={"X-Test": "test-value"}
|
||||
)
|
||||
|
||||
# 创建 JSON Schema 验证器实例
|
||||
self.schema_validator = JSONSchemaValidator()
|
||||
|
||||
# 井数据的 JSON Schema
|
||||
self.well_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["wellName", "wellID", "status", "coordinates"],
|
||||
"properties": {
|
||||
"wellName": {
|
||||
"type": "string",
|
||||
"description": "Well name"
|
||||
},
|
||||
"wellID": {
|
||||
"type": "string",
|
||||
"pattern": "^W[0-9]{10}$",
|
||||
"description": "Well unique identifier, must be W followed by 10 digits"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["active", "inactive", "abandoned", "suspended"],
|
||||
"description": "Current well status"
|
||||
},
|
||||
"coordinates": {
|
||||
"type": "object",
|
||||
"required": ["longitude", "latitude"],
|
||||
"properties": {
|
||||
"longitude": {
|
||||
"type": "number",
|
||||
"minimum": -180,
|
||||
"maximum": 180,
|
||||
"description": "Longitude coordinate"
|
||||
},
|
||||
"latitude": {
|
||||
"type": "number",
|
||||
"minimum": -90,
|
||||
"maximum": 90,
|
||||
"description": "Latitude coordinate"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 地震数据的 JSON Schema
|
||||
self.seismic_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["projectId", "surveyId", "seismicName", "dimensions"],
|
||||
"properties": {
|
||||
"projectId": {
|
||||
"type": "string",
|
||||
"description": "Project identifier"
|
||||
},
|
||||
"surveyId": {
|
||||
"type": "string",
|
||||
"description": "Survey identifier"
|
||||
},
|
||||
"seismicName": {
|
||||
"type": "string",
|
||||
"description": "Seismic volume name"
|
||||
},
|
||||
"dsType": {
|
||||
"type": "integer",
|
||||
"enum": [1, 2],
|
||||
"description": "Dataset type: 1 for base seismic, 2 for attribute body"
|
||||
},
|
||||
"dimensions": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["dimensionNo", "dimensionName", "serviceMin", "serviceMax"],
|
||||
"properties": {
|
||||
"dimensionNo": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "Dimension number"
|
||||
},
|
||||
"dimensionName": {
|
||||
"type": "string",
|
||||
"description": "Dimension name"
|
||||
},
|
||||
"serviceMin": {
|
||||
"type": "integer",
|
||||
"description": "Minimum value"
|
||||
},
|
||||
"serviceMax": {
|
||||
"type": "integer",
|
||||
"description": "Maximum value"
|
||||
},
|
||||
"serviceSpan": {
|
||||
"type": "integer",
|
||||
"description": "Sample interval"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": { "dsType": { "enum": [2] } },
|
||||
"required": ["dsType"]
|
||||
},
|
||||
"then": {
|
||||
"required": ["baseSeismicId"],
|
||||
"properties": {
|
||||
"baseSeismicId": {
|
||||
"type": "string",
|
||||
"description": "Base seismic identifier required for attribute bodies"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 创建 Schema 规则
|
||||
self.well_schema_rule = JSONSchemaDefinition(
|
||||
id="well-data-schema",
|
||||
name="Well Data Schema",
|
||||
description="Defines JSON structure for well data",
|
||||
category=RuleCategory.JSON_SCHEMA,
|
||||
version="1.0.0",
|
||||
target_type=TargetType.DATA_OBJECT,
|
||||
target_identifier="Well",
|
||||
schema_content=self.well_schema
|
||||
)
|
||||
|
||||
self.seismic_schema_rule = JSONSchemaDefinition(
|
||||
id="seismic-data-schema",
|
||||
name="Seismic Data Schema",
|
||||
description="Defines JSON structure for seismic data",
|
||||
category=RuleCategory.JSON_SCHEMA,
|
||||
version="1.0.0",
|
||||
target_type=TargetType.DATA_OBJECT,
|
||||
target_identifier="Seismic",
|
||||
schema_content=self.seismic_schema
|
||||
)
|
||||
|
||||
@mock.patch('requests.request')
|
||||
def test_valid_well_data_integration(self, mock_request):
|
||||
"""测试有效井数据的 API 调用和 Schema 验证集成"""
|
||||
# 设置 mock 返回值
|
||||
valid_well_data = {
|
||||
"wellName": "Test Well-01",
|
||||
"wellID": "W0123456789",
|
||||
"status": "active",
|
||||
"coordinates": {
|
||||
"longitude": 116.3833,
|
||||
"latitude": 39.9167
|
||||
},
|
||||
"depth": 3500,
|
||||
"operator": "TestCorp"
|
||||
}
|
||||
|
||||
mock_response = MockResponse(
|
||||
json_data=valid_well_data,
|
||||
status_code=200
|
||||
)
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
# 创建 API 请求
|
||||
request = APIRequest(
|
||||
method="GET",
|
||||
url="https://api.example.com/wells/W0123456789",
|
||||
headers={"Accept": "application/json"}
|
||||
)
|
||||
|
||||
# 调用 API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证 API 调用
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 验证数据符合 Schema
|
||||
validation_result = self.schema_validator.validate(response.json_content, self.well_schema)
|
||||
|
||||
# 断言
|
||||
self.assertTrue(validation_result.is_valid)
|
||||
self.assertEqual(len(validation_result.errors), 0)
|
||||
|
||||
# 使用规则对象进行验证
|
||||
rule_validation_result = self.schema_validator.validate_with_rule(response.json_content, self.well_schema_rule)
|
||||
self.assertTrue(rule_validation_result.is_valid)
|
||||
|
||||
@mock.patch('requests.request')
|
||||
def test_invalid_well_data_integration(self, mock_request):
|
||||
"""测试无效井数据的 API 调用和 Schema 验证集成"""
|
||||
# 设置 mock 返回值 - 缺少必填字段 status
|
||||
invalid_well_data = {
|
||||
"wellName": "Test Well-02",
|
||||
"wellID": "W0123456789",
|
||||
# 缺少 status
|
||||
"coordinates": {
|
||||
"longitude": 116.3833,
|
||||
"latitude": 39.9167
|
||||
}
|
||||
}
|
||||
|
||||
mock_response = MockResponse(
|
||||
json_data=invalid_well_data,
|
||||
status_code=200
|
||||
)
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
# 创建 API 请求
|
||||
request = APIRequest(
|
||||
method="GET",
|
||||
url="https://api.example.com/wells/W0123456789",
|
||||
headers={"Accept": "application/json"}
|
||||
)
|
||||
|
||||
# 调用 API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证 API 调用
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 验证数据不符合 Schema
|
||||
validation_result = self.schema_validator.validate(response.json_content, self.well_schema)
|
||||
|
||||
# 断言
|
||||
self.assertFalse(validation_result.is_valid)
|
||||
self.assertTrue(any("status" in error for error in validation_result.errors))
|
||||
|
||||
# 使用规则对象进行验证
|
||||
rule_validation_result = self.schema_validator.validate_with_rule(response.json_content, self.well_schema_rule)
|
||||
self.assertFalse(rule_validation_result.is_valid)
|
||||
|
||||
@mock.patch('requests.request')
|
||||
def test_valid_seismic_data_integration(self, mock_request):
|
||||
"""测试有效地震数据的 API 调用和 Schema 验证集成"""
|
||||
# 设置 mock 返回值
|
||||
valid_seismic_data = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "西部地震体-01",
|
||||
"dsType": 1,
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 2,
|
||||
"dimensionName": "xline",
|
||||
"serviceMin": 200,
|
||||
"serviceMax": 600,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 3,
|
||||
"dimensionName": "slice",
|
||||
"serviceMin": 3500,
|
||||
"serviceMax": 3600,
|
||||
"serviceSpan": 4
|
||||
}
|
||||
],
|
||||
"sampleRate": 2.0
|
||||
}
|
||||
|
||||
mock_response = MockResponse(
|
||||
json_data=valid_seismic_data,
|
||||
status_code=200
|
||||
)
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
# 创建 API 请求
|
||||
request = APIRequest(
|
||||
method="GET",
|
||||
url="https://api.example.com/seismic/20230117135924_2",
|
||||
headers={"Accept": "application/json"}
|
||||
)
|
||||
|
||||
# 调用 API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证 API 调用
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 验证数据符合 Schema
|
||||
validation_result = self.schema_validator.validate(response.json_content, self.seismic_schema)
|
||||
|
||||
# 断言
|
||||
self.assertTrue(validation_result.is_valid)
|
||||
self.assertEqual(len(validation_result.errors), 0)
|
||||
|
||||
# 使用规则对象进行验证
|
||||
rule_validation_result = self.schema_validator.validate_with_rule(response.json_content, self.seismic_schema_rule)
|
||||
self.assertTrue(rule_validation_result.is_valid)
|
||||
|
||||
@mock.patch('requests.request')
|
||||
def test_invalid_seismic_data_integration(self, mock_request):
|
||||
"""测试无效地震数据的 API 调用和 Schema 验证集成"""
|
||||
# 设置 mock 返回值 - 属性体缺少 baseSeismicId
|
||||
invalid_seismic_data = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "西部地震体-02",
|
||||
"dsType": 2, # dsType 为 2 时需要 baseSeismicId
|
||||
# 缺少 baseSeismicId
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
mock_response = MockResponse(
|
||||
json_data=invalid_seismic_data,
|
||||
status_code=200
|
||||
)
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
# 创建 API 请求
|
||||
request = APIRequest(
|
||||
method="GET",
|
||||
url="https://api.example.com/seismic/20230117135924_2",
|
||||
headers={"Accept": "application/json"}
|
||||
)
|
||||
|
||||
# 调用 API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证 API 调用
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 验证数据不符合 Schema
|
||||
validation_result = self.schema_validator.validate(response.json_content, self.seismic_schema)
|
||||
|
||||
# 断言
|
||||
self.assertFalse(validation_result.is_valid)
|
||||
# 验证错误信息包含 baseSeismicId
|
||||
self.assertTrue(any("baseSeismicId" in error for error in validation_result.errors))
|
||||
|
||||
# 使用规则对象进行验证
|
||||
rule_validation_result = self.schema_validator.validate_with_rule(response.json_content, self.seismic_schema_rule)
|
||||
self.assertFalse(rule_validation_result.is_valid)
|
||||
|
||||
@mock.patch('requests.request')
|
||||
def test_api_error_with_validation_integration(self, mock_request):
|
||||
"""测试 API 调用错误时的集成流程"""
|
||||
# 设置 mock 抛出异常
|
||||
from requests.exceptions import HTTPError
|
||||
mock_response = mock.Mock()
|
||||
mock_response.status_code = 404
|
||||
mock_response.headers = {"Content-Type": "application/json"}
|
||||
mock_response.content = b"Not Found"
|
||||
|
||||
http_error = HTTPError("404 Client Error")
|
||||
http_error.response = mock_response
|
||||
mock_request.side_effect = http_error
|
||||
|
||||
# 创建 API 请求
|
||||
request = APIRequest(
|
||||
method="GET",
|
||||
url="https://api.example.com/wells/nonexistent",
|
||||
headers={"Accept": "application/json"}
|
||||
)
|
||||
|
||||
# 调用 API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证 API 调用失败
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertIsNone(response.json_content)
|
||||
|
||||
# 尝试验证空数据
|
||||
validation_result = self.schema_validator.validate(response.json_content, self.well_schema)
|
||||
|
||||
# 断言
|
||||
self.assertFalse(validation_result.is_valid)
|
||||
# 数据为空或无效时应该有相应的错误消息
|
||||
self.assertTrue(len(validation_result.errors) > 0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -1,476 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
综合集成测试模块
|
||||
|
||||
该测试模块集成测试以下功能:
|
||||
1. API 调用(模拟和实际)
|
||||
2. JSON Schema 加载(从文件系统)
|
||||
3. JSON Schema 验证(验证API响应)
|
||||
4. 错误处理和边界条件
|
||||
|
||||
此测试依赖于运行中的模拟地震体API服务器(在端口5001上)。
|
||||
可以通过 `python -m tests.run_mock_seismic_api` 启动服务器。
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import unittest
|
||||
import logging
|
||||
import requests
|
||||
from unittest import mock
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
# 导入测试所需的模块
|
||||
from ddms_compliance_suite.api_caller.caller import APICaller, APIRequest
|
||||
from ddms_compliance_suite.json_schema_validator.validator import JSONSchemaValidator
|
||||
from ddms_compliance_suite.rule_repository.repository import RuleRepository
|
||||
from ddms_compliance_suite.models.rule_models import RuleQuery, RuleCategory, TargetType
|
||||
from ddms_compliance_suite.models.config_models import RuleRepositoryConfig, RuleStorageConfig
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TestComprehensiveIntegration(unittest.TestCase):
|
||||
"""综合集成测试类"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""测试类初始化,检查模拟服务器是否运行"""
|
||||
cls.mock_server_url = "http://localhost:5001"
|
||||
try:
|
||||
response = requests.get(cls.mock_server_url) #这里是测试服务器能否正常访问
|
||||
cls.server_running = True
|
||||
logger.info(f"模拟API服务器可用,状态码: {response.status_code}")
|
||||
except Exception as e:
|
||||
cls.server_running = False
|
||||
logger.warning(f"模拟API服务器不可用,请先启动服务器: {e}")
|
||||
logger.warning("可以使用命令: python -m tests.run_mock_seismic_api")
|
||||
raise unittest.SkipTest("模拟API服务器未运行")
|
||||
|
||||
def setUp(self):
|
||||
"""每个测试前的初始化"""
|
||||
# 初始化API调用器
|
||||
self.api_caller = APICaller(
|
||||
default_timeout=5,
|
||||
default_headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
# 初始化JSON Schema验证器
|
||||
self.schema_validator = JSONSchemaValidator()
|
||||
|
||||
# 初始化规则仓库
|
||||
rule_config = RuleRepositoryConfig(
|
||||
storage=RuleStorageConfig(path="./rules"),
|
||||
preload_rules=True
|
||||
)
|
||||
self.rule_repository = RuleRepository(rule_config)
|
||||
|
||||
# 记录测试数据
|
||||
self.test_project_id = "testPrj1"
|
||||
self.test_survey_id = "20230117135924_2"
|
||||
self.sample_seismic_id = "20221113181927_1" # 服务器预置的样例地震体ID
|
||||
|
||||
def test_1_load_schemas_from_repository(self):
|
||||
"""测试从规则仓库加载Schema"""
|
||||
# 加载井数据Schema
|
||||
well_schema_rule = self.rule_repository.get_rule("well-data-schema")
|
||||
self.assertIsNotNone(well_schema_rule, "井数据Schema规则未找到")
|
||||
self.assertEqual(well_schema_rule.category, RuleCategory.JSON_SCHEMA)
|
||||
self.assertIsNotNone(well_schema_rule.schema_content, "井数据Schema内容为空")
|
||||
logger.info("成功加载井数据Schema")
|
||||
|
||||
# 加载地震体数据Schema
|
||||
seismic_schema_rule = self.rule_repository.get_rule("seismic-data-schema")
|
||||
self.assertIsNotNone(seismic_schema_rule, "地震体数据Schema规则未找到")
|
||||
self.assertEqual(seismic_schema_rule.category, RuleCategory.JSON_SCHEMA)
|
||||
self.assertIsNotNone(seismic_schema_rule.schema_content, "地震体数据Schema内容为空")
|
||||
logger.info("成功加载地震体数据Schema")
|
||||
|
||||
# 加载API响应Schema
|
||||
api_schema_rule = self.rule_repository.get_rule("seismic-api-response-schema")
|
||||
self.assertIsNotNone(api_schema_rule, "API响应Schema规则未找到")
|
||||
self.assertEqual(api_schema_rule.category, RuleCategory.JSON_SCHEMA)
|
||||
self.assertIsNotNone(api_schema_rule.schema_content, "API响应Schema内容为空")
|
||||
logger.info("成功加载API响应Schema")
|
||||
|
||||
# 确保规则查询功能正常工作
|
||||
schema_rules = self.rule_repository.query_rules(RuleQuery(
|
||||
category=RuleCategory.JSON_SCHEMA,
|
||||
is_enabled=True
|
||||
))
|
||||
self.assertGreaterEqual(len(schema_rules), 3, "应至少有3个Schema规则")
|
||||
logger.info(f"查询到 {len(schema_rules)} 个Schema规则")
|
||||
|
||||
def test_2_validate_schemas_structure(self):
|
||||
"""测试验证已加载Schema的结构是否正确"""
|
||||
# 验证井数据Schema结构
|
||||
well_schema_rule = self.rule_repository.get_rule("well-data-schema")
|
||||
self.assertIn("properties", well_schema_rule.schema_content, "井数据Schema结构不正确")
|
||||
self.assertIn("required", well_schema_rule.schema_content, "井数据Schema缺少required字段")
|
||||
self.assertIn("wellName", well_schema_rule.schema_content["required"], "井数据Schema必填字段不正确")
|
||||
logger.info("井数据Schema结构正确")
|
||||
|
||||
# 验证地震体数据Schema结构
|
||||
seismic_schema_rule = self.rule_repository.get_rule("seismic-data-schema")
|
||||
self.assertIn("properties", seismic_schema_rule.schema_content, "地震体数据Schema结构不正确")
|
||||
self.assertIn("required", seismic_schema_rule.schema_content, "地震体数据Schema缺少required字段")
|
||||
self.assertIn("projectId", seismic_schema_rule.schema_content["required"], "地震体数据Schema必填字段不正确")
|
||||
logger.info("地震体数据Schema结构正确")
|
||||
|
||||
# 验证API响应Schema结构
|
||||
api_schema_rule = self.rule_repository.get_rule("seismic-api-response-schema")
|
||||
self.assertIn("properties", api_schema_rule.schema_content, "API响应Schema结构不正确")
|
||||
self.assertIn("required", api_schema_rule.schema_content, "API响应Schema缺少required字段")
|
||||
self.assertIn("code", api_schema_rule.schema_content["required"], "API响应Schema必填字段不正确")
|
||||
logger.info("API响应Schema结构正确")
|
||||
|
||||
def test_3_test_api_call_and_validate_response(self):
|
||||
"""测试API调用并验证响应"""
|
||||
# 从规则仓库获取API响应Schema
|
||||
api_schema_rule = self.rule_repository.get_rule("seismic-api-response-schema")
|
||||
self.assertIsNotNone(api_schema_rule, "API响应Schema规则未找到")
|
||||
api_response_schema = api_schema_rule.schema_content
|
||||
|
||||
# 创建API请求:查询地震体道数
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{self.mock_server_url}/api/gsc/appmodel/api/v1/seismic/traces/count",
|
||||
json_data={
|
||||
"projectId": self.test_project_id,
|
||||
"surveyId": self.test_survey_id,
|
||||
"seismicId": self.sample_seismic_id
|
||||
}
|
||||
)
|
||||
|
||||
# 执行API调用
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证HTTP状态码
|
||||
self.assertEqual(response.status_code, 200, f"API调用失败,状态码: {response.status_code}")
|
||||
logger.info(f"API调用成功,状态码: {response.status_code}")
|
||||
|
||||
# 验证JSON响应格式
|
||||
self.assertIsNotNone(response.json_content, "API响应不是有效的JSON")
|
||||
|
||||
# 使用修改后的更宽松的Schema验证API响应
|
||||
# 注意:不同的API端点可能有略微不同的响应格式
|
||||
# 为了测试的稳健性,我们这里做一些调整
|
||||
relaxed_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["code", "msg", "result"], # 放宽要求,不要求一定有flag字段
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": ["string", "integer", "number"],
|
||||
"description": "响应码,可以是字符串或数字"
|
||||
},
|
||||
"flag": {
|
||||
"type": "boolean",
|
||||
"description": "操作是否成功的标志"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string",
|
||||
"description": "响应消息"
|
||||
},
|
||||
"result": {
|
||||
"description": "响应结果,可以是任意类型"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validation_result = self.schema_validator.validate(response.json_content, relaxed_schema)
|
||||
self.assertTrue(validation_result.is_valid, f"API响应验证失败: {validation_result.errors}")
|
||||
logger.info("API响应验证成功")
|
||||
|
||||
# 验证响应内容
|
||||
self.assertIn("result", response.json_content, "API响应缺少result字段")
|
||||
logger.info(f"地震体 {self.sample_seismic_id} 的道数: {response.json_content.get('result')}")
|
||||
|
||||
def test_4_create_new_seismic_and_validate(self):
|
||||
"""测试创建新地震体并验证"""
|
||||
# 从规则仓库获取地震体数据Schema
|
||||
seismic_schema_rule = self.rule_repository.get_rule("seismic-data-schema")
|
||||
self.assertIsNotNone(seismic_schema_rule, "地震体数据Schema规则未找到")
|
||||
seismic_schema = seismic_schema_rule.schema_content
|
||||
|
||||
# 准备有效的地震体数据
|
||||
valid_seismic_data = {
|
||||
"projectId": self.test_project_id,
|
||||
"surveyId": self.test_survey_id,
|
||||
"seismicName": "测试地震体-综合集成",
|
||||
"dsType": 1,
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 2,
|
||||
"dimensionName": "xline",
|
||||
"serviceMin": 200,
|
||||
"serviceMax": 600,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 3,
|
||||
"dimensionName": "slice",
|
||||
"serviceMin": 3500,
|
||||
"serviceMax": 3600,
|
||||
"serviceSpan": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 首先验证地震体数据是否符合Schema
|
||||
validation_result = self.schema_validator.validate(valid_seismic_data, seismic_schema)
|
||||
self.assertTrue(validation_result.is_valid, f"地震体数据验证失败: {validation_result.errors}")
|
||||
logger.info("地震体数据验证成功")
|
||||
|
||||
# 创建API请求:添加地震体
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{self.mock_server_url}/api/gsc/appmodel/api/v1/seismic/file/add",
|
||||
json_data=valid_seismic_data
|
||||
)
|
||||
|
||||
# 执行API调用
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证HTTP状态码
|
||||
self.assertEqual(response.status_code, 200, f"创建地震体API调用失败,状态码: {response.status_code}")
|
||||
logger.info(f"创建地震体API调用成功,状态码: {response.status_code}")
|
||||
|
||||
# 验证响应内容
|
||||
self.assertIsNotNone(response.json_content, "API响应不是有效的JSON")
|
||||
self.assertIn("result", response.json_content, "API响应缺少result字段")
|
||||
self.assertTrue(response.json_content.get("flag", False), "API操作标志为失败")
|
||||
|
||||
# 获取创建的地震体ID
|
||||
seismic_id = response.json_content.get("result")
|
||||
self.assertIsNotNone(seismic_id, "未返回地震体ID")
|
||||
logger.info(f"成功创建地震体,ID: {seismic_id}")
|
||||
|
||||
# 现在测试查询新创建的地震体
|
||||
query_request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{self.mock_server_url}/api/gsc/appmodel/api/v1/seismic/traces/count",
|
||||
json_data={
|
||||
"projectId": self.test_project_id,
|
||||
"surveyId": self.test_survey_id,
|
||||
"seismicId": seismic_id
|
||||
}
|
||||
)
|
||||
|
||||
query_response = self.api_caller.call_api(query_request)
|
||||
self.assertEqual(query_response.status_code, 200, f"查询地震体API调用失败,状态码: {query_response.status_code}")
|
||||
self.assertIn("result", query_response.json_content, "查询API响应缺少result字段")
|
||||
logger.info(f"新创建地震体 {seismic_id} 的道数: {query_response.json_content.get('result')}")
|
||||
|
||||
def test_5_validate_invalid_data(self):
|
||||
"""测试验证无效数据"""
|
||||
# 从规则仓库获取地震体数据Schema
|
||||
seismic_schema_rule = self.rule_repository.get_rule("seismic-data-schema")
|
||||
self.assertIsNotNone(seismic_schema_rule, "地震体数据Schema规则未找到")
|
||||
seismic_schema = seismic_schema_rule.schema_content
|
||||
|
||||
# 准备无效的地震体数据(缺少必填字段)
|
||||
invalid_seismic_data = {
|
||||
"projectId": self.test_project_id,
|
||||
# 缺少 surveyId
|
||||
"seismicName": "无效地震体-缺少字段",
|
||||
"dsType": 1,
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 验证无效数据应该失败
|
||||
validation_result = self.schema_validator.validate(invalid_seismic_data, seismic_schema)
|
||||
self.assertFalse(validation_result.is_valid, "无效地震体数据验证应该失败")
|
||||
self.assertTrue(any("surveyId" in error for error in validation_result.errors),
|
||||
"验证错误应指出缺少surveyId字段")
|
||||
logger.info(f"成功验证无效数据错误: {validation_result.errors}")
|
||||
|
||||
# 准备另一种无效地震体数据(维度参数错误)
|
||||
invalid_dimension_data = {
|
||||
"projectId": self.test_project_id,
|
||||
"surveyId": self.test_survey_id,
|
||||
"seismicName": "无效地震体-维度参数错误",
|
||||
"dsType": 1,
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 500, # 最小值大于最大值
|
||||
"serviceMax": 100,
|
||||
"serviceSpan": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 验证无效维度参数,这种情况可能需要自定义验证,因为JSON Schema难以验证属性间的关系
|
||||
# 测试自定义维度验证逻辑
|
||||
def validate_dimensions(data):
|
||||
errors = []
|
||||
if "dimensions" in data and isinstance(data["dimensions"], list):
|
||||
for dim in data["dimensions"]:
|
||||
min_val = dim.get("serviceMin")
|
||||
max_val = dim.get("serviceMax")
|
||||
if min_val is not None and max_val is not None:
|
||||
if min_val > max_val:
|
||||
errors.append(f"维度 {dim.get('dimensionName')} 的最小值({min_val})大于最大值({max_val})")
|
||||
return errors
|
||||
|
||||
# 运行自定义验证
|
||||
dimension_errors = validate_dimensions(invalid_dimension_data)
|
||||
self.assertTrue(dimension_errors, "维度验证应检测到错误")
|
||||
logger.info(f"成功验证维度参数错误: {dimension_errors}")
|
||||
|
||||
def test_6_coordinate_conversion_integration(self):
|
||||
"""测试坐标转换API集成"""
|
||||
# 准备坐标点
|
||||
coordinate_points = [
|
||||
[606406.1281141682, 6082083.338731234],
|
||||
[609767.8725899048, 6080336.549935018],
|
||||
[615271.9052119441, 6082017.422172886]
|
||||
]
|
||||
|
||||
# 创建API请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{self.mock_server_url}/api/gsc/appmodel/api/v1/seismic/coordinate/geodetic/toline",
|
||||
json_data={
|
||||
"projectId": self.test_project_id,
|
||||
"surveyId": self.test_survey_id,
|
||||
"seismicId": self.sample_seismic_id,
|
||||
"points": coordinate_points
|
||||
}
|
||||
)
|
||||
|
||||
# 执行API调用
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证HTTP状态码
|
||||
self.assertEqual(response.status_code, 200, f"坐标转换API调用失败,状态码: {response.status_code}")
|
||||
logger.info(f"坐标转换API调用成功,状态码: {response.status_code}")
|
||||
|
||||
# 验证响应内容
|
||||
self.assertIsNotNone(response.json_content, "API响应不是有效的JSON")
|
||||
self.assertIn("result", response.json_content, "API响应缺少result字段")
|
||||
|
||||
# 获取转换结果
|
||||
converted_points = response.json_content.get("result", [])
|
||||
self.assertEqual(len(converted_points), len(coordinate_points), "转换结果点数量与输入不一致")
|
||||
|
||||
# 打印转换结果
|
||||
for i, (coord, converted) in enumerate(zip(coordinate_points, converted_points)):
|
||||
logger.info(f"坐标点 {i+1}: {coord} → 线点: {converted}")
|
||||
|
||||
def test_7_export_task_integration(self):
|
||||
"""测试导出任务API集成"""
|
||||
# 创建导出任务API请求
|
||||
submit_request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{self.mock_server_url}/api/gsc/appmodel/api/v1/seismic/export/submit",
|
||||
json_data={
|
||||
"projectId": self.test_project_id,
|
||||
"surveyId": self.test_survey_id,
|
||||
"seismicId": self.sample_seismic_id,
|
||||
"saveDir": f"/export/{self.sample_seismic_id}.sgy"
|
||||
}
|
||||
)
|
||||
|
||||
# 执行API调用
|
||||
submit_response = self.api_caller.call_api(submit_request)
|
||||
|
||||
# 验证HTTP状态码
|
||||
self.assertEqual(submit_response.status_code, 200, f"提交导出任务API调用失败,状态码: {submit_response.status_code}")
|
||||
logger.info(f"提交导出任务API调用成功,状态码: {submit_response.status_code}")
|
||||
|
||||
# 验证响应内容
|
||||
self.assertIsNotNone(submit_response.json_content, "API响应不是有效的JSON")
|
||||
self.assertIn("result", submit_response.json_content, "API响应缺少result字段")
|
||||
|
||||
# 获取任务ID
|
||||
task_result = submit_response.json_content.get("result", {})
|
||||
self.assertIn("taskId", task_result, "任务结果缺少taskId字段")
|
||||
task_id = task_result.get("taskId")
|
||||
logger.info(f"导出任务ID: {task_id}")
|
||||
|
||||
# 查询导出进度API请求
|
||||
progress_request = APIRequest(
|
||||
method="GET",
|
||||
url=f"{self.mock_server_url}/api/gsc/appmodel/api/v1/seismic/export/progress",
|
||||
params={"taskId": task_id}
|
||||
)
|
||||
|
||||
# 执行API调用
|
||||
progress_response = self.api_caller.call_api(progress_request)
|
||||
|
||||
# 验证HTTP状态码
|
||||
self.assertEqual(progress_response.status_code, 200, f"查询导出进度API调用失败,状态码: {progress_response.status_code}")
|
||||
logger.info(f"查询导出进度API调用成功,状态码: {progress_response.status_code}")
|
||||
|
||||
# 验证响应内容
|
||||
self.assertIsNotNone(progress_response.json_content, "API响应不是有效的JSON")
|
||||
self.assertIn("result", progress_response.json_content, "API响应缺少result字段")
|
||||
|
||||
# 获取进度信息
|
||||
progress_result = progress_response.json_content.get("result", {})
|
||||
self.assertIn("status", progress_result, "进度结果缺少status字段")
|
||||
self.assertIn("progress", progress_result, "进度结果缺少progress字段")
|
||||
|
||||
status = progress_result.get("status")
|
||||
progress = progress_result.get("progress")
|
||||
logger.info(f"导出任务 {task_id} 状态: {status}, 进度: {progress}%")
|
||||
|
||||
def test_8_h400_keywords_integration(self):
|
||||
"""测试获取H400关键字API集成"""
|
||||
# 创建API请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{self.mock_server_url}/api/gsc/appmodel/api/v1/seismic/h400/keyword/list",
|
||||
json_data={
|
||||
"projectId": self.test_project_id,
|
||||
"surveyId": self.test_survey_id,
|
||||
"seismicId": self.sample_seismic_id
|
||||
}
|
||||
)
|
||||
|
||||
# 执行API调用
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证HTTP状态码
|
||||
self.assertEqual(response.status_code, 200, f"获取H400关键字API调用失败,状态码: {response.status_code}")
|
||||
logger.info(f"获取H400关键字API调用成功,状态码: {response.status_code}")
|
||||
|
||||
# 验证响应内容
|
||||
self.assertIsNotNone(response.json_content, "API响应不是有效的JSON")
|
||||
self.assertIn("result", response.json_content, "API响应缺少result字段")
|
||||
|
||||
# 获取关键字列表
|
||||
keywords = response.json_content.get("result", [])
|
||||
logger.info(f"地震体 {self.sample_seismic_id} 的H400关键字数量: {len(keywords)}")
|
||||
|
||||
# 如果有关键字,打印第一个
|
||||
if keywords:
|
||||
logger.info(f"第一个关键字: {keywords[0]}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,232 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
JSON Schema Validator Test Module
|
||||
|
||||
Unit tests for ddms_compliance_suite.json_schema_validator.validator module,
|
||||
validates JSON data against schema definitions.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
import json
|
||||
from unittest import mock
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to Python path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
# Import classes and functions to test
|
||||
from ddms_compliance_suite.json_schema_validator.validator import JSONSchemaValidator
|
||||
from ddms_compliance_suite.models.rule_models import JSONSchemaDefinition, RuleCategory, TargetType
|
||||
|
||||
class TestJSONSchemaValidator(unittest.TestCase):
|
||||
"""JSON Schema Validator Test Class"""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup before tests"""
|
||||
# Create validator instance
|
||||
self.validator = JSONSchemaValidator()
|
||||
|
||||
# Test schema definition
|
||||
self.well_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["wellName", "wellID", "status", "coordinates"],
|
||||
"properties": {
|
||||
"wellName": {
|
||||
"type": "string",
|
||||
"description": "Well name"
|
||||
},
|
||||
"wellID": {
|
||||
"type": "string",
|
||||
"pattern": "^W[0-9]{10}$",
|
||||
"description": "Well unique identifier, must be W followed by 10 digits"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["active", "inactive", "abandoned", "suspended"],
|
||||
"description": "Current well status"
|
||||
},
|
||||
"coordinates": {
|
||||
"type": "object",
|
||||
"required": ["longitude", "latitude"],
|
||||
"properties": {
|
||||
"longitude": {
|
||||
"type": "number",
|
||||
"minimum": -180,
|
||||
"maximum": 180,
|
||||
"description": "Longitude coordinate"
|
||||
},
|
||||
"latitude": {
|
||||
"type": "number",
|
||||
"minimum": -90,
|
||||
"maximum": 90,
|
||||
"description": "Latitude coordinate"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Create a schema rule
|
||||
self.schema_rule = JSONSchemaDefinition(
|
||||
id="well-data-schema",
|
||||
name="Well Data Schema",
|
||||
description="Defines JSON structure for well data",
|
||||
category=RuleCategory.JSON_SCHEMA,
|
||||
version="1.0.0",
|
||||
target_type=TargetType.DATA_OBJECT,
|
||||
target_identifier="Well",
|
||||
schema_content=self.well_schema
|
||||
)
|
||||
|
||||
def test_valid_data(self):
|
||||
"""Test that valid data passes validation"""
|
||||
# Valid test data
|
||||
valid_data = {
|
||||
"wellName": "Test Well-01",
|
||||
"wellID": "W0123456789",
|
||||
"status": "active",
|
||||
"coordinates": {
|
||||
"longitude": 116.3833,
|
||||
"latitude": 39.9167
|
||||
}
|
||||
}
|
||||
|
||||
# Validate
|
||||
result = self.validator.validate(valid_data, self.well_schema)
|
||||
|
||||
# Assert
|
||||
self.assertTrue(result.is_valid)
|
||||
self.assertEqual(len(result.errors), 0)
|
||||
|
||||
def test_missing_required_field(self):
|
||||
"""Test missing required field scenario"""
|
||||
# Data missing required field
|
||||
invalid_data = {
|
||||
"wellName": "Test Well-01",
|
||||
"wellID": "W0123456789",
|
||||
# Missing status field
|
||||
"coordinates": {
|
||||
"longitude": 116.3833,
|
||||
"latitude": 39.9167
|
||||
}
|
||||
}
|
||||
|
||||
# Validate
|
||||
result = self.validator.validate(invalid_data, self.well_schema)
|
||||
|
||||
# Assert
|
||||
self.assertFalse(result.is_valid)
|
||||
self.assertTrue(any("status" in error for error in result.errors))
|
||||
|
||||
def test_invalid_pattern(self):
|
||||
"""Test field with invalid pattern"""
|
||||
# Data with invalid wellID format
|
||||
invalid_data = {
|
||||
"wellName": "Test Well-01",
|
||||
"wellID": "ABCDEFGHIJK", # Does not match W + 10 digits pattern
|
||||
"status": "active",
|
||||
"coordinates": {
|
||||
"longitude": 116.3833,
|
||||
"latitude": 39.9167
|
||||
}
|
||||
}
|
||||
|
||||
# Validate
|
||||
result = self.validator.validate(invalid_data, self.well_schema)
|
||||
|
||||
# Assert
|
||||
self.assertFalse(result.is_valid)
|
||||
self.assertTrue(any("pattern" in error for error in result.errors))
|
||||
|
||||
def test_invalid_enum(self):
|
||||
"""Test value not in enum"""
|
||||
# Data with status not in enum list
|
||||
invalid_data = {
|
||||
"wellName": "Test Well-01",
|
||||
"wellID": "W0123456789",
|
||||
"status": "unknown", # Not in enum list
|
||||
"coordinates": {
|
||||
"longitude": 116.3833,
|
||||
"latitude": 39.9167
|
||||
}
|
||||
}
|
||||
|
||||
# Validate
|
||||
result = self.validator.validate(invalid_data, self.well_schema)
|
||||
|
||||
# Assert
|
||||
self.assertFalse(result.is_valid)
|
||||
self.assertTrue(any("enum" in error for error in result.errors))
|
||||
|
||||
def test_invalid_number_range(self):
|
||||
"""Test number out of range"""
|
||||
# Data with longitude out of range
|
||||
invalid_data = {
|
||||
"wellName": "Test Well-01",
|
||||
"wellID": "W0123456789",
|
||||
"status": "active",
|
||||
"coordinates": {
|
||||
"longitude": 200.0, # Outside [-180, 180] range
|
||||
"latitude": 39.9167
|
||||
}
|
||||
}
|
||||
|
||||
# Validate
|
||||
result = self.validator.validate(invalid_data, self.well_schema)
|
||||
|
||||
# Assert
|
||||
self.assertFalse(result.is_valid)
|
||||
self.assertTrue(any("maximum" in error for error in result.errors))
|
||||
|
||||
def test_validate_with_rule(self):
|
||||
"""Test validation using rule object"""
|
||||
# Valid test data
|
||||
valid_data = {
|
||||
"wellName": "Test Well-01",
|
||||
"wellID": "W0123456789",
|
||||
"status": "active",
|
||||
"coordinates": {
|
||||
"longitude": 116.3833,
|
||||
"latitude": 39.9167
|
||||
}
|
||||
}
|
||||
|
||||
# Validate using rule object
|
||||
result = self.validator.validate_with_rule(valid_data, self.schema_rule)
|
||||
|
||||
# Assert
|
||||
self.assertTrue(result.is_valid)
|
||||
self.assertEqual(len(result.errors), 0)
|
||||
|
||||
def test_additional_properties(self):
|
||||
"""Test handling of additional properties"""
|
||||
# Create a schema that prohibits additional properties
|
||||
strict_schema = dict(self.well_schema)
|
||||
strict_schema["additionalProperties"] = False
|
||||
|
||||
# Data with extra property
|
||||
data_with_extra = {
|
||||
"wellName": "Test Well-01",
|
||||
"wellID": "W0123456789",
|
||||
"status": "active",
|
||||
"coordinates": {
|
||||
"longitude": 116.3833,
|
||||
"latitude": 39.9167
|
||||
},
|
||||
"extra_field": "Extra Field" # Extra field
|
||||
}
|
||||
|
||||
# Validate
|
||||
result = self.validator.validate(data_with_extra, strict_schema)
|
||||
|
||||
# Assert
|
||||
self.assertFalse(result.is_valid)
|
||||
self.assertTrue(any("additionalProperties" in error for error in result.errors))
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -1,521 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
实时地震体 API 集成测试
|
||||
|
||||
本测试模块验证连接到真实运行的地震体 API 服务(端口5001),
|
||||
使用 API 调用器和 JSON Schema 验证器对其响应进行验证。
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from ddms_compliance_suite.api_caller.caller import APICaller, APIRequest
|
||||
from ddms_compliance_suite.json_schema_validator.validator import JSONSchemaValidator
|
||||
from ddms_compliance_suite.models.rule_models import JSONSchemaDefinition, RuleCategory, TargetType
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# API 服务器地址
|
||||
LIVE_API_SERVER = "http://localhost:5001"
|
||||
|
||||
class TestLiveSeismicAPI(unittest.TestCase):
|
||||
"""实时地震体 API 集成测试类"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""测试类开始前检查API服务器是否可用"""
|
||||
cls.server_available = False
|
||||
try:
|
||||
# 使用简单的健康检查请求来测试服务器是否运行
|
||||
response = requests.get(f"{LIVE_API_SERVER}/", timeout=2)
|
||||
# 模拟服务器可能返回404,但只要能连接就说明服务器在运行
|
||||
cls.server_available = True
|
||||
logger.info("API服务器可用,将执行集成测试")
|
||||
except (requests.ConnectionError, requests.Timeout):
|
||||
logger.warning(f"无法连接到API服务器 {LIVE_API_SERVER},集成测试将被跳过")
|
||||
cls.server_available = False
|
||||
|
||||
def setUp(self):
|
||||
"""测试前的设置"""
|
||||
# 如果服务器不可用,跳过所有测试
|
||||
if not self.__class__.server_available:
|
||||
self.skipTest(f"API服务器 {LIVE_API_SERVER} 不可用")
|
||||
|
||||
# 创建 API 调用器实例
|
||||
self.api_caller = APICaller(
|
||||
default_timeout=30,
|
||||
default_headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
# 创建 JSON Schema 验证器实例
|
||||
self.schema_validator = JSONSchemaValidator()
|
||||
|
||||
# 用于添加新地震体和验证响应的 JSON Schema
|
||||
self.add_seismic_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["code", "flag", "msg", "result"],
|
||||
"properties": {
|
||||
"code": {"type": ["string", "integer"]}, # 允许整数或字符串类型
|
||||
"flag": {"type": "boolean"},
|
||||
"msg": {"type": "string"},
|
||||
"result": {"type": ["string", "null", "object"]} # 允许字符串、空或对象类型
|
||||
}
|
||||
}
|
||||
|
||||
# 地震体数据的 JSON Schema
|
||||
self.seismic_data_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["projectId", "surveyId", "seismicName", "dsType", "dimensions"],
|
||||
"properties": {
|
||||
"projectId": {
|
||||
"type": "string",
|
||||
"description": "项目ID"
|
||||
},
|
||||
"surveyId": {
|
||||
"type": "string",
|
||||
"description": "测量ID"
|
||||
},
|
||||
"seismicName": {
|
||||
"type": "string",
|
||||
"description": "地震体名称"
|
||||
},
|
||||
"dsType": {
|
||||
"type": "integer",
|
||||
"enum": [1, 2],
|
||||
"description": "数据集类型: 1 为基础地震体, 2 为属性体"
|
||||
},
|
||||
"dimensions": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["dimensionNo", "dimensionName", "serviceMin", "serviceMax"],
|
||||
"properties": {
|
||||
"dimensionNo": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "维度编号"
|
||||
},
|
||||
"dimensionName": {
|
||||
"type": "string",
|
||||
"description": "维度名称"
|
||||
},
|
||||
"serviceMin": {
|
||||
"type": "integer",
|
||||
"description": "最小值"
|
||||
},
|
||||
"serviceMax": {
|
||||
"type": "integer",
|
||||
"description": "最大值"
|
||||
},
|
||||
"serviceSpan": {
|
||||
"type": "integer",
|
||||
"description": "采样间隔"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": { "dsType": { "enum": [2] } },
|
||||
"required": ["dsType"]
|
||||
},
|
||||
"then": {
|
||||
"required": ["baseSeismicId"],
|
||||
"properties": {
|
||||
"baseSeismicId": {
|
||||
"type": "string",
|
||||
"description": "属性体必须的基础地震体标识符"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 创建一个新的地震体ID存储
|
||||
self.created_seismic_ids = []
|
||||
|
||||
def test_0_health_check(self):
|
||||
"""测试服务器健康状态"""
|
||||
# 此测试主要用于确认服务器是否正常运行
|
||||
# 即使返回404,只要能连接就视为服务器可用
|
||||
try:
|
||||
response = requests.get(f"{LIVE_API_SERVER}/health", timeout=2)
|
||||
logger.info(f"服务器健康检查响应: {response.status_code}")
|
||||
self.assertTrue(True, "服务器可用")
|
||||
except (requests.ConnectionError, requests.Timeout) as e:
|
||||
self.fail(f"无法连接到API服务器: {str(e)}")
|
||||
|
||||
def test_1_add_valid_seismic_file(self):
|
||||
"""测试添加有效的地震体文件"""
|
||||
# 创建有效的地震体数据
|
||||
valid_seismic_data = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "有效地震体",
|
||||
"dsType": 1,
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 2,
|
||||
"dimensionName": "xline",
|
||||
"serviceMin": 200,
|
||||
"serviceMax": 600,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 3,
|
||||
"dimensionName": "slice",
|
||||
"serviceMin": 3500,
|
||||
"serviceMax": 3600,
|
||||
"serviceSpan": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 验证请求数据满足架构
|
||||
request_validation = self.schema_validator.validate(
|
||||
valid_seismic_data,
|
||||
self.seismic_data_schema
|
||||
)
|
||||
self.assertTrue(request_validation.is_valid, f"请求数据不符合架构: {request_validation.errors}")
|
||||
|
||||
# 创建 API 请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{LIVE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/file/add",
|
||||
body=valid_seismic_data
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用 API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证 API 调用结果
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 验证响应符合 Schema
|
||||
validation_result = self.schema_validator.validate(
|
||||
response.json_content,
|
||||
self.add_seismic_schema
|
||||
)
|
||||
|
||||
# 断言
|
||||
self.assertTrue(validation_result.is_valid, f"响应数据不符合架构: {validation_result.errors}")
|
||||
self.assertTrue(response.json_content.get("flag", False))
|
||||
|
||||
# 保存创建的地震体ID,用于后续测试
|
||||
seismic_id = response.json_content.get("result")
|
||||
if seismic_id:
|
||||
self.created_seismic_ids.append(seismic_id)
|
||||
logger.info(f"创建了地震体ID: {seismic_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"测试添加有效地震体文件失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def test_2_add_attribute_body(self):
|
||||
"""测试添加属性体"""
|
||||
# 确保有至少一个地震体ID可用于测试
|
||||
# 获取已知存在的样例地震体
|
||||
sample_seismic_id = "20221113181927_1"
|
||||
|
||||
# 创建属性体数据
|
||||
attribute_seismic_data = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "测试属性体",
|
||||
"dsType": 2, # dsType 为 2 表示属性体
|
||||
"baseSeismicId": sample_seismic_id, # 引用已知存在的基础地震体ID
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 2,
|
||||
"dimensionName": "xline",
|
||||
"serviceMin": 200,
|
||||
"serviceMax": 600,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 3,
|
||||
"dimensionName": "slice",
|
||||
"serviceMin": 3500,
|
||||
"serviceMax": 3600,
|
||||
"serviceSpan": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 验证请求数据满足架构
|
||||
request_validation = self.schema_validator.validate(
|
||||
attribute_seismic_data,
|
||||
self.seismic_data_schema
|
||||
)
|
||||
self.assertTrue(request_validation.is_valid, f"请求数据不符合架构: {request_validation.errors}")
|
||||
|
||||
# 创建 API 请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{LIVE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/file/add",
|
||||
body=attribute_seismic_data
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用 API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证 API 调用结果
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 验证响应符合 Schema
|
||||
validation_result = self.schema_validator.validate(
|
||||
response.json_content,
|
||||
self.add_seismic_schema
|
||||
)
|
||||
|
||||
# 断言
|
||||
self.assertTrue(validation_result.is_valid, f"响应数据不符合架构: {validation_result.errors}")
|
||||
self.assertTrue(response.json_content.get("flag", False))
|
||||
|
||||
# 保存创建的地震体ID,用于后续测试
|
||||
seismic_id = response.json_content.get("result")
|
||||
if seismic_id:
|
||||
self.created_seismic_ids.append(seismic_id)
|
||||
logger.info(f"创建了属性体ID: {seismic_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"测试添加属性体失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def test_3_trace_count(self):
|
||||
"""测试查询地震体总道数"""
|
||||
# 使用已知存在的样例地震体ID
|
||||
sample_seismic_id = "20221113181927_1"
|
||||
|
||||
# 创建查询道数的请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{LIVE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/traces/count",
|
||||
body={"seismicId": sample_seismic_id}
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用 API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证 API 调用结果
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 验证响应格式
|
||||
self.assertIn("result", response.json_content)
|
||||
self.assertIn("code", response.json_content)
|
||||
self.assertIn("msg", response.json_content)
|
||||
|
||||
# 即使道数为0也是有效响应
|
||||
logger.info(f"地震体 {sample_seismic_id} 的道数: {response.json_content.get('result')}")
|
||||
except Exception as e:
|
||||
logger.error(f"测试查询地震体总道数失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def test_4_export_task(self):
|
||||
"""测试导出地震体任务提交"""
|
||||
# 使用已知存在的样例地震体ID
|
||||
sample_seismic_id = "20221113181927_1"
|
||||
|
||||
# 创建提交导出任务的请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{LIVE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/export/submit",
|
||||
body={
|
||||
"seismicId": sample_seismic_id,
|
||||
"saveDir": f"/export/{sample_seismic_id}.sgy"
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用 API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证 API 调用结果
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 验证响应
|
||||
self.assertEqual(response.json_content.get("code"), "0")
|
||||
self.assertTrue(response.json_content.get("flag", False))
|
||||
self.assertIn("result", response.json_content)
|
||||
|
||||
# 获取任务ID
|
||||
task_id = None
|
||||
result = response.json_content.get("result")
|
||||
if isinstance(result, dict) and "taskId" in result:
|
||||
task_id = result.get("taskId")
|
||||
|
||||
self.assertIsNotNone(task_id, "未能获取导出任务ID")
|
||||
logger.info(f"创建了导出任务ID: {task_id}")
|
||||
|
||||
# 检查导出任务进度
|
||||
if task_id:
|
||||
# 创建查询进度的请求
|
||||
progress_request = APIRequest(
|
||||
method="GET",
|
||||
url=f"{LIVE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/export/progress?taskId={task_id}"
|
||||
)
|
||||
|
||||
# 调用 API
|
||||
progress_response = self.api_caller.call_api(progress_request)
|
||||
|
||||
# 验证 API 调用结果
|
||||
self.assertEqual(progress_response.status_code, 200)
|
||||
self.assertIsNotNone(progress_response.json_content)
|
||||
|
||||
# 验证响应
|
||||
self.assertEqual(progress_response.json_content.get("code"), "0")
|
||||
self.assertTrue(progress_response.json_content.get("flag", False))
|
||||
progress_result = progress_response.json_content.get("result", {})
|
||||
self.assertIn("status", progress_result)
|
||||
self.assertIn("progress", progress_result)
|
||||
|
||||
logger.info(f"导出任务 {task_id} 状态: {progress_result.get('status')}, 进度: {progress_result.get('progress')}%")
|
||||
except Exception as e:
|
||||
logger.error(f"测试导出地震体任务提交失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def test_5_h3200_header(self):
|
||||
"""测试查询地震体3200头"""
|
||||
# 使用已知存在的样例地震体ID
|
||||
sample_seismic_id = "20221113181927_1"
|
||||
|
||||
# 创建查询3200头的请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{LIVE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/head/h3200",
|
||||
body={"seismicId": sample_seismic_id}
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用 API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证 API 调用结果 - 成功返回二进制数据
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# 不是JSON响应,应该是二进制数据
|
||||
self.assertIsNotNone(response.content)
|
||||
|
||||
logger.info(f"地震体 {sample_seismic_id} 的3200头数据长度: {len(response.content)}")
|
||||
except Exception as e:
|
||||
logger.error(f"测试查询地震体3200头失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def test_6_h400_keywords(self):
|
||||
"""测试查询地震体卷头关键字信息"""
|
||||
# 使用已知存在的样例地震体ID
|
||||
sample_seismic_id = "20221113181927_1"
|
||||
|
||||
# 创建查询卷头关键字的请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{LIVE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/h400/keyword/list",
|
||||
body={"seismicId": sample_seismic_id}
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用 API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证 API 调用结果
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 验证响应
|
||||
self.assertTrue(response.json_content.get("flag", False))
|
||||
|
||||
# 验证响应中包含关键字列表
|
||||
self.assertIn("result", response.json_content)
|
||||
|
||||
logger.info(f"地震体 {sample_seismic_id} 的卷头关键字数量: {len(response.json_content.get('result', []))}")
|
||||
except Exception as e:
|
||||
logger.error(f"测试查询地震体卷头关键字信息失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def test_7_coordinate_conversion(self):
|
||||
"""测试查询地震体点线坐标"""
|
||||
# 使用已知存在的样例地震体ID
|
||||
sample_seismic_id = "20221113181927_1"
|
||||
|
||||
# 测试点坐标
|
||||
test_points = [
|
||||
[606406.1281141682, 6082083.338731234], # 模拟服务器样例中的一个坐标
|
||||
[609767.8725899048, 6080336.549935018],
|
||||
[615271.9052119441, 6082017.422172886]
|
||||
]
|
||||
|
||||
# 创建坐标转换请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{LIVE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/coordinate/geodetic/toline",
|
||||
body={
|
||||
"seismicId": sample_seismic_id,
|
||||
"points": test_points
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用 API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证 API 调用结果
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 验证响应
|
||||
self.assertEqual(response.json_content.get("code"), 0)
|
||||
self.assertIn("result", response.json_content)
|
||||
|
||||
# 验证返回的结果是否与测试点一一对应
|
||||
result = response.json_content.get("result", [])
|
||||
self.assertEqual(len(result), len(test_points))
|
||||
|
||||
# 记录转换结果
|
||||
for i, (point, line_point) in enumerate(zip(test_points, result)):
|
||||
logger.info(f"坐标点 {i+1}: {point} → 线点: {line_point}")
|
||||
except Exception as e:
|
||||
logger.error(f"测试查询地震体点线坐标失败: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -1,213 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
测试从文件加载 JSON Schema 功能
|
||||
|
||||
这个测试脚本验证系统能够正确地从文件系统加载 JSON Schema 定义,
|
||||
并使用这些 Schema 进行数据验证。
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from ddms_compliance_suite.json_schema_validator.validator import JSONSchemaValidator
|
||||
from ddms_compliance_suite.rule_repository.repository import RuleRepository
|
||||
from ddms_compliance_suite.models.config_models import RuleRepositoryConfig
|
||||
from ddms_compliance_suite.models.rule_models import TargetType
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TestLoadSchemaFromFile(unittest.TestCase):
|
||||
"""测试从文件加载 JSON Schema 功能"""
|
||||
|
||||
def setUp(self):
|
||||
"""测试前的设置"""
|
||||
# 创建规则仓库配置
|
||||
config = RuleRepositoryConfig(
|
||||
storage={"type": "filesystem", "path": "./rules"},
|
||||
preload_rules=True
|
||||
)
|
||||
|
||||
# 初始化规则仓库
|
||||
self.rule_repository = RuleRepository(config)
|
||||
|
||||
# 创建 JSON Schema 验证器
|
||||
self.schema_validator = JSONSchemaValidator()
|
||||
|
||||
def test_load_well_schema(self):
|
||||
"""测试加载井数据 Schema"""
|
||||
# 从仓库加载 Schema
|
||||
well_schema = self.rule_repository.get_schema_for_target(
|
||||
TargetType.API_RESPONSE, "getWellData"
|
||||
)
|
||||
|
||||
# 验证 Schema 是否已加载
|
||||
self.assertIsNotNone(well_schema, "无法加载井数据 Schema")
|
||||
self.assertIsInstance(well_schema, dict, "Schema 不是字典类型")
|
||||
self.assertIn("properties", well_schema, "Schema 缺少 properties 字段")
|
||||
|
||||
# 验证 Schema 中的必要字段
|
||||
self.assertIn("wellName", well_schema["properties"], "Schema 缺少 wellName 字段")
|
||||
self.assertIn("wellID", well_schema["properties"], "Schema 缺少 wellID 字段")
|
||||
|
||||
logger.info("成功加载井数据 Schema")
|
||||
|
||||
# 测试使用加载的 Schema 验证数据
|
||||
valid_data = {
|
||||
"wellName": "测试井-01",
|
||||
"wellID": "W0123456789",
|
||||
"status": "active",
|
||||
"coordinates": {
|
||||
"longitude": 116.3833,
|
||||
"latitude": 39.9167
|
||||
}
|
||||
}
|
||||
|
||||
result = self.schema_validator.validate(valid_data, well_schema)
|
||||
self.assertTrue(result.is_valid, f"验证失败: {result.errors}")
|
||||
|
||||
# 测试无效数据
|
||||
invalid_data = {
|
||||
"wellName": "测试井-01",
|
||||
# 缺少 wellID
|
||||
"status": "active",
|
||||
"coordinates": {
|
||||
"longitude": 116.3833,
|
||||
"latitude": 39.9167
|
||||
}
|
||||
}
|
||||
|
||||
result = self.schema_validator.validate(invalid_data, well_schema)
|
||||
self.assertFalse(result.is_valid, "验证应该失败但成功了")
|
||||
self.assertGreater(len(result.errors), 0, "应该有验证错误")
|
||||
|
||||
def test_load_seismic_schema(self):
|
||||
"""测试加载地震体数据 Schema"""
|
||||
# 从仓库加载 Schema
|
||||
seismic_schema = self.rule_repository.get_schema_for_target(
|
||||
TargetType.DATA_OBJECT, "Seismic"
|
||||
)
|
||||
|
||||
# 验证 Schema 是否已加载
|
||||
self.assertIsNotNone(seismic_schema, "无法加载地震体数据 Schema")
|
||||
self.assertIsInstance(seismic_schema, dict, "Schema 不是字典类型")
|
||||
self.assertIn("properties", seismic_schema, "Schema 缺少 properties 字段")
|
||||
|
||||
# 验证 Schema 中的必要字段
|
||||
self.assertIn("projectId", seismic_schema["properties"], "Schema 缺少 projectId 字段")
|
||||
self.assertIn("dimensions", seismic_schema["properties"], "Schema 缺少 dimensions 字段")
|
||||
|
||||
logger.info("成功加载地震体数据 Schema")
|
||||
|
||||
# 测试使用加载的 Schema 验证数据
|
||||
valid_data = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "西部地震体-01",
|
||||
"dsType": 1,
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
result = self.schema_validator.validate(valid_data, seismic_schema)
|
||||
self.assertTrue(result.is_valid, f"验证失败: {result.errors}")
|
||||
|
||||
# 测试无效数据 - 缺少必要字段
|
||||
invalid_data = {
|
||||
"projectId": "testPrj1",
|
||||
# 缺少 surveyId
|
||||
"seismicName": "西部地震体-01",
|
||||
"dsType": 1
|
||||
# 缺少 dimensions
|
||||
}
|
||||
|
||||
result = self.schema_validator.validate(invalid_data, seismic_schema)
|
||||
self.assertFalse(result.is_valid, "验证应该失败但成功了")
|
||||
self.assertGreater(len(result.errors), 0, "应该有验证错误")
|
||||
|
||||
# 测试属性体无效数据 - 缺少 baseSeismicId
|
||||
invalid_attribute_data = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "属性体",
|
||||
"dsType": 2, # 属性体类型
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
}
|
||||
]
|
||||
# 缺少 baseSeismicId
|
||||
}
|
||||
|
||||
result = self.schema_validator.validate(invalid_attribute_data, seismic_schema)
|
||||
self.assertFalse(result.is_valid, "属性体验证应该失败但成功了")
|
||||
self.assertGreater(len(result.errors), 0, "应该有验证错误")
|
||||
|
||||
def test_load_api_response_schema(self):
|
||||
"""测试加载 API 响应 Schema"""
|
||||
# 从仓库加载 Schema
|
||||
api_schema = self.rule_repository.get_schema_for_target(
|
||||
TargetType.API_RESPONSE, "SeismicAPIResponse"
|
||||
)
|
||||
|
||||
# 验证 Schema 是否已加载
|
||||
self.assertIsNotNone(api_schema, "无法加载 API 响应 Schema")
|
||||
self.assertIsInstance(api_schema, dict, "Schema 不是字典类型")
|
||||
self.assertIn("properties", api_schema, "Schema 缺少 properties 字段")
|
||||
|
||||
# 验证 Schema 中的必要字段
|
||||
self.assertIn("code", api_schema["properties"], "Schema 缺少 code 字段")
|
||||
self.assertIn("flag", api_schema["properties"], "Schema 缺少 flag 字段")
|
||||
self.assertIn("msg", api_schema["properties"], "Schema 缺少 msg 字段")
|
||||
|
||||
logger.info("成功加载 API 响应 Schema")
|
||||
|
||||
# 测试使用加载的 Schema 验证数据
|
||||
valid_data = {
|
||||
"code": "0",
|
||||
"flag": True,
|
||||
"msg": "操作成功",
|
||||
"result": "20230601123456_1"
|
||||
}
|
||||
|
||||
result = self.schema_validator.validate(valid_data, api_schema)
|
||||
self.assertTrue(result.is_valid, f"验证失败: {result.errors}")
|
||||
|
||||
# 测试无效数据
|
||||
invalid_data = {
|
||||
"code": "0",
|
||||
"flag": True,
|
||||
# 缺少 msg
|
||||
"result": "20230601123456_1"
|
||||
}
|
||||
|
||||
result = self.schema_validator.validate(invalid_data, api_schema)
|
||||
self.assertFalse(result.is_valid, "验证应该失败但成功了")
|
||||
self.assertGreater(len(result.errors), 0, "应该有验证错误")
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -1,238 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
远程地震体 API 集成测试
|
||||
|
||||
本测试模块连接到用户在5001端口启动的远程地震体API服务,
|
||||
测试API调用器和JSON Schema验证器与外部API服务的集成。
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import unittest
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from ddms_compliance_suite.api_caller.caller import APICaller, APIRequest, APIResponse
|
||||
from ddms_compliance_suite.json_schema_validator.validator import JSONSchemaValidator
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 远程API服务器地址
|
||||
REMOTE_API_SERVER = "http://localhost:5001"
|
||||
|
||||
class TestRemoteSeismicAPI(unittest.TestCase):
|
||||
"""远程地震体 API 集成测试类"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""测试类开始前检查API服务器是否可用"""
|
||||
cls.server_available = False
|
||||
cls.server_endpoints = []
|
||||
|
||||
try:
|
||||
# 尝试连接到服务器,只要能连接就认为服务器在运行
|
||||
response = requests.get(f"{REMOTE_API_SERVER}/", timeout=2)
|
||||
cls.server_available = True
|
||||
logger.info(f"远程API服务器 {REMOTE_API_SERVER} 可访问,HTTP状态码: {response.status_code}")
|
||||
|
||||
# 尝试列出所有可用的端点(通常在开发环境中有帮助)
|
||||
try:
|
||||
routes_response = requests.get(f"{REMOTE_API_SERVER}/api/routes", timeout=2)
|
||||
if routes_response.status_code == 200:
|
||||
cls.server_endpoints = routes_response.json()
|
||||
logger.info(f"服务器提供了 {len(cls.server_endpoints)} 个端点")
|
||||
else:
|
||||
# 如果服务器不支持自省,查询几个关键端点确认API类型
|
||||
for endpoint in [
|
||||
"/api/gsc/appmodel/api/v1/seismic/file/list",
|
||||
"/api/gsc/appmodel/api/v1/seismic/traces/count"
|
||||
]:
|
||||
try:
|
||||
endpoint_response = requests.head(f"{REMOTE_API_SERVER}{endpoint}", timeout=1)
|
||||
if endpoint_response.status_code not in (404, 405): # 405是方法不允许,表示端点存在
|
||||
cls.server_endpoints.append(endpoint)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if cls.server_endpoints:
|
||||
logger.info(f"检测到地震体API服务器,可访问的端点: {cls.server_endpoints}")
|
||||
except Exception as e:
|
||||
logger.warning(f"无法列出服务器端点: {str(e)}")
|
||||
except Exception as e:
|
||||
logger.warning(f"无法连接到远程API服务器 {REMOTE_API_SERVER}: {str(e)}")
|
||||
cls.server_available = False
|
||||
|
||||
def setUp(self):
|
||||
"""为每个测试初始化环境"""
|
||||
if not self.__class__.server_available:
|
||||
self.skipTest(f"远程API服务器 {REMOTE_API_SERVER} 不可用")
|
||||
|
||||
# 创建API调用器
|
||||
self.api_caller = APICaller(
|
||||
default_timeout=10,
|
||||
default_headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
# 创建JSON Schema验证器
|
||||
self.schema_validator = JSONSchemaValidator()
|
||||
|
||||
# 保存测试中创建的资源ID,以便后续测试或清理
|
||||
self.created_resource_ids = []
|
||||
|
||||
def test_api_connection(self):
|
||||
"""测试与API服务器的基本连接"""
|
||||
# 选择一个简单的端点进行测试,或尝试服务器根目录
|
||||
try:
|
||||
response = requests.get(f"{REMOTE_API_SERVER}/", timeout=2)
|
||||
# 只要能连接,不管返回什么状态码,测试都通过
|
||||
self.assertTrue(True, "成功连接到API服务器")
|
||||
logger.info(f"服务器连接测试成功,HTTP状态码: {response.status_code}")
|
||||
except Exception as e:
|
||||
self.fail(f"连接到API服务器失败: {str(e)}")
|
||||
|
||||
def test_create_seismic_file(self):
|
||||
"""测试创建地震体文件"""
|
||||
# 准备创建地震体的请求数据
|
||||
seismic_data = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "远程测试地震体",
|
||||
"dsType": 1,
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 2,
|
||||
"dimensionName": "xline",
|
||||
"serviceMin": 200,
|
||||
"serviceMax": 600,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 3,
|
||||
"dimensionName": "slice",
|
||||
"serviceMin": 3500,
|
||||
"serviceMax": 3600,
|
||||
"serviceSpan": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 创建API请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{REMOTE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/file/add",
|
||||
body=seismic_data
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证响应状态码
|
||||
logger.info(f"创建地震体API调用返回状态码: {response.status_code}")
|
||||
|
||||
# 如果API服务器接受请求,保存响应用于进一步测试
|
||||
if 200 <= response.status_code < 300 and response.json_content:
|
||||
# 响应可能包含更多字段,但至少应该有一个ID标识新创建的资源
|
||||
if "result" in response.json_content:
|
||||
created_id = response.json_content.get("result")
|
||||
if created_id:
|
||||
self.created_resource_ids.append(created_id)
|
||||
logger.info(f"成功创建地震体,ID: {created_id}")
|
||||
else:
|
||||
logger.warning("API响应中的result字段为空")
|
||||
else:
|
||||
logger.warning(f"API响应中没有result字段: {response.json_content}")
|
||||
else:
|
||||
logger.warning(f"API调用未返回成功状态码或JSON内容: {response.status_code}, {response.content}")
|
||||
except Exception as e:
|
||||
logger.error(f"创建地震体过程中发生错误: {str(e)}")
|
||||
# 不触发测试失败,只记录错误,因为这是探索性测试
|
||||
|
||||
def test_query_sample_seismic(self):
|
||||
"""查询样例地震体信息"""
|
||||
# 使用预定义的样例ID,通常由服务器在启动时创建
|
||||
sample_id = "20221113181927_1"
|
||||
|
||||
# 尝试对这个ID进行多种查询操作
|
||||
self._try_query_trace_count(sample_id)
|
||||
self._try_query_h3200(sample_id)
|
||||
self._try_query_h400_keywords(sample_id)
|
||||
|
||||
def _try_query_trace_count(self, seismic_id):
|
||||
"""尝试查询地震体道数"""
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{REMOTE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/traces/count",
|
||||
body={"seismicId": seismic_id}
|
||||
)
|
||||
|
||||
try:
|
||||
response = self.api_caller.call_api(request)
|
||||
logger.info(f"查询道数API调用返回状态码: {response.status_code}")
|
||||
|
||||
if 200 <= response.status_code < 300 and response.json_content:
|
||||
result = response.json_content.get("result", "未知")
|
||||
logger.info(f"地震体 {seismic_id} 的道数: {result}")
|
||||
except Exception as e:
|
||||
logger.warning(f"查询道数失败: {str(e)}")
|
||||
|
||||
def _try_query_h3200(self, seismic_id):
|
||||
"""尝试查询地震体3200头"""
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{REMOTE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/head/h3200",
|
||||
body={"seismicId": seismic_id}
|
||||
)
|
||||
|
||||
try:
|
||||
response = self.api_caller.call_api(request)
|
||||
logger.info(f"查询3200头API调用返回状态码: {response.status_code}")
|
||||
|
||||
if 200 <= response.status_code < 300:
|
||||
if response.content:
|
||||
logger.info(f"地震体 {seismic_id} 的3200头数据长度: {len(response.content)} 字节")
|
||||
except Exception as e:
|
||||
logger.warning(f"查询3200头失败: {str(e)}")
|
||||
|
||||
def _try_query_h400_keywords(self, seismic_id):
|
||||
"""尝试查询地震体卷头关键字"""
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{REMOTE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/h400/keyword/list",
|
||||
body={"seismicId": seismic_id}
|
||||
)
|
||||
|
||||
try:
|
||||
response = self.api_caller.call_api(request)
|
||||
logger.info(f"查询卷头关键字API调用返回状态码: {response.status_code}")
|
||||
|
||||
if 200 <= response.status_code < 300 and response.json_content:
|
||||
keywords = response.json_content.get("result", [])
|
||||
logger.info(f"地震体 {seismic_id} 的卷头关键字数量: {len(keywords)}")
|
||||
if keywords:
|
||||
logger.info(f"第一个关键字: {keywords[0]}")
|
||||
except Exception as e:
|
||||
logger.warning(f"查询卷头关键字失败: {str(e)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -1,311 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
远程地震体 API 直接测试
|
||||
|
||||
使用 requests 库直接调用远程地震体 API 服务,
|
||||
不通过 APICaller 类,以测试基本功能。
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import unittest
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 远程API服务器地址
|
||||
REMOTE_API_SERVER = "http://localhost:5001"
|
||||
|
||||
class TestRemoteSeismicAPIDirect(unittest.TestCase):
|
||||
"""远程地震体 API 直接测试类"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""测试类开始前检查API服务器是否可用"""
|
||||
cls.server_available = False
|
||||
|
||||
try:
|
||||
# 尝试连接到服务器,只要能连接就认为服务器在运行
|
||||
response = requests.get(f"{REMOTE_API_SERVER}/", timeout=2)
|
||||
cls.server_available = True
|
||||
logger.info(f"远程API服务器 {REMOTE_API_SERVER} 可访问,HTTP状态码: {response.status_code}")
|
||||
except Exception as e:
|
||||
logger.warning(f"无法连接到远程API服务器 {REMOTE_API_SERVER}: {str(e)}")
|
||||
cls.server_available = False
|
||||
|
||||
def setUp(self):
|
||||
"""为每个测试初始化环境"""
|
||||
if not self.__class__.server_available:
|
||||
self.skipTest(f"远程API服务器 {REMOTE_API_SERVER} 不可用")
|
||||
|
||||
# 保存测试中创建的资源ID,以便后续测试或清理
|
||||
self.created_resource_ids = []
|
||||
|
||||
def test_create_seismic_file(self):
|
||||
"""测试创建地震体文件"""
|
||||
# 准备创建地震体的请求数据
|
||||
seismic_data = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "测试地震体-直接调用",
|
||||
"dsType": 1,
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 2,
|
||||
"dimensionName": "xline",
|
||||
"serviceMin": 200,
|
||||
"serviceMax": 600,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 3,
|
||||
"dimensionName": "slice",
|
||||
"serviceMin": 3500,
|
||||
"serviceMax": 3600,
|
||||
"serviceSpan": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
try:
|
||||
# 直接使用requests库调用API
|
||||
response = requests.post(
|
||||
f"{REMOTE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/file/add",
|
||||
json=seismic_data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 验证响应状态码
|
||||
self.assertEqual(response.status_code, 200)
|
||||
logger.info(f"创建地震体API调用返回状态码: {response.status_code}")
|
||||
|
||||
# 解析响应JSON
|
||||
response_json = response.json()
|
||||
self.assertIn("result", response_json)
|
||||
|
||||
# 保存创建的ID用于后续测试
|
||||
created_id = response_json.get("result")
|
||||
if created_id:
|
||||
self.created_resource_ids.append(created_id)
|
||||
logger.info(f"成功创建地震体,ID: {created_id}")
|
||||
|
||||
# 验证响应的其他字段
|
||||
self.assertEqual(response_json.get("code"), "0")
|
||||
self.assertTrue(response_json.get("flag", False))
|
||||
except Exception as e:
|
||||
logger.error(f"创建地震体过程中发生错误: {str(e)}")
|
||||
raise
|
||||
|
||||
def test_query_trace_count(self):
|
||||
"""测试查询地震体道数"""
|
||||
# 使用样例ID
|
||||
sample_id = "20221113181927_1"
|
||||
|
||||
try:
|
||||
# 直接使用requests库调用API
|
||||
response = requests.post(
|
||||
f"{REMOTE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/traces/count",
|
||||
json={"seismicId": sample_id},
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 验证响应状态码
|
||||
self.assertEqual(response.status_code, 200)
|
||||
logger.info(f"查询道数API调用返回状态码: {response.status_code}")
|
||||
|
||||
# 解析响应JSON
|
||||
response_json = response.json()
|
||||
self.assertIn("result", response_json)
|
||||
self.assertIn("code", response_json)
|
||||
self.assertIn("msg", response_json)
|
||||
|
||||
trace_count = response_json.get("result")
|
||||
logger.info(f"地震体 {sample_id} 的道数: {trace_count}")
|
||||
except Exception as e:
|
||||
logger.error(f"查询道数过程中发生错误: {str(e)}")
|
||||
raise
|
||||
|
||||
def test_query_h3200_header(self):
|
||||
"""测试查询地震体3200头"""
|
||||
# 使用样例ID
|
||||
sample_id = "20221113181927_1"
|
||||
|
||||
try:
|
||||
# 直接使用requests库调用API
|
||||
response = requests.post(
|
||||
f"{REMOTE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/head/h3200",
|
||||
json={"seismicId": sample_id},
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 验证响应状态码
|
||||
self.assertEqual(response.status_code, 200)
|
||||
logger.info(f"查询3200头API调用返回状态码: {response.status_code}")
|
||||
|
||||
# 验证返回了二进制数据
|
||||
self.assertTrue(len(response.content) > 0)
|
||||
logger.info(f"地震体 {sample_id} 的3200头数据长度: {len(response.content)} 字节")
|
||||
except Exception as e:
|
||||
logger.error(f"查询3200头过程中发生错误: {str(e)}")
|
||||
raise
|
||||
|
||||
def test_query_h400_keywords(self):
|
||||
"""测试查询地震体卷头关键字列表"""
|
||||
# 使用样例ID
|
||||
sample_id = "20221113181927_1"
|
||||
|
||||
try:
|
||||
# 直接使用requests库调用API
|
||||
response = requests.post(
|
||||
f"{REMOTE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/h400/keyword/list",
|
||||
json={"seismicId": sample_id},
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 验证响应状态码
|
||||
self.assertEqual(response.status_code, 200)
|
||||
logger.info(f"查询卷头关键字API调用返回状态码: {response.status_code}")
|
||||
|
||||
# 解析响应JSON
|
||||
response_json = response.json()
|
||||
self.assertIn("result", response_json)
|
||||
self.assertTrue(response_json.get("flag", False))
|
||||
|
||||
keywords = response_json.get("result", [])
|
||||
logger.info(f"地震体 {sample_id} 的卷头关键字数量: {len(keywords)}")
|
||||
if keywords:
|
||||
logger.info(f"第一个关键字: {keywords[0]}")
|
||||
except Exception as e:
|
||||
logger.error(f"查询卷头关键字过程中发生错误: {str(e)}")
|
||||
raise
|
||||
|
||||
def test_coordinate_conversion(self):
|
||||
"""测试坐标转换"""
|
||||
# 使用样例ID
|
||||
sample_id = "20221113181927_1"
|
||||
|
||||
# 测试点坐标
|
||||
test_points = [
|
||||
[606406.1281141682, 6082083.338731234], # 模拟服务器样例中的一个坐标
|
||||
[609767.8725899048, 6080336.549935018],
|
||||
[615271.9052119441, 6082017.422172886]
|
||||
]
|
||||
|
||||
try:
|
||||
# 直接使用requests库调用API
|
||||
response = requests.post(
|
||||
f"{REMOTE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/coordinate/geodetic/toline",
|
||||
json={
|
||||
"seismicId": sample_id,
|
||||
"points": test_points
|
||||
},
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 验证响应状态码
|
||||
self.assertEqual(response.status_code, 200)
|
||||
logger.info(f"坐标转换API调用返回状态码: {response.status_code}")
|
||||
|
||||
# 解析响应JSON
|
||||
response_json = response.json()
|
||||
self.assertIn("result", response_json)
|
||||
|
||||
result = response_json.get("result", [])
|
||||
self.assertEqual(len(result), len(test_points))
|
||||
|
||||
# 记录转换结果
|
||||
for i, (point, line_point) in enumerate(zip(test_points, result)):
|
||||
logger.info(f"坐标点 {i+1}: {point} → 线点: {line_point}")
|
||||
except Exception as e:
|
||||
logger.error(f"坐标转换过程中发生错误: {str(e)}")
|
||||
raise
|
||||
|
||||
def test_export_task(self):
|
||||
"""测试导出地震体任务"""
|
||||
# 使用样例ID
|
||||
sample_id = "20221113181927_1"
|
||||
|
||||
try:
|
||||
# 1. 提交导出任务
|
||||
submit_response = requests.post(
|
||||
f"{REMOTE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/export/submit",
|
||||
json={
|
||||
"seismicId": sample_id,
|
||||
"saveDir": f"/export/{sample_id}.sgy"
|
||||
},
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 验证响应状态码
|
||||
self.assertEqual(submit_response.status_code, 200)
|
||||
logger.info(f"提交导出任务API调用返回状态码: {submit_response.status_code}")
|
||||
|
||||
# 解析响应JSON
|
||||
submit_json = submit_response.json()
|
||||
self.assertEqual(submit_json.get("code"), "0")
|
||||
self.assertTrue(submit_json.get("flag", False))
|
||||
self.assertIn("result", submit_json)
|
||||
|
||||
# 获取任务ID
|
||||
task_result = submit_json.get("result", {})
|
||||
if isinstance(task_result, dict) and "taskId" in task_result:
|
||||
task_id = task_result["taskId"]
|
||||
else:
|
||||
task_id = task_result # 某些实现可能直接返回任务ID
|
||||
|
||||
self.assertIsNotNone(task_id, "导出任务ID不应为空")
|
||||
logger.info(f"创建导出任务成功,任务ID: {task_id}")
|
||||
|
||||
# 2. 查询导出进度
|
||||
if task_id:
|
||||
progress_response = requests.get(
|
||||
f"{REMOTE_API_SERVER}/api/gsc/appmodel/api/v1/seismic/export/progress?taskId={task_id}",
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 验证响应状态码
|
||||
self.assertEqual(progress_response.status_code, 200)
|
||||
logger.info(f"查询导出进度API调用返回状态码: {progress_response.status_code}")
|
||||
|
||||
# 解析响应JSON
|
||||
progress_json = progress_response.json()
|
||||
self.assertEqual(progress_json.get("code"), "0")
|
||||
self.assertTrue(progress_json.get("flag", False))
|
||||
|
||||
# 验证进度信息
|
||||
progress_result = progress_json.get("result", {})
|
||||
if isinstance(progress_result, dict):
|
||||
self.assertIn("status", progress_result)
|
||||
self.assertIn("progress", progress_result)
|
||||
logger.info(f"导出任务 {task_id} 状态: {progress_result.get('status')}, 进度: {progress_result.get('progress')}%")
|
||||
except Exception as e:
|
||||
logger.error(f"导出任务测试过程中发生错误: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -1,566 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
地震体模拟API集成测试
|
||||
|
||||
本测试模块验证与地震体模拟API服务器的集成测试,
|
||||
确保API调用器和JSON Schema验证器能够正确处理地震体数据API。
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import threading
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from ddms_compliance_suite.api_caller.caller import APICaller, APIRequest
|
||||
from ddms_compliance_suite.json_schema_validator.validator import JSONSchemaValidator
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# API服务器地址
|
||||
MOCK_API_SERVER = "http://localhost:5001"
|
||||
|
||||
class TestSeismicAPIIntegration(unittest.TestCase):
|
||||
"""地震体模拟API集成测试类"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""在所有测试前启动模拟API服务器"""
|
||||
# 尝试启动模拟服务器
|
||||
cls.server_process = None
|
||||
cls.server_running = False
|
||||
|
||||
try:
|
||||
# 检查服务器是否已经在运行
|
||||
import requests
|
||||
try:
|
||||
response = requests.get(f"{MOCK_API_SERVER}/", timeout=0.5)
|
||||
cls.server_running = True
|
||||
logger.info("模拟API服务器已经在运行")
|
||||
except:
|
||||
# 服务器没有运行,需要启动
|
||||
logger.info("正在启动模拟API服务器...")
|
||||
|
||||
# 启动模拟服务器的线程
|
||||
def run_server():
|
||||
try:
|
||||
project_root = Path(__file__).resolve().parents[1]
|
||||
process = subprocess.Popen(
|
||||
[sys.executable, '-m', 'tests.run_mock_seismic_api'],
|
||||
cwd=str(project_root),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
cls.server_process = process
|
||||
logger.info("模拟API服务器启动成功!")
|
||||
except Exception as e:
|
||||
logger.error(f"启动模拟API服务器失败: {str(e)}")
|
||||
|
||||
# 在后台线程中启动服务器
|
||||
server_thread = threading.Thread(target=run_server)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
# 等待服务器启动
|
||||
max_retries = 10
|
||||
retry_count = 0
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
time.sleep(1) # 等待一秒
|
||||
response = requests.get(f"{MOCK_API_SERVER}/", timeout=0.5)
|
||||
cls.server_running = True
|
||||
logger.info("模拟API服务器已启动并可用")
|
||||
break
|
||||
except:
|
||||
retry_count += 1
|
||||
logger.info(f"等待模拟API服务器启动... ({retry_count}/{max_retries})")
|
||||
|
||||
if not cls.server_running:
|
||||
logger.warning("无法确认模拟API服务器是否成功启动")
|
||||
except Exception as e:
|
||||
logger.error(f"设置模拟API服务器时出错: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""在所有测试结束后关闭模拟API服务器"""
|
||||
if cls.server_process:
|
||||
try:
|
||||
cls.server_process.terminate()
|
||||
cls.server_process.wait(timeout=5)
|
||||
logger.info("模拟API服务器已关闭")
|
||||
except Exception as e:
|
||||
logger.error(f"关闭模拟API服务器时出错: {str(e)}")
|
||||
|
||||
def setUp(self):
|
||||
"""测试前的设置"""
|
||||
# 如果服务器未运行,跳过测试
|
||||
if not self.__class__.server_running:
|
||||
self.skipTest("模拟API服务器未运行")
|
||||
|
||||
# 创建API调用器
|
||||
self.api_caller = APICaller(
|
||||
default_timeout=10,
|
||||
default_headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
# 创建JSON Schema验证器
|
||||
self.schema_validator = JSONSchemaValidator()
|
||||
|
||||
# 地震体数据的JSON Schema
|
||||
self.seismic_data_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["projectId", "surveyId", "seismicName", "dsType", "dimensions"],
|
||||
"properties": {
|
||||
"projectId": {
|
||||
"type": "string",
|
||||
"description": "项目ID"
|
||||
},
|
||||
"surveyId": {
|
||||
"type": "string",
|
||||
"description": "测量ID"
|
||||
},
|
||||
"seismicName": {
|
||||
"type": "string",
|
||||
"description": "地震体名称"
|
||||
},
|
||||
"dsType": {
|
||||
"type": "integer",
|
||||
"enum": [1, 2],
|
||||
"description": "数据集类型: 1 为基础地震体, 2 为属性体"
|
||||
},
|
||||
"dimensions": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["dimensionNo", "dimensionName", "serviceMin", "serviceMax"],
|
||||
"properties": {
|
||||
"dimensionNo": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "维度编号"
|
||||
},
|
||||
"dimensionName": {
|
||||
"type": "string",
|
||||
"description": "维度名称"
|
||||
},
|
||||
"serviceMin": {
|
||||
"type": "integer",
|
||||
"description": "最小值"
|
||||
},
|
||||
"serviceMax": {
|
||||
"type": "integer",
|
||||
"description": "最大值"
|
||||
},
|
||||
"serviceSpan": {
|
||||
"type": "integer",
|
||||
"description": "采样间隔"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": { "dsType": { "enum": [2] } },
|
||||
"required": ["dsType"]
|
||||
},
|
||||
"then": {
|
||||
"required": ["baseSeismicId"],
|
||||
"properties": {
|
||||
"baseSeismicId": {
|
||||
"type": "string",
|
||||
"description": "属性体必须的基础地震体标识符"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# API响应的JSON Schema
|
||||
self.api_response_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {"type": ["string", "integer"]},
|
||||
"flag": {"type": "boolean"},
|
||||
"msg": {"type": "string"},
|
||||
"result": {} # 允许任何类型的结果
|
||||
}
|
||||
}
|
||||
|
||||
# 测试数据集
|
||||
self.valid_seismic_data = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "西部地震体-01",
|
||||
"dsType": 1,
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 2,
|
||||
"dimensionName": "xline",
|
||||
"serviceMin": 200,
|
||||
"serviceMax": 600,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 3,
|
||||
"dimensionName": "slice",
|
||||
"serviceMin": 3500,
|
||||
"serviceMax": 3600,
|
||||
"serviceSpan": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
self.attribute_seismic_data = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "振幅属性体",
|
||||
"dsType": 2,
|
||||
"baseSeismicId": "20221113181927_1", # 假设这是有效的基础地震体ID
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 2,
|
||||
"dimensionName": "xline",
|
||||
"serviceMin": 200,
|
||||
"serviceMax": 600,
|
||||
"serviceSpan": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
self.invalid_seismic_data = {
|
||||
"projectId": "testPrj1",
|
||||
"seismicName": "缺少必填字段",
|
||||
"dsType": 1, # 缺少 surveyId
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 存储测试过程中创建的资源ID
|
||||
self.created_resources = []
|
||||
|
||||
def test_server_connection(self):
|
||||
"""测试与模拟服务器的连接"""
|
||||
import requests
|
||||
try:
|
||||
response = requests.get(f"{MOCK_API_SERVER}/", timeout=2)
|
||||
# 即使状态码是404,但只要能连接到服务器就算成功
|
||||
logger.info(f"服务器连接测试 - 状态码: {response.status_code}")
|
||||
self.assertTrue(True, "成功连接到模拟API服务器")
|
||||
except Exception as e:
|
||||
self.fail(f"连接到模拟API服务器失败: {str(e)}")
|
||||
|
||||
def test_add_valid_seismic_file(self):
|
||||
"""测试添加有效的地震体文件"""
|
||||
# 先验证数据结构符合Schema
|
||||
validation_result = self.schema_validator.validate(
|
||||
self.valid_seismic_data,
|
||||
self.seismic_data_schema
|
||||
)
|
||||
self.assertTrue(validation_result.is_valid, f"数据验证失败: {validation_result.errors}")
|
||||
|
||||
# 创建API请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{MOCK_API_SERVER}/api/gsc/appmodel/api/v1/seismic/file/add",
|
||||
body=self.valid_seismic_data
|
||||
)
|
||||
|
||||
# 调用API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证响应状态码
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 验证响应符合Schema
|
||||
response_validation = self.schema_validator.validate(
|
||||
response.json_content,
|
||||
self.api_response_schema
|
||||
)
|
||||
self.assertTrue(response_validation.is_valid, f"响应验证失败: {response_validation.errors}")
|
||||
|
||||
# 验证业务逻辑
|
||||
self.assertTrue(response.json_content.get("flag", False))
|
||||
self.assertEqual(response.json_content.get("code"), "0")
|
||||
|
||||
# 保存创建的资源ID
|
||||
result_id = response.json_content.get("result")
|
||||
if result_id:
|
||||
self.created_resources.append(result_id)
|
||||
logger.info(f"创建地震体成功,ID: {result_id}")
|
||||
|
||||
def test_add_attribute_seismic_file(self):
|
||||
"""测试添加属性体文件"""
|
||||
# 先验证数据结构符合Schema
|
||||
validation_result = self.schema_validator.validate(
|
||||
self.attribute_seismic_data,
|
||||
self.seismic_data_schema
|
||||
)
|
||||
self.assertTrue(validation_result.is_valid, f"数据验证失败: {validation_result.errors}")
|
||||
|
||||
# 创建API请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{MOCK_API_SERVER}/api/gsc/appmodel/api/v1/seismic/file/add",
|
||||
body=self.attribute_seismic_data
|
||||
)
|
||||
|
||||
# 调用API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证响应状态码
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 验证响应符合Schema
|
||||
response_validation = self.schema_validator.validate(
|
||||
response.json_content,
|
||||
self.api_response_schema
|
||||
)
|
||||
self.assertTrue(response_validation.is_valid, f"响应验证失败: {response_validation.errors}")
|
||||
|
||||
# 验证业务逻辑
|
||||
self.assertTrue(response.json_content.get("flag", False))
|
||||
self.assertEqual(response.json_content.get("code"), "0")
|
||||
|
||||
# 保存创建的资源ID
|
||||
result_id = response.json_content.get("result")
|
||||
if result_id:
|
||||
self.created_resources.append(result_id)
|
||||
logger.info(f"创建属性体成功,ID: {result_id}")
|
||||
|
||||
def test_add_invalid_seismic_file(self):
|
||||
"""测试添加无效的地震体文件"""
|
||||
# 先验证数据结构不符合Schema
|
||||
validation_result = self.schema_validator.validate(
|
||||
self.invalid_seismic_data,
|
||||
self.seismic_data_schema
|
||||
)
|
||||
self.assertFalse(validation_result.is_valid, "无效数据居然通过了Schema验证")
|
||||
|
||||
# 创建API请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{MOCK_API_SERVER}/api/gsc/appmodel/api/v1/seismic/file/add",
|
||||
body=self.invalid_seismic_data
|
||||
)
|
||||
|
||||
# 调用API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证响应状态码
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 注意:模拟服务器可能不严格执行验证,此处仅验证API调用能够成功完成
|
||||
logger.info(f"添加无效地震体返回: {response.json_content}")
|
||||
|
||||
def test_query_trace_count(self):
|
||||
"""测试查询地震体道数"""
|
||||
# 使用预定义的样例ID
|
||||
sample_id = "20221113181927_1"
|
||||
|
||||
# 创建API请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{MOCK_API_SERVER}/api/gsc/appmodel/api/v1/seismic/traces/count",
|
||||
body={"seismicId": sample_id}
|
||||
)
|
||||
|
||||
# 调用API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证响应状态码
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 模拟服务器可能返回不同格式的响应,此处仅检查API调用成功
|
||||
logger.info(f"查询道数响应: {response.json_content}")
|
||||
|
||||
# 验证结果包含信息(可能是数字或其他)
|
||||
self.assertIn("result", response.json_content)
|
||||
result = response.json_content.get("result")
|
||||
logger.info(f"地震体 {sample_id} 的道数: {result}")
|
||||
|
||||
def test_coordinate_conversion(self):
|
||||
"""测试坐标转换"""
|
||||
# 使用预定义的样例ID和坐标点
|
||||
sample_id = "20221113181927_1"
|
||||
test_points = [
|
||||
[606406.1281141682, 6082083.338731234],
|
||||
[609767.8725899048, 6080336.549935018],
|
||||
[615271.9052119441, 6082017.422172886]
|
||||
]
|
||||
|
||||
# 创建API请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{MOCK_API_SERVER}/api/gsc/appmodel/api/v1/seismic/coordinate/geodetic/toline",
|
||||
body={
|
||||
"seismicId": sample_id,
|
||||
"points": test_points
|
||||
}
|
||||
)
|
||||
|
||||
# 调用API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证响应状态码
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 模拟服务器可能有不同的响应格式,记录响应信息
|
||||
logger.info(f"坐标转换响应: {response.json_content}")
|
||||
|
||||
# 验证结果包含信息
|
||||
self.assertIn("result", response.json_content)
|
||||
result = response.json_content.get("result", [])
|
||||
|
||||
# 记录转换结果
|
||||
logger.info(f"坐标转换结果: {result}")
|
||||
|
||||
def test_export_task(self):
|
||||
"""测试导出地震体任务"""
|
||||
# 使用预定义的样例ID
|
||||
sample_id = "20221113181927_1"
|
||||
|
||||
# 创建API请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{MOCK_API_SERVER}/api/gsc/appmodel/api/v1/seismic/export/submit",
|
||||
body={
|
||||
"seismicId": sample_id,
|
||||
"saveDir": f"/export/{sample_id}.sgy"
|
||||
}
|
||||
)
|
||||
|
||||
# 调用API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证响应状态码
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 验证响应符合Schema
|
||||
response_validation = self.schema_validator.validate(
|
||||
response.json_content,
|
||||
self.api_response_schema
|
||||
)
|
||||
self.assertTrue(response_validation.is_valid, f"响应验证失败: {response_validation.errors}")
|
||||
|
||||
# 验证业务逻辑
|
||||
self.assertTrue(response.json_content.get("flag", False))
|
||||
self.assertEqual(response.json_content.get("code"), "0")
|
||||
|
||||
# 验证结果包含任务ID
|
||||
result = response.json_content.get("result", {})
|
||||
task_id = None
|
||||
if isinstance(result, dict) and "taskId" in result:
|
||||
task_id = result.get("taskId")
|
||||
else:
|
||||
task_id = result # 某些实现可能直接返回任务ID
|
||||
|
||||
self.assertIsNotNone(task_id, "任务ID不应为空")
|
||||
logger.info(f"导出任务ID: {task_id}")
|
||||
|
||||
# 查询任务进度
|
||||
if task_id:
|
||||
# 等待一小段时间以确保任务已经开始处理
|
||||
time.sleep(0.5)
|
||||
|
||||
# 创建API请求
|
||||
progress_request = APIRequest(
|
||||
method="GET",
|
||||
url=f"{MOCK_API_SERVER}/api/gsc/appmodel/api/v1/seismic/export/progress?taskId={task_id}"
|
||||
)
|
||||
|
||||
# 调用API
|
||||
progress_response = self.api_caller.call_api(progress_request)
|
||||
|
||||
# 验证响应状态码
|
||||
self.assertEqual(progress_response.status_code, 200)
|
||||
self.assertIsNotNone(progress_response.json_content)
|
||||
|
||||
# 验证业务逻辑
|
||||
self.assertTrue(progress_response.json_content.get("flag", False))
|
||||
|
||||
# 验证结果包含进度信息
|
||||
progress_result = progress_response.json_content.get("result", {})
|
||||
self.assertIn("status", progress_result)
|
||||
self.assertIn("progress", progress_result)
|
||||
|
||||
logger.info(f"导出任务 {task_id} 状态: {progress_result.get('status')}, 进度: {progress_result.get('progress')}%")
|
||||
|
||||
def test_h400_keywords(self):
|
||||
"""测试查询地震体卷头关键字"""
|
||||
# 使用预定义的样例ID
|
||||
sample_id = "20221113181927_1"
|
||||
|
||||
# 创建API请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{MOCK_API_SERVER}/api/gsc/appmodel/api/v1/seismic/h400/keyword/list",
|
||||
body={"seismicId": sample_id}
|
||||
)
|
||||
|
||||
# 调用API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证响应状态码
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 记录响应信息
|
||||
logger.info(f"卷头关键字响应: {response.json_content}")
|
||||
|
||||
# 验证结果包含关键字列表
|
||||
self.assertIn("result", response.json_content)
|
||||
keywords = response.json_content.get("result", [])
|
||||
logger.info(f"地震体 {sample_id} 的卷头关键字数量: {len(keywords)}")
|
||||
if isinstance(keywords, list) and keywords:
|
||||
logger.info(f"第一个关键字: {keywords[0]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -1,517 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
地震体数据Schema验证和API集成测试
|
||||
|
||||
测试地震体数据的Schema验证和API调用的集成,
|
||||
确保Schema验证器和API调用器能够协同工作,正确处理地震体数据。
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from ddms_compliance_suite.api_caller.caller import APICaller, APIRequest, APIResponse
|
||||
from ddms_compliance_suite.json_schema_validator.validator import JSONSchemaValidator
|
||||
from ddms_compliance_suite.rule_repository.repository import RuleRepository
|
||||
from ddms_compliance_suite.models.config_models import RuleRepositoryConfig
|
||||
from ddms_compliance_suite.models.rule_models import TargetType
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 模拟API服务器
|
||||
MOCK_API_SERVER = "http://localhost:5001"
|
||||
|
||||
class MockResponse:
|
||||
"""模拟API响应对象"""
|
||||
|
||||
def __init__(self, status_code, data=None, content=None, headers=None, elapsed_time=0.1):
|
||||
self.status_code = status_code
|
||||
self.data = data
|
||||
self.content = content if content is not None else (json.dumps(data).encode() if data else b'')
|
||||
self.headers = headers or {"Content-Type": "application/json"}
|
||||
self.elapsed = mock.Mock()
|
||||
self.elapsed.total_seconds.return_value = elapsed_time
|
||||
|
||||
def json(self):
|
||||
"""获取JSON内容"""
|
||||
if isinstance(self.data, dict):
|
||||
return self.data
|
||||
raise ValueError("Response does not contain valid JSON")
|
||||
|
||||
|
||||
class TestSeismicSchemaValidation(unittest.TestCase):
|
||||
"""地震体数据Schema验证和API集成测试类"""
|
||||
|
||||
def setUp(self):
|
||||
"""测试前的设置"""
|
||||
# 创建规则仓库
|
||||
config = RuleRepositoryConfig(
|
||||
storage={"type": "filesystem", "path": "./rules"},
|
||||
preload_rules=True
|
||||
)
|
||||
self.rule_repository = RuleRepository(config)
|
||||
|
||||
# 创建JSON Schema验证器
|
||||
self.schema_validator = JSONSchemaValidator()
|
||||
|
||||
# 创建API调用器
|
||||
self.api_caller = APICaller(
|
||||
default_timeout=10,
|
||||
default_headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
# 从规则仓库加载Schema
|
||||
self.seismic_data_schema = self.rule_repository.get_schema_for_target(
|
||||
TargetType.DATA_OBJECT, "Seismic"
|
||||
)
|
||||
if not self.seismic_data_schema:
|
||||
self.fail("无法加载地震体数据Schema,请确保rules/json_schemas/seismic-data-schema目录中存在有效的Schema文件")
|
||||
|
||||
# 从规则仓库加载API响应Schema
|
||||
self.api_response_schema = self.rule_repository.get_schema_for_target(
|
||||
TargetType.API_RESPONSE, "SeismicAPIResponse"
|
||||
)
|
||||
if not self.api_response_schema:
|
||||
logger.warning("无法加载API响应Schema,将使用默认定义")
|
||||
# 默认的API响应Schema
|
||||
self.api_response_schema = {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["code", "flag", "msg", "result"],
|
||||
"properties": {
|
||||
"code": {"type": ["string", "integer"]},
|
||||
"flag": {"type": "boolean"},
|
||||
"msg": {"type": "string"},
|
||||
"result": {"type": ["string", "null", "object"]}
|
||||
}
|
||||
}
|
||||
|
||||
# 测试数据集
|
||||
self.valid_seismic_data = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "西部地震体-01",
|
||||
"dsType": 1,
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 2,
|
||||
"dimensionName": "xline",
|
||||
"serviceMin": 200,
|
||||
"serviceMax": 600,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 3,
|
||||
"dimensionName": "slice",
|
||||
"serviceMin": 3500,
|
||||
"serviceMax": 3600,
|
||||
"serviceSpan": 4
|
||||
}
|
||||
],
|
||||
"sampleRate": 2.0,
|
||||
"dataRange": {
|
||||
"min": -10000,
|
||||
"max": 10000
|
||||
},
|
||||
"coordinates": [
|
||||
[116.3833, 39.9167],
|
||||
[116.4024, 39.9008],
|
||||
[116.4100, 39.9200]
|
||||
]
|
||||
}
|
||||
|
||||
self.attribute_seismic_data = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "振幅属性体",
|
||||
"dsType": 2,
|
||||
"baseSeismicId": "20221113181927_1",
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 2,
|
||||
"dimensionName": "xline",
|
||||
"serviceMin": 200,
|
||||
"serviceMax": 600,
|
||||
"serviceSpan": 1
|
||||
}
|
||||
],
|
||||
"sampleRate": 2.0,
|
||||
"dataRange": {
|
||||
"min": 0,
|
||||
"max": 255
|
||||
}
|
||||
}
|
||||
|
||||
self.invalid_dimension_data = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "错误维度地震体",
|
||||
"dsType": 1,
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 500,
|
||||
"serviceMax": 100, # 最小值大于最大值,应该报错
|
||||
"serviceSpan": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
self.duplicate_dimension_data = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "重复维度地震体",
|
||||
"dsType": 1,
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
},
|
||||
{
|
||||
"dimensionNo": 1, # 重复的维度编号
|
||||
"dimensionName": "xline",
|
||||
"serviceMin": 200,
|
||||
"serviceMax": 600,
|
||||
"serviceSpan": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
self.missing_required_data = {
|
||||
"projectId": "testPrj1",
|
||||
"seismicName": "缺少必填字段",
|
||||
"dsType": 1, # 缺少 surveyId
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
self.missing_baseid_data = {
|
||||
"projectId": "testPrj1",
|
||||
"surveyId": "20230117135924_2",
|
||||
"seismicName": "缺少基础ID的属性体",
|
||||
"dsType": 2, # 属性体类型,但缺少 baseSeismicId
|
||||
"dimensions": [
|
||||
{
|
||||
"dimensionNo": 1,
|
||||
"dimensionName": "inline",
|
||||
"serviceMin": 100,
|
||||
"serviceMax": 500,
|
||||
"serviceSpan": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 模拟API响应
|
||||
self.mock_success_response = {
|
||||
"code": "0",
|
||||
"flag": True,
|
||||
"msg": "操作成功",
|
||||
"result": "20230601123456_1"
|
||||
}
|
||||
|
||||
self.mock_error_response = {
|
||||
"code": "1",
|
||||
"flag": False,
|
||||
"msg": "操作失败:缺少必填字段",
|
||||
"result": None
|
||||
}
|
||||
|
||||
def test_schema_loaded_from_file(self):
|
||||
"""测试从文件加载Schema"""
|
||||
self.assertIsNotNone(self.seismic_data_schema, "地震体数据Schema未加载")
|
||||
self.assertIsInstance(self.seismic_data_schema, dict, "地震体数据Schema不是字典类型")
|
||||
self.assertIn("properties", self.seismic_data_schema, "地震体数据Schema缺少properties字段")
|
||||
self.assertIn("dimensions", self.seismic_data_schema["properties"], "地震体数据Schema缺少dimensions属性")
|
||||
|
||||
logger.info("成功从文件加载地震体数据Schema")
|
||||
|
||||
self.assertIsNotNone(self.api_response_schema, "API响应Schema未加载")
|
||||
self.assertIsInstance(self.api_response_schema, dict, "API响应Schema不是字典类型")
|
||||
self.assertIn("properties", self.api_response_schema, "API响应Schema缺少properties字段")
|
||||
self.assertIn("code", self.api_response_schema["properties"], "API响应Schema缺少code属性")
|
||||
|
||||
logger.info("成功加载API响应Schema")
|
||||
|
||||
@mock.patch('ddms_compliance_suite.api_caller.caller.requests.request')
|
||||
def test_valid_seismic_data_validation(self, mock_request):
|
||||
"""测试验证有效的地震体数据"""
|
||||
# 使用Schema验证器验证数据
|
||||
validation_result = self.schema_validator.validate(
|
||||
self.valid_seismic_data,
|
||||
self.seismic_data_schema
|
||||
)
|
||||
|
||||
# 断言验证结果
|
||||
self.assertTrue(validation_result.is_valid, f"验证失败: {validation_result.errors}")
|
||||
self.assertEqual(len(validation_result.errors), 0)
|
||||
self.assertEqual(len(validation_result.warnings), 0)
|
||||
|
||||
# 配置模拟请求
|
||||
mock_response = MockResponse(200, self.mock_success_response)
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
# 创建API请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{MOCK_API_SERVER}/api/gsc/appmodel/api/v1/seismic/file/add",
|
||||
body=self.valid_seismic_data
|
||||
)
|
||||
|
||||
# 调用API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证模拟方法被调用
|
||||
mock_request.assert_called_once()
|
||||
|
||||
# 验证响应
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 验证响应符合Schema
|
||||
response_validation = self.schema_validator.validate(
|
||||
response.json_content,
|
||||
self.api_response_schema
|
||||
)
|
||||
|
||||
# 断言API响应验证
|
||||
self.assertTrue(response_validation.is_valid, f"API响应验证失败: {response_validation.errors}")
|
||||
self.assertEqual(response.json_content["code"], "0")
|
||||
self.assertTrue(response.json_content["flag"])
|
||||
|
||||
@mock.patch('ddms_compliance_suite.api_caller.caller.requests.request')
|
||||
def test_attribute_seismic_data_validation(self, mock_request):
|
||||
"""测试验证属性体数据"""
|
||||
# 使用Schema验证器验证数据
|
||||
validation_result = self.schema_validator.validate(
|
||||
self.attribute_seismic_data,
|
||||
self.seismic_data_schema
|
||||
)
|
||||
|
||||
# 断言验证结果
|
||||
self.assertTrue(validation_result.is_valid, f"验证失败: {validation_result.errors}")
|
||||
self.assertEqual(len(validation_result.errors), 0)
|
||||
|
||||
# 配置模拟请求
|
||||
mock_response = MockResponse(200, self.mock_success_response)
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
# 创建API请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{MOCK_API_SERVER}/api/gsc/appmodel/api/v1/seismic/file/add",
|
||||
body=self.attribute_seismic_data
|
||||
)
|
||||
|
||||
# 调用API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证响应
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.json_content["flag"])
|
||||
|
||||
def test_invalid_dimension_validation(self):
|
||||
"""测试维度参数无效的验证"""
|
||||
# 使用Schema验证器验证数据
|
||||
validation_result = self.schema_validator.validate(
|
||||
self.invalid_dimension_data,
|
||||
self.seismic_data_schema
|
||||
)
|
||||
|
||||
# 基本Schema验证不会检查 serviceMin < serviceMax,这需要额外验证逻辑
|
||||
# 因此这个测试仅检查基本结构符合Schema
|
||||
self.assertTrue(validation_result.is_valid)
|
||||
|
||||
# 所以在这里我们可以补充一个额外的业务逻辑验证
|
||||
for dimension in self.invalid_dimension_data["dimensions"]:
|
||||
if dimension["serviceMin"] > dimension["serviceMax"]:
|
||||
# 在实际代码中,这应该添加到验证结果的错误列表中
|
||||
logger.error(f"维度 {dimension['dimensionName']} 的最小值({dimension['serviceMin']})大于最大值({dimension['serviceMax']})")
|
||||
validation_result.is_valid = False
|
||||
validation_result.errors.append(f"维度{dimension['dimensionNo']}的最小值不能大于最大值")
|
||||
|
||||
# 断言最终的验证结果
|
||||
self.assertFalse(validation_result.is_valid)
|
||||
self.assertGreater(len(validation_result.errors), 0)
|
||||
|
||||
def test_duplicate_dimension_validation(self):
|
||||
"""测试重复维度编号的验证"""
|
||||
# 使用Schema验证器验证数据
|
||||
validation_result = self.schema_validator.validate(
|
||||
self.duplicate_dimension_data,
|
||||
self.seismic_data_schema
|
||||
)
|
||||
|
||||
# 基本Schema验证不会检查重复的dimensionNo,这需要额外验证逻辑
|
||||
self.assertTrue(validation_result.is_valid)
|
||||
|
||||
# 实现额外的检查
|
||||
dimension_nos = [dim["dimensionNo"] for dim in self.duplicate_dimension_data["dimensions"]]
|
||||
if len(dimension_nos) != len(set(dimension_nos)):
|
||||
# 在实际代码中,这应该添加到验证结果的错误列表中
|
||||
logger.error(f"存在重复的维度编号: {dimension_nos}")
|
||||
validation_result.is_valid = False
|
||||
validation_result.errors.append("维度编号不能重复")
|
||||
|
||||
# 断言最终的验证结果
|
||||
self.assertFalse(validation_result.is_valid)
|
||||
self.assertGreater(len(validation_result.errors), 0)
|
||||
|
||||
def test_missing_required_field_validation(self):
|
||||
"""测试缺少必填字段的验证"""
|
||||
# 使用Schema验证器验证数据
|
||||
validation_result = self.schema_validator.validate(
|
||||
self.missing_required_data,
|
||||
self.seismic_data_schema
|
||||
)
|
||||
|
||||
# 断言验证结果
|
||||
self.assertFalse(validation_result.is_valid)
|
||||
self.assertGreater(len(validation_result.errors), 0)
|
||||
|
||||
# 检查错误消息是否包含缺少的字段
|
||||
has_missing_field_error = any("surveyId" in error for error in validation_result.errors)
|
||||
self.assertTrue(has_missing_field_error, f"错误消息中没有包含缺少的surveyId字段: {validation_result.errors}")
|
||||
|
||||
def test_missing_baseid_for_attribute_validation(self):
|
||||
"""测试缺少基础地震体ID的属性体验证"""
|
||||
# 使用Schema验证器验证数据
|
||||
validation_result = self.schema_validator.validate(
|
||||
self.missing_baseid_data,
|
||||
self.seismic_data_schema
|
||||
)
|
||||
|
||||
# 断言验证结果
|
||||
self.assertFalse(validation_result.is_valid)
|
||||
self.assertGreater(len(validation_result.errors), 0)
|
||||
|
||||
# 检查错误消息是否包含缺少的基础地震体ID
|
||||
has_missing_baseid_error = any("baseSeismicId" in error for error in validation_result.errors)
|
||||
self.assertTrue(has_missing_baseid_error, f"错误消息中没有包含缺少的baseSeismicId: {validation_result.errors}")
|
||||
|
||||
@mock.patch('ddms_compliance_suite.api_caller.caller.requests.request')
|
||||
def test_api_error_response_validation(self, mock_request):
|
||||
"""测试API错误响应的验证"""
|
||||
# 配置模拟请求返回错误响应
|
||||
mock_response = MockResponse(200, self.mock_error_response)
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
# 创建API请求 - 使用缺少必填字段的数据
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{MOCK_API_SERVER}/api/gsc/appmodel/api/v1/seismic/file/add",
|
||||
body=self.missing_required_data
|
||||
)
|
||||
|
||||
# 调用API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证模拟方法被调用
|
||||
mock_request.assert_called_once()
|
||||
|
||||
# 验证响应
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIsNotNone(response.json_content)
|
||||
|
||||
# 验证响应符合Schema但标志为失败
|
||||
response_validation = self.schema_validator.validate(
|
||||
response.json_content,
|
||||
self.api_response_schema
|
||||
)
|
||||
|
||||
# 断言API响应验证
|
||||
self.assertTrue(response_validation.is_valid, f"API响应验证失败: {response_validation.errors}")
|
||||
self.assertEqual(response.json_content["code"], "1")
|
||||
self.assertFalse(response.json_content["flag"])
|
||||
|
||||
@mock.patch('ddms_compliance_suite.api_caller.caller.requests.request')
|
||||
def test_schema_and_api_integration(self, mock_request):
|
||||
"""测试Schema验证和API调用的完整集成"""
|
||||
test_cases = [
|
||||
(self.valid_seismic_data, True, self.mock_success_response),
|
||||
(self.attribute_seismic_data, True, self.mock_success_response),
|
||||
(self.missing_required_data, False, self.mock_error_response),
|
||||
(self.missing_baseid_data, False, self.mock_error_response)
|
||||
]
|
||||
|
||||
for seismic_data, expected_valid, api_response in test_cases:
|
||||
# 1. 验证Schema
|
||||
validation_result = self.schema_validator.validate(
|
||||
seismic_data,
|
||||
self.seismic_data_schema
|
||||
)
|
||||
|
||||
# 断言Schema验证结果符合预期
|
||||
self.assertEqual(validation_result.is_valid, expected_valid,
|
||||
f"Schema验证结果与预期不符: {seismic_data.get('seismicName', 'Unknown')}")
|
||||
|
||||
# 2. 如果Schema验证通过,调用API
|
||||
if validation_result.is_valid:
|
||||
# 配置模拟请求
|
||||
mock_response = MockResponse(200, api_response)
|
||||
mock_request.return_value = mock_response
|
||||
|
||||
# 创建API请求
|
||||
request = APIRequest(
|
||||
method="POST",
|
||||
url=f"{MOCK_API_SERVER}/api/gsc/appmodel/api/v1/seismic/file/add",
|
||||
body=seismic_data
|
||||
)
|
||||
|
||||
# 调用API
|
||||
response = self.api_caller.call_api(request)
|
||||
|
||||
# 验证响应
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json_content["flag"], api_response["flag"])
|
||||
|
||||
# 验证响应符合Schema
|
||||
response_validation = self.schema_validator.validate(
|
||||
response.json_content,
|
||||
self.api_response_schema
|
||||
)
|
||||
|
||||
# 断言API响应验证
|
||||
self.assertTrue(response_validation.is_valid)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
325
tests/test_test_case_registry.py
Normal file
325
tests/test_test_case_registry.py
Normal file
@ -0,0 +1,325 @@
|
||||
import unittest
|
||||
import os
|
||||
import shutil
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
# 调整导入路径以适应测试文件在 tests/ 目录下的情况
|
||||
# 我们假设 tests/ 和 ddms_compliance_suite/ 在同一级别 (项目根目录下)
|
||||
import sys
|
||||
# 获取当前文件 (test_test_case_registry.py) 的目录 (tests/)
|
||||
current_file_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# 获取项目根目录 (tests/ 的上一级)
|
||||
project_root = os.path.dirname(current_file_dir)
|
||||
# 将项目根目录添加到 sys.path 中,以便可以找到 ddms_compliance_suite 包
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
from ddms_compliance_suite.test_case_registry import TestCaseRegistry
|
||||
|
||||
# 为了测试,我们需要一个临时的测试用例目录
|
||||
TEMP_TEST_CASES_DIR = os.path.join(current_file_dir, "temp_custom_testcases_for_registry_test")
|
||||
|
||||
# 禁用 TestCaseRegistry 和 BaseAPITestCase 在测试期间的 INFO 和 DEBUG 日志,除非特意捕获
|
||||
# logging.getLogger("ddms_compliance_suite.test_case_registry").setLevel(logging.WARNING)
|
||||
# logging.getLogger("testcase").setLevel(logging.WARNING) # BaseAPITestCase uses testcase.<id>
|
||||
|
||||
class TestTestCaseRegistry(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""在每个测试方法运行前创建临时测试用例目录。"""
|
||||
if os.path.exists(TEMP_TEST_CASES_DIR):
|
||||
shutil.rmtree(TEMP_TEST_CASES_DIR)
|
||||
os.makedirs(TEMP_TEST_CASES_DIR)
|
||||
self.registry = None # 确保每个测试都重新初始化registry
|
||||
|
||||
def tearDown(self):
|
||||
"""在每个测试方法运行后清理临时测试用例目录。"""
|
||||
if os.path.exists(TEMP_TEST_CASES_DIR):
|
||||
shutil.rmtree(TEMP_TEST_CASES_DIR)
|
||||
|
||||
def _create_test_case_file(self, filename: str, content: str):
|
||||
"""辅助方法,在临时目录中创建测试用例文件。"""
|
||||
with open(os.path.join(TEMP_TEST_CASES_DIR, filename), "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
def test_init_with_non_existent_dir(self):
|
||||
"""测试使用不存在的目录初始化 TestCaseRegistry。"""
|
||||
non_existent_dir = os.path.join(TEMP_TEST_CASES_DIR, "_i_do_not_exist_")
|
||||
with self.assertLogs(level='WARNING') as log_watcher:
|
||||
registry = TestCaseRegistry(test_cases_dir=non_existent_dir)
|
||||
self.assertTrue(any(f"测试用例目录不存在或不是一个目录: {non_existent_dir}" in msg for msg in log_watcher.output))
|
||||
self.assertEqual(len(registry.get_all_test_case_classes()), 0)
|
||||
|
||||
def test_init_with_empty_dir(self):
|
||||
"""测试使用空的目录初始化 TestCaseRegistry。"""
|
||||
registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR)
|
||||
self.assertEqual(len(registry.get_all_test_case_classes()), 0)
|
||||
# 应该有一条INFO日志表明发现完成且数量为0
|
||||
|
||||
def test_discover_single_valid_test_case(self):
|
||||
"""测试发现单个有效的测试用例。"""
|
||||
content = """
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
class MyTest(BaseAPITestCase):
|
||||
id = "TC-001"
|
||||
name = "Test Case 1"
|
||||
description = "Desc 1"
|
||||
severity = TestSeverity.HIGH
|
||||
tags = ["tag1"]
|
||||
"""
|
||||
self._create_test_case_file("test_001.py", content)
|
||||
registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR)
|
||||
all_cases = registry.get_all_test_case_classes()
|
||||
self.assertEqual(len(all_cases), 1)
|
||||
self.assertEqual(all_cases[0].id, "TC-001")
|
||||
self.assertIsNotNone(registry.get_test_case_by_id("TC-001"))
|
||||
|
||||
def test_discover_multiple_test_cases_in_one_file(self):
|
||||
"""测试在单个文件中发现多个测试用例。"""
|
||||
content = """
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
class TestA(BaseAPITestCase):
|
||||
id = "TC-A"
|
||||
name = "A"
|
||||
class TestB(BaseAPITestCase):
|
||||
id = "TC-B"
|
||||
name = "B"
|
||||
"""
|
||||
self._create_test_case_file("test_ab.py", content)
|
||||
registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR)
|
||||
all_cases = registry.get_all_test_case_classes()
|
||||
self.assertEqual(len(all_cases), 2)
|
||||
self.assertIsNotNone(registry.get_test_case_by_id("TC-A"))
|
||||
self.assertIsNotNone(registry.get_test_case_by_id("TC-B"))
|
||||
# 确保顺序与文件中定义的顺序(或至少是可预测的)一致,inspect.getmembers 通常按字母顺序
|
||||
ids = sorted([case.id for case in all_cases])
|
||||
self.assertEqual(ids, ["TC-A", "TC-B"])
|
||||
|
||||
def test_discover_test_cases_in_multiple_files(self):
|
||||
"""测试在多个文件中发现测试用例。"""
|
||||
content1 = """
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
class Test1(BaseAPITestCase):
|
||||
id = "TC-1"
|
||||
"""
|
||||
content2 = """
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
class Test2(BaseAPITestCase):
|
||||
id = "TC-2"
|
||||
"""
|
||||
self._create_test_case_file("file1.py", content1)
|
||||
self._create_test_case_file("file2.py", content2)
|
||||
registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR)
|
||||
all_cases = registry.get_all_test_case_classes()
|
||||
self.assertEqual(len(all_cases), 2)
|
||||
self.assertIsNotNone(registry.get_test_case_by_id("TC-1"))
|
||||
self.assertIsNotNone(registry.get_test_case_by_id("TC-2"))
|
||||
|
||||
def test_ignore_non_py_files_and_dunder_files(self):
|
||||
"""测试忽略非.py文件和以__开头的文件。"""
|
||||
self._create_test_case_file("not_a_test.txt", "text content")
|
||||
self._create_test_case_file("__init__.py", "# I am an init file")
|
||||
content = """
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
class RealTest(BaseAPITestCase):
|
||||
id = "TC-REAL"
|
||||
"""
|
||||
self._create_test_case_file("real_test.py", content)
|
||||
registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR)
|
||||
all_cases = registry.get_all_test_case_classes()
|
||||
self.assertEqual(len(all_cases), 1)
|
||||
self.assertEqual(all_cases[0].id, "TC-REAL")
|
||||
|
||||
def test_handle_import_error_in_test_file(self):
|
||||
"""测试处理测试用例文件中的导入错误。"""
|
||||
content = "import non_existent_module\n" # This will cause ImportError
|
||||
self._create_test_case_file("importerror_test.py", content)
|
||||
with self.assertLogs(level='ERROR') as log_watcher:
|
||||
registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR)
|
||||
self.assertTrue(any("导入模块 'importerror_test' 从" in msg and "失败" in msg for msg in log_watcher.output))
|
||||
self.assertEqual(len(registry.get_all_test_case_classes()), 0)
|
||||
|
||||
def test_handle_attribute_error_missing_id(self):
|
||||
"""测试处理测试用例类缺少 'id' 属性的情况。"""
|
||||
content = """
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
class MissingIdTest(BaseAPITestCase):
|
||||
name = "Missing ID"
|
||||
# id is missing
|
||||
"""
|
||||
self._create_test_case_file("missing_id.py", content)
|
||||
# AttributeError is caught by the generic Exception in discover_test_cases if not directly handled
|
||||
# It might also depend on when/how inspect.getmembers tries to access obj.id
|
||||
# Forcing access here to ensure the test scenario is valid if discovery itself doesn't raise it immediately
|
||||
with self.assertLogs(level='ERROR') as log_watcher:
|
||||
registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR)
|
||||
# The error log in discover_test_cases for AttributeError on obj.id might be tricky to assert precisely
|
||||
# We'll check that no test cases were loaded from this problematic file, but other files might load.
|
||||
self.assertTrue(any("在模块 'missing_id'" in msg and "查找测试用例时出错" in msg for msg in log_watcher.output),
|
||||
msg=f"Did not find expected error log. Logs: {log_watcher.output}")
|
||||
# Ensure no test cases are registered if the only file has this error
|
||||
self.assertEqual(len(registry.get_all_test_case_classes()), 0)
|
||||
|
||||
|
||||
def test_duplicate_test_case_id(self):
|
||||
"""测试发现重复的测试用例 ID。"""
|
||||
content1 = """
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
class TestOne(BaseAPITestCase):
|
||||
id = "DUPLICATE-ID-001"
|
||||
name = "First with ID"
|
||||
"""
|
||||
content2 = """
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
class TestTwo(BaseAPITestCase):
|
||||
id = "DUPLICATE-ID-001" # Same ID
|
||||
name = "Second with ID"
|
||||
"""
|
||||
self._create_test_case_file("file_one.py", content1)
|
||||
self._create_test_case_file("file_two.py", content2)
|
||||
with self.assertLogs(level='WARNING') as log_watcher:
|
||||
registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR)
|
||||
self.assertTrue(any("发现重复的测试用例 ID: 'DUPLICATE-ID-001'" in msg for msg in log_watcher.output))
|
||||
|
||||
all_cases = registry.get_all_test_case_classes()
|
||||
# inspect.getmembers order is not guaranteed across files, so we can't be sure which one is kept.
|
||||
# However, the _registry (by ID) should have only one entry.
|
||||
self.assertEqual(len(registry._registry), 1)
|
||||
# The _test_case_classes list might have two if they are distinct class objects, but one overrides in _registry
|
||||
# Depending on load order, one will overwrite the other in _registry. Let's check the final one.
|
||||
registered_case = registry.get_test_case_by_id("DUPLICATE-ID-001")
|
||||
self.assertIsNotNone(registered_case)
|
||||
# We cannot reliably assert which name ('First with ID' or 'Second with ID') is kept due to file load order.
|
||||
# Check that _test_case_classes might have more if classes are distinct but _registry has one.
|
||||
# If the class objects are truly distinct, len(all_cases) could be 2. The important part is that by ID, only one is retrievable.
|
||||
# A more robust check for `_test_case_classes` would be to ensure it contains the class that `_registry` points to.
|
||||
self.assertIn(registered_case, all_cases)
|
||||
# If the goal is that _test_case_classes should also be unique by some criteria after discovery, that logic would need adjustment.
|
||||
# For now, get_all_test_case_classes returns all *discovered* classes that are BaseAPITestCase subclasses.
|
||||
# And get_test_case_by_id returns the one that won the ID race.
|
||||
# A typical use case iterates get_all_test_case_classes() for filtering, so this list should ideally be clean or documented.
|
||||
# For now, we accept it might contain classes whose IDs were superseded if they are distinct objects.
|
||||
|
||||
def test_get_applicable_test_cases_no_restrictions(self):
|
||||
"""测试 get_applicable_test_cases,当测试用例没有适用性限制时。"""
|
||||
content = """
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
class NoRestrictionTest(BaseAPITestCase):
|
||||
id = "TC-NR-001"
|
||||
name = "No Restrictions"
|
||||
"""
|
||||
self._create_test_case_file("no_restriction.py", content)
|
||||
registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR)
|
||||
applicable = registry.get_applicable_test_cases("GET", "/api/items")
|
||||
self.assertEqual(len(applicable), 1)
|
||||
self.assertEqual(applicable[0].id, "TC-NR-001")
|
||||
|
||||
def test_get_applicable_by_method(self):
|
||||
"""测试根据 applicable_methods 进行筛选。"""
|
||||
content = """
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
class GetOnlyTest(BaseAPITestCase):
|
||||
id = "TC-GET-ONLY"
|
||||
name = "GET Only"
|
||||
applicable_methods = ["GET", "HEAD"]
|
||||
class PostOnlyTest(BaseAPITestCase):
|
||||
id = "TC-POST-ONLY"
|
||||
name = "POST Only"
|
||||
applicable_methods = ["POST"]
|
||||
"""
|
||||
self._create_test_case_file("method_tests.py", content)
|
||||
registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR)
|
||||
|
||||
applicable_get = registry.get_applicable_test_cases("GET", "/api/data")
|
||||
self.assertEqual(len(applicable_get), 1)
|
||||
self.assertEqual(applicable_get[0].id, "TC-GET-ONLY")
|
||||
|
||||
applicable_post = registry.get_applicable_test_cases("POST", "/api/data")
|
||||
self.assertEqual(len(applicable_post), 1)
|
||||
self.assertEqual(applicable_post[0].id, "TC-POST-ONLY")
|
||||
|
||||
applicable_put = registry.get_applicable_test_cases("PUT", "/api/data")
|
||||
self.assertEqual(len(applicable_put), 0)
|
||||
|
||||
applicable_head = registry.get_applicable_test_cases("HEAD", "/api/data") # Case sensitive check for methods in list
|
||||
self.assertEqual(len(applicable_head), 1)
|
||||
self.assertEqual(applicable_head[0].id, "TC-GET-ONLY")
|
||||
|
||||
def test_get_applicable_by_path_regex(self):
|
||||
"""测试根据 applicable_paths_regex 进行筛选。"""
|
||||
content = """
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
class UserPathTest(BaseAPITestCase):
|
||||
id = "TC-USER-PATH"
|
||||
name = "User Path"
|
||||
applicable_paths_regex = r"^/api/users/\\d+$" # Matches /api/users/<number>
|
||||
class OrderPathTest(BaseAPITestCase):
|
||||
id = "TC-ORDER-PATH"
|
||||
name = "Order Path"
|
||||
applicable_paths_regex = r"^/api/orders"
|
||||
"""
|
||||
self._create_test_case_file("path_tests.py", content)
|
||||
registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR)
|
||||
|
||||
applicable_user = registry.get_applicable_test_cases("GET", "/api/users/123")
|
||||
self.assertEqual(len(applicable_user), 1)
|
||||
self.assertEqual(applicable_user[0].id, "TC-USER-PATH")
|
||||
|
||||
applicable_order = registry.get_applicable_test_cases("POST", "/api/orders/new")
|
||||
self.assertEqual(len(applicable_order), 1)
|
||||
self.assertEqual(applicable_order[0].id, "TC-ORDER-PATH")
|
||||
|
||||
applicable_none1 = registry.get_applicable_test_cases("GET", "/api/products/789")
|
||||
self.assertEqual(len(applicable_none1), 0)
|
||||
|
||||
applicable_none2 = registry.get_applicable_test_cases("GET", "/api/users/profile") # Does not match \d+
|
||||
self.assertEqual(len(applicable_none2), 0)
|
||||
|
||||
def test_get_applicable_by_method_and_path(self):
|
||||
"""测试同时根据方法和路径进行筛选。"""
|
||||
content = """
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
class SpecificGetTest(BaseAPITestCase):
|
||||
id = "TC-SPECIFIC-GET"
|
||||
name = "Specific GET"
|
||||
applicable_methods = ["GET"]
|
||||
applicable_paths_regex = r"^/data/\\w+$"
|
||||
"""
|
||||
self._create_test_case_file("specific_get.py", content)
|
||||
registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR)
|
||||
|
||||
applicable = registry.get_applicable_test_cases("GET", "/data/item1")
|
||||
self.assertEqual(len(applicable), 1)
|
||||
self.assertEqual(applicable[0].id, "TC-SPECIFIC-GET")
|
||||
|
||||
not_applicable_method = registry.get_applicable_test_cases("POST", "/data/item1")
|
||||
self.assertEqual(len(not_applicable_method), 0)
|
||||
|
||||
not_applicable_path = registry.get_applicable_test_cases("GET", "/data/item1/details")
|
||||
self.assertEqual(len(not_applicable_path), 0)
|
||||
|
||||
def test_invalid_path_regex_handling(self):
|
||||
"""测试处理无效的路径正则表达式。"""
|
||||
content = """
|
||||
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, TestSeverity
|
||||
class InvalidRegexTest(BaseAPITestCase):
|
||||
id = "TC-INVALID-REGEX"
|
||||
name = "Invalid Regex"
|
||||
applicable_paths_regex = r"^/api/path_(" # Unbalanced parenthesis
|
||||
"""
|
||||
self._create_test_case_file("invalid_regex.py", content)
|
||||
with self.assertLogs(level='ERROR') as log_watcher:
|
||||
registry = TestCaseRegistry(test_cases_dir=TEMP_TEST_CASES_DIR)
|
||||
self.assertTrue(any("中的路径正则表达式 'invalid_regex.InvalidRegexTest' 无效" in msg for msg in log_watcher.output))
|
||||
|
||||
# The test case with invalid regex should not match any path
|
||||
applicable = registry.get_applicable_test_cases("GET", "/api/path_something")
|
||||
self.assertEqual(len(applicable), 0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Configure logging for detailed output when running directly
|
||||
# logging.basicConfig(stream=sys.stdout, level=logging.DEBUG,
|
||||
# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
unittest.main()
|
||||
Loading…
x
Reference in New Issue
Block a user