517 lines
20 KiB
Python
517 lines
20 KiB
Python
#!/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() |