fix:stage_group
This commit is contained in:
Binary file not shown.
@@ -2,431 +2,504 @@ import time
|
||||
import uuid
|
||||
import logging
|
||||
import re
|
||||
from typing import List, Dict, Any, Optional, Callable
|
||||
from typing import List, Dict, Any, Optional, Callable, Union
|
||||
|
||||
from ddms_compliance_suite.stage_framework import BaseAPIStage, StageStepDefinition, APIOperationSpec, ExecutedStageResult, ExecutedStageStepResult
|
||||
from ddms_compliance_suite.test_framework_core import ValidationResult, APIResponseContext
|
||||
from ddms_compliance_suite.input_parser.parser import ParsedAPISpec
|
||||
|
||||
# =====================================================================================
|
||||
# USER CONFIGURATION: RESOURCE KEYWORD
|
||||
# =====================================================================================
|
||||
# 请用户根据实际要测试的资源修改此关键字,例如 "用户", "订单", "测井曲线" 等。
|
||||
# 这个关键字将用于在API规范的端点标题中模糊查找相关的操作。
|
||||
RESOURCE_KEYWORD = "地质单元" # <-- 【【【请修改这里!】】】
|
||||
# =====================================================================================
|
||||
from ddms_compliance_suite.input_parser.parser import ParsedAPISpec, YAPIEndpoint, SwaggerEndpoint
|
||||
|
||||
# --- Action Keywords for Discovery (Usually these can remain as is) ---
|
||||
CREATE_ACTION_KEYWORDS = ["添加", "新建", "创建"]
|
||||
LIST_ACTION_KEYWORDS = ["列表查询", "查询列表", "获取列表", "分页查询"]
|
||||
DETAIL_ACTION_KEYWORDS = ["查询详情", "获取详情", "根据ID获取"]
|
||||
UPDATE_ACTION_KEYWORDS = ["修改", "更新"]
|
||||
DELETE_ACTION_KEYWORDS = ["删除"]
|
||||
CREATE_ACTION_KEYWORDS = ["add", "create", "new", "添加", "新建", "创建"]
|
||||
LIST_ACTION_KEYWORDS = ["list", "query", "search", "getall", "getlist", "列表查询", "查询列表", "获取列表", "分页查询"]
|
||||
DETAIL_ACTION_KEYWORDS = ["detail", "getbyid", "getone", "查询详情", "获取详情", "根据ID获取"]
|
||||
UPDATE_ACTION_KEYWORDS = ["update", "edit", "put", "修改", "更新"]
|
||||
DELETE_ACTION_KEYWORDS = ["delete", "remove", "删除"]
|
||||
|
||||
# --- Default values that might come from config or context in a real scenario ---
|
||||
# --- 这些值也可能需要根据您的API进行调整 ---
|
||||
DEFAULT_DMS_INSTANCE_CODE_PLACEHOLDER = "your_dms_instance_code" # 示例占位符
|
||||
DEFAULT_API_VERSION_PLACEHOLDER = "1.0.0" # 示例占位符
|
||||
DEFAULT_TENANT_ID_PLACEHOLDER = "your-tenant-id" # 示例占位符
|
||||
DEFAULT_AUTHORIZATION_PLACEHOLDER = "Bearer your-test-token" # 示例占位符
|
||||
# --- Default values that might come from config or context ---
|
||||
DEFAULT_DMS_INSTANCE_CODE_PLACEHOLDER = "your_dms_instance_code"
|
||||
DEFAULT_API_VERSION_PLACEHOLDER = "1.0.0"
|
||||
DEFAULT_TENANT_ID_PLACEHOLDER = "your-tenant-id"
|
||||
DEFAULT_AUTHORIZATION_PLACEHOLDER = "Bearer your-test-token"
|
||||
|
||||
|
||||
# --- Custom Assertion Helper Functions & Context Manipulators ---
|
||||
# 【【【重要提示】】】: 下面的这些辅助函数中的JSON路径 (如 'data.list', 'wellCommonName', 'dsid')
|
||||
# 是基于 '井筒API示例_simple.json' 的结构。
|
||||
# 当您修改 RESOURCE_KEYWORD 并适配您自己的API时,您【必须】检查并修改这些路径以匹配您API的实际响应结构。
|
||||
|
||||
def find_and_extract_id_by_name(response_ctx: APIResponseContext, stage_ctx: dict) -> ValidationResult:
|
||||
"""
|
||||
(通用版) 在列表响应中通过唯一名称查找新创建的资源,
|
||||
验证其存在,并将其唯一标识符(例如 'id', 'dsid', 'uuid')提取到阶段上下文中。
|
||||
# --- Helper to navigate JSON path ---
|
||||
def _get_value_by_path(data: Optional[Dict[str, Any]], path_str: Optional[str]):
|
||||
if data is None or path_str is None or not path_str:
|
||||
return data
|
||||
|
||||
需要用户在 'outputs_to_context' 中配置如何从响应中提取这个 'id'。
|
||||
此函数主要用于验证是否能找到特定名称的条目。
|
||||
实际的ID提取最好通过 StageStepDefinition 的 'outputs_to_context' 来完成,因为它更灵活。
|
||||
|
||||
此函数假设:
|
||||
1. 'unique_resource_name' 在 stage_ctx 中。
|
||||
2. 响应体中有一个列表,其路径是 'data.list' (这可能需要修改!)。
|
||||
3. 列表中的每个条目是一个字典,并且包含一个 'name_field_in_list' 指定的字段用于匹配名称 (需要用户配置或硬编码修改)。
|
||||
4. 找到的条目包含一个 'id_field_in_list' 指定的字段作为其唯一ID (需要用户配置或硬编码修改)。
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
unique_name_to_find = stage_ctx.get("unique_resource_name")
|
||||
if not unique_name_to_find:
|
||||
return ValidationResult(passed=False, message="上下文中未找到 'unique_resource_name'。无法查找已创建的资源。")
|
||||
|
||||
# 【【【请用户根据实际API修改下面的JSON路径和字段名】】】
|
||||
LIST_PATH_IN_RESPONSE = "data.list" # 例如: "items", "data", "result.records"
|
||||
NAME_FIELD_IN_LIST_ITEM = "wellCommonName" # 例如: "name", "title", "username"
|
||||
ID_FIELD_IN_LIST_ITEM = "dsid" # 例如: "id", "uuid", "_id"
|
||||
# 【【【修改结束】】】
|
||||
|
||||
response_data = response_ctx.json_content
|
||||
|
||||
# Helper to navigate path
|
||||
def _get_value_by_path(data, path_str):
|
||||
current = data
|
||||
current = data
|
||||
try:
|
||||
for part in path_str.split('.'):
|
||||
if isinstance(current, dict) and part in current:
|
||||
match_list_index = re.fullmatch(r"([a-zA-Z_][a-zA-Z0-9_]*)\[(\d+)\]", part)
|
||||
if match_list_index:
|
||||
list_name, index_str = match_list_index.groups()
|
||||
index = int(index_str)
|
||||
if isinstance(current, dict) and list_name in current and isinstance(current[list_name], list) and 0 <= index < len(current[list_name]):
|
||||
current = current[list_name][index]
|
||||
else: return None
|
||||
elif isinstance(current, list) and part.isdigit():
|
||||
idx = int(part)
|
||||
if 0 <= idx < len(current):
|
||||
current = current[idx]
|
||||
else:
|
||||
return None
|
||||
elif isinstance(current, dict) and part in current:
|
||||
current = current[part]
|
||||
elif isinstance(current, list) and part.isdigit() and 0 <= int(part) < len(current):
|
||||
current = current[int(part)]
|
||||
else:
|
||||
return None
|
||||
return current
|
||||
except (TypeError, ValueError, IndexError, KeyError) as e:
|
||||
logging.getLogger(__name__).debug(f"Error navigating path '{path_str}': {e}")
|
||||
return None
|
||||
return current
|
||||
|
||||
resource_list = _get_value_by_path(response_data, LIST_PATH_IN_RESPONSE)
|
||||
# --- Custom Assertion Helper Functions & Context Manipulators ---
|
||||
# These functions now read field names and paths from stage_ctx,
|
||||
# which should be populated by the Stage based on its configuration.
|
||||
|
||||
def find_resource_in_list_and_extract_id(response_ctx: APIResponseContext, stage_ctx: dict) -> ValidationResult:
|
||||
logger = logging.getLogger(__name__)
|
||||
unique_name_to_find = stage_ctx.get("unique_resource_name")
|
||||
|
||||
list_path = stage_ctx.get("cfg_list_path_in_listresponse", "data.list")
|
||||
name_field = stage_ctx.get("cfg_name_field_in_listitem", "name")
|
||||
id_field = stage_ctx.get("cfg_id_field_in_listitem", "id")
|
||||
|
||||
if not unique_name_to_find:
|
||||
return ValidationResult(passed=False, message="Context error: 'unique_resource_name' not found in stage_context.")
|
||||
|
||||
response_data = response_ctx.json_content
|
||||
resource_list = _get_value_by_path(response_data, list_path)
|
||||
|
||||
if not isinstance(resource_list, list):
|
||||
return ValidationResult(passed=False, message=f"响应格式错误:期望路径 '{LIST_PATH_IN_RESPONSE}' 返回一个列表,实际得到 {type(resource_list)}。")
|
||||
return ValidationResult(passed=False, message=f"Response format error: Expected path '{list_path}' to be a list, got {type(resource_list)}.")
|
||||
|
||||
found_item = None
|
||||
for item in resource_list:
|
||||
if isinstance(item, dict) and item.get(NAME_FIELD_IN_LIST_ITEM) == unique_name_to_find:
|
||||
if isinstance(item, dict) and item.get(name_field) == unique_name_to_find:
|
||||
found_item = item
|
||||
break
|
||||
|
||||
if not found_item:
|
||||
return ValidationResult(passed=False, message=f"在列表响应中未找到名称为 '{unique_name_to_find}' (字段: {NAME_FIELD_IN_LIST_ITEM}) 的资源。")
|
||||
return ValidationResult(passed=False, message=f"Resource not found: Name '{unique_name_to_find}' (field: {name_field}) not found in list at path '{list_path}'.")
|
||||
|
||||
resource_id = found_item.get(ID_FIELD_IN_LIST_ITEM)
|
||||
if not resource_id:
|
||||
return ValidationResult(passed=False, message=f"找到名称为 '{unique_name_to_find}' 的资源,但它缺少ID字段 '{ID_FIELD_IN_LIST_ITEM}'。")
|
||||
resource_id = found_item.get(id_field)
|
||||
if resource_id is None:
|
||||
return ValidationResult(passed=False, message=f"Resource found by name, but it lacks the ID field '{id_field}'.")
|
||||
|
||||
# 将找到的ID存入上下文,变量名可由outputs_to_context覆盖
|
||||
stage_ctx["created_resource_id"] = resource_id
|
||||
logger.info(f"成功在列表中找到资源 '{unique_name_to_find}' 并提取其ID ({ID_FIELD_IN_LIST_ITEM}): {resource_id}。")
|
||||
return ValidationResult(passed=True, message=f"找到资源 '{unique_name_to_find}' 并提取ID: {resource_id}。建议使用 outputs_to_context 获取ID。")
|
||||
logger.info(f"Successfully found resource '{unique_name_to_find}' in list and extracted its ID ({id_field}): {resource_id}.")
|
||||
return ValidationResult(passed=True, message=f"Found resource '{unique_name_to_find}' by name (field '{name_field}'). ID ('{id_field}': {resource_id}) available in context. Prefer 'outputs_to_context' for extraction.")
|
||||
|
||||
|
||||
def check_resource_details(response_ctx: APIResponseContext, stage_ctx: dict) -> ValidationResult:
|
||||
"""
|
||||
(通用版) 验证获取到的资源详情。
|
||||
假设 'created_resource_id' 和 'unique_resource_name' 在 stage_ctx 中。
|
||||
假设响应直接就是资源对象,或者在 'data.list[0]' 或 'data' 中 (需要用户调整)。
|
||||
"""
|
||||
# 【【【请用户根据实际API修改下面的JSON路径和字段名】】】
|
||||
# 响应中资源对象的位置。留空表示响应体本身就是资源对象。
|
||||
# 例如:"data.list[0]" (如果详情接口返回带列表的结构), "data" (如果返回带data包装的结构)
|
||||
RESOURCE_OBJECT_PATH_IN_RESPONSE = "data.list[0]"
|
||||
NAME_FIELD_IN_DETAIL = "wellCommonName"
|
||||
ID_FIELD_IN_DETAIL = "dsid"
|
||||
# 【【【修改结束】】】
|
||||
resource_object_path = stage_ctx.get("cfg_resource_object_path_in_detailresponse", "")
|
||||
name_field = stage_ctx.get("cfg_name_field_in_detailresponse", "name")
|
||||
id_field = stage_ctx.get("cfg_id_field_in_detailresponse", "id")
|
||||
feature_field_name = stage_ctx.get("cfg_feature_field_name_for_validation")
|
||||
expected_feature_value = stage_ctx.get("unique_feature_value")
|
||||
|
||||
created_id = stage_ctx.get("created_resource_id")
|
||||
initial_name = stage_ctx.get("unique_resource_name")
|
||||
|
||||
response_data = response_ctx.json_content
|
||||
|
||||
def _get_value_by_path(data, path_str): # Duplicated helper for clarity, could be refactored
|
||||
if not path_str: return data # No path, return whole data
|
||||
current = data
|
||||
for part in path_str.split('.'):
|
||||
# Basic list index handling like "list[0]"
|
||||
match_list_index = re.fullmatch(r"([a-zA-Z_][a-zA-Z0-9_]*)\[(\d+)\]", part)
|
||||
if match_list_index:
|
||||
list_name, index_str = match_list_index.groups()
|
||||
index = int(index_str)
|
||||
if isinstance(current, dict) and list_name in current and isinstance(current[list_name], list) and 0 <= index < len(current[list_name]):
|
||||
current = current[list_name][index]
|
||||
else: return None
|
||||
elif isinstance(current, dict) and part in current:
|
||||
current = current[part]
|
||||
else: return None
|
||||
return current
|
||||
|
||||
resource_data = _get_value_by_path(response_data, RESOURCE_OBJECT_PATH_IN_RESPONSE)
|
||||
resource_data = _get_value_by_path(response_ctx.json_content, resource_object_path)
|
||||
|
||||
if not isinstance(resource_data, dict):
|
||||
return ValidationResult(passed=False, message=f"响应格式错误:期望路径 '{RESOURCE_OBJECT_PATH_IN_RESPONSE}' 返回一个对象,实际得到 {type(resource_data)}。")
|
||||
return ValidationResult(passed=False, message=f"Response format error: Expected path '{resource_object_path}' to be an object, got {type(resource_data)}.")
|
||||
|
||||
if resource_data.get(ID_FIELD_IN_DETAIL) != created_id:
|
||||
return ValidationResult(passed=False, message=f"ID不匹配。期望 {created_id}, 得到 {resource_data.get(ID_FIELD_IN_DETAIL)} (字段: {ID_FIELD_IN_DETAIL})。")
|
||||
if NAME_FIELD_IN_DETAIL and resource_data.get(NAME_FIELD_IN_DETAIL) != initial_name: # Name check is optional
|
||||
return ValidationResult(passed=False, message=f"名称不匹配。期望 '{initial_name}', 得到 '{resource_data.get(NAME_FIELD_IN_DETAIL)}' (字段: {NAME_FIELD_IN_DETAIL})。")
|
||||
validation_results = []
|
||||
if resource_data.get(id_field) != created_id:
|
||||
validation_results.append(ValidationResult(passed=False, message=f"ID mismatch. Expected '{created_id}', got '{resource_data.get(id_field)}' (field: {id_field})."))
|
||||
else:
|
||||
validation_results.append(ValidationResult(passed=True, message=f"ID match: '{created_id}' (field: {id_field})."))
|
||||
|
||||
if name_field and resource_data.get(name_field) != initial_name:
|
||||
validation_results.append(ValidationResult(passed=False, message=f"Name mismatch. Expected '{initial_name}', got '{resource_data.get(name_field)}' (field: {name_field})."))
|
||||
elif name_field:
|
||||
validation_results.append(ValidationResult(passed=True, message=f"Name match: '{initial_name}' (field: {name_field})."))
|
||||
|
||||
if feature_field_name and expected_feature_value is not None:
|
||||
actual_feature_value = resource_data.get(feature_field_name)
|
||||
if actual_feature_value != expected_feature_value:
|
||||
validation_results.append(ValidationResult(passed=False, message=f"Feature field '{feature_field_name}' mismatch. Expected '{expected_feature_value}', got '{actual_feature_value}'."))
|
||||
else:
|
||||
validation_results.append(ValidationResult(passed=True, message=f"Feature field '{feature_field_name}' match: '{expected_feature_value}'."))
|
||||
|
||||
return ValidationResult(passed=True, message=f"资源详情 (ID: {created_id}) 校验成功。")
|
||||
overall_passed = all(vr.passed for vr in validation_results)
|
||||
combined_message = "Resource details check: " + "; ".join([vr.message for vr in validation_results])
|
||||
return ValidationResult(passed=overall_passed, message=combined_message, details=[vr.to_dict() for vr in validation_results])
|
||||
|
||||
|
||||
def check_resource_updated_details(response_ctx: APIResponseContext, stage_ctx: dict) -> ValidationResult:
|
||||
"""
|
||||
(通用版) 验证更新后的资源详情。
|
||||
假设 'created_resource_id' 和 'updated_resource_name' 在 stage_ctx 中。
|
||||
"""
|
||||
# 【【【请用户根据实际API修改下面的JSON路径和字段名】】】
|
||||
RESOURCE_OBJECT_PATH_IN_RESPONSE = "data.list[0]"
|
||||
NAME_FIELD_IN_DETAIL = "wellCommonName"
|
||||
ID_FIELD_IN_DETAIL = "dsid"
|
||||
# 【【【修改结束】】】
|
||||
resource_object_path = stage_ctx.get("cfg_resource_object_path_in_updateresponse", stage_ctx.get("cfg_resource_object_path_in_detailresponse", ""))
|
||||
name_field = stage_ctx.get("cfg_name_field_in_detailresponse", "name")
|
||||
id_field = stage_ctx.get("cfg_id_field_in_detailresponse", "id")
|
||||
feature_field_name = stage_ctx.get("cfg_feature_field_name_for_validation")
|
||||
expected_updated_feature_value = stage_ctx.get("updated_feature_value")
|
||||
|
||||
created_id = stage_ctx.get("created_resource_id")
|
||||
updated_name = stage_ctx.get("updated_resource_name")
|
||||
|
||||
# (Code is very similar to check_resource_details, could be refactored if desired)
|
||||
response_data = response_ctx.json_content
|
||||
def _get_value_by_path(data, path_str):
|
||||
if not path_str: return data
|
||||
current = data
|
||||
for part in path_str.split('.'):
|
||||
match_list_index = re.fullmatch(r"([a-zA-Z_][a-zA-Z0-9_]*)\[(\d+)\]", part)
|
||||
if match_list_index:
|
||||
list_name, index_str = match_list_index.groups()
|
||||
index = int(index_str)
|
||||
if isinstance(current, dict) and list_name in current and isinstance(current[list_name], list) and 0 <= index < len(current[list_name]):
|
||||
current = current[list_name][index]
|
||||
else: return None
|
||||
elif isinstance(current, dict) and part in current:
|
||||
current = current[part]
|
||||
else: return None
|
||||
return current
|
||||
resource_data = _get_value_by_path(response_data, RESOURCE_OBJECT_PATH_IN_RESPONSE)
|
||||
resource_data = _get_value_by_path(response_ctx.json_content, resource_object_path)
|
||||
|
||||
if not isinstance(resource_data, dict):
|
||||
return ValidationResult(passed=False, message=f"更新后详情响应格式错误:期望路径 '{RESOURCE_OBJECT_PATH_IN_RESPONSE}' 返回一个对象,实际得到 {type(resource_data)}。")
|
||||
return ValidationResult(passed=False, message=f"Updated detail response format error: Expected path '{resource_object_path}' to be an object, got {type(resource_data)}.")
|
||||
|
||||
if resource_data.get(ID_FIELD_IN_DETAIL) != created_id:
|
||||
return ValidationResult(passed=False, message=f"更新后ID不匹配。期望 {created_id}, 得到 {resource_data.get(ID_FIELD_IN_DETAIL)}。")
|
||||
if NAME_FIELD_IN_DETAIL and resource_data.get(NAME_FIELD_IN_DETAIL) != updated_name:
|
||||
return ValidationResult(passed=False, message=f"更新后名称不匹配。期望 '{updated_name}', 得到 '{resource_data.get(NAME_FIELD_IN_DETAIL)}'。")
|
||||
|
||||
return ValidationResult(passed=True, message=f"更新后资源详情 (ID: {created_id}) 校验成功,名称为 '{updated_name}'。")
|
||||
validation_results = []
|
||||
if resource_data.get(id_field) != created_id:
|
||||
validation_results.append(ValidationResult(passed=False, message=f"Updated ID mismatch. Expected '{created_id}', got '{resource_data.get(id_field)}'."))
|
||||
else:
|
||||
validation_results.append(ValidationResult(passed=True, message=f"Updated ID match: '{created_id}'."))
|
||||
|
||||
if name_field and resource_data.get(name_field) != updated_name:
|
||||
validation_results.append(ValidationResult(passed=False, message=f"Updated name mismatch. Expected '{updated_name}', got '{resource_data.get(name_field)}'."))
|
||||
elif name_field:
|
||||
validation_results.append(ValidationResult(passed=True, message=f"Updated name match: '{updated_name}'."))
|
||||
|
||||
if feature_field_name and expected_updated_feature_value is not None:
|
||||
actual_feature_value = resource_data.get(feature_field_name)
|
||||
if actual_feature_value != expected_updated_feature_value:
|
||||
validation_results.append(ValidationResult(passed=False, message=f"Updated feature field '{feature_field_name}' mismatch. Expected '{expected_updated_feature_value}', got '{actual_feature_value}'."))
|
||||
else:
|
||||
validation_results.append(ValidationResult(passed=True, message=f"Updated feature field '{feature_field_name}' match: '{expected_updated_feature_value}'."))
|
||||
|
||||
overall_passed = all(vr.passed for vr in validation_results)
|
||||
combined_message = "Updated resource details check: " + "; ".join([vr.message for vr in validation_results])
|
||||
return ValidationResult(passed=overall_passed, message=combined_message, details=[vr.to_dict() for vr in validation_results])
|
||||
|
||||
|
||||
class KeywordDrivenCRUDStage(BaseAPIStage):
|
||||
id = "keyword_driven_crud_example"
|
||||
name = "Keyword-Driven Generic CRUD Stage Example"
|
||||
class GenericCRUDValidationStage(BaseAPIStage):
|
||||
id = "generic_crud_validation_stage"
|
||||
name = "Generic CRUD Validation Stage"
|
||||
description = (
|
||||
"Demonstrates a CRUD (Create, List, Read, Update, Delete) flow. "
|
||||
"This stage dynamically finds API operations based on a configurable RESOURCE_KEYWORD "
|
||||
"and predefined action keywords (e.g., '添加', '查询列表'). "
|
||||
"IMPORTANT: User MUST configure RESOURCE_KEYWORD at the top of this file. "
|
||||
"Request bodies, response paths in assertions, and 'outputs_to_context' "
|
||||
"are EXAMPLES based on '井筒API示例_simple.json' and WILL LIKELY NEED MODIFICATION "
|
||||
"to match your specific API's structure."
|
||||
"Performs a generic CRUD (Create, List, Read, Update, Delete) flow. "
|
||||
"It tries to dynamically discover API operations based on common keywords and HTTP methods. "
|
||||
"Field names for IDs, names, features, and JSON paths for list/detail objects are configurable "
|
||||
"via class attributes or can be set in `before_stage` based on `api_group_name`."
|
||||
)
|
||||
tags = ["crud", "keyword_driven", "example"]
|
||||
continue_on_failure = False # Set to True if you want to attempt all steps even if one fails
|
||||
tags = ["crud", "generic_validation"]
|
||||
continue_on_failure = False
|
||||
fail_if_not_applicable_to_any_group = False
|
||||
|
||||
# This will be populated by is_applicable_to_api_group
|
||||
discovered_op_keys: dict = {}
|
||||
# This will be populated in before_stage
|
||||
# --- Configurable field names and JSON paths ---
|
||||
cfg_id_field_in_create_response: str = "data.0.dsid"
|
||||
cfg_name_field_in_create_payload: str = "wellCommonName"
|
||||
cfg_feature_field_in_create_payload: str = "dataRegion"
|
||||
|
||||
cfg_list_path_in_list_response: str = "data.list"
|
||||
cfg_id_field_in_list_item: str = "dsid"
|
||||
cfg_name_field_in_list_item: str = "wellCommonName"
|
||||
cfg_feature_field_in_list_item: str = "dataRegion"
|
||||
|
||||
cfg_resource_object_path_in_detail_response: str = "data.list.0"
|
||||
cfg_id_field_in_detail_response: str = "dsid"
|
||||
cfg_name_field_in_detail_response: str = "wellCommonName"
|
||||
cfg_feature_field_in_detail_response: str = "dataRegion"
|
||||
|
||||
cfg_resource_object_path_in_update_response: str = "data.list.0"
|
||||
|
||||
cfg_path_param_name_for_id: str = "id"
|
||||
|
||||
discovered_op_keys: Dict[str, Optional[str]] = {}
|
||||
steps: list = []
|
||||
|
||||
def _find_operation_title_or_id(self, endpoints: list, resource_kw: str, action_kws: list, http_method: str | None = None) -> str | None:
|
||||
"""Tries to find an endpoint by matching keywords in its title and optionally method."""
|
||||
self.logger.debug(f"Searching for op with resource='{resource_kw}', actions='{action_kws}', method='{http_method}'")
|
||||
for ep_obj in endpoints: # ep_obj is YAPIEndpoint or SwaggerEndpoint
|
||||
# YAPI uses 'title', Swagger often uses 'summary' or 'operationId'
|
||||
# We prioritize 'title' (from YAPI) then 'summary', then 'operationId' as lookup keys
|
||||
title_to_check = getattr(ep_obj, 'title', None)
|
||||
summary_to_check = getattr(ep_obj, 'summary', None)
|
||||
op_id_to_check = getattr(ep_obj, 'operation_id', None)
|
||||
def _determine_resource_config(self, api_group_name: Optional[str], global_api_spec: ParsedAPISpec):
|
||||
self.logger.info(f"Using default field/path configurations for group '{api_group_name}'. Customize by overriding _determine_resource_config or cfg_* attributes.")
|
||||
pass
|
||||
|
||||
|
||||
def _find_operation_best_match(self, endpoints: List[Union[YAPIEndpoint, SwaggerEndpoint]],
|
||||
action_kws: List[str],
|
||||
target_method: Optional[str] = None,
|
||||
path_must_contain_id: bool = False,
|
||||
path_must_not_contain_id: bool = False) -> Optional[str]:
|
||||
self.logger.debug(f"Attempting to find operation: target_actions='{action_kws}', target_method='{target_method}', path_id_required={path_must_contain_id}, path_id_forbidden={path_must_not_contain_id}")
|
||||
|
||||
best_candidate_key: Optional[str] = None
|
||||
|
||||
for ep_obj in endpoints:
|
||||
ep_title = getattr(ep_obj, 'title', 'N/A')
|
||||
ep_method_actual = getattr(ep_obj, 'method', 'N/A').upper()
|
||||
ep_path = getattr(ep_obj, 'path', 'N/A')
|
||||
self.logger.debug(f" Checking endpoint: Title='{ep_title}', Method='{ep_method_actual}', Path='{ep_path}', Type='{type(ep_obj).__name__}'")
|
||||
# Log all attributes of the endpoint object to see what the parser provided
|
||||
if isinstance(ep_obj, YAPIEndpoint):
|
||||
self.logger.debug(f" Raw YAPIEndpoint object attributes for '{ep_title}': {vars(ep_obj)}")
|
||||
|
||||
text_to_match = getattr(ep_obj, 'title', None) or \
|
||||
getattr(ep_obj, 'summary', None) or \
|
||||
getattr(ep_obj, 'operation_id', None)
|
||||
|
||||
# Determine primary text for keyword matching
|
||||
text_for_matching = title_to_check or summary_to_check or op_id_to_check
|
||||
if not text_for_matching:
|
||||
if not text_to_match:
|
||||
self.logger.debug(f" Skipping endpoint Title='{ep_title}', Path='{ep_path}': No text_to_match (title/summary/opId).")
|
||||
continue
|
||||
|
||||
# The key we will use to lookup the endpoint later in the orchestrator
|
||||
# The orchestrator uses 'title' for YAPI and 'operationId' or (method+path) for Swagger
|
||||
# For simplicity in this dynamic stage, we will try to return YAPI's 'title' if available and it matches,
|
||||
# otherwise 'operationId' if available and it matches.
|
||||
# If using Swagger and 'operationId' is preferred, adjust logic or ensure operationIds are descriptive.
|
||||
|
||||
lookup_key_to_return = title_to_check # Default to title (YAPI primary)
|
||||
lookup_key = getattr(ep_obj, 'title', None) or \
|
||||
getattr(ep_obj, 'operation_id', None) or \
|
||||
f"{ep_method_actual} {ep_path}"
|
||||
|
||||
text_lower = text_for_matching.lower()
|
||||
resource_kw_lower = resource_kw.lower()
|
||||
text_lower = text_to_match.lower()
|
||||
|
||||
if resource_kw_lower in text_lower:
|
||||
if any(action_kw.lower() in text_lower for action_kw in action_kws):
|
||||
current_ep_method = getattr(ep_obj, 'method', '').upper()
|
||||
if http_method and current_ep_method == http_method.upper():
|
||||
self.logger.info(f"Discovered endpoint: Key='{lookup_key_to_return or op_id_to_check}', MatchedText='{text_for_matching}' (for resource='{resource_kw}', action='{action_kws}', method='{http_method}')")
|
||||
return lookup_key_to_return or op_id_to_check # Return title or operationId
|
||||
elif not http_method:
|
||||
self.logger.info(f"Discovered endpoint: Key='{lookup_key_to_return or op_id_to_check}', MatchedText='{text_for_matching}' (for resource='{resource_kw}', action='{action_kws}', any method)")
|
||||
return lookup_key_to_return or op_id_to_check
|
||||
self.logger.warning(f"No operation found for resource='{resource_kw}', actions='{action_kws}', method='{http_method}'")
|
||||
return None
|
||||
action_match = any(action_kw.lower() in text_lower for action_kw in action_kws)
|
||||
self.logger.debug(f" For '{ep_title}': Action match with '{action_kws}'? {action_match}")
|
||||
|
||||
def is_applicable_to_api_group(self, api_group_name: str | None, global_api_spec: ParsedAPISpec) -> bool:
|
||||
self.logger.info(f"Checking applicability of '{self.name}' (RESOURCE_KEYWORD: '{RESOURCE_KEYWORD}') for API group '{api_group_name}'.")
|
||||
method_match = (not target_method) or (ep_method_actual == target_method.upper())
|
||||
self.logger.debug(f" For '{ep_title}': Method match with '{target_method}'? {method_match} (Endpoint method: {ep_method_actual})")
|
||||
|
||||
path_contains_id_param = False
|
||||
if isinstance(ep_obj, YAPIEndpoint):
|
||||
self.logger.debug(f" For YAPI Endpoint '{ep_title}': Checking req_params for path ID: {ep_obj.req_params}")
|
||||
for param in ep_obj.req_params: # YAPI req_params are path parameters
|
||||
if isinstance(param, dict):
|
||||
param_name = param.get('name', 'UnnamedParam')
|
||||
if param_name == self.cfg_path_param_name_for_id or 'id' in param_name.lower():
|
||||
path_contains_id_param = True
|
||||
self.logger.debug(f" Found ID path parameter in YAPI req_params: '{param_name}'")
|
||||
break
|
||||
elif isinstance(ep_obj, SwaggerEndpoint):
|
||||
swagger_params = getattr(ep_obj, 'parameters', None)
|
||||
self.logger.debug(f" For Swagger Endpoint '{ep_title}': Checking swagger_params for path ID: {swagger_params}")
|
||||
if isinstance(swagger_params, list):
|
||||
for param in swagger_params:
|
||||
if isinstance(param, dict):
|
||||
param_name = param.get('name', 'UnnamedParam')
|
||||
param_in = param.get('in', 'N/A')
|
||||
if param_in == 'path' and \
|
||||
(param_name == self.cfg_path_param_name_for_id or 'id' in param_name.lower()):
|
||||
path_contains_id_param = True
|
||||
self.logger.debug(f" Found ID path parameter in Swagger parameters: '{param_name}'")
|
||||
break
|
||||
else:
|
||||
self.logger.warning(f" For '{ep_title}': Endpoint is of unknown type {type(ep_obj).__name__} for parameter extraction. Assuming no path ID param.")
|
||||
|
||||
self.logger.debug(f" For '{ep_title}': Calculated path_contains_id_param={path_contains_id_param} (cfg_path_param_name_for_id='{self.cfg_path_param_name_for_id}')")
|
||||
|
||||
path_condition_met = True
|
||||
if path_must_contain_id and not path_contains_id_param:
|
||||
path_condition_met = False
|
||||
if path_must_not_contain_id and path_contains_id_param:
|
||||
path_condition_met = False
|
||||
self.logger.debug(f" For '{ep_title}': Calculated path_condition_met={path_condition_met} (required={path_must_contain_id}, forbidden={path_must_not_contain_id})")
|
||||
|
||||
if action_match and method_match and path_condition_met:
|
||||
self.logger.info(f" SUCCESS: Op match for actions '{action_kws}': Key='{lookup_key}', MatchedText='{text_to_match}', Method='{ep_method_actual}', Path='{ep_path}'")
|
||||
best_candidate_key = lookup_key
|
||||
break
|
||||
else:
|
||||
self.logger.debug(f" Criteria NOT met for '{ep_title}' (Action: {action_match}, Method: {method_match}, PathCond: {path_condition_met}). Continuing search.")
|
||||
|
||||
if not best_candidate_key:
|
||||
self.logger.warning(f" FAILURE: No operation found after checking all endpoints for: actions='{action_kws}', method='{target_method}', path_id_required={path_must_contain_id}, path_id_forbidden={path_must_not_contain_id}")
|
||||
return best_candidate_key
|
||||
|
||||
|
||||
def is_applicable_to_api_group(self, api_group_name: Optional[str], global_api_spec: ParsedAPISpec) -> bool:
|
||||
self.logger.info(f"Checking applicability of '{self.name}' for API group '{api_group_name}'.")
|
||||
if not global_api_spec or not global_api_spec.endpoints:
|
||||
self.logger.warning(f"'{self.name}' cannot determine applicability: global_api_spec or its endpoints are missing.")
|
||||
return False
|
||||
|
||||
# Reset for this applicability check
|
||||
self._determine_resource_config(api_group_name, global_api_spec)
|
||||
|
||||
self.discovered_op_keys = {}
|
||||
all_endpoints = global_api_spec.endpoints
|
||||
endpoints_in_group = self.apis_in_group
|
||||
|
||||
self.discovered_op_keys["create"] = self._find_operation_title_or_id(all_endpoints, RESOURCE_KEYWORD, CREATE_ACTION_KEYWORDS, "POST")
|
||||
self.discovered_op_keys["list"] = self._find_operation_title_or_id(all_endpoints, RESOURCE_KEYWORD, LIST_ACTION_KEYWORDS, "POST") # Assuming list is POST for this example
|
||||
self.discovered_op_keys["detail"] = self._find_operation_title_or_id(all_endpoints, RESOURCE_KEYWORD, DETAIL_ACTION_KEYWORDS, "GET")
|
||||
self.discovered_op_keys["update"] = self._find_operation_title_or_id(all_endpoints, RESOURCE_KEYWORD, UPDATE_ACTION_KEYWORDS, "PUT")
|
||||
self.discovered_op_keys["delete"] = self._find_operation_title_or_id(all_endpoints, RESOURCE_KEYWORD, DELETE_ACTION_KEYWORDS, "DELETE")
|
||||
self.discovered_op_keys["create"] = self._find_operation_best_match(endpoints_in_group, CREATE_ACTION_KEYWORDS, "POST", path_must_not_contain_id=True)
|
||||
self.discovered_op_keys["list"] = self._find_operation_best_match(endpoints_in_group, LIST_ACTION_KEYWORDS, None, path_must_not_contain_id=True)
|
||||
self.discovered_op_keys["detail"] = self._find_operation_best_match(endpoints_in_group, DETAIL_ACTION_KEYWORDS, "GET", path_must_contain_id=True)
|
||||
self.discovered_op_keys["update"] = self._find_operation_best_match(endpoints_in_group, UPDATE_ACTION_KEYWORDS, "PUT", path_must_contain_id=True)
|
||||
self.discovered_op_keys["delete"] = self._find_operation_best_match(endpoints_in_group, DELETE_ACTION_KEYWORDS, "DELETE", path_must_contain_id=False)
|
||||
|
||||
missing_ops = [op_type for op_type, key_val in self.discovered_op_keys.items() if not key_val]
|
||||
required_ops = ["create", "detail", "delete"]
|
||||
missing_required_ops = [op for op in required_ops if not self.discovered_op_keys.get(op)]
|
||||
|
||||
if not missing_ops:
|
||||
self.logger.info(f"'{self.name}' is APPLICABLE for group '{api_group_name}'. All CRUD operations found for '{RESOURCE_KEYWORD}'. Discovered keys: {self.discovered_op_keys}")
|
||||
if not missing_required_ops:
|
||||
self.logger.info(f"'{self.name}' is APPLICABLE for group '{api_group_name}'. Required CRUD operations found. Discovered keys: {self.discovered_op_keys}")
|
||||
return True
|
||||
else:
|
||||
self.logger.warning(f"'{self.name}' is NOT APPLICABLE for group '{api_group_name}'. Missing operations for '{RESOURCE_KEYWORD}': {missing_ops}. Discovered keys: {self.discovered_op_keys}")
|
||||
self.logger.warning(f"'{self.name}' is NOT APPLICABLE for group '{api_group_name}'. Missing required operations: {missing_required_ops}. Discovered keys: {self.discovered_op_keys}")
|
||||
return False
|
||||
|
||||
def before_stage(self, stage_context: dict, global_api_spec: ParsedAPISpec, api_group_name: str | None):
|
||||
self._determine_resource_config(api_group_name, global_api_spec)
|
||||
|
||||
self.logger.info(f"Starting stage '{self.name}' for API group '{api_group_name}'. Discovered op keys: {self.discovered_op_keys}")
|
||||
|
||||
run_timestamp = int(time.time())
|
||||
run_uuid_short = uuid.uuid4().hex[:6]
|
||||
|
||||
# Generate unique names for this run, using the configured RESOURCE_KEYWORD
|
||||
# Ensure RESOURCE_KEYWORD is simple enough for this concatenation.
|
||||
safe_resource_keyword_part = re.sub(r'\W+', '', RESOURCE_KEYWORD) # Remove non-alphanumeric for safety
|
||||
|
||||
unique_name_for_run = f"Test_{safe_resource_keyword_part}_{run_timestamp}_{run_uuid_short}"
|
||||
unique_name_for_run = f"TestResource_{run_timestamp}_{run_uuid_short}"
|
||||
updated_name_for_run = f"{unique_name_for_run}_UPDATED"
|
||||
unique_feature_val = f"Feature_{run_timestamp}"
|
||||
updated_feature_val = f"{unique_feature_val}_UPDATED"
|
||||
|
||||
stage_context["unique_resource_name"] = unique_name_for_run
|
||||
stage_context["updated_resource_name"] = updated_name_for_run
|
||||
stage_context["unique_feature_value"] = unique_feature_val
|
||||
stage_context["updated_feature_value"] = updated_feature_val
|
||||
|
||||
# These are placeholders and likely need to be adapted or fetched from config/context
|
||||
stage_context["cfg_list_path_in_listresponse"] = self.cfg_list_path_in_list_response
|
||||
stage_context["cfg_name_field_in_listitem"] = self.cfg_name_field_in_list_item
|
||||
stage_context["cfg_id_field_in_listitem"] = self.cfg_id_field_in_list_item
|
||||
stage_context["cfg_resource_object_path_in_detailresponse"] = self.cfg_resource_object_path_in_detail_response
|
||||
stage_context["cfg_name_field_in_detailresponse"] = self.cfg_name_field_in_detail_response
|
||||
stage_context["cfg_id_field_in_detailresponse"] = self.cfg_id_field_in_detail_response
|
||||
stage_context["cfg_resource_object_path_in_updateresponse"] = self.cfg_resource_object_path_in_update_response
|
||||
stage_context["cfg_feature_field_name_for_validation"] = self.cfg_feature_field_in_create_payload
|
||||
|
||||
stage_context["dms_instance_code"] = DEFAULT_DMS_INSTANCE_CODE_PLACEHOLDER
|
||||
stage_context["api_version"] = DEFAULT_API_VERSION_PLACEHOLDER
|
||||
|
||||
self.logger.info(f"Initial stage context: {stage_context}")
|
||||
self.logger.info(f"Initial stage context (with cfg): {stage_context}")
|
||||
|
||||
if not all(self.discovered_op_keys.values()):
|
||||
self.logger.error("Cannot build steps as not all operation keys were discovered. This should have been caught by is_applicable. Stage will have no steps.")
|
||||
if not self.discovered_op_keys.get("create") or \
|
||||
not self.discovered_op_keys.get("detail") or \
|
||||
not self.discovered_op_keys.get("delete"):
|
||||
self.logger.error("Cannot build steps as not all essential operation keys (create, detail, delete) were discovered. Stage will have no steps.")
|
||||
self.steps = []
|
||||
return
|
||||
|
||||
create_body_data = {
|
||||
self.cfg_name_field_in_create_payload: "{{stage_context.unique_resource_name}}",
|
||||
}
|
||||
if self.cfg_feature_field_in_create_payload:
|
||||
create_body_data[self.cfg_feature_field_in_create_payload] = "{{stage_context.unique_feature_value}}"
|
||||
|
||||
# --- Dynamically build steps using discovered keys ---
|
||||
# 【【【重要提示】】】: 下面的 request_overrides (尤其是 'body') 和 'outputs_to_context'
|
||||
# 是基于 '井筒API示例_simple.json' (当RESOURCE_KEYWORD="地质单元"时)的结构。
|
||||
# 当您修改 RESOURCE_KEYWORD 并用于您自己的API时,
|
||||
# 您【必须】相应地修改这些 'body' 结构和 'outputs_to_context' 中的JSON路径。
|
||||
self.steps = [
|
||||
StageStepDefinition(
|
||||
name=f"Add New {RESOURCE_KEYWORD}",
|
||||
final_create_body = {
|
||||
"version": "{{stage_context.api_version}}",
|
||||
"data": [create_body_data]
|
||||
}
|
||||
|
||||
self.steps = []
|
||||
|
||||
if self.discovered_op_keys["create"]:
|
||||
self.steps.append(StageStepDefinition(
|
||||
name="Create New Resource",
|
||||
endpoint_spec_lookup_key=self.discovered_op_keys["create"],
|
||||
request_overrides={
|
||||
"path_params": {"dms_instance_code": "{{stage_context.dms_instance_code}}"}, # Example path param
|
||||
"path_params": {"dms_instance_code": "{{stage_context.dms_instance_code}}"},
|
||||
"headers": {"tenant-id": DEFAULT_TENANT_ID_PLACEHOLDER, "Authorization": DEFAULT_AUTHORIZATION_PLACEHOLDER},
|
||||
"body": { # EXAMPLE BODY - MODIFY FOR YOUR API
|
||||
"version": "{{stage_context.api_version}}",
|
||||
"data": [{
|
||||
"bsflag": 0, # Field specific to GeoUnit example
|
||||
"wellCommonName": "{{stage_context.unique_resource_name}}", # Field specific to GeoUnit example
|
||||
"wellId": f"ExampleWellID_{run_timestamp}", # Field specific to GeoUnit example
|
||||
"dataRegion": "TEST_REGION" # Field specific to GeoUnit example
|
||||
}]
|
||||
}
|
||||
"body": final_create_body
|
||||
},
|
||||
expected_status_codes=[200], # Modify as per your API
|
||||
# outputs_to_context: {"created_resource_id": "body.data[0].dsid"} # MODIFY FOR YOUR API
|
||||
),
|
||||
StageStepDefinition(
|
||||
name=f"List and Find Created {RESOURCE_KEYWORD}",
|
||||
expected_status_codes=[200, 201],
|
||||
outputs_to_context={"created_resource_id": self.cfg_id_field_in_create_response}
|
||||
))
|
||||
|
||||
if self.discovered_op_keys["list"] and self.discovered_op_keys["create"]:
|
||||
list_query_filter = {
|
||||
"key": self.cfg_name_field_in_list_item,
|
||||
"symbol": "=",
|
||||
"realValue": ["{{stage_context.unique_resource_name}}"]
|
||||
}
|
||||
self.steps.append(StageStepDefinition(
|
||||
name="List and Find Created Resource",
|
||||
endpoint_spec_lookup_key=self.discovered_op_keys["list"],
|
||||
request_overrides={
|
||||
"path_params": { # Example path params
|
||||
"path_params": {
|
||||
"dms_instance_code": "{{stage_context.dms_instance_code}}",
|
||||
"version": "{{stage_context.api_version}}"
|
||||
},
|
||||
"headers": {"tenant-id": DEFAULT_TENANT_ID_PLACEHOLDER, "Authorization": DEFAULT_AUTHORIZATION_PLACEHOLDER},
|
||||
"query_params": {"pageNo": 1, "pageSize": 10}, # Example query params
|
||||
"body": { # EXAMPLE BODY for a POST list query - MODIFY FOR YOUR API
|
||||
"query_params": {"pageNo": 1, "pageSize": 10},
|
||||
"body": {
|
||||
"isSearchCount": True,
|
||||
"query": {
|
||||
"fields": ["dsid", "wellCommonName"], # Example fields - MODIFY
|
||||
"filter": { # Example filter - MODIFY
|
||||
"key": "wellCommonName",
|
||||
"symbol": "=",
|
||||
"realValue": ["{{stage_context.unique_resource_name}}"]
|
||||
}
|
||||
"fields": [self.cfg_id_field_in_list_item, self.cfg_name_field_in_list_item, self.cfg_feature_field_in_list_item],
|
||||
"filter": list_query_filter
|
||||
}
|
||||
}
|
||||
},
|
||||
expected_status_codes=[200],
|
||||
response_assertions=[find_and_extract_id_by_name], # Uses generic helper, ensure its internal paths are also updated!
|
||||
outputs_to_context={"created_resource_id": "body.data.list[0].dsid"} # EXAMPLE output - MODIFY
|
||||
),
|
||||
StageStepDefinition(
|
||||
name=f"Get Created {RESOURCE_KEYWORD} Details",
|
||||
response_assertions=[find_resource_in_list_and_extract_id],
|
||||
outputs_to_context={"found_id_from_list": f"{self.cfg_list_path_in_list_response}.0.{self.cfg_id_field_in_list_item}"}
|
||||
))
|
||||
|
||||
if self.discovered_op_keys["detail"] and self.discovered_op_keys["create"]:
|
||||
self.steps.append(StageStepDefinition(
|
||||
name="Get Created Resource Details",
|
||||
endpoint_spec_lookup_key=self.discovered_op_keys["detail"],
|
||||
request_overrides={
|
||||
"path_params": { # Example path params - MODIFY
|
||||
"path_params": {
|
||||
"dms_instance_code": "{{stage_context.dms_instance_code}}",
|
||||
"version": "{{stage_context.api_version}}",
|
||||
"id": "{{stage_context.created_resource_id}}" # Assumes 'id' is the path param name
|
||||
self.cfg_path_param_name_for_id: "{{stage_context.created_resource_id}}"
|
||||
},
|
||||
"headers": {"tenant-id": DEFAULT_TENANT_ID_PLACEHOLDER, "Authorization": DEFAULT_AUTHORIZATION_PLACEHOLDER}
|
||||
},
|
||||
expected_status_codes=[200],
|
||||
response_assertions=[check_resource_details] # Uses generic helper, ensure its internal paths are also updated!
|
||||
),
|
||||
StageStepDefinition(
|
||||
name=f"Update Created {RESOURCE_KEYWORD}",
|
||||
response_assertions=[check_resource_details]
|
||||
))
|
||||
|
||||
if self.discovered_op_keys["update"] and self.discovered_op_keys["create"]:
|
||||
update_body_data = {
|
||||
self.cfg_id_field_in_detail_response: "{{stage_context.created_resource_id}}",
|
||||
self.cfg_name_field_in_detail_response: "{{stage_context.updated_resource_name}}",
|
||||
}
|
||||
if self.cfg_feature_field_in_create_payload:
|
||||
update_body_data[self.cfg_feature_field_in_create_payload] = "{{stage_context.updated_feature_value}}"
|
||||
|
||||
final_update_body = {
|
||||
"id": "{{stage_context.created_resource_id}}",
|
||||
"version": "{{stage_context.api_version}}",
|
||||
**update_body_data
|
||||
}
|
||||
self.steps.append(StageStepDefinition(
|
||||
name="Update Created Resource",
|
||||
endpoint_spec_lookup_key=self.discovered_op_keys["update"],
|
||||
request_overrides={
|
||||
"path_params": {"dms_instance_code": "{{stage_context.dms_instance_code}}"}, # Example
|
||||
"query_params": {"id": "{{stage_context.created_resource_id}}"}, # Example if ID is in query for PUT
|
||||
"path_params": {
|
||||
"dms_instance_code": "{{stage_context.dms_instance_code}}",
|
||||
self.cfg_path_param_name_for_id: "{{stage_context.created_resource_id}}"
|
||||
},
|
||||
"headers": {"tenant-id": DEFAULT_TENANT_ID_PLACEHOLDER, "Authorization": DEFAULT_AUTHORIZATION_PLACEHOLDER},
|
||||
"body": { # EXAMPLE BODY - MODIFY FOR YOUR API
|
||||
"id": "{{stage_context.created_resource_id}}",
|
||||
"version": "{{stage_context.api_version}}",
|
||||
"wellCommonName": "{{stage_context.updated_resource_name}}", # Example field
|
||||
"dataRegion": "TEST_REGION_UPDATED" # Example field
|
||||
# Add other required fields for your API's update operation
|
||||
}
|
||||
"body": final_update_body
|
||||
},
|
||||
expected_status_codes=[200],
|
||||
),
|
||||
StageStepDefinition(
|
||||
name=f"Get Updated {RESOURCE_KEYWORD} Details",
|
||||
))
|
||||
|
||||
if self.discovered_op_keys["detail"] and self.discovered_op_keys["update"] and self.discovered_op_keys["create"]:
|
||||
self.steps.append(StageStepDefinition(
|
||||
name="Get Updated Resource Details",
|
||||
endpoint_spec_lookup_key=self.discovered_op_keys["detail"],
|
||||
request_overrides={
|
||||
"path_params": {
|
||||
"dms_instance_code": "{{stage_context.dms_instance_code}}",
|
||||
"version": "{{stage_context.api_version}}",
|
||||
"id": "{{stage_context.created_resource_id}}"
|
||||
self.cfg_path_param_name_for_id: "{{stage_context.created_resource_id}}"
|
||||
},
|
||||
"headers": {"tenant-id": DEFAULT_TENANT_ID_PLACEHOLDER, "Authorization": DEFAULT_AUTHORIZATION_PLACEHOLDER}
|
||||
},
|
||||
expected_status_codes=[200],
|
||||
response_assertions=[check_resource_updated_details] # Uses generic helper, check paths!
|
||||
),
|
||||
StageStepDefinition(
|
||||
name=f"Delete Created {RESOURCE_KEYWORD}",
|
||||
response_assertions=[check_resource_updated_details]
|
||||
))
|
||||
|
||||
if self.discovered_op_keys["delete"] and self.discovered_op_keys["create"]:
|
||||
self.steps.append(StageStepDefinition(
|
||||
name="Delete Created Resource",
|
||||
endpoint_spec_lookup_key=self.discovered_op_keys["delete"],
|
||||
request_overrides={
|
||||
"path_params": {"dms_instance_code": "{{stage_context.dms_instance_code}}"}, # Example
|
||||
"query_params": {"id": "{{stage_context.created_resource_id}}"}, # Example if ID is in query for DELETE
|
||||
"path_params": {
|
||||
"dms_instance_code": "{{stage_context.dms_instance_code}}",
|
||||
self.cfg_path_param_name_for_id: "{{stage_context.created_resource_id}}"
|
||||
},
|
||||
"headers": {"tenant-id": DEFAULT_TENANT_ID_PLACEHOLDER, "Authorization": DEFAULT_AUTHORIZATION_PLACEHOLDER},
|
||||
"body": { # EXAMPLE BODY for delete (if it takes a body) - MODIFY FOR YOUR API
|
||||
"version": "{{stage_context.api_version}}",
|
||||
"data": ["{{stage_context.created_resource_id}}"]
|
||||
}
|
||||
},
|
||||
expected_status_codes=[204], # Or 204 No Content, etc.
|
||||
),
|
||||
StageStepDefinition(
|
||||
name=f"Verify {RESOURCE_KEYWORD} Deletion",
|
||||
endpoint_spec_lookup_key=self.discovered_op_keys["detail"], # Try to get it again
|
||||
expected_status_codes=[200, 204],
|
||||
))
|
||||
|
||||
if self.discovered_op_keys["detail"] and self.discovered_op_keys["delete"] and self.discovered_op_keys["create"]:
|
||||
self.steps.append(StageStepDefinition(
|
||||
name="Verify Resource Deletion",
|
||||
endpoint_spec_lookup_key=self.discovered_op_keys["detail"],
|
||||
request_overrides={
|
||||
"path_params": {
|
||||
"dms_instance_code": "{{stage_context.dms_instance_code}}",
|
||||
"version": "{{stage_context.api_version}}",
|
||||
"id": "{{stage_context.created_resource_id}}"
|
||||
self.cfg_path_param_name_for_id: "{{stage_context.created_resource_id}}"
|
||||
},
|
||||
"headers": {"tenant-id": DEFAULT_TENANT_ID_PLACEHOLDER, "Authorization": DEFAULT_AUTHORIZATION_PLACEHOLDER}
|
||||
},
|
||||
expected_status_codes=[404], # Expect Not Found
|
||||
),
|
||||
]
|
||||
expected_status_codes=[404],
|
||||
))
|
||||
|
||||
def after_stage(self, stage_result: 'ExecutedStageResult', stage_context: dict, global_api_spec: ParsedAPISpec, api_group_name: str | None):
|
||||
self.logger.info(f"结束阶段 '{self.name}' (资源关键字: '{RESOURCE_KEYWORD}'). API分组: '{api_group_name}'. 最终状态: {stage_result.overall_status}. 最终上下文: {stage_context}")
|
||||
def after_stage(self, stage_result: ExecutedStageResult, stage_context: dict, global_api_spec: ParsedAPISpec, api_group_name: str | None):
|
||||
self.logger.info(f"Finished stage '{self.name}'. API Group: '{api_group_name}'. Final Status: {stage_result.overall_status}. Final Context Keys: {list(stage_context.keys())}")
|
||||
Reference in New Issue
Block a user