add:stage

This commit is contained in:
gongwenxin
2025-07-11 17:56:56 +08:00
parent 8fb86a34e9
commit cd1c6a340e
386 changed files with 272591 additions and 316502 deletions
+248
View File
@@ -0,0 +1,248 @@
import uuid
import logging
from typing import List, Dict, Any, Optional, Union
from collections import defaultdict
from ddms_compliance_suite.stage_framework import BaseAPIStage, StageStepDefinition, ExecutedStageResult
from ddms_compliance_suite.input_parser.parser import Endpoint, DMSEndpoint, ParsedAPISpec
from ddms_compliance_suite.test_framework_core import ValidationResult, APIResponseContext
from ddms_compliance_suite.api_caller.caller import APICaller
# --- Helper function to get value from nested dict ---
def _get_value_by_path(data: Optional[Dict[str, Any]], path_str: Optional[str]):
if data is None or not path_str:
return data
current = data
try:
for part in path_str.split('.'):
if isinstance(current, dict) and part in current:
current = current[part]
else:
return None
except (TypeError, KeyError):
return None
return current
# --- Custom Assertion & Helper Functions ---
def validate_response_is_true(response_ctx: APIResponseContext, stage_ctx: dict) -> ValidationResult:
"""Checks if response body's 'data' field is True."""
response_data = response_ctx.json_content
if not isinstance(response_data, dict):
return ValidationResult(passed=False, message=f"Response is not a JSON object. Got: {response_data}")
if response_data.get('data') is True:
return ValidationResult(passed=True, message="Response data is true as expected.")
else:
return ValidationResult(passed=False, message=f"Expected response data to be true, but it was '{response_data.get('data')}'.")
def validate_resource_details(response_ctx: APIResponseContext, stage_ctx: dict) -> ValidationResult:
"""Validates the details of a resource against the context."""
pk_name = stage_ctx.get("pk_name")
pk_value = stage_ctx.get("pk_value")
payload = stage_ctx.get("current_payload")
response_data = _get_value_by_path(response_ctx.json_content, "data")
if not isinstance(response_data, dict):
return ValidationResult(passed=False, message=f"Response 'data' field is not a JSON object. Got: {response_data}")
# Check if all fields from the payload exist in the response and match
for key, expected_value in payload.items():
if key not in response_data:
return ValidationResult(passed=False, message=f"Field '{key}' from payload not found in response.")
if response_data[key] != expected_value:
return ValidationResult(passed=False, message=f"Field '{key}' mismatch. Expected '{expected_value}', got '{response_data[key]}'.")
return ValidationResult(passed=True, message="Resource details successfully validated against payload.")
def validate_resource_is_deleted(response_ctx: APIResponseContext, stage_ctx: dict) -> ValidationResult:
"""Checks if the resource is no longer in the list response."""
pk_name = stage_ctx.get("pk_name")
pk_value = stage_ctx.get("pk_value")
response_list = _get_value_by_path(response_ctx.json_content, "data")
if not isinstance(response_list, list):
# It could be 'data.list' or other structures in a real app, for DMS it's 'data'
response_list = _get_value_by_path(response_ctx.json_content, "data.list")
if not isinstance(response_list, list):
return ValidationResult(passed=False, message=f"Response 'data' or 'data.list' field is not a list. Got: {response_ctx.json_content}")
for item in response_list:
if isinstance(item, dict) and item.get(pk_name) == pk_value:
return ValidationResult(passed=False, message=f"Resource with PK '{pk_value}' was found in the list after deletion.")
return ValidationResult(passed=True, message="Resource is not in the list as expected after deletion.")
# --- The Stage Definition ---
class DmsCrudScenarioStage(BaseAPIStage):
id = "dms_crud_scenario_stage"
name = "DMS Full CRUD Scenario"
description = "Performs a full Create -> Read -> Update -> Read -> Delete -> List workflow for a single DMS business object."
tags = ["dms", "crud", "scenario"]
continue_on_failure = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# scenarios will be populated by is_applicable_to_api_group
self.scenarios: List[Dict[str, Endpoint]] = []
self.current_scenario_index = -1
def is_applicable_to_api_group(self, api_group_name: Optional[str], global_api_spec: ParsedAPISpec) -> bool:
"""
Checks if the group of APIs contains at least one full CRUD set for a DMS object.
A full set consists of create, read, update, delete, and list operations.
"""
endpoints_in_group = self.apis_in_group
# We only care about DMS endpoints for this stage
dms_endpoints = [ep for ep in endpoints_in_group if isinstance(ep, DMSEndpoint)]
if not dms_endpoints:
return False
# Group endpoints by base resource name from operation_id
grouped_ops = defaultdict(dict)
for ep in dms_endpoints:
if not ep.operation_id:
continue
parts = ep.operation_id.split('_', 1)
if len(parts) != 2:
continue
op_type, resource_name = parts
grouped_ops[resource_name][op_type] = ep
# Find complete scenarios
required_ops = {'create', 'read', 'update', 'delete', 'list'}
for resource_name, ops in grouped_ops.items():
if required_ops.issubset(ops.keys()):
self.scenarios.append(ops)
self.logger.info(f"Found complete CRUD scenario for DMS resource: '{resource_name}'")
return len(self.scenarios) > 0
def before_stage(self, stage_context: dict, global_api_spec: ParsedAPISpec, api_group_name: str | None):
"""
Set up the context for the next scenario to run.
This will be called by the orchestrator. We will use it to prepare for a single scenario execution.
"""
self.current_scenario_index += 1
if self.current_scenario_index >= len(self.scenarios):
# Should not happen if orchestrator works as expected (one stage instance per scenario)
# but as a safeguard.
raise Exception("No more scenarios to run.")
current_scenario = self.scenarios[self.current_scenario_index]
self.logger.info(f"Setting up before_stage for scenario: {list(current_scenario.keys())}")
# Get the 'create' endpoint to determine the primary key
create_op = current_scenario['create']
# This is a bit of a hack. The primary key should ideally be discoverable
# from the schema without relying on request body definitions.
# Based on our parser, the delete request body contains the PK.
delete_op = current_scenario['delete']
pk_name = next(iter(delete_op.request_body['content']['application/json']['schema']['properties']['data']['items']['properties']))
pk_value = str(uuid.uuid4())
# Prepare a sample payload. We'd need a proper data generator for this.
# For now, let's create a placeholder. The test framework should handle generation.
# Let's assume the framework's parameter generator will handle the real payload.
# We just need to provide the PK.
create_payload = { pk_name: pk_value, "description": "test-entry-from-scenario" }
update_payload = { pk_name: pk_value, "description": "updated-test-entry-from-scenario" }
# Populate stage context
stage_context["pk_name"] = pk_name
stage_context["pk_value"] = pk_value
stage_context["current_payload"] = create_payload
stage_context["update_payload"] = update_payload
stage_context["scenario_endpoints"] = current_scenario
def get_api_spec_for_operation(self, lookup_key: str, *args, **kwargs) -> Optional[Endpoint]:
"""
Resolves a lookup key like "CREATE" to the actual endpoint for the current scenario.
"""
op_map = {
"CREATE": "create", "READ": "read", "UPDATE": "update",
"DELETE": "delete", "LIST": "list"
}
op_type = op_map.get(lookup_key)
if not op_type:
return None
scenario = self.scenarios[self.current_scenario_index]
return scenario.get(op_type)
steps: List[StageStepDefinition] = [
StageStepDefinition(
name="Step 1: Create Resource",
endpoint_spec_lookup_key="CREATE",
request_overrides={
# We assume the body is an array under 'data' key
"body": {"data": ["{{stage_context.current_payload}}"]}
},
response_assertions=[validate_response_is_true],
outputs_to_context={}
),
StageStepDefinition(
name="Step 2: Read Resource to Verify Creation",
endpoint_spec_lookup_key="READ",
request_overrides={
"path_params": {"id": "{{stage_context.pk_value}}"}
},
response_assertions=[validate_resource_details]
),
StageStepDefinition(
name="Step 3: Update Resource",
endpoint_spec_lookup_key="UPDATE",
request_overrides={
"body": {"data": ["{{stage_context.update_payload}}"]},
# The context needs to be updated for the next validation step
"context_updates": {"current_payload": "{{stage_context.update_payload}}"}
},
response_assertions=[validate_response_is_true]
),
StageStepDefinition(
name="Step 4: Read Resource to Verify Update",
endpoint_spec_lookup_key="READ",
request_overrides={
"path_params": {"id": "{{stage_context.pk_value}}"}
},
response_assertions=[validate_resource_details]
),
StageStepDefinition(
name="Step 5: Delete Resource",
endpoint_spec_lookup_key="DELETE",
request_overrides={
"body": {"data": [{"{{stage_context.pk_name}}": "{{stage_context.pk_value}}"}]}
},
response_assertions=[validate_response_is_true]
),
StageStepDefinition(
name="Step 6: List to Verify Deletion",
endpoint_spec_lookup_key="LIST",
response_assertions=[validate_resource_is_deleted]
),
]
def after_stage(self, stage_result: ExecutedStageResult, stage_context: dict, global_api_spec: ParsedAPISpec, api_group_name: str | None):
"""
Can be used for cleanup or to modify the final result.
"""
# If there are more scenarios, we might want to indicate this, but the orchestrator should handle it.
if self.current_scenario_index < len(self.scenarios) - 1:
self.logger.info(f"Finished scenario {self.current_scenario_index + 1}/{len(self.scenarios)} for this group.")
else:
self.logger.info("All scenarios for this API group have been executed.")
# We can add the specific resource name to the stage result description
if self.scenarios:
scenario = self.scenarios[self.current_scenario_index]
# get resource name from first op
op_id = next(iter(scenario.values())).operation_id
resource_name = op_id.split('_', 1)[1]
stage_result.description += f" (Scenario for: {resource_name})"
+419 -419
View File
@@ -1,505 +1,505 @@
import time
import uuid
import logging
import re
from typing import List, Dict, Any, Optional, Callable, Union
# import time
# import uuid
# import logging
# import re
# 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, YAPIEndpoint, SwaggerEndpoint
# 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, YAPIEndpoint, SwaggerEndpoint
# --- Action Keywords for Discovery (Usually these can remain as is) ---
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", "删除"]
# # --- Action Keywords for Discovery (Usually these can remain as is) ---
# 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 ---
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"
# --- 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
# # --- 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
current = data
try:
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, 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]
else:
return None
except (TypeError, ValueError, IndexError, KeyError) as e:
logging.getLogger(__name__).debug(f"Error navigating path '{path_str}': {e}")
return None
return current
# current = data
# try:
# 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, 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]
# else:
# return None
# except (TypeError, ValueError, IndexError, KeyError) as e:
# logging.getLogger(__name__).debug(f"Error navigating path '{path_str}': {e}")
# return None
# return current
# --- 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.
# # --- 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")
# 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")
# 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.")
# 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)
# 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"Response format error: Expected path '{list_path}' to be a list, got {type(resource_list)}.")
# if not isinstance(resource_list, 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) == unique_name_to_find:
found_item = item
break
# found_item = None
# for item in resource_list:
# 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"Resource not found: Name '{unique_name_to_find}' (field: {name_field}) not found in list at path '{list_path}'.")
# if not found_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)
if resource_id is None:
return ValidationResult(passed=False, message=f"Resource found by name, but it lacks the ID field '{id_field}'.")
# 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}'.")
stage_ctx["created_resource_id"] = resource_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.")
# stage_ctx["created_resource_id"] = resource_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:
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")
# def check_resource_details(response_ctx: APIResponseContext, stage_ctx: dict) -> ValidationResult:
# 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")
# created_id = stage_ctx.get("created_resource_id")
# initial_name = stage_ctx.get("unique_resource_name")
resource_data = _get_value_by_path(response_ctx.json_content, resource_object_path)
# 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"Response format error: Expected path '{resource_object_path}' to be an object, got {type(resource_data)}.")
# if not isinstance(resource_data, dict):
# return ValidationResult(passed=False, message=f"Response format error: Expected path '{resource_object_path}' to be an object, got {type(resource_data)}.")
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})."))
# 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 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}'."))
# 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}'."))
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])
# 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:
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")
# def check_resource_updated_details(response_ctx: APIResponseContext, stage_ctx: dict) -> ValidationResult:
# 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")
# created_id = stage_ctx.get("created_resource_id")
# updated_name = stage_ctx.get("updated_resource_name")
resource_data = _get_value_by_path(response_ctx.json_content, resource_object_path)
# 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"Updated detail response format error: Expected path '{resource_object_path}' to be an object, got {type(resource_data)}.")
# if not isinstance(resource_data, dict):
# return ValidationResult(passed=False, message=f"Updated detail response format error: Expected path '{resource_object_path}' to be an object, got {type(resource_data)}.")
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}'."))
# 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 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}'."))
# 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])
# 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 GenericCRUDValidationStage(BaseAPIStage):
id = "generic_crud_validation_stage"
name = "Generic CRUD Validation Stage"
description = (
"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", "generic_validation"]
continue_on_failure = False
fail_if_not_applicable_to_any_group = False
# class GenericCRUDValidationStage(BaseAPIStage):
# id = "generic_crud_validation_stage"
# name = "Generic CRUD Validation Stage"
# description = (
# "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", "generic_validation"]
# continue_on_failure = False
# fail_if_not_applicable_to_any_group = False
# --- 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"
# # --- 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_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_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_resource_object_path_in_update_response: str = "data.list.0"
cfg_path_param_name_for_id: str = "id"
# cfg_path_param_name_for_id: str = "id"
discovered_op_keys: Dict[str, Optional[str]] = {}
steps: list = []
# discovered_op_keys: Dict[str, Optional[str]] = {}
# steps: list = []
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 _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}")
# 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
# 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)}")
# 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)
# text_to_match = getattr(ep_obj, 'title', None) or \
# getattr(ep_obj, 'summary', None) or \
# getattr(ep_obj, 'operation_id', None)
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
# 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
lookup_key = getattr(ep_obj, 'title', None) or \
getattr(ep_obj, 'operation_id', None) or \
f"{ep_method_actual} {ep_path}"
# lookup_key = getattr(ep_obj, 'title', None) or \
# getattr(ep_obj, 'operation_id', None) or \
# f"{ep_method_actual} {ep_path}"
text_lower = text_to_match.lower()
# text_lower = text_to_match.lower()
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}")
# 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}")
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})")
# 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.")
# 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}')")
# 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})")
# 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 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
# 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
# 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
self._determine_resource_config(api_group_name, global_api_spec)
# self._determine_resource_config(api_group_name, global_api_spec)
self.discovered_op_keys = {}
endpoints_in_group = self.apis_in_group
# self.discovered_op_keys = {}
# endpoints_in_group = self.apis_in_group
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)
# 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)
required_ops = ["create", "detail", "delete"]
missing_required_ops = [op for op in required_ops if not self.discovered_op_keys.get(op)]
# 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_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 required operations: {missing_required_ops}. Discovered keys: {self.discovered_op_keys}")
return False
# 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 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)
# 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}")
# 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]
# run_timestamp = int(time.time())
# run_uuid_short = uuid.uuid4().hex[:6]
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"
# 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
# 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
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["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
# 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 (with cfg): {stage_context}")
# self.logger.info(f"Initial stage context (with cfg): {stage_context}")
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
# 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}}"
# 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}}"
final_create_body = {
"version": "{{stage_context.api_version}}",
"data": [create_body_data]
}
# final_create_body = {
# "version": "{{stage_context.api_version}}",
# "data": [create_body_data]
# }
self.steps = []
# 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}}"},
"headers": {"tenant-id": DEFAULT_TENANT_ID_PLACEHOLDER, "Authorization": DEFAULT_AUTHORIZATION_PLACEHOLDER},
"body": final_create_body
},
expected_status_codes=[200, 201],
outputs_to_context={"created_resource_id": self.cfg_id_field_in_create_response}
))
# 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}}"},
# "headers": {"tenant-id": DEFAULT_TENANT_ID_PLACEHOLDER, "Authorization": DEFAULT_AUTHORIZATION_PLACEHOLDER},
# "body": final_create_body
# },
# 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": {
"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},
"body": {
"isSearchCount": True,
"query": {
"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_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["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": {
# "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},
# "body": {
# "isSearchCount": True,
# "query": {
# "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_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": {
"dms_instance_code": "{{stage_context.dms_instance_code}}",
"version": "{{stage_context.api_version}}",
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]
))
# 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": {
# "dms_instance_code": "{{stage_context.dms_instance_code}}",
# "version": "{{stage_context.api_version}}",
# 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]
# ))
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}}"
# 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}}",
self.cfg_path_param_name_for_id: "{{stage_context.created_resource_id}}"
},
"headers": {"tenant-id": DEFAULT_TENANT_ID_PLACEHOLDER, "Authorization": DEFAULT_AUTHORIZATION_PLACEHOLDER},
"body": final_update_body
},
expected_status_codes=[200],
))
# 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}}",
# self.cfg_path_param_name_for_id: "{{stage_context.created_resource_id}}"
# },
# "headers": {"tenant-id": DEFAULT_TENANT_ID_PLACEHOLDER, "Authorization": DEFAULT_AUTHORIZATION_PLACEHOLDER},
# "body": final_update_body
# },
# expected_status_codes=[200],
# ))
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}}",
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]
))
# 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}}",
# 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]
# ))
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}}",
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, 204],
))
# 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}}",
# 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, 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}}",
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],
))
# 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}}",
# 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],
# ))
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())}")
# 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())}")