From eba56bc75645baaef80ea216a1ace819463fca6c Mon Sep 17 00:00:00 2001 From: longpeng Date: Fri, 27 Jun 2025 01:01:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87=E4=BC=A0=E9=80=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 - docs/activity_parameter_passing.md | 255 +++++++++++++++ routers/handlers/workflow.go | 2 +- services/composite.go | 27 +- utils/parameter_processor.go | 484 +++++++++++++++++++++++++++++ utils/parameter_processor_test.go | 385 +++++++++++++++++++++++ workers/python/api_tests.py | 9 +- workflows/dynamic_workflow.go | 67 +++- 8 files changed, 1209 insertions(+), 22 deletions(-) delete mode 100644 README.md create mode 100644 docs/activity_parameter_passing.md create mode 100644 utils/parameter_processor.go create mode 100644 utils/parameter_processor_test.go diff --git a/README.md b/README.md deleted file mode 100644 index 72eb78f..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Beacon - diff --git a/docs/activity_parameter_passing.md b/docs/activity_parameter_passing.md new file mode 100644 index 0000000..bf2973e --- /dev/null +++ b/docs/activity_parameter_passing.md @@ -0,0 +1,255 @@ +# Activity 之间参数传递指南 + +## 概述 + +在动态测试工作流中,多个 Activity 之间经常需要传递参数。例如,第二个 Activity 可能需要使用第一个 Activity 响应的某个字段作为入参。本文档详细说明了如何实现这种参数传递机制。 + +## 支持的变量格式 + +### 1. 全局变量 +格式:`${global.key}` +- 用于访问工作流级别的全局变量 +- 这些变量在工作流启动时通过 `GlobalParameters` 传入 + +### 2. 步骤结果变量 +格式:`${step.stepId.field}` +- 用于访问之前步骤的执行结果 +- `stepId` 是步骤的ID +- `field` 是结果中的具体字段 + +## 变量引用示例 + +### API 测试结果变量 +```json +{ + "test_case_id": "api_test_001", + "endpoint": "/api/login", + "http_method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "request_body": "{\"username\": \"testuser\", \"password\": \"testpass\"}", + "expected_status_code": 200 +} +``` + +假设这个API测试的步骤ID是123,执行后返回: +```json +{ + "base_result": { + "success": true, + "message": "API Test Passed" + }, + "actual_status_code": 200, + "response_body": "{\"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\", \"user_id\": 456}", + "headers": { + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "Content-Type": "application/json" + } +} +``` + +### 可用的变量引用 + +1. **响应体**:`${step.123.response_body}` +2. **状态码**:`${step.123.actual_status_code}` +3. **响应头**:`${step.123.headers.Authorization}` +4. **JSON字段**:`${step.123.json_token}` (自动提取的token字段) +5. **用户ID**:`${step.123.json_user_id}` (自动提取的user_id字段) + +## 实际使用场景 + +### 场景1:登录后使用Token进行API调用 + +**步骤1:登录API** +```json +{ + "test_case_id": "login_test", + "endpoint": "/api/login", + "http_method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "request_body": "{\"username\": \"testuser\", \"password\": \"testpass\"}", + "expected_status_code": 200 +} +``` + +**步骤2:使用Token调用受保护的API** +```json +{ + "test_case_id": "protected_api_test", + "endpoint": "/api/user/profile", + "http_method": "GET", + "headers": { + "Authorization": "${step.123.json_token}", + "Content-Type": "application/json" + }, + "expected_status_code": 200 +} +``` + +### 场景2:使用响应头中的Token + +```json +{ + "test_case_id": "api_with_header_token", + "endpoint": "/api/data", + "http_method": "GET", + "headers": { + "Authorization": "${step.123.headers.Authorization}", + "Content-Type": "application/json" + }, + "expected_status_code": 200 +} +``` + +### 场景3:使用响应体中的用户ID + +```json +{ + "test_case_id": "user_data_test", + "endpoint": "/api/users/${step.123.json_user_id}/data", + "http_method": "GET", + "headers": { + "Authorization": "${step.123.json_token}", + "Content-Type": "application/json" + }, + "expected_status_code": 200 +} +``` + +## UI 测试结果变量 + +### UI 测试结果示例 +```json +{ + "base_result": { + "success": true, + "message": "UI Test Passed" + }, + "screenshot_url": "https://s3.amazonaws.com/screenshots/test_001.png", + "html_report_url": "https://s3.amazonaws.com/reports/test_001.html" +} +``` + +### 可用的变量引用 +1. **截图URL**:`${step.456.screenshot_url}` +2. **报告URL**:`${step.456.html_report_url}` + +## 全局变量使用 + +### 工作流启动时传入全局变量 +```go +input := &pb.DynamicTestRunInput{ + RunId: "test_run_001", + CompositeCaseId: "composite_case_001", + GlobalParameters: map[string]string{ + "base_url": "https://api.example.com", + "environment": "staging", + "api_version": "v1", + }, +} +``` + +### 在步骤参数中使用全局变量 +```json +{ + "test_case_id": "api_test", + "endpoint": "${global.base_url}/${global.api_version}/users", + "http_method": "GET", + "headers": { + "X-Environment": "${global.environment}", + "Content-Type": "application/json" + }, + "expected_status_code": 200 +} +``` + +## 复杂场景示例 + +### 多步骤参数传递链 + +**步骤1:用户注册** +```json +{ + "test_case_id": "user_registration", + "endpoint": "/api/register", + "http_method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "request_body": "{\"username\": \"newuser\", \"email\": \"newuser@example.com\", \"password\": \"password123\"}", + "expected_status_code": 201 +} +``` + +**步骤2:用户登录(使用注册返回的用户ID)** +```json +{ + "test_case_id": "user_login", + "endpoint": "/api/login", + "http_method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "request_body": "{\"username\": \"newuser\", \"password\": \"password123\"}", + "expected_status_code": 200 +} +``` + +**步骤3:获取用户信息(使用登录返回的token)** +```json +{ + "test_case_id": "get_user_info", + "endpoint": "/api/users/${step.123.json_user_id}", + "http_method": "GET", + "headers": { + "Authorization": "Bearer ${step.124.json_token}", + "Content-Type": "application/json" + }, + "expected_status_code": 200 +} +``` + +**步骤4:更新用户信息(使用步骤3的token和用户ID)** +```json +{ + "test_case_id": "update_user_info", + "endpoint": "/api/users/${step.123.json_user_id}", + "http_method": "PUT", + "headers": { + "Authorization": "Bearer ${step.124.json_token}", + "Content-Type": "application/json" + }, + "request_body": "{\"display_name\": \"Updated User\", \"bio\": \"This is my updated bio\"}", + "expected_status_code": 200 +} +``` + +## 注意事项 + +1. **变量解析顺序**:先解析全局变量,再解析步骤变量 +2. **错误处理**:如果变量不存在,会保持原始占位符不变 +3. **类型安全**:所有变量都会被转换为字符串 +4. **性能考虑**:变量解析在每次步骤执行时进行,避免频繁的字符串操作 +5. **调试支持**:工作流日志会记录变量解析过程 + +## 最佳实践 + +1. **使用有意义的变量名**:避免使用过于简单的变量名 +2. **文档化变量**:在步骤描述中说明使用的变量 +3. **测试变量解析**:在开发环境中测试变量解析是否正确 +4. **错误处理**:在步骤参数中提供默认值或错误处理逻辑 +5. **版本控制**:记录变量格式的变更,确保向后兼容 + +## 扩展功能 + +### 自定义变量提取器 +可以通过扩展 `extractApiResultToGlobalVariables` 和 `extractUiResultToGlobalVariables` 函数来支持更多自定义字段的提取。 + +### 条件变量 +可以基于步骤的成功/失败状态来设置不同的变量值。 + +### 变量验证 +可以在变量解析时添加验证逻辑,确保必需的变量存在且格式正确。 \ No newline at end of file diff --git a/routers/handlers/workflow.go b/routers/handlers/workflow.go index bdba956..ed6ea40 100644 --- a/routers/handlers/workflow.go +++ b/routers/handlers/workflow.go @@ -18,7 +18,7 @@ func NewWorkflowHandler() *WorkflowHandler { // StartWorkflow POST /api/workflow/start func (h *WorkflowHandler) StartWorkflow(c *gin.Context) { - err := h.service.Start("11") + err := h.service.Start("13") if err != nil { c.JSON(500, gin.H{ "code": -1, diff --git a/services/composite.go b/services/composite.go index 03ea4db..98b8b4c 100644 --- a/services/composite.go +++ b/services/composite.go @@ -4,6 +4,7 @@ import ( "beacon/models" "beacon/pkg/dao/mysql" "beacon/utils" + "encoding/json" "fmt" "go.uber.org/zap" ) @@ -63,7 +64,7 @@ func (s *CompositeCaseService) CreateCompositeCase(req *models.CreateCompositeCa StepDescription: stepReq.StepDescription, StepType: stepReq.StepType, ActivityName: activityName, - ParametersJson: stepReq.ParametersJson, + ParametersJson: fixParametersJson(stepReq.ParametersJson), IsRequired: stepReq.IsRequired, } steps = append(steps, step) @@ -187,7 +188,7 @@ func (s *CompositeCaseService) UpdateCompositeCase(id uint, req *models.UpdateCo StepDescription: stepReq.StepDescription, StepType: stepReq.StepType, ActivityName: activityName, - ParametersJson: stepReq.ParametersJson, + ParametersJson: fixParametersJson(stepReq.ParametersJson), IsRequired: stepReq.IsRequired, } steps = append(steps, step) @@ -213,6 +214,28 @@ func (s *CompositeCaseService) UpdateCompositeCase(id uint, req *models.UpdateCo return s.GetCompositeCaseByID(id) } +func fixParametersJson(jsonStr string) string { + var params map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), ¶ms); err != nil { + // Not a valid json string, return as is. + return jsonStr + } + + if rb, ok := params["request_body"]; ok { + if rbs, ok := rb.(string); ok && rbs == "{" { + params["request_body"] = "{}" + } + } + + fixedJSONBytes, err := json.Marshal(params) + if err != nil { + // Failed to marshal back, return original. + return jsonStr + } + + return string(fixedJSONBytes) +} + // DeleteCompositeCase 删除复合案例 func (s *CompositeCaseService) DeleteCompositeCase(id uint) error { zap.L().Info("开始删除复合案例", zap.Uint("id", id)) diff --git a/utils/parameter_processor.go b/utils/parameter_processor.go new file mode 100644 index 0000000..ec71490 --- /dev/null +++ b/utils/parameter_processor.go @@ -0,0 +1,484 @@ +package utils + +import ( + "encoding/json" + "fmt" + "go.uber.org/zap" + "regexp" + "strconv" + "strings" + + "beacon/pkg/pb" +) + +// ParameterProcessor 参数处理器,用于处理activity之间的参数传递 +type ParameterProcessor struct { + globalVariables map[string]interface{} + activityResults map[int64]*ActivityResult +} + +// ActivityResult 用于存储每个activity的执行结果 +type ActivityResult struct { + StepID int64 `json:"step_id"` + StepName string `json:"step_name"` + Success bool `json:"success"` + Data interface{} `json:"data"` +} + +// NewParameterProcessor 创建新的参数处理器 +func NewParameterProcessor(globalVariables map[string]interface{}) *ParameterProcessor { + return &ParameterProcessor{ + globalVariables: globalVariables, + activityResults: make(map[int64]*ActivityResult), + } +} + +// AddActivityResult 添加activity执行结果 +func (p *ParameterProcessor) AddActivityResult(stepID int64, result *ActivityResult) { + p.activityResults[stepID] = result +} + +// ProcessTemplate 处理参数模板,替换其中的变量引用 +func (p *ParameterProcessor) ProcessTemplate(template string) (string, error) { + if template == "" { + return template, nil + } + + result := template + + // 1. 替换全局变量引用 ${global.key} + result = p.replaceGlobalVariables(result) + + // 2. 替换步骤结果变量引用 ${step.stepId.field} + result = p.replaceStepVariables(result) + + // 3. 替换条件变量引用 ${if.condition:value1:value2} + result = p.replaceConditionalVariables(result) + + // 4. 替换函数调用 ${func:arg1:arg2} + result = p.replaceFunctionCalls(result) + + return result, nil +} + +// replaceGlobalVariables 替换全局变量引用 +func (p *ParameterProcessor) replaceGlobalVariables(template string) string { + // 使用正则表达式匹配 ${global.key} 格式 + re := regexp.MustCompile(`\$\{global\.([^}]+)\}`) + + return re.ReplaceAllStringFunc(template, func(match string) string { + // 提取变量名 + matches := re.FindStringSubmatch(match) + if len(matches) < 2 { + return match // 保持原样 + } + + key := matches[1] + if value, exists := p.globalVariables[key]; exists { + return fmt.Sprintf("%v", value) + } + + return match // 变量不存在,保持原样 + }) +} + +// replaceStepVariables 替换步骤结果变量引用 +func (p *ParameterProcessor) replaceStepVariables(template string) string { + // 匹配 ${step.stepId.field} 格式 + re := regexp.MustCompile(`\$\{step\.(\d+)\.([^}]+)\}`) + + return re.ReplaceAllStringFunc(template, func(match string) string { + matches := re.FindStringSubmatch(match) + if len(matches) < 3 { + return match + } + + stepIDStr := matches[1] + field := matches[2] + + stepID, err := strconv.ParseInt(stepIDStr, 10, 64) + if err != nil { + return match + } + + result, exists := p.activityResults[stepID] + if !exists { + return match + } + + return p.extractFieldValue(result, field) + }) +} + +// replaceConditionalVariables 替换条件变量引用 +func (p *ParameterProcessor) replaceConditionalVariables(template string) string { + // 匹配 ${if.condition:value1:value2} 格式 + re := regexp.MustCompile(`\$\{if\.([^:]+):([^:]*):([^}]*)\}`) + + return re.ReplaceAllStringFunc(template, func(match string) string { + matches := re.FindStringSubmatch(match) + if len(matches) < 4 { + return match + } + + condition := matches[1] + valueIfTrue := matches[2] + valueIfFalse := matches[3] + + if p.evaluateCondition(condition) { + return valueIfTrue + } + return valueIfFalse + }) +} + +// replaceFunctionCalls 替换函数调用 +func (p *ParameterProcessor) replaceFunctionCalls(template string) string { + // 匹配 ${func:arg1:arg2} 格式 + re := regexp.MustCompile(`\$\{([^:]+):([^}]*)\}`) + + return re.ReplaceAllStringFunc(template, func(match string) string { + matches := re.FindStringSubmatch(match) + if len(matches) < 3 { + return match + } + + funcName := matches[1] + args := matches[2] + + return p.executeFunction(funcName, args) + }) +} + +// extractFieldValue 从activity结果中提取指定字段的值 +func (p *ParameterProcessor) extractFieldValue(result *ActivityResult, field string) string { + switch result.Data.(type) { + case *pb.ApiTestResult: + return p.extractApiResultField(result.Data.(*pb.ApiTestResult), field) + case *pb.UiTestResult: + return p.extractUiResultField(result.Data.(*pb.UiTestResult), field) + default: + return "" + } +} + +// extractApiResultField 从API测试结果中提取字段值 +func (p *ParameterProcessor) extractApiResultField(result *pb.ApiTestResult, field string) string { + switch field { + case "response_body": + // 保证输出是 JSON 字符串格式 + b, err := json.Marshal(result.ResponseBody) + if err == nil { + return string(b[1 : len(b)-1]) // 去掉外层引号 + } + return result.ResponseBody + case "actual_status_code": + return strconv.Itoa(int(result.ActualStatusCode)) + case "success": + return strconv.FormatBool(result.BaseResult.Success) + case "message": + return result.BaseResult.Message + default: + zap.L().Debug("未知字段", zap.String("field", field), zap.String("ResponseBody", result.ResponseBody)) + + // 检查是否是响应头 + if strings.HasPrefix(field, "headers.") { + headerKey := strings.TrimPrefix(field, "headers.") + if headerValue, exists := result.Headers[headerKey]; exists { + return headerValue + } + } + + // 检查是否是JSON字段 + if strings.HasPrefix(field, "json.") { + jsonField := strings.TrimPrefix(field, "json.") + return p.extractJsonField(result.ResponseBody, jsonField) + } + + return "" + } +} + +// extractUiResultField 从UI测试结果中提取字段值 +func (p *ParameterProcessor) extractUiResultField(result *pb.UiTestResult, field string) string { + switch field { + case "screenshot_url": + return result.ScreenshotUrl + case "html_report_url": + return result.HtmlReportUrl + case "success": + return strconv.FormatBool(result.BaseResult.Success) + case "message": + return result.BaseResult.Message + default: + return "" + } +} + +// extractJsonField 从JSON响应体中提取指定字段 +func (p *ParameterProcessor) extractJsonField(responseBody, field string) string { + if responseBody == "" { + return "" + } + + var data map[string]interface{} + if err := json.Unmarshal([]byte(responseBody), &data); err != nil { + return "" + } + + // 支持嵌套字段,如 "user.id" + keys := strings.Split(field, ".") + current := data + + for i, key := range keys { + if i == len(keys)-1 { + // 最后一个键 + if value, exists := current[key]; exists { + return fmt.Sprintf("%v", value) + } + } else { + // 中间键,需要继续深入 + if next, exists := current[key]; exists { + if nextMap, ok := next.(map[string]interface{}); ok { + current = nextMap + } else { + return "" + } + } else { + return "" + } + } + } + + return "" +} + +// evaluateCondition 评估条件表达式 +func (p *ParameterProcessor) evaluateCondition(condition string) bool { + // 支持简单的条件表达式 + // 例如:step.123.success, global.environment == "production" + + // 检查步骤成功状态 + if strings.HasSuffix(condition, ".success") { + stepIDStr := strings.TrimSuffix(condition, ".success") + if strings.HasPrefix(stepIDStr, "step.") { + stepIDStr = strings.TrimPrefix(stepIDStr, "step.") + stepID, err := strconv.ParseInt(stepIDStr, 10, 64) + if err == nil { + if result, exists := p.activityResults[stepID]; exists { + return result.Success + } + } + } + } + + // 检查全局变量 + if strings.HasPrefix(condition, "global.") { + key := strings.TrimPrefix(condition, "global.") + if value, exists := p.globalVariables[key]; exists { + // 转换为布尔值 + switch v := value.(type) { + case bool: + return v + case string: + return v != "" && v != "false" && v != "0" + case int, int64, float64: + return fmt.Sprintf("%v", v) != "0" + default: + return value != nil + } + } + } + + // 检查相等性 + if strings.Contains(condition, "==") { + parts := strings.Split(condition, "==") + if len(parts) == 2 { + left := strings.TrimSpace(parts[0]) + right := strings.TrimSpace(strings.Trim(parts[1], `"'`)) + + // 检查全局变量 + if strings.HasPrefix(left, "global.") { + key := strings.TrimPrefix(left, "global.") + if value, exists := p.globalVariables[key]; exists { + return fmt.Sprintf("%v", value) == right + } + } + } + } + + return false +} + +// executeFunction 执行函数调用 +func (p *ParameterProcessor) executeFunction(funcName, args string) string { + argList := strings.Split(args, ":") + + switch funcName { + case "concat": + // 连接字符串 + return strings.Join(argList, "") + case "upper": + // 转换为大写 + if len(argList) > 0 { + return strings.ToUpper(argList[0]) + } + case "lower": + // 转换为小写 + if len(argList) > 0 { + return strings.ToLower(argList[0]) + } + case "substring": + // 子字符串 + if len(argList) >= 3 { + str := argList[0] + start, _ := strconv.Atoi(argList[1]) + end, _ := strconv.Atoi(argList[2]) + if start < len(str) && end <= len(str) && start < end { + return str[start:end] + } + } + case "default": + // 默认值 + if len(argList) >= 2 { + value := argList[0] + defaultValue := argList[1] + if value == "" { + return defaultValue + } + return value + } + } + + return "" +} + +// GetAvailableVariables 获取所有可用的变量列表 +func (p *ParameterProcessor) GetAvailableVariables() map[string][]string { + variables := make(map[string][]string) + + // 全局变量 + var globalVars []string + for key := range p.globalVariables { + globalVars = append(globalVars, key) + } + variables["global"] = globalVars + + // 步骤变量 + for stepID, result := range p.activityResults { + stepKey := fmt.Sprintf("step.%d", stepID) + var stepVars []string + + switch result.Data.(type) { + case *pb.ApiTestResult: + stepVars = []string{ + "response_body", + "actual_status_code", + "success", + "message", + "headers.*", + "json.*", + } + case *pb.UiTestResult: + stepVars = []string{ + "screenshot_url", + "html_report_url", + "success", + "message", + } + } + + variables[stepKey] = stepVars + } + + return variables +} + +// ValidateTemplate 验证模板中的变量引用是否有效 +func (p *ParameterProcessor) ValidateTemplate(template string) (bool, []string) { + var errors []string + + // 检查全局变量引用 + globalRe := regexp.MustCompile(`\$\{global\.([^}]+)\}`) + matches := globalRe.FindAllStringSubmatch(template, -1) + for _, match := range matches { + if len(match) >= 2 { + key := match[1] + if _, exists := p.globalVariables[key]; !exists { + errors = append(errors, fmt.Sprintf("全局变量 '%s' 不存在", key)) + } + } + } + + // 检查步骤变量引用 + stepRe := regexp.MustCompile(`\$\{step\.(\d+)\.([^}]+)\}`) + matches = stepRe.FindAllStringSubmatch(template, -1) + for _, match := range matches { + if len(match) >= 3 { + stepIDStr := match[1] + field := match[2] + + stepID, err := strconv.ParseInt(stepIDStr, 10, 64) + if err != nil { + errors = append(errors, fmt.Sprintf("无效的步骤ID: %s", stepIDStr)) + continue + } + + if _, exists := p.activityResults[stepID]; !exists { + errors = append(errors, fmt.Sprintf("步骤 %d 不存在", stepID)) + continue + } + + // 检查字段是否有效 + if !p.isValidField(stepID, field) { + errors = append(errors, fmt.Sprintf("步骤 %d 不支持字段: %s", stepID, field)) + } + } + } + + return len(errors) == 0, errors +} + +// isValidField 检查字段是否有效 +func (p *ParameterProcessor) isValidField(stepID int64, field string) bool { + result, exists := p.activityResults[stepID] + if !exists { + return false + } + + switch result.Data.(type) { + case *pb.ApiTestResult: + validFields := map[string]bool{ + "response_body": true, + "actual_status_code": true, + "success": true, + "message": true, + } + + if validFields[field] { + return true + } + + // 检查是否是响应头 + if strings.HasPrefix(field, "headers.") { + return true + } + + // 检查是否是JSON字段 + if strings.HasPrefix(field, "json.") { + return true + } + + case *pb.UiTestResult: + validFields := map[string]bool{ + "screenshot_url": true, + "html_report_url": true, + "success": true, + "message": true, + } + + return validFields[field] + } + + return false +} diff --git a/utils/parameter_processor_test.go b/utils/parameter_processor_test.go new file mode 100644 index 0000000..1160901 --- /dev/null +++ b/utils/parameter_processor_test.go @@ -0,0 +1,385 @@ +package utils + +import ( + "beacon/pkg/pb" + "testing" +) + +func TestParameterProcessor_ProcessTemplate(t *testing.T) { + // 创建测试用的全局变量 + globalVariables := map[string]interface{}{ + "base_url": "https://api.example.com", + "environment": "staging", + "api_version": "v1", + "timeout": "30", + "retry_count": "3", + } + + // 创建参数处理器 + processor := NewParameterProcessor(globalVariables) + + // 模拟API测试结果 + apiResult := &pb.ApiTestResult{ + BaseResult: &pb.BaseTestResult{ + Success: true, + Message: "API Test Passed", + }, + ActualStatusCode: 200, + ResponseBody: `{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user_id": 456, "name": "John Doe"}`, + Headers: map[string]string{ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "Content-Type": "application/json", + }, + } + + // 添加activity结果 + processor.AddActivityResult(123, &ActivityResult{ + StepID: 123, + StepName: "RunApiTest", + Success: true, + Data: apiResult, + }) + + // 测试用例 + tests := []struct { + name string + template string + expected string + }{ + { + name: "全局变量替换", + template: `{"endpoint": "${global.base_url}/${global.api_version}/users"}`, + expected: `{"endpoint": "https://api.example.com/v1/users"}`, + }, + { + name: "API响应体替换", + template: `{"token": "${step.123.response_body}"}`, + expected: `{"token": "{\"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\", \"user_id\": 456, \"name\": \"John Doe\"}"}`, + }, + { + name: "API状态码替换", + template: `{"status": "${step.123.actual_status_code}"}`, + expected: `{"status": "200"}`, + }, + { + name: "API响应头替换", + template: `{"auth": "${step.123.headers.Authorization}"}`, + expected: `{"auth": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}`, + }, + { + name: "JSON字段提取", + template: `{"user_id": "${step.123.json.user_id}", "name": "${step.123.json.name}"}`, + expected: `{"user_id": "456", "name": "John Doe"}`, + }, + { + name: "混合变量替换", + template: `{"endpoint": "${global.base_url}/${global.api_version}/users/${step.123.json.user_id}", "headers": {"Authorization": "${step.123.json.token}"}}`, + expected: `{"endpoint": "https://api.example.com/v1/users/456", "headers": {"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}}`, + }, + { + name: "条件变量替换", + template: `{"env": "${if.step.123.success:production:staging}"}`, + expected: `{"env": "production"}`, + }, + { + name: "函数调用", + template: `{"upper_name": "${upper:${step.123.json.name}}"}`, + expected: `{"upper_name": "JOHN DOE"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := processor.ProcessTemplate(tt.template) + if err != nil { + t.Errorf("ProcessTemplate() error = %v", err) + return + } + if result != tt.expected { + t.Errorf("ProcessTemplate() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestParameterProcessor_ValidateTemplate(t *testing.T) { + // 创建测试用的全局变量 + globalVariables := map[string]interface{}{ + "base_url": "https://api.example.com", + "environment": "staging", + } + + // 创建参数处理器 + processor := NewParameterProcessor(globalVariables) + + // 添加一个activity结果 + apiResult := &pb.ApiTestResult{ + BaseResult: &pb.BaseTestResult{ + Success: true, + Message: "API Test Passed", + }, + ActualStatusCode: 200, + ResponseBody: `{"token": "test-token"}`, + Headers: map[string]string{ + "Authorization": "Bearer test-token", + }, + } + + processor.AddActivityResult(123, &ActivityResult{ + StepID: 123, + StepName: "RunApiTest", + Success: true, + Data: apiResult, + }) + + // 测试用例 + tests := []struct { + name string + template string + expectValid bool + expectErrors int + }{ + { + name: "有效模板", + template: `{"endpoint": "${global.base_url}/users", "token": "${step.123.json.token}"}`, + expectValid: true, + expectErrors: 0, + }, + { + name: "无效的全局变量", + template: `{"endpoint": "${global.invalid_key}/users"}`, + expectValid: false, + expectErrors: 1, + }, + { + name: "无效的步骤ID", + template: `{"token": "${step.999.json.token}"}`, + expectValid: false, + expectErrors: 1, + }, + { + name: "无效的字段", + template: `{"invalid": "${step.123.invalid_field}"}`, + expectValid: false, + expectErrors: 1, + }, + { + name: "多个错误", + template: `{"endpoint": "${global.invalid_key}/users", "token": "${step.999.json.token}"}`, + expectValid: false, + expectErrors: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isValid, errors := processor.ValidateTemplate(tt.template) + if isValid != tt.expectValid { + t.Errorf("ValidateTemplate() isValid = %v, want %v", isValid, tt.expectValid) + } + if len(errors) != tt.expectErrors { + t.Errorf("ValidateTemplate() errors count = %d, want %d", len(errors), tt.expectErrors) + } + }) + } +} + +func TestParameterProcessor_GetAvailableVariables(t *testing.T) { + // 创建测试用的全局变量 + globalVariables := map[string]interface{}{ + "base_url": "https://api.example.com", + "environment": "staging", + "api_version": "v1", + } + + // 创建参数处理器 + processor := NewParameterProcessor(globalVariables) + + // 添加API测试结果 + apiResult := &pb.ApiTestResult{ + BaseResult: &pb.BaseTestResult{ + Success: true, + Message: "API Test Passed", + }, + ActualStatusCode: 200, + ResponseBody: `{"token": "test-token"}`, + Headers: map[string]string{ + "Authorization": "Bearer test-token", + }, + } + + processor.AddActivityResult(123, &ActivityResult{ + StepID: 123, + StepName: "RunApiTest", + Success: true, + Data: apiResult, + }) + + // 添加UI测试结果 + uiResult := &pb.UiTestResult{ + BaseResult: &pb.BaseTestResult{ + Success: true, + Message: "UI Test Passed", + }, + ScreenshotUrl: "https://s3.amazonaws.com/screenshots/test.png", + HtmlReportUrl: "https://s3.amazonaws.com/reports/test.html", + } + + processor.AddActivityResult(456, &ActivityResult{ + StepID: 456, + StepName: "RunUiTest", + Success: true, + Data: uiResult, + }) + + // 获取可用变量 + variables := processor.GetAvailableVariables() + + // 验证全局变量 + if globalVars, exists := variables["global"]; !exists { + t.Error("Global variables not found") + } else { + expectedGlobalVars := []string{"base_url", "environment", "api_version"} + for _, expected := range expectedGlobalVars { + found := false + for _, actual := range globalVars { + if actual == expected { + found = true + break + } + } + if !found { + t.Errorf("Global variable '%s' not found", expected) + } + } + } + + // 验证步骤变量 + if step123Vars, exists := variables["step.123"]; !exists { + t.Error("Step 123 variables not found") + } else { + expectedStepVars := []string{"response_body", "actual_status_code", "success", "message", "headers.*", "json.*"} + for _, expected := range expectedStepVars { + found := false + for _, actual := range step123Vars { + if actual == expected { + found = true + break + } + } + if !found { + t.Errorf("Step 123 variable '%s' not found", expected) + } + } + } + + if step456Vars, exists := variables["step.456"]; !exists { + t.Error("Step 456 variables not found") + } else { + expectedStepVars := []string{"screenshot_url", "html_report_url", "success", "message"} + for _, expected := range expectedStepVars { + found := false + for _, actual := range step456Vars { + if actual == expected { + found = true + break + } + } + if !found { + t.Errorf("Step 456 variable '%s' not found", expected) + } + } + } +} + +func TestParameterProcessor_ComplexScenarios(t *testing.T) { + // 创建测试用的全局变量 + globalVariables := map[string]interface{}{ + "base_url": "https://api.example.com", + "environment": "production", + "api_version": "v2", + "timeout": "60", + } + + // 创建参数处理器 + processor := NewParameterProcessor(globalVariables) + + // 模拟用户注册结果 + registerResult := &pb.ApiTestResult{ + BaseResult: &pb.BaseTestResult{ + Success: true, + Message: "User registered successfully", + }, + ActualStatusCode: 201, + ResponseBody: `{"user_id": 789, "username": "newuser", "email": "newuser@example.com"}`, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + } + + // 模拟用户登录结果 + loginResult := &pb.ApiTestResult{ + BaseResult: &pb.BaseTestResult{ + Success: true, + Message: "User logged in successfully", + }, + ActualStatusCode: 200, + ResponseBody: `{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "refresh123", "expires_in": 3600}`, + Headers: map[string]string{ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "Content-Type": "application/json", + }, + } + + // 添加activity结果 + processor.AddActivityResult(201, &ActivityResult{ + StepID: 201, + StepName: "RunApiTest", + Success: true, + Data: registerResult, + }) + + processor.AddActivityResult(202, &ActivityResult{ + StepID: 202, + StepName: "RunApiTest", + Success: true, + Data: loginResult, + }) + + // 测试复杂的参数传递场景 + complexTemplate := `{ + "test_case_id": "get_user_profile", + "endpoint": "${global.base_url}/${global.api_version}/users/${step.201.json.user_id}/profile", + "http_method": "GET", + "headers": { + "Authorization": "Bearer ${step.202.json.token}", + "X-Environment": "${global.environment}", + "X-Timeout": "${global.timeout}", + "Content-Type": "application/json" + }, + "expected_status_code": 200 + }` + + expected := `{ + "test_case_id": "get_user_profile", + "endpoint": "https://api.example.com/v2/users/789/profile", + "http_method": "GET", + "headers": { + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "X-Environment": "production", + "X-Timeout": "60", + "Content-Type": "application/json" + }, + "expected_status_code": 200 + }` + + result, err := processor.ProcessTemplate(complexTemplate) + if err != nil { + t.Errorf("ProcessTemplate() error = %v", err) + return + } + + if result != expected { + t.Errorf("ProcessTemplate() = %v, want %v", result, expected) + } +} diff --git a/workers/python/api_tests.py b/workers/python/api_tests.py index 835da6d..5013f0a 100644 --- a/workers/python/api_tests.py +++ b/workers/python/api_tests.py @@ -10,7 +10,7 @@ def execute_api_test_case(test_case_id: str, endpoint: str, http_method: str, he 实际执行API测试的函数。 可以集成 pytest, requests 等库。 """ - base_url = "http://101.89.127.197:9080" # 假设 API 服务的基地址 + base_url = "" # 假设 API 服务的基地址 full_url = f"{base_url}{endpoint}" log_output = [] @@ -25,7 +25,12 @@ def execute_api_test_case(test_case_id: str, endpoint: str, http_method: str, he if http_method.upper() == "GET": response = requests.get(full_url, headers=headers, timeout=10) elif http_method.upper() == "POST": - json_data = json.loads(request_body) if isinstance(request_body, str) else request_body + print(request_body) + if request_body == "": + json_data = "{}" + else: + # 如果 request_body 是字符串,尝试将其解析为 JSON + json_data = json.loads(request_body) if isinstance(request_body, str) else request_body response = requests.post(full_url, headers=headers, data=json_data, timeout=10) # ... 其他 HTTP 方法 else: diff --git a/workflows/dynamic_workflow.go b/workflows/dynamic_workflow.go index 3d13402..9a3c285 100644 --- a/workflows/dynamic_workflow.go +++ b/workflows/dynamic_workflow.go @@ -9,6 +9,7 @@ import ( "beacon/activities" // 假设你的 activity 包在这个路径 "beacon/pkg/pb" // 你的 Protobuf 路径 + "beacon/utils" // 导入参数处理器 "go.temporal.io/sdk/workflow" // 假设你有数据库访问层,Workflow 不能直接访问 DB,需要通过 Activity 或预加载数据 // "your_module_path/go-server/dal" // 例如 Data Access Layer @@ -78,6 +79,19 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu currentStepOrder = 0 // 当前执行步骤的索引,支持非线性跳转 ) + // 初始化全局变量,包含输入参数中的全局参数 + globalVariables := make(map[string]interface{}) + if input.GlobalParameters != nil { + for key, value := range input.GlobalParameters { + globalVariables[key] = value + } + } + + // 创建参数处理器 + parameterProcessor := utils.NewParameterProcessor(globalVariables) + + logger.Info("Initialized global variables", "variables", globalVariables) + // ======================================================================================== // 步骤4: 主执行循环 - 动态执行各个测试步骤 // ======================================================================================== @@ -87,30 +101,45 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu logger.Info("Executing step", "stepOrder", step.StepOrder, "activityName", step.ActivityName) // ------------------------------------------------------------------------------------ - // 步骤4.1: 动态构造 Activity 输入参数 + // 步骤4.1: 动态构造 Activity 输入参数(支持参数模板解析) // ------------------------------------------------------------------------------------ // 因为不同的 Activity 需要不同类型的输入参数,这里使用工厂模式 // 根据 activity_name 决定使用哪个 Protobuf 结构来反序列化 JSON 参数 var activityInput interface{} // 存储特定 Activity 的输入参数(类型由 activity_name 决定) var activityResult interface{} // 存储特定 Activity 的输出结果(类型由 activity_name 决定) + // 使用参数处理器解析参数模板,替换其中的变量引用 + processedParametersJson, err := parameterProcessor.ProcessTemplate(step.ParametersJson) + if err != nil { + logger.Error("Failed to process parameter template", "error", err, "stepOrder", step.StepOrder) + overallSuccess = false + break + } + + // 验证模板中的变量引用是否有效 + if isValid, errors := parameterProcessor.ValidateTemplate(step.ParametersJson); !isValid { + logger.Error("Parameter template validation failed", "errors", errors, "stepOrder", step.StepOrder) + overallSuccess = false + break + } + // 根据不同的 Activity 类型,将 JSON 字符串参数反序列化为对应的 Protobuf 结构 // 这是动态工作流的核心:同一个工作流可以执行不同类型的测试 switch step.ActivityName { case "RunApiTest": apiReq := &pb.ApiTestRequest{} // 1. 首先验证JSON格式 - if !json.Valid([]byte(step.ParametersJson)) { + if !json.Valid([]byte(processedParametersJson)) { logger.Error("Invalid JSON format in parameters") overallSuccess = false break } // 2. 解析JSON时增加错误详情 - if err := json.Unmarshal([]byte(step.ParametersJson), apiReq); err != nil { + if err := json.Unmarshal([]byte(processedParametersJson), apiReq); err != nil { logger.Error("Failed to unmarshal API test parameters", "error", err, - "raw_json", step.ParametersJson) + "raw_json", processedParametersJson) overallSuccess = false break } @@ -121,22 +150,13 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu overallSuccess = false break } - /*// API 测试:创建 API 测试请求结构并解析参数 - logger.Info("Running api test activity", "ParametersJson", step.ParametersJson) - fmt.Println(reflect.TypeOf(step.ParametersJson)) - apiReq := &pb.ApiTestRequest{} - if err := json.Unmarshal([]byte(step.ParametersJson), apiReq); err != nil { - logger.Error("Failed to unmarshal API test parameters", "error", err) - overallSuccess = false - break // 参数解析失败,跳出当前步骤 - }*/ activityInput = apiReq activityResult = &pb.ApiTestResult{} // 预创建结果容器 case "RunUiTest": // UI 测试:创建 UI 测试请求结构并解析参数 uiReq := &pb.UiTestRequest{} - if err := json.Unmarshal([]byte(step.ParametersJson), uiReq); err != nil { + if err := json.Unmarshal([]byte(processedParametersJson), uiReq); err != nil { logger.Error("Failed to unmarshal UI test parameters", "error", err) overallSuccess = false break // 参数解析失败,跳出当前步骤 @@ -149,7 +169,7 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu // TODO: 实现环境准备的参数解析 /* prepReq := &pb.PrepareEnvRequest{} - if err := json.Unmarshal([]byte(step.ParametersJson), prepReq); err != nil { + if err := json.Unmarshal([]byte(processedParametersJson), prepReq); err != nil { logger.Error("Failed to unmarshal prepare env parameters", "error", err) overallSuccess = false break @@ -215,12 +235,29 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu stepPassed = false overallSuccess = false } + // 存储activity结果用于参数传递 + activityResult := &utils.ActivityResult{ + StepID: step.StepId, + StepName: step.ActivityName, + Success: res.BaseResult.Success, + Data: res, + } + parameterProcessor.AddActivityResult(step.StepId, activityResult) + case *pb.UiTestResult: uiResults = append(uiResults, res) // 收集 UI 测试结果 if !res.BaseResult.Success { // 检查业务逻辑是否成功 stepPassed = false overallSuccess = false } + // 存储activity结果用于参数传递 + activityResult := &utils.ActivityResult{ + StepID: step.StepId, + StepName: step.ActivityName, + Success: res.BaseResult.Success, + Data: res, + } + parameterProcessor.AddActivityResult(step.StepId, activityResult) // 可以在这里添加更多结果类型的处理 } logger.Info("Activity execution finished", "activityName", step.ActivityName, "success", stepPassed)