import re from typing import Dict, Any, List, Optional from ddms_compliance_suite.test_framework_core import BaseAPITestCase, ValidationResult, APIResponseContext, APIRequestContext, TestSeverity class URLVersionCheckCase(BaseAPITestCase): """ 检查API URL是否包含版本号(如v1, api/v2, v3.0等)并以/api开头 """ id = "TC-DMS-URL-VERSION-001" name = "DMS API URL版本号检查" description = "检查API URL是否包含标准格式的版本号,支持的格式包括:v1, api/v2, v3.0, version/1, 1.0等,并且路径需要以/api开头" severity = TestSeverity.MEDIUM tags = ["url", "version", "dms-core", "api-design"] # 这个测试用例不需要发送实际请求 skip_execution = True 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) def validate_response(self, response_context: APIResponseContext, request_context: APIRequestContext) -> List[ValidationResult]: """ 检查API URL是否以/api开头并包含版本号 """ results = [] # 获取API路径 path = self.endpoint_spec.get('path', '') if not path: results.append(self.failed( message="无法获取API路径", details={"endpoint_spec_keys": list(self.endpoint_spec.keys())} )) return results # 检查是否是系统级API(可能不需要遵循标准路径格式) is_system_api = re.match(r'^/(health|ping|status|metrics|system)(/|$)', path) # 1. 检查路径是否以/api开头 starts_with_api = path.startswith('/api/') if not starts_with_api and not is_system_api: results.append(self.failed( message=f"API路径 '{path}' 不是以'/api/'开头", details={"full_path": path, "requirement": "路径必须以'/api/'开头"} )) elif starts_with_api: results.append(self.passed( message=f"API路径 '{path}' 正确以'/api/'开头", details={"full_path": path} )) # 2. 检查路径中是否包含版本号 version_patterns = [ # 标准版本格式: /v1/, /v2/, /v3/ 等 r'/v\d+/', # 带小数点的版本: /v1.0/, /v2.1/ 等 r'/v\d+\.\d+/', # 使用 'version' 单词: /version/1/, /version/2/ 等 r'/version/\d+/', # API前缀版本: /api/v1/, /api/v2/ 等 r'/api/v\d+/', # 直接数字版本: /1/, /2/ (仅在特定位置) r'/api/\d+/', # 特殊格式: 如 /v1-beta/, /v2-alpha/ 等 r'/v\d+[\-_](alpha|beta|rc\d*)/', # 年份版本: /2023/, /2024/ 等 (仅在特定位置) r'/20\d{2}/', ] # 检查是否包含版本号 matched_pattern = None version_str = None for pattern in version_patterns: match = re.search(pattern, path) if match: matched_pattern = pattern version_str = match.group(0).strip('/') break if matched_pattern and version_str: results.append(self.passed( message=f"API路径 '{path}' 包含版本标识: '{version_str}'", details={ "pattern_matched": matched_pattern, "version_string": version_str, "full_path": path } )) else: # 特殊情况:检查是否是根API或系统级API(可能不需要版本号) if is_system_api: results.append(self.passed( message=f"API路径 '{path}' 是系统级API,不需要版本号", details={"full_path": path, "api_type": "system"} )) else: results.append(self.failed( message=f"API路径 '{path}' 不包含任何已知格式的版本标识", details={ "full_path": path, "supported_patterns": [p.replace('\\d+', 'N').replace('\\d{2}', 'NN') for p in version_patterns] } )) # 提供改进建议 # 确保建议路径始终以/api开头并包含版本号 base_path_parts = path.split('/') base_path_parts = [p for p in base_path_parts if p] # 移除空字符串 if not starts_with_api: # 如果不是以/api开头,建议路径应该是/api/v1/原始路径 suggested_path = f"/api/v1/{'/'.join(base_path_parts)}" else: # 如果已经以/api开头但缺少版本号,插入v1在api之后 suggested_path = "/api/v1" if len(base_path_parts) > 1: # 有api后面的部分 suggested_path += f"/{'/'.join(base_path_parts[1:])}" results.append(ValidationResult( passed=False, message=f"建议将路径修改为符合规范的格式,例如: '{suggested_path}'", details={"original_path": path, "suggested_path": suggested_path} )) return results