上下文传递
This commit is contained in:
parent
4dcbef8307
commit
eba56bc756
255
docs/activity_parameter_passing.md
Normal file
255
docs/activity_parameter_passing.md
Normal file
@ -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` 函数来支持更多自定义字段的提取。
|
||||||
|
|
||||||
|
### 条件变量
|
||||||
|
可以基于步骤的成功/失败状态来设置不同的变量值。
|
||||||
|
|
||||||
|
### 变量验证
|
||||||
|
可以在变量解析时添加验证逻辑,确保必需的变量存在且格式正确。
|
@ -18,7 +18,7 @@ func NewWorkflowHandler() *WorkflowHandler {
|
|||||||
// StartWorkflow POST /api/workflow/start
|
// StartWorkflow POST /api/workflow/start
|
||||||
func (h *WorkflowHandler) StartWorkflow(c *gin.Context) {
|
func (h *WorkflowHandler) StartWorkflow(c *gin.Context) {
|
||||||
|
|
||||||
err := h.service.Start("11")
|
err := h.service.Start("13")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(500, gin.H{
|
c.JSON(500, gin.H{
|
||||||
"code": -1,
|
"code": -1,
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"beacon/models"
|
"beacon/models"
|
||||||
"beacon/pkg/dao/mysql"
|
"beacon/pkg/dao/mysql"
|
||||||
"beacon/utils"
|
"beacon/utils"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@ -63,7 +64,7 @@ func (s *CompositeCaseService) CreateCompositeCase(req *models.CreateCompositeCa
|
|||||||
StepDescription: stepReq.StepDescription,
|
StepDescription: stepReq.StepDescription,
|
||||||
StepType: stepReq.StepType,
|
StepType: stepReq.StepType,
|
||||||
ActivityName: activityName,
|
ActivityName: activityName,
|
||||||
ParametersJson: stepReq.ParametersJson,
|
ParametersJson: fixParametersJson(stepReq.ParametersJson),
|
||||||
IsRequired: stepReq.IsRequired,
|
IsRequired: stepReq.IsRequired,
|
||||||
}
|
}
|
||||||
steps = append(steps, step)
|
steps = append(steps, step)
|
||||||
@ -187,7 +188,7 @@ func (s *CompositeCaseService) UpdateCompositeCase(id uint, req *models.UpdateCo
|
|||||||
StepDescription: stepReq.StepDescription,
|
StepDescription: stepReq.StepDescription,
|
||||||
StepType: stepReq.StepType,
|
StepType: stepReq.StepType,
|
||||||
ActivityName: activityName,
|
ActivityName: activityName,
|
||||||
ParametersJson: stepReq.ParametersJson,
|
ParametersJson: fixParametersJson(stepReq.ParametersJson),
|
||||||
IsRequired: stepReq.IsRequired,
|
IsRequired: stepReq.IsRequired,
|
||||||
}
|
}
|
||||||
steps = append(steps, step)
|
steps = append(steps, step)
|
||||||
@ -213,6 +214,28 @@ func (s *CompositeCaseService) UpdateCompositeCase(id uint, req *models.UpdateCo
|
|||||||
return s.GetCompositeCaseByID(id)
|
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 删除复合案例
|
// DeleteCompositeCase 删除复合案例
|
||||||
func (s *CompositeCaseService) DeleteCompositeCase(id uint) error {
|
func (s *CompositeCaseService) DeleteCompositeCase(id uint) error {
|
||||||
zap.L().Info("开始删除复合案例", zap.Uint("id", id))
|
zap.L().Info("开始删除复合案例", zap.Uint("id", id))
|
||||||
|
484
utils/parameter_processor.go
Normal file
484
utils/parameter_processor.go
Normal file
@ -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
|
||||||
|
}
|
385
utils/parameter_processor_test.go
Normal file
385
utils/parameter_processor_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,7 @@ def execute_api_test_case(test_case_id: str, endpoint: str, http_method: str, he
|
|||||||
实际执行API测试的函数。
|
实际执行API测试的函数。
|
||||||
可以集成 pytest, requests 等库。
|
可以集成 pytest, requests 等库。
|
||||||
"""
|
"""
|
||||||
base_url = "http://101.89.127.197:9080" # 假设 API 服务的基地址
|
base_url = "" # 假设 API 服务的基地址
|
||||||
|
|
||||||
full_url = f"{base_url}{endpoint}"
|
full_url = f"{base_url}{endpoint}"
|
||||||
log_output = []
|
log_output = []
|
||||||
@ -25,6 +25,11 @@ def execute_api_test_case(test_case_id: str, endpoint: str, http_method: str, he
|
|||||||
if http_method.upper() == "GET":
|
if http_method.upper() == "GET":
|
||||||
response = requests.get(full_url, headers=headers, timeout=10)
|
response = requests.get(full_url, headers=headers, timeout=10)
|
||||||
elif http_method.upper() == "POST":
|
elif http_method.upper() == "POST":
|
||||||
|
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
|
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)
|
response = requests.post(full_url, headers=headers, data=json_data, timeout=10)
|
||||||
# ... 其他 HTTP 方法
|
# ... 其他 HTTP 方法
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"beacon/activities" // 假设你的 activity 包在这个路径
|
"beacon/activities" // 假设你的 activity 包在这个路径
|
||||||
"beacon/pkg/pb" // 你的 Protobuf 路径
|
"beacon/pkg/pb" // 你的 Protobuf 路径
|
||||||
|
"beacon/utils" // 导入参数处理器
|
||||||
"go.temporal.io/sdk/workflow"
|
"go.temporal.io/sdk/workflow"
|
||||||
// 假设你有数据库访问层,Workflow 不能直接访问 DB,需要通过 Activity 或预加载数据
|
// 假设你有数据库访问层,Workflow 不能直接访问 DB,需要通过 Activity 或预加载数据
|
||||||
// "your_module_path/go-server/dal" // 例如 Data Access Layer
|
// "your_module_path/go-server/dal" // 例如 Data Access Layer
|
||||||
@ -78,6 +79,19 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu
|
|||||||
currentStepOrder = 0 // 当前执行步骤的索引,支持非线性跳转
|
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: 主执行循环 - 动态执行各个测试步骤
|
// 步骤4: 主执行循环 - 动态执行各个测试步骤
|
||||||
// ========================================================================================
|
// ========================================================================================
|
||||||
@ -87,30 +101,45 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu
|
|||||||
logger.Info("Executing step", "stepOrder", step.StepOrder, "activityName", step.ActivityName)
|
logger.Info("Executing step", "stepOrder", step.StepOrder, "activityName", step.ActivityName)
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------
|
||||||
// 步骤4.1: 动态构造 Activity 输入参数
|
// 步骤4.1: 动态构造 Activity 输入参数(支持参数模板解析)
|
||||||
// ------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------
|
||||||
// 因为不同的 Activity 需要不同类型的输入参数,这里使用工厂模式
|
// 因为不同的 Activity 需要不同类型的输入参数,这里使用工厂模式
|
||||||
// 根据 activity_name 决定使用哪个 Protobuf 结构来反序列化 JSON 参数
|
// 根据 activity_name 决定使用哪个 Protobuf 结构来反序列化 JSON 参数
|
||||||
var activityInput interface{} // 存储特定 Activity 的输入参数(类型由 activity_name 决定)
|
var activityInput interface{} // 存储特定 Activity 的输入参数(类型由 activity_name 决定)
|
||||||
var activityResult 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 结构
|
// 根据不同的 Activity 类型,将 JSON 字符串参数反序列化为对应的 Protobuf 结构
|
||||||
// 这是动态工作流的核心:同一个工作流可以执行不同类型的测试
|
// 这是动态工作流的核心:同一个工作流可以执行不同类型的测试
|
||||||
switch step.ActivityName {
|
switch step.ActivityName {
|
||||||
case "RunApiTest":
|
case "RunApiTest":
|
||||||
apiReq := &pb.ApiTestRequest{}
|
apiReq := &pb.ApiTestRequest{}
|
||||||
// 1. 首先验证JSON格式
|
// 1. 首先验证JSON格式
|
||||||
if !json.Valid([]byte(step.ParametersJson)) {
|
if !json.Valid([]byte(processedParametersJson)) {
|
||||||
logger.Error("Invalid JSON format in parameters")
|
logger.Error("Invalid JSON format in parameters")
|
||||||
overallSuccess = false
|
overallSuccess = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 解析JSON时增加错误详情
|
// 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",
|
logger.Error("Failed to unmarshal API test parameters",
|
||||||
"error", err,
|
"error", err,
|
||||||
"raw_json", step.ParametersJson)
|
"raw_json", processedParametersJson)
|
||||||
overallSuccess = false
|
overallSuccess = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -121,22 +150,13 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu
|
|||||||
overallSuccess = false
|
overallSuccess = false
|
||||||
break
|
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
|
activityInput = apiReq
|
||||||
activityResult = &pb.ApiTestResult{} // 预创建结果容器
|
activityResult = &pb.ApiTestResult{} // 预创建结果容器
|
||||||
|
|
||||||
case "RunUiTest":
|
case "RunUiTest":
|
||||||
// UI 测试:创建 UI 测试请求结构并解析参数
|
// UI 测试:创建 UI 测试请求结构并解析参数
|
||||||
uiReq := &pb.UiTestRequest{}
|
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)
|
logger.Error("Failed to unmarshal UI test parameters", "error", err)
|
||||||
overallSuccess = false
|
overallSuccess = false
|
||||||
break // 参数解析失败,跳出当前步骤
|
break // 参数解析失败,跳出当前步骤
|
||||||
@ -149,7 +169,7 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu
|
|||||||
// TODO: 实现环境准备的参数解析
|
// TODO: 实现环境准备的参数解析
|
||||||
/*
|
/*
|
||||||
prepReq := &pb.PrepareEnvRequest{}
|
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)
|
logger.Error("Failed to unmarshal prepare env parameters", "error", err)
|
||||||
overallSuccess = false
|
overallSuccess = false
|
||||||
break
|
break
|
||||||
@ -215,12 +235,29 @@ func DynamicTestSuiteWorkflow(ctx workflow.Context, input *pb.DynamicTestRunInpu
|
|||||||
stepPassed = false
|
stepPassed = false
|
||||||
overallSuccess = 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:
|
case *pb.UiTestResult:
|
||||||
uiResults = append(uiResults, res) // 收集 UI 测试结果
|
uiResults = append(uiResults, res) // 收集 UI 测试结果
|
||||||
if !res.BaseResult.Success { // 检查业务逻辑是否成功
|
if !res.BaseResult.Success { // 检查业务逻辑是否成功
|
||||||
stepPassed = false
|
stepPassed = false
|
||||||
overallSuccess = 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)
|
logger.Info("Activity execution finished", "activityName", step.ActivityName, "success", stepPassed)
|
||||||
|
Loading…
Reference in New Issue
Block a user