94 lines
5.4 KiB
Python
94 lines
5.4 KiB
Python
from ddms_compliance_suite.test_framework_core import BaseAPITestCase, ValidationResult, APIResponseContext, APIRequestContext, TestSeverity
|
||
import re
|
||
from typing import Dict, Any, List, Optional
|
||
from ddms_compliance_suite.utils import schema_utils
|
||
|
||
class TimeFormatCheckTestCase(BaseAPITestCase):
|
||
id = "TC-RESTful-003"
|
||
name = "时间字段ISO 8601格式检查"
|
||
description = "验证返回的时间字段是否遵循 ISO 8601 格式。此检查为静态检查,会检查规范中 `format` 为 `date-time` 的字段,以及常见的时间字段名(如 createTime, update_time 等),是否包含推荐的 `pattern`。"
|
||
severity = TestSeverity.MEDIUM
|
||
tags = ["normative", "schema", "time-format"]
|
||
|
||
def __init__(self, endpoint_spec: Dict[str, Any], global_api_spec: Dict[str, Any], json_schema_validator: Optional[Any] = None, llm_service: Optional[Any] = None):
|
||
super().__init__(endpoint_spec, global_api_spec, json_schema_validator, llm_service=llm_service)
|
||
# 推荐的 pattern
|
||
self.recommended_iso_8601_pattern = r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{2}:\d{2}|Z)$'
|
||
# 常见时间字段名称(小写,用于不区分大小写匹配)
|
||
self.time_field_names = {
|
||
"createtime", "updatetime", "starttime", "endtime", "publishtime", "timestamp",
|
||
"created_at", "updated_at", "create_time", "update_time", "start_time", "end_time",
|
||
"gmtcreate", "gmtmodified", "datetime", "date_time", "time"
|
||
}
|
||
|
||
def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]:
|
||
results = []
|
||
# 此检查为静态检查,分析API规范中的响应部分
|
||
responses = self.endpoint_spec.get('responses', {})
|
||
for status_code, response_spec in responses.items():
|
||
# 通常只关心成功的响应
|
||
if not status_code.startswith('2'):
|
||
continue
|
||
|
||
content = self._get_resolved_schema(response_spec.get('content', {}))
|
||
if content:
|
||
for media_type, media_spec in content.items():
|
||
if 'schema' in media_spec:
|
||
self._check_schema_properties(media_spec['schema'], results)
|
||
|
||
if not results:
|
||
return [self.passed("在API规范中未找到可供静态检查的时间相关字段(如 format: date-time 或 常见时间字段名)。")]
|
||
|
||
return results
|
||
|
||
def _check_schema_properties(self, schema, results, path=""):
|
||
if not schema or not isinstance(schema, dict):
|
||
return
|
||
|
||
# 处理 allOf, oneOf, anyOf
|
||
for keyword in ['allOf', 'oneOf', 'anyOf']:
|
||
if keyword in schema:
|
||
for sub_schema in schema[keyword]:
|
||
self._check_schema_properties(sub_schema, results, path)
|
||
|
||
if 'properties' in schema:
|
||
for prop_name, prop_spec in schema['properties'].items():
|
||
prop_spec = self._get_resolved_schema(prop_spec)
|
||
current_path = f"{path}.{prop_name}" if path else prop_name
|
||
|
||
# 检查是否为时间相关字段
|
||
is_datetime_format = prop_spec.get('format') == 'date-time'
|
||
is_common_time_name = prop_name.lower() in self.time_field_names
|
||
|
||
# 必须是string类型,且满足 (format是date-time) 或 (字段名在常见列表里)
|
||
if prop_spec.get('type') == 'string' and (is_datetime_format or is_common_time_name):
|
||
pattern = prop_spec.get('pattern')
|
||
|
||
# 确定字段来源以提供更清晰的消息
|
||
source_reason = ""
|
||
if is_datetime_format and is_common_time_name:
|
||
source_reason = f"(format: date-time, name: '{prop_name}')"
|
||
elif is_datetime_format:
|
||
source_reason = f"(format: date-time)"
|
||
else: # is_common_time_name
|
||
source_reason = f"(name: '{prop_name}')"
|
||
|
||
message = f"时间字段 '{current_path}' {source_reason} "
|
||
if not pattern:
|
||
results.append(self.failed(
|
||
message + f"缺少建议的 `pattern` ({self.recommended_iso_8601_pattern}) 来强制执行ISO 8601格式。",
|
||
details={'field': current_path}
|
||
))
|
||
elif pattern != self.recommended_iso_8601_pattern:
|
||
results.append(self.failed(
|
||
message + f"其 `pattern` ('{pattern}') 与建议的模式不完全匹配。",
|
||
details={'field': current_path, 'current_pattern': pattern, 'recommended': self.recommended_iso_8601_pattern}
|
||
))
|
||
else:
|
||
results.append(self.passed(message + "已定义了建议的 `pattern` 用于格式校验。"))
|
||
|
||
# 递归检查
|
||
if 'properties' in prop_spec or 'allOf' in prop_spec or 'oneOf' in prop_spec or 'anyOf' in prop_spec:
|
||
self._check_schema_properties(prop_spec, results, current_path)
|
||
elif prop_spec.get('type') == 'array' and 'items' in prop_spec:
|
||
self._check_schema_properties(self._get_resolved_schema(prop_spec['items']), results, f"{current_path}[]") |