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 = "验证返回的时间字段是否遵循 YYYY-MM-DDTHH:MM:SS+08:00 的ISO 8601格式。此检查为静态检查,验证规范中`string`类型且`format`为`date-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)$' 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规范中未找到可供静态检查的时间相关字段(类型为string且格式为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 # 检查类型为string且格式为date-time的字段 if prop_spec.get('type') == 'string' and prop_spec.get('format') == 'date-time': pattern = prop_spec.get('pattern') message = f"时间字段 '{current_path}' (format: date-time) " 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}[]") def _get_resolved_schema(self, schema_or_ref): if '$ref' in schema_or_ref: return schema_utils.util_resolve_ref(schema_or_ref['$ref'], self.global_api_spec) return schema_or_ref