2025-06-20 10:39:34 +08:00

94 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}[]")