This commit is contained in:
longpeng 2025-06-30 08:30:05 +08:00
parent 27f0bdbe8f
commit a2fc0f4067
15 changed files with 2310 additions and 77 deletions

102
CLAUDE.md Normal file
View File

@ -0,0 +1,102 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Beacon is an automated testing orchestration platform built in Go with Python workers. It uses Temporal workflows to coordinate complex testing scenarios that combine API and UI testing with advanced parameter passing and browser session management.
## Architecture
### Core Components
- **main.go**: Entry point that starts both HTTP server (Gin) and Temporal worker
- **routers/**: HTTP routing and handlers using Gin framework
- **services/**: Business logic layer
- **models/**: Data models and DTOs
- **workers/**: Temporal workers (Go orchestration + Python execution)
- **workflows/**: Temporal workflow definitions
- **activities/**: Temporal activities
- **pkg/**: Shared utilities and libraries
- **proto/**: Protocol Buffer definitions for Go-Python communication
### Technology Stack
- **Backend**: Go with Gin framework, GORM for MySQL, Zap logging, Viper configuration
- **Workers**: Python with Temporal SDK, Playwright for UI testing
- **Database**: MySQL (hosted at 43.134.133.27)
- **Temporal Server**: temporal.newai.day:17233
- **Storage**: AWS S3 integration for file handling
## Common Commands
### Build & Development
```bash
make all # Generate both Go and Python protobuf code
make go # Generate Go protobuf only
make py # Generate Python protobuf only
make clean # Clean generated files
```
### Running the Application
```bash
go run main.go # Use default config
go run main.go -conf ./config/config.yaml # Specify config file
```
## Key Features
### Composite Test Cases
The platform supports complex multi-step test scenarios defined in the database:
- **composite_cases**: Test case definitions
- **composite_case_steps**: Individual test steps with ordering and parameters
### Parameter Passing System
Advanced variable interpolation between test steps:
- `${global.key}` - Workflow-level parameters
- `${step.stepId.field}` - Results from previous steps
- `${step.123.json_token}` - Auto-extracted JSON fields
### Temporal Workflows
- **TestRunWorkflow**: Individual test execution
- **DynamicTestSuiteWorkflow**: Complex composite test orchestration
- Separate task queues for Go and Python workers
## API Endpoints
### Composite Cases
- `POST /v1/api/composite-cases` - Create test case
- `GET /v1/api/composite-cases` - List test cases
- `GET /v1/api/composite-cases/:id` - Get specific case
- `PUT /v1/api/composite-cases/:id` - Update case
- `DELETE /v1/api/composite-cases/:id` - Delete case
### Workflows
- `POST /v1/api/workflows/start` - Start workflow execution
- `GET /v1/api/workflows/:id` - Get workflow status
- `GET /v1/api/workflows/:id/results` - Get workflow results
## Database Schema
The system uses MySQL with key tables:
- `composite_cases`: Test case definitions with soft delete support
- `composite_case_steps`: Ordered test steps with type-specific parameters
- Foreign key relationships maintain referential integrity
## Configuration
- Configuration via YAML files (default: `./config/config.yaml`)
- Environment-specific settings for database, Temporal, and logging
- Hot-reload support with Viper configuration management
## Development Notes
### Protocol Buffers
When modifying proto definitions, regenerate code with `make all`. Proto files define the interface between Go orchestration and Python test execution.
### Temporal Integration
Understanding Temporal concepts is essential - the platform heavily relies on workflows, activities, and task queues for distributed test execution.
### Browser Session Management
UI testing supports browser session reuse across test steps for complex user journeys.
### Error Handling
Multi-level error handling with structured logging (Zap) and comprehensive retry policies in Temporal workflows.

View File

@ -1,9 +1,11 @@
package activities
import (
"beacon/models"
"beacon/pkg/dao/mysql"
"beacon/pkg/pb"
"context"
"encoding/json"
"fmt"
"go.temporal.io/sdk/activity"
)
@ -40,22 +42,22 @@ func LoadCompositeCaseSteps(ctx context.Context, compositeCaseId string) ([]*pb.
// 转换数据库模型为 Protobuf 结构
var pbSteps []*pb.CompositeCaseStepDefinition
for _, step := range steps {
// 序列化参数为JSON
parametersJson, err := serializeStepParameters(step)
if err != nil {
logger.Error("Failed to serialize step parameters", "stepId", step.ID, "error", err)
return nil, fmt.Errorf("failed to serialize step parameters for step %d: %w", step.ID, err)
}
pbStep := &pb.CompositeCaseStepDefinition{
StepId: int64(step.ID),
StepOrder: int32(step.StepOrder),
StepType: step.StepType,
ActivityName: step.ActivityName,
ParametersJson: step.ParametersJson,
//SuccessNextStepOrder: convertToInt32Ptr(step.SuccessNextStepOrder),
//FailureNextStepOrder: convertToInt32Ptr(step.FailureNextStepOrder),
//IsParallel: step.IsParallel,
//DependsOnStepIds: step.DependsOnStepIds,
//ContinueOnFailure: step.ContinueOnFailure,
//TimeoutSeconds: int32(step.TimeoutSeconds),
//RetryCount: int32(step.RetryCount),
//Description: step.Description,
//CreatedAt: step.CreatedAt.Unix(),
//UpdatedAt: step.UpdatedAt.Unix(),
StepId: int64(step.ID),
StepOrder: int32(step.StepOrder),
StepType: step.StepType,
ActivityName: step.ActivityName,
ParametersJson: parametersJson,
SuccessNextStepOrder: convertToInt32Ptr(step.SuccessNextStepOrder),
FailureNextStepOrder: convertToInt32Ptr(step.FailureNextStepOrder),
RunConditionJson: step.RunCondition,
}
pbSteps = append(pbSteps, pbStep)
}
@ -69,3 +71,82 @@ func LoadCompositeCaseSteps(ctx context.Context, compositeCaseId string) ([]*pb.
return pbSteps, nil
}
// convertToInt32Ptr 转换 *int 为 *int32
func convertToInt32Ptr(val *int) *int32 {
if val == nil {
return nil
}
int32Val := int32(*val)
return &int32Val
}
// serializeStepParameters 序列化步骤参数为JSON
func serializeStepParameters(step models.CompositeCaseStep) (string, error) {
var params interface{}
// 根据步骤类型序列化不同的参数
switch step.StepType {
case "API_TEST":
if step.ApiTestParameters != nil {
// 转换API参数
apiParams := map[string]interface{}{
"endpoint": step.ApiTestParameters.Endpoint,
"method": step.ApiTestParameters.HttpMethod,
"body": step.ApiTestParameters.Body,
"timeout": step.ApiTestParameters.Timeout,
"retry_count": step.ApiTestParameters.RetryCount,
}
// 解析headers和query_params JSON字符串
if step.ApiTestParameters.Headers != "" {
var headers map[string]interface{}
if err := json.Unmarshal([]byte(step.ApiTestParameters.Headers), &headers); err == nil {
apiParams["headers"] = headers
}
}
if step.ApiTestParameters.QueryParams != "" {
var queryParams map[string]interface{}
if err := json.Unmarshal([]byte(step.ApiTestParameters.QueryParams), &queryParams); err == nil {
apiParams["query_params"] = queryParams
}
}
params = apiParams
} else {
params = map[string]interface{}{}
}
case "UI_TEST":
if step.UiTestParameters != nil {
// 转换UI参数
uiParams := map[string]interface{}{
"selector": step.UiTestParameters.Selector,
"selector_type": step.UiTestParameters.SelectorType,
"event_type": step.UiTestParameters.EventType,
"input_value": step.UiTestParameters.InputValue,
"offset_x": step.UiTestParameters.OffsetX,
"offset_y": step.UiTestParameters.OffsetY,
"wait_time_seconds": step.UiTestParameters.WaitTimeSeconds,
"screenshot_name": step.UiTestParameters.ScreenshotName,
"assertion_type": step.UiTestParameters.AssertionType,
"assertion_value": step.UiTestParameters.AssertionValue,
}
params = uiParams
} else {
params = map[string]interface{}{}
}
default:
// 其他类型步骤,返回空对象
params = map[string]interface{}{}
}
// 序列化为JSON字符串
paramsJson, err := json.Marshal(params)
if err != nil {
return "", fmt.Errorf("failed to marshal parameters to JSON: %w", err)
}
return string(paramsJson), nil
}

BIN
beacon Executable file

Binary file not shown.

View File

@ -0,0 +1,209 @@
# Composite Case Creation Examples
This document provides examples of how to create composite test cases using the new separated parameter structure.
## Overview
The composite case system now uses separate parameter tables for different test types:
- **API tests** use `api_test_parameters`
- **UI tests** use `ui_test_parameters`
This provides better type safety, validation, and easier maintenance.
## Basic Usage
### Simple Example
Use `create_composite_case_simple.json` for a basic example with one API test and one UI test.
```bash
curl -X POST http://localhost:8080/v1/api/composite-cases \
-H "Content-Type: application/json" \
-d @docs/create_composite_case_simple.json
```
### Complex Example
Use `create_composite_case_sample.json` for a comprehensive example with:
- Multiple test steps with flow control
- Parameter passing between steps
- Global variable usage
- Conditional execution
- Error handling and cleanup
```bash
curl -X POST http://localhost:8080/v1/api/composite-cases \
-H "Content-Type: application/json" \
-d @docs/create_composite_case_sample.json
```
## Parameter Structure
### API Test Parameters
```json
{
"api_test_parameters": {
"endpoint": "/api/endpoint", // Required: API endpoint URL
"http_method": "POST", // Required: HTTP method
"headers": { // Optional: HTTP headers
"Content-Type": "application/json",
"Authorization": "Bearer ${step.1.json.token}"
},
"query_params": { // Optional: Query parameters
"page": "1",
"size": "10"
},
"body": "{\"key\": \"value\"}", // Optional: Request body
"timeout": 30, // Optional: Timeout in seconds (default: 30)
"retry_count": 2 // Optional: Number of retries (default: 0)
}
}
```
### UI Test Parameters
```json
{
"ui_test_parameters": {
"selector": "#element-id", // Required: Element selector
"selector_type": "css", // Optional: css, xpath, id, class, name (default: css)
"event_type": "click", // Required: click, input, hover, scroll, wait, screenshot, verify_text, navigate
"input_value": "text to input", // Optional: Text to input (for input events)
"offset_x": 10, // Optional: X offset for clicks (default: 0)
"offset_y": 20, // Optional: Y offset for clicks (default: 0)
"wait_time_seconds": 2.5, // Optional: Wait time in seconds (default: 1.0)
"screenshot_name": "screenshot.png", // Optional: Screenshot filename
"assertion_type": "element_visible", // Optional: Assertion type for verification
"assertion_value": "#success-message" // Optional: Expected value for assertion
}
}
```
## Flow Control
### Step Order and Conditional Execution
```json
{
"step_order": 1,
"success_next_step_order": 2, // Jump to step 2 on success
"failure_next_step_order": 6, // Jump to step 6 on failure
"run_condition": "{\"previous_step\": \"1\", \"status\": \"success\"}"
}
```
### Common Run Conditions
- `{"initial_step": true}` - First step of the workflow
- `{"previous_step": "1", "status": "success"}` - Run if step 1 succeeded
- `{"previous_step": "2", "status": "failure"}` - Run if step 2 failed
- `{"any_previous_failed": true}` - Run if any previous step failed
## Parameter Passing and Templating
### Global Variables
Use global variables that are defined at the workflow level:
```json
{
"body": "{\"username\": \"${global.test_username}\", \"password\": \"${global.test_password}\"}"
}
```
### Step Result Variables
Reference results from previous steps:
```json
{
"endpoint": "/api/users/${step.1.json.user_id}",
"headers": {
"Authorization": "Bearer ${step.2.json.access_token}"
}
}
```
### Available Step Variables
- `${step.X.json.field_name}` - Extract field from JSON response
- `${step.X.response_body}` - Full response body
- `${step.X.actual_status_code}` - HTTP status code
- `${step.X.headers.Header-Name}` - Response header value
- `${step.X.success}` - Boolean success status
- `${step.X.message}` - Result message
## Event Types for UI Tests
### Common UI Events
- `click` - Click on an element
- `input` - Type text into an input field
- `hover` - Hover over an element
- `scroll` - Scroll to an element
- `wait` - Wait for a specified time
- `screenshot` - Take a screenshot
- `navigate` - Navigate to a URL (use `input_value` for URL)
- `verify_text` - Verify element contains text
### Assertion Types
- `element_visible` - Check if element is visible
- `element_hidden` - Check if element is hidden
- `text_contains` - Check if element contains text
- `text_equals` - Check if element text equals value
- `element_count` - Check number of matching elements
- `page_loaded` - Check if page title contains text
- `input_has_value` - Check if input has specific value
## Best Practices
1. **Use descriptive step names** that clearly indicate what each step does
2. **Set appropriate timeouts** based on expected response times
3. **Include error handling steps** with cleanup logic
4. **Use parameter passing** to avoid hardcoding values
5. **Add screenshots for UI tests** to aid in debugging
6. **Structure tests logically** with clear success/failure paths
7. **Use assertions** to verify expected outcomes
8. **Keep retry counts reasonable** to avoid excessive delays
## Error Handling
Always include cleanup steps that run when tests fail:
```json
{
"step_order": 999,
"step_name": "Cleanup on Failure",
"step_type": "API_TEST",
"is_required": false,
"run_condition": "{\"any_previous_failed\": true}",
"api_test_parameters": {
"endpoint": "/api/cleanup",
"http_method": "DELETE"
}
}
```
## Testing Your Composite Cases
After creating a composite case, you can execute it through the workflow API:
```bash
curl -X POST http://localhost:8080/v1/api/workflows/start \
-H "Content-Type: application/json" \
-d '{"composite_case_id": 1, "global_variables": {"test_username": "user123", "test_email": "test@example.com"}}'
```
Monitor the workflow status:
```bash
curl http://localhost:8080/v1/api/workflows/{workflow_id}
```
Get the workflow results:
```bash
curl http://localhost:8080/v1/api/workflows/{workflow_id}/results
```

View File

@ -0,0 +1,225 @@
{
"name": "E2E User Registration and Profile Management Test",
"description": "Complete end-to-end test covering user registration, login, profile management, and UI verification with advanced parameter passing and flow control",
"status": "active",
"steps": [
{
"step_order": 1,
"step_name": "User Registration API",
"step_description": "Register a new user account via API",
"step_type": "API_TEST",
"activity_name": "RunApiTest",
"is_required": true,
"success_next_step_order": 2,
"failure_next_step_order": 6,
"run_condition": "{\"initial_step\": true}",
"api_test_parameters": {
"endpoint": "/api/v1/users/register",
"http_method": "POST",
"headers": {
"Content-Type": "application/json",
"Accept": "application/json",
"X-API-Version": "1.0"
},
"body": "{\"username\": \"${global.test_username}\", \"email\": \"${global.test_email}\", \"password\": \"${global.test_password}\", \"first_name\": \"Test\", \"last_name\": \"User\"}",
"timeout": 30,
"retry_count": 2
}
},
{
"step_order": 2,
"step_name": "User Login API",
"step_description": "Login with the newly registered user credentials",
"step_type": "API_TEST",
"activity_name": "RunApiTest",
"is_required": true,
"success_next_step_order": 3,
"failure_next_step_order": 6,
"run_condition": "{\"previous_step\": \"1\", \"status\": \"success\"}",
"api_test_parameters": {
"endpoint": "/api/v1/auth/login",
"http_method": "POST",
"headers": {
"Content-Type": "application/json",
"Accept": "application/json"
},
"body": "{\"username\": \"${global.test_username}\", \"password\": \"${global.test_password}\"}",
"timeout": 20,
"retry_count": 3
}
},
{
"step_order": 3,
"step_name": "Get User Profile API",
"step_description": "Retrieve user profile information using the login token",
"step_type": "API_TEST",
"activity_name": "RunApiTest",
"is_required": true,
"success_next_step_order": 4,
"failure_next_step_order": 6,
"run_condition": "{\"previous_step\": \"2\", \"status\": \"success\"}",
"api_test_parameters": {
"endpoint": "/api/v1/users/${step.1.json.user_id}/profile",
"http_method": "GET",
"headers": {
"Authorization": "Bearer ${step.2.json.access_token}",
"Accept": "application/json"
},
"query_params": {
"include": "preferences,settings,avatar",
"format": "detailed"
},
"timeout": 15,
"retry_count": 1
}
},
{
"step_order": 4,
"step_name": "Navigate to Profile Page",
"step_description": "Open the user profile page in browser",
"step_type": "UI_TEST",
"activity_name": "RunUiTest",
"is_required": true,
"success_next_step_order": 5,
"failure_next_step_order": 6,
"run_condition": "{\"previous_step\": \"3\", \"status\": \"success\"}",
"ui_test_parameters": {
"selector": "body",
"selector_type": "css",
"event_type": "navigate",
"input_value": "${global.base_url}/profile/${step.1.json.user_id}",
"wait_time_seconds": 3.0,
"screenshot_name": "profile_page_loaded.png",
"assertion_type": "page_loaded",
"assertion_value": "Profile"
}
},
{
"step_order": 5,
"step_name": "Verify Profile Information",
"step_description": "Verify that the UI displays correct user information",
"step_type": "UI_TEST",
"activity_name": "RunUiTest",
"is_required": true,
"success_next_step_order": 7,
"failure_next_step_order": 6,
"run_condition": "{\"previous_step\": \"4\", \"status\": \"success\"}",
"ui_test_parameters": {
"selector": "#user-profile-name",
"selector_type": "css",
"event_type": "verify_text",
"offset_x": 0,
"offset_y": 0,
"wait_time_seconds": 2.0,
"screenshot_name": "profile_verification.png",
"assertion_type": "text_contains",
"assertion_value": "${step.3.json.first_name} ${step.3.json.last_name}"
}
},
{
"step_order": 6,
"step_name": "Cleanup Failed Test Data",
"step_description": "Clean up any test data if previous steps failed",
"step_type": "API_TEST",
"activity_name": "RunApiTest",
"is_required": false,
"run_condition": "{\"any_previous_failed\": true}",
"api_test_parameters": {
"endpoint": "/api/v1/users/${step.1.json.user_id}",
"http_method": "DELETE",
"headers": {
"Authorization": "Bearer ${step.2.json.access_token}",
"Accept": "application/json"
},
"timeout": 10,
"retry_count": 0
}
},
{
"step_order": 7,
"step_name": "Update Profile via UI",
"step_description": "Test profile update functionality through the UI",
"step_type": "UI_TEST",
"activity_name": "RunUiTest",
"is_required": true,
"success_next_step_order": 8,
"failure_next_step_order": 6,
"run_condition": "{\"previous_step\": \"5\", \"status\": \"success\"}",
"ui_test_parameters": {
"selector": "#edit-profile-button",
"selector_type": "css",
"event_type": "click",
"offset_x": 10,
"offset_y": 5,
"wait_time_seconds": 1.5,
"screenshot_name": "edit_profile_clicked.png",
"assertion_type": "element_visible",
"assertion_value": "#profile-edit-form"
}
},
{
"step_order": 8,
"step_name": "Fill Profile Form",
"step_description": "Fill out the profile edit form with new information",
"step_type": "UI_TEST",
"activity_name": "RunUiTest",
"is_required": true,
"success_next_step_order": 9,
"failure_next_step_order": 6,
"run_condition": "{\"previous_step\": \"7\", \"status\": \"success\"}",
"ui_test_parameters": {
"selector": "input[name='bio']",
"selector_type": "css",
"event_type": "input",
"input_value": "Updated bio from automated test - ${global.test_timestamp}",
"offset_x": 0,
"offset_y": 0,
"wait_time_seconds": 1.0,
"screenshot_name": "profile_form_filled.png",
"assertion_type": "input_has_value",
"assertion_value": "Updated bio from automated test"
}
},
{
"step_order": 9,
"step_name": "Submit Profile Changes",
"step_description": "Submit the updated profile information",
"step_type": "UI_TEST",
"activity_name": "RunUiTest",
"is_required": true,
"success_next_step_order": 10,
"failure_next_step_order": 6,
"run_condition": "{\"previous_step\": \"8\", \"status\": \"success\"}",
"ui_test_parameters": {
"selector": "#save-profile-button",
"selector_type": "css",
"event_type": "click",
"offset_x": 0,
"offset_y": 0,
"wait_time_seconds": 3.0,
"screenshot_name": "profile_saved.png",
"assertion_type": "text_contains",
"assertion_value": "Profile updated successfully"
}
},
{
"step_order": 10,
"step_name": "Verify Profile Update API",
"step_description": "Verify the profile changes were saved correctly via API",
"step_type": "API_TEST",
"activity_name": "RunApiTest",
"is_required": true,
"run_condition": "{\"previous_step\": \"9\", \"status\": \"success\"}",
"api_test_parameters": {
"endpoint": "/api/v1/users/${step.1.json.user_id}/profile",
"http_method": "GET",
"headers": {
"Authorization": "Bearer ${step.2.json.access_token}",
"Accept": "application/json"
},
"timeout": 15,
"retry_count": 1
}
}
]
}

View File

@ -0,0 +1,44 @@
{
"name": "Simple API and UI Test",
"description": "Basic example with one API test and one UI test",
"status": "active",
"steps": [
{
"step_order": 1,
"step_name": "Login API Test",
"step_description": "Test login endpoint",
"step_type": "API_TEST",
"activity_name": "RunApiTest",
"is_required": true,
"run_condition": "{\"initial_step\": true}",
"api_test_parameters": {
"endpoint": "/api/auth/login",
"http_method": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": "{\"username\": \"testuser\", \"password\": \"testpass\"}",
"timeout": 30,
"retry_count": 2
}
},
{
"step_order": 2,
"step_name": "Click Login Button",
"step_description": "Click the login button on the UI",
"step_type": "UI_TEST",
"activity_name": "RunUiTest",
"is_required": true,
"run_condition": "{\"previous_step\": \"1\", \"status\": \"success\"}",
"ui_test_parameters": {
"selector": "#login-button",
"selector_type": "css",
"event_type": "click",
"wait_time_seconds": 2.0,
"screenshot_name": "login_clicked.png",
"assertion_type": "element_visible",
"assertion_value": "#dashboard"
}
}
]
}

View File

@ -70,8 +70,8 @@ CREATE TABLE `api_test_parameters`
`step_id` BIGINT NOT NULL COMMENT '关联的步骤ID',
`endpoint` VARCHAR(512) NOT NULL COMMENT 'API端点例如/api/v1/users',
`http_method` VARCHAR(10) NOT NULL DEFAULT 'GET' COMMENT 'HTTP方法GET, POST, PUT, DELETE等',
`headers` JSON NULL COMMENT 'HTTP请求头例如{"Content-Type": "application/json", "Authorization": "Bearer token"}',
`query_params` JSON NULL COMMENT '查询参数,例如:{"page": 1, "size": 10}',
`headers` JSON DEFAULT ('{}') COMMENT 'HTTP请求头例如{"Content-Type": "application/json", "Authorization": "Bearer token"}',
`query_params` JSON DEFAULT ('{}') COMMENT '查询参数,例如:{"page": 1, "size": 10}',
`body` TEXT NULL COMMENT '请求体内容',
`timeout` INT DEFAULT 30 COMMENT '超时时间(秒)',
`retry_count` INT DEFAULT 0 COMMENT '重试次数',

View File

@ -19,21 +19,107 @@ type CompositeCase struct {
Steps []CompositeCaseStep `json:"steps" gorm:"foreignKey:CompositeCaseID"`
}
// CompositeCaseStep 复合案例步骤
type CompositeCaseStep struct {
// ApiTestParameters API测试参数
type ApiTestParameters struct {
ID uint `json:"id" gorm:"primaryKey"`
StepID uint `json:"step_id" gorm:"not null"`
Endpoint string `json:"endpoint" gorm:"not null;size:512"`
HttpMethod string `json:"http_method" gorm:"not null;size:10;default:'GET'"`
Headers string `json:"headers" gorm:"type:json;default:'{}'"`
QueryParams string `json:"query_params" gorm:"type:json;default:'{}'"`
Body string `json:"body" gorm:"type:text"`
Timeout int `json:"timeout" gorm:"default:30"`
RetryCount int `json:"retry_count" gorm:"default:0"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// BeforeCreate GORM hook to ensure JSON fields are never empty
func (params *ApiTestParameters) BeforeCreate(tx *gorm.DB) error {
if params.Headers == "" || params.Headers == "null" {
params.Headers = "{}"
}
if params.QueryParams == "" || params.QueryParams == "null" {
params.QueryParams = "{}"
}
return nil
}
// BeforeUpdate GORM hook to ensure JSON fields are never empty
func (params *ApiTestParameters) BeforeUpdate(tx *gorm.DB) error {
if params.Headers == "" || params.Headers == "null" {
params.Headers = "{}"
}
if params.QueryParams == "" || params.QueryParams == "null" {
params.QueryParams = "{}"
}
return nil
}
// UiTestParameters UI测试参数
type UiTestParameters struct {
ID uint `json:"id" gorm:"primaryKey"`
CompositeCaseID uint `json:"composite_case_id" gorm:"not null"`
StepOrder int `json:"step_order" gorm:"not null"`
StepName string `json:"step_name" gorm:"not null;size:255"`
StepDescription string `json:"step_description" gorm:"type:text"`
StepType string `json:"step_type" gorm:"not null;size:100"`
ActivityName string `json:"activity_name" gorm:"not null;size:255"`
ParametersJson string `json:"parameters_json" gorm:"type:json"`
IsRequired bool `json:"is_required" gorm:"default:true"`
StepID uint `json:"step_id" gorm:"not null"`
Selector string `json:"selector" gorm:"not null;size:512"`
SelectorType string `json:"selector_type" gorm:"default:'css';size:20"`
EventType string `json:"event_type" gorm:"not null;size:50"`
InputValue string `json:"input_value" gorm:"type:text"`
OffsetX int `json:"offset_x" gorm:"default:0"`
OffsetY int `json:"offset_y" gorm:"default:0"`
WaitTimeSeconds float64 `json:"wait_time_seconds" gorm:"type:decimal(5,2);default:1.0"`
ScreenshotName string `json:"screenshot_name" gorm:"size:255"`
AssertionType string `json:"assertion_type" gorm:"size:50"`
AssertionValue string `json:"assertion_value" gorm:"type:text"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ApiTestParametersRequest API测试参数请求
type ApiTestParametersRequest struct {
Endpoint string `json:"endpoint" binding:"required"`
HttpMethod string `json:"http_method" binding:"required"`
Headers map[string]string `json:"headers"`
QueryParams map[string]string `json:"query_params"`
Body string `json:"body"`
Timeout int `json:"timeout"`
RetryCount int `json:"retry_count"`
}
// UiTestParametersRequest UI测试参数请求
type UiTestParametersRequest struct {
Selector string `json:"selector" binding:"required"`
SelectorType string `json:"selector_type"`
EventType string `json:"event_type" binding:"required"`
InputValue string `json:"input_value"`
OffsetX int `json:"offset_x"`
OffsetY int `json:"offset_y"`
WaitTimeSeconds float64 `json:"wait_time_seconds"`
ScreenshotName string `json:"screenshot_name"`
AssertionType string `json:"assertion_type"`
AssertionValue string `json:"assertion_value"`
}
// CompositeCaseStep 复合案例步骤
type CompositeCaseStep struct {
ID uint `json:"id" gorm:"primaryKey"`
CompositeCaseID uint `json:"composite_case_id" gorm:"not null"`
StepOrder int `json:"step_order" gorm:"not null"`
StepName string `json:"step_name" gorm:"not null;size:255"`
StepDescription string `json:"step_description" gorm:"type:text"`
StepType string `json:"step_type" gorm:"not null;size:100"`
ActivityName string `json:"activity_name" gorm:"not null;size:255"`
IsRequired bool `json:"is_required" gorm:"default:true"`
SuccessNextStepOrder *int `json:"success_next_step_order" gorm:"column:success_next_step_order"`
FailureNextStepOrder *int `json:"failure_next_step_order" gorm:"column:failure_next_step_order"`
RunCondition string `json:"run_condition" gorm:"type:json"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// 关联的参数表
ApiTestParameters *ApiTestParameters `json:"api_test_parameters,omitempty" gorm:"foreignKey:StepID"`
UiTestParameters *UiTestParameters `json:"ui_test_parameters,omitempty" gorm:"foreignKey:StepID"`
}
// CreateCompositeCaseRequest 创建复合案例请求
type CreateCompositeCaseRequest struct {
Name string `json:"name" binding:"required"`
@ -44,13 +130,17 @@ type CreateCompositeCaseRequest struct {
// CreateCompositeCaseStepRequest 创建复合案例步骤请求
type CreateCompositeCaseStepRequest struct {
StepOrder int `json:"step_order" binding:"required"`
StepName string `json:"step_name" binding:"required"`
StepDescription string `json:"step_description"`
StepType string `json:"step_type" binding:"required"`
ActivityName string `json:"activity_name"`
ParametersJson string `json:"parameters_json"`
IsRequired bool `json:"is_required"`
StepOrder int `json:"step_order" binding:"required"`
StepName string `json:"step_name" binding:"required"`
StepDescription string `json:"step_description"`
StepType string `json:"step_type" binding:"required"`
ActivityName string `json:"activity_name"`
IsRequired bool `json:"is_required"`
SuccessNextStepOrder *int `json:"success_next_step_order"`
FailureNextStepOrder *int `json:"failure_next_step_order"`
RunCondition string `json:"run_condition"`
ApiTestParameters *ApiTestParametersRequest `json:"api_test_parameters,omitempty"`
UiTestParameters *UiTestParametersRequest `json:"ui_test_parameters,omitempty"`
}
// UpdateCompositeCaseRequest 更新复合案例请求
@ -63,12 +153,16 @@ type UpdateCompositeCaseRequest struct {
// UpdateCompositeCaseStepRequest 更新复合案例步骤请求
type UpdateCompositeCaseStepRequest struct {
ID uint `json:"id"`
StepOrder int `json:"step_order"`
StepName string `json:"step_name"`
StepDescription string `json:"step_description"`
StepType string `json:"step_type"`
ActivityName string `json:"activity_name"`
ParametersJson string `json:"parameters_json"`
IsRequired bool `json:"is_required"`
ID uint `json:"id"`
StepOrder int `json:"step_order"`
StepName string `json:"step_name"`
StepDescription string `json:"step_description"`
StepType string `json:"step_type"`
ActivityName string `json:"activity_name"`
IsRequired bool `json:"is_required"`
SuccessNextStepOrder *int `json:"success_next_step_order"`
FailureNextStepOrder *int `json:"failure_next_step_order"`
RunCondition string `json:"run_condition"`
ApiTestParameters *ApiTestParametersRequest `json:"api_test_parameters,omitempty"`
UiTestParameters *UiTestParametersRequest `json:"ui_test_parameters,omitempty"`
}

351
models/composite_test.go Normal file
View File

@ -0,0 +1,351 @@
package models
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCompositeCase_JSON_Serialization(t *testing.T) {
tests := []struct {
name string
compositeCase *CompositeCase
}{
{
name: "basic composite case",
compositeCase: &CompositeCase{
ID: 1,
Name: "Test Case",
Description: "Test Description",
Status: "active",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
},
{
name: "composite case with steps",
compositeCase: &CompositeCase{
ID: 2,
Name: "Complex Test Case",
Description: "Complex Test Description",
Status: "active",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Steps: []CompositeCaseStep{
{
ID: 1,
CompositeCaseID: 2,
StepOrder: 1,
StepName: "Login Step",
StepType: "API_TEST",
ActivityName: "RunApiTest",
IsRequired: true,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test JSON marshaling
jsonData, err := json.Marshal(tt.compositeCase)
require.NoError(t, err)
assert.NotEmpty(t, jsonData)
// Test JSON unmarshaling
var unmarshaled CompositeCase
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
assert.Equal(t, tt.compositeCase.ID, unmarshaled.ID)
assert.Equal(t, tt.compositeCase.Name, unmarshaled.Name)
assert.Equal(t, tt.compositeCase.Description, unmarshaled.Description)
assert.Equal(t, tt.compositeCase.Status, unmarshaled.Status)
})
}
}
func TestCompositeCaseStep_JSON_Serialization(t *testing.T) {
successOrder := 2
failureOrder := 3
step := &CompositeCaseStep{
ID: 1,
CompositeCaseID: 1,
StepOrder: 1,
StepName: "Test Step",
StepDescription: "Test Description",
StepType: "API_TEST",
ActivityName: "RunApiTest",
IsRequired: true,
SuccessNextStepOrder: &successOrder,
FailureNextStepOrder: &failureOrder,
RunCondition: `{"previous_step": "success"}`,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Test JSON marshaling
jsonData, err := json.Marshal(step)
require.NoError(t, err)
assert.NotEmpty(t, jsonData)
// Test JSON unmarshaling
var unmarshaled CompositeCaseStep
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
assert.Equal(t, step.ID, unmarshaled.ID)
assert.Equal(t, step.StepName, unmarshaled.StepName)
assert.Equal(t, step.StepType, unmarshaled.StepType)
assert.Equal(t, *step.SuccessNextStepOrder, *unmarshaled.SuccessNextStepOrder)
assert.Equal(t, *step.FailureNextStepOrder, *unmarshaled.FailureNextStepOrder)
}
func TestApiTestParameters_JSON_Serialization(t *testing.T) {
params := &ApiTestParameters{
ID: 1,
StepID: 1,
Endpoint: "/api/v1/test",
HttpMethod: "POST",
Headers: `{"Content-Type": "application/json"}`,
QueryParams: `{"page": 1, "size": 10}`,
Body: `{"name": "test"}`,
Timeout: 30,
RetryCount: 3,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Test JSON marshaling
jsonData, err := json.Marshal(params)
require.NoError(t, err)
assert.NotEmpty(t, jsonData)
// Test JSON unmarshaling
var unmarshaled ApiTestParameters
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
assert.Equal(t, params.Endpoint, unmarshaled.Endpoint)
assert.Equal(t, params.HttpMethod, unmarshaled.HttpMethod)
assert.Equal(t, params.Headers, unmarshaled.Headers)
assert.Equal(t, params.QueryParams, unmarshaled.QueryParams)
assert.Equal(t, params.Body, unmarshaled.Body)
assert.Equal(t, params.Timeout, unmarshaled.Timeout)
assert.Equal(t, params.RetryCount, unmarshaled.RetryCount)
}
func TestUiTestParameters_JSON_Serialization(t *testing.T) {
params := &UiTestParameters{
ID: 1,
StepID: 1,
Selector: "#login-button",
SelectorType: "css",
EventType: "click",
InputValue: "test@example.com",
OffsetX: 10,
OffsetY: 20,
WaitTimeSeconds: 2.5,
ScreenshotName: "login_screen.png",
AssertionType: "element_visible",
AssertionValue: "#success-message",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Test JSON marshaling
jsonData, err := json.Marshal(params)
require.NoError(t, err)
assert.NotEmpty(t, jsonData)
// Test JSON unmarshaling
var unmarshaled UiTestParameters
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
assert.Equal(t, params.Selector, unmarshaled.Selector)
assert.Equal(t, params.SelectorType, unmarshaled.SelectorType)
assert.Equal(t, params.EventType, unmarshaled.EventType)
assert.Equal(t, params.InputValue, unmarshaled.InputValue)
assert.Equal(t, params.OffsetX, unmarshaled.OffsetX)
assert.Equal(t, params.OffsetY, unmarshaled.OffsetY)
assert.Equal(t, params.WaitTimeSeconds, unmarshaled.WaitTimeSeconds)
assert.Equal(t, params.ScreenshotName, unmarshaled.ScreenshotName)
assert.Equal(t, params.AssertionType, unmarshaled.AssertionType)
assert.Equal(t, params.AssertionValue, unmarshaled.AssertionValue)
}
func TestCreateCompositeCaseRequest_Validation(t *testing.T) {
tests := []struct {
name string
request *CreateCompositeCaseRequest
valid bool
}{
{
name: "valid request",
request: &CreateCompositeCaseRequest{
Name: "Test Case",
Description: "Test Description",
Status: "active",
Steps: []CreateCompositeCaseStepRequest{
{
StepOrder: 1,
StepName: "Login",
StepType: "API_TEST",
ActivityName: "RunApiTest",
IsRequired: true,
ApiTestParameters: &ApiTestParametersRequest{
Endpoint: "/login",
HttpMethod: "POST",
Headers: map[string]string{"Content-Type": "application/json"},
Body: `{"username": "test", "password": "test"}`,
},
},
},
},
valid: true,
},
{
name: "empty name",
request: &CreateCompositeCaseRequest{
Name: "",
},
valid: false,
},
{
name: "invalid step",
request: &CreateCompositeCaseRequest{
Name: "Test Case",
Steps: []CreateCompositeCaseStepRequest{
{
StepName: "Invalid Step",
// Missing required StepOrder and StepType
},
},
},
valid: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
jsonData, err := json.Marshal(tt.request)
require.NoError(t, err)
var unmarshaled CreateCompositeCaseRequest
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
if tt.valid {
assert.Equal(t, tt.request.Name, unmarshaled.Name)
assert.Equal(t, len(tt.request.Steps), len(unmarshaled.Steps))
}
})
}
}
func TestApiTestParametersRequest_JSONConversion(t *testing.T) {
request := &ApiTestParametersRequest{
Endpoint: "/api/test",
HttpMethod: "POST",
Headers: map[string]string{
"Content-Type": "application/json",
"Authorization": "Bearer token",
},
QueryParams: map[string]string{
"page": "1",
"size": "10",
},
Body: `{"test": "data"}`,
Timeout: 30,
RetryCount: 3,
}
// Test JSON marshaling
jsonData, err := json.Marshal(request)
require.NoError(t, err)
assert.NotEmpty(t, jsonData)
// Test JSON unmarshaling
var unmarshaled ApiTestParametersRequest
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
assert.Equal(t, request.Endpoint, unmarshaled.Endpoint)
assert.Equal(t, request.HttpMethod, unmarshaled.HttpMethod)
assert.Equal(t, request.Headers, unmarshaled.Headers)
assert.Equal(t, request.QueryParams, unmarshaled.QueryParams)
assert.Equal(t, request.Body, unmarshaled.Body)
assert.Equal(t, request.Timeout, unmarshaled.Timeout)
assert.Equal(t, request.RetryCount, unmarshaled.RetryCount)
}
func TestUiTestParametersRequest_JSONConversion(t *testing.T) {
request := &UiTestParametersRequest{
Selector: "#submit-button",
SelectorType: "css",
EventType: "click",
InputValue: "test input",
OffsetX: 5,
OffsetY: 10,
WaitTimeSeconds: 1.5,
ScreenshotName: "test.png",
AssertionType: "text_contains",
AssertionValue: "Success",
}
// Test JSON marshaling
jsonData, err := json.Marshal(request)
require.NoError(t, err)
assert.NotEmpty(t, jsonData)
// Test JSON unmarshaling
var unmarshaled UiTestParametersRequest
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
assert.Equal(t, request.Selector, unmarshaled.Selector)
assert.Equal(t, request.SelectorType, unmarshaled.SelectorType)
assert.Equal(t, request.EventType, unmarshaled.EventType)
assert.Equal(t, request.InputValue, unmarshaled.InputValue)
assert.Equal(t, request.OffsetX, unmarshaled.OffsetX)
assert.Equal(t, request.OffsetY, unmarshaled.OffsetY)
assert.Equal(t, request.WaitTimeSeconds, unmarshaled.WaitTimeSeconds)
assert.Equal(t, request.ScreenshotName, unmarshaled.ScreenshotName)
assert.Equal(t, request.AssertionType, unmarshaled.AssertionType)
assert.Equal(t, request.AssertionValue, unmarshaled.AssertionValue)
}
func TestCompositeCaseStep_WithAssociations(t *testing.T) {
step := &CompositeCaseStep{
ID: 1,
CompositeCaseID: 1,
StepOrder: 1,
StepName: "API Test Step",
StepType: "API_TEST",
ActivityName: "RunApiTest",
IsRequired: true,
ApiTestParameters: &ApiTestParameters{
ID: 1,
StepID: 1,
Endpoint: "/api/test",
HttpMethod: "GET",
Timeout: 30,
},
}
// Test JSON marshaling with associations
jsonData, err := json.Marshal(step)
require.NoError(t, err)
assert.NotEmpty(t, jsonData)
// Verify that the association is included
var jsonMap map[string]interface{}
err = json.Unmarshal(jsonData, &jsonMap)
require.NoError(t, err)
assert.Contains(t, jsonMap, "api_test_parameters")
apiParams, ok := jsonMap["api_test_parameters"].(map[string]interface{})
assert.True(t, ok)
assert.Equal(t, "/api/test", apiParams["endpoint"])
assert.Equal(t, "GET", apiParams["http_method"])
}

View File

@ -2,6 +2,7 @@ package dao
import (
"beacon/models"
"encoding/json"
"errors"
"fmt"
"gorm.io/gorm"
@ -25,7 +26,7 @@ func CreateCompositeCase(tx *gorm.DB, compositeCase *models.CompositeCase) error
func GetCompositeCaseByID(id uint) (*models.CompositeCase, error) {
var compositeCase models.CompositeCase
err := DB.Preload("Steps", func(db *gorm.DB) *gorm.DB {
err := DB.Preload("Steps.ApiTestParameters").Preload("Steps.UiTestParameters").Preload("Steps", func(db *gorm.DB) *gorm.DB {
return db.Order("step_order ASC")
}).First(&compositeCase, id).Error
@ -79,7 +80,7 @@ func ListCompositeCases(page, pageSize int, status string) ([]models.CompositeCa
// 分页查询
offset := (page - 1) * pageSize
err := query.Preload("Steps", func(db *gorm.DB) *gorm.DB {
err := query.Preload("Steps.ApiTestParameters").Preload("Steps.UiTestParameters").Preload("Steps", func(db *gorm.DB) *gorm.DB {
return db.Order("step_order ASC")
}).Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&compositeCases).Error
@ -131,7 +132,7 @@ func GetCompositeCaseSteps(compositeCaseId string) ([]models.CompositeCaseStep,
return nil, fmt.Errorf("invalid composite case id: %w", err)
}
err = DB.Where("composite_case_id = ?", uint(id)).
err = DB.Preload("ApiTestParameters").Preload("UiTestParameters").Where("composite_case_id = ?", uint(id)).
Order("step_order ASC").
Find(&steps).Error
@ -151,6 +152,83 @@ func DeleteCompositeCaseStepsByCompositeCaseID(tx *gorm.DB, compositeCaseId uint
return tx.Where("composite_case_id = ?", compositeCaseId).Delete(&models.CompositeCaseStep{}).Error
}
// CreateApiTestParameters 创建API测试参数
func CreateApiTestParameters(tx *gorm.DB, params *models.ApiTestParameters) error {
if tx == nil {
tx = DB
}
return tx.Create(params).Error
}
// CreateUiTestParameters 创建UI测试参数
func CreateUiTestParameters(tx *gorm.DB, params *models.UiTestParameters) error {
if tx == nil {
tx = DB
}
return tx.Create(params).Error
}
// UpdateApiTestParameters 更新API测试参数
func UpdateApiTestParameters(tx *gorm.DB, stepID uint, params *models.ApiTestParametersRequest) error {
if tx == nil {
tx = DB
}
// 转换headers和query_params为JSON字符串
headersJson, _ := json.Marshal(params.Headers)
queryParamsJson, _ := json.Marshal(params.QueryParams)
updates := map[string]interface{}{
"endpoint": params.Endpoint,
"http_method": params.HttpMethod,
"headers": string(headersJson),
"query_params": string(queryParamsJson),
"body": params.Body,
"timeout": params.Timeout,
"retry_count": params.RetryCount,
}
return tx.Model(&models.ApiTestParameters{}).Where("step_id = ?", stepID).Updates(updates).Error
}
// UpdateUiTestParameters 更新UI测试参数
func UpdateUiTestParameters(tx *gorm.DB, stepID uint, params *models.UiTestParametersRequest) error {
if tx == nil {
tx = DB
}
updates := map[string]interface{}{
"selector": params.Selector,
"selector_type": params.SelectorType,
"event_type": params.EventType,
"input_value": params.InputValue,
"offset_x": params.OffsetX,
"offset_y": params.OffsetY,
"wait_time_seconds": params.WaitTimeSeconds,
"screenshot_name": params.ScreenshotName,
"assertion_type": params.AssertionType,
"assertion_value": params.AssertionValue,
}
return tx.Model(&models.UiTestParameters{}).Where("step_id = ?", stepID).Updates(updates).Error
}
// DeleteApiTestParametersByStepID 根据步骤ID删除API测试参数
func DeleteApiTestParametersByStepID(tx *gorm.DB, stepID uint) error {
if tx == nil {
tx = DB
}
return tx.Where("step_id = ?", stepID).Delete(&models.ApiTestParameters{}).Error
}
// DeleteUiTestParametersByStepID 根据步骤ID删除UI测试参数
func DeleteUiTestParametersByStepID(tx *gorm.DB, stepID uint) error {
if tx == nil {
tx = DB
}
return tx.Where("step_id = ?", stepID).Delete(&models.UiTestParameters{}).Error
}
// BeginTransaction 开启事务
func BeginTransaction() *gorm.DB {
return DB.Begin()

View File

@ -44,7 +44,7 @@ func (h *CompositeCaseHandler) CreateCompositeCase(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{
"message": "创建复合案例成功",
"data": "compositeCase",
"data": compositeCase,
})
}

View File

@ -0,0 +1,386 @@
package handlers
import (
"beacon/models"
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupTestRouter() *gin.Engine {
gin.SetMode(gin.TestMode)
router := gin.New()
return router
}
// Skipping database-dependent tests - they require full database setup
func TestCompositeCaseHandler_CreateCompositeCase_InvalidJSON(t *testing.T) {
router := setupTestRouter()
handler := NewCompositeCaseHandler()
router.POST("/api/composite-cases", handler.CreateCompositeCase)
// Send invalid JSON
invalidJSON := `{"name": "test", "invalid_json": }`
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/composite-cases", bytes.NewBufferString(invalidJSON))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response, "error")
assert.Equal(t, "请求参数错误", response["error"])
}
func TestCompositeCaseHandler_CreateCompositeCase_MissingRequiredFields(t *testing.T) {
router := setupTestRouter()
handler := NewCompositeCaseHandler()
router.POST("/api/composite-cases", handler.CreateCompositeCase)
// Request missing required name field
request := models.CreateCompositeCaseRequest{
Description: "Test Description",
Steps: []models.CreateCompositeCaseStepRequest{},
}
jsonData, err := json.Marshal(request)
require.NoError(t, err)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/composite-cases", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response, "error")
}
func TestCreateCompositeCaseStepRequest_APITestValidation(t *testing.T) {
tests := []struct {
name string
stepReq models.CreateCompositeCaseStepRequest
expectErr bool
}{
{
name: "valid API test step",
stepReq: models.CreateCompositeCaseStepRequest{
StepOrder: 1,
StepName: "Valid API Test",
StepType: "API_TEST",
ActivityName: "RunApiTest",
IsRequired: true,
ApiTestParameters: &models.ApiTestParametersRequest{
Endpoint: "/api/test",
HttpMethod: "GET",
Headers: map[string]string{
"Authorization": "Bearer token",
},
QueryParams: map[string]string{
"page": "1",
},
Timeout: 30,
},
},
expectErr: false,
},
{
name: "API test without parameters",
stepReq: models.CreateCompositeCaseStepRequest{
StepOrder: 1,
StepName: "Invalid API Test",
StepType: "API_TEST",
ActivityName: "RunApiTest",
IsRequired: true,
ApiTestParameters: nil,
},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test JSON marshaling/unmarshaling
jsonData, err := json.Marshal(tt.stepReq)
require.NoError(t, err)
var unmarshaled models.CreateCompositeCaseStepRequest
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
// Verify the structure is preserved
assert.Equal(t, tt.stepReq.StepOrder, unmarshaled.StepOrder)
assert.Equal(t, tt.stepReq.StepName, unmarshaled.StepName)
assert.Equal(t, tt.stepReq.StepType, unmarshaled.StepType)
if tt.stepReq.ApiTestParameters != nil {
require.NotNil(t, unmarshaled.ApiTestParameters)
assert.Equal(t, tt.stepReq.ApiTestParameters.Endpoint, unmarshaled.ApiTestParameters.Endpoint)
assert.Equal(t, tt.stepReq.ApiTestParameters.HttpMethod, unmarshaled.ApiTestParameters.HttpMethod)
assert.Equal(t, tt.stepReq.ApiTestParameters.Headers, unmarshaled.ApiTestParameters.Headers)
assert.Equal(t, tt.stepReq.ApiTestParameters.QueryParams, unmarshaled.ApiTestParameters.QueryParams)
} else {
assert.Nil(t, unmarshaled.ApiTestParameters)
}
})
}
}
func TestCreateCompositeCaseStepRequest_UITestValidation(t *testing.T) {
tests := []struct {
name string
stepReq models.CreateCompositeCaseStepRequest
expectErr bool
}{
{
name: "valid UI test step",
stepReq: models.CreateCompositeCaseStepRequest{
StepOrder: 1,
StepName: "Valid UI Test",
StepType: "UI_TEST",
ActivityName: "RunUiTest",
IsRequired: true,
UiTestParameters: &models.UiTestParametersRequest{
Selector: "#login-button",
SelectorType: "css",
EventType: "click",
InputValue: "test@example.com",
OffsetX: 10,
OffsetY: 20,
WaitTimeSeconds: 2.5,
ScreenshotName: "login.png",
AssertionType: "element_visible",
AssertionValue: "#success-message",
},
},
expectErr: false,
},
{
name: "UI test without parameters",
stepReq: models.CreateCompositeCaseStepRequest{
StepOrder: 1,
StepName: "Invalid UI Test",
StepType: "UI_TEST",
ActivityName: "RunUiTest",
IsRequired: true,
UiTestParameters: nil,
},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test JSON marshaling/unmarshaling
jsonData, err := json.Marshal(tt.stepReq)
require.NoError(t, err)
var unmarshaled models.CreateCompositeCaseStepRequest
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
// Verify the structure is preserved
assert.Equal(t, tt.stepReq.StepOrder, unmarshaled.StepOrder)
assert.Equal(t, tt.stepReq.StepName, unmarshaled.StepName)
assert.Equal(t, tt.stepReq.StepType, unmarshaled.StepType)
if tt.stepReq.UiTestParameters != nil {
require.NotNil(t, unmarshaled.UiTestParameters)
assert.Equal(t, tt.stepReq.UiTestParameters.Selector, unmarshaled.UiTestParameters.Selector)
assert.Equal(t, tt.stepReq.UiTestParameters.SelectorType, unmarshaled.UiTestParameters.SelectorType)
assert.Equal(t, tt.stepReq.UiTestParameters.EventType, unmarshaled.UiTestParameters.EventType)
assert.Equal(t, tt.stepReq.UiTestParameters.InputValue, unmarshaled.UiTestParameters.InputValue)
assert.Equal(t, tt.stepReq.UiTestParameters.OffsetX, unmarshaled.UiTestParameters.OffsetX)
assert.Equal(t, tt.stepReq.UiTestParameters.OffsetY, unmarshaled.UiTestParameters.OffsetY)
assert.Equal(t, tt.stepReq.UiTestParameters.WaitTimeSeconds, unmarshaled.UiTestParameters.WaitTimeSeconds)
} else {
assert.Nil(t, unmarshaled.UiTestParameters)
}
})
}
}
func TestCompositeCaseRequest_ComplexScenario(t *testing.T) {
// Test a complex scenario with multiple step types and flow control
successOrder := 3
failureOrder := 4
request := models.CreateCompositeCaseRequest{
Name: "Complex E2E Test",
Description: "Login -> API Call -> UI Verification -> Cleanup",
Status: "active",
Steps: []models.CreateCompositeCaseStepRequest{
{
StepOrder: 1,
StepName: "Login API",
StepType: "API_TEST",
ActivityName: "RunApiTest",
IsRequired: true,
SuccessNextStepOrder: &successOrder,
FailureNextStepOrder: &failureOrder,
RunCondition: `{"requires": "none"}`,
ApiTestParameters: &models.ApiTestParametersRequest{
Endpoint: "/api/login",
HttpMethod: "POST",
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: `{"username": "${global.username}", "password": "${global.password}"}`,
Timeout: 30,
RetryCount: 3,
},
},
{
StepOrder: 2,
StepName: "Get User Data",
StepType: "API_TEST",
ActivityName: "RunApiTest",
IsRequired: true,
RunCondition: `{"previous_step": "1", "status": "success"}`,
ApiTestParameters: &models.ApiTestParametersRequest{
Endpoint: "/api/user/profile",
HttpMethod: "GET",
Headers: map[string]string{
"Authorization": "Bearer ${step.1.json_token}",
},
QueryParams: map[string]string{
"include": "preferences,settings",
},
Timeout: 15,
},
},
{
StepOrder: 3,
StepName: "Verify UI Elements",
StepType: "UI_TEST",
ActivityName: "RunUiTest",
IsRequired: true,
RunCondition: `{"previous_step": "2", "status": "success"}`,
UiTestParameters: &models.UiTestParametersRequest{
Selector: "#user-profile-name",
SelectorType: "css",
EventType: "verify_text",
AssertionType: "text_contains",
AssertionValue: "${step.2.json_name}",
WaitTimeSeconds: 3.0,
ScreenshotName: "profile_verification.png",
},
},
{
StepOrder: 4,
StepName: "Cleanup on Failure",
StepType: "API_TEST",
ActivityName: "RunApiTest",
IsRequired: false,
RunCondition: `{"any_previous_failed": true}`,
ApiTestParameters: &models.ApiTestParametersRequest{
Endpoint: "/api/cleanup",
HttpMethod: "POST",
Headers: map[string]string{
"Authorization": "Bearer ${step.1.json_token}",
},
Timeout: 10,
},
},
},
}
// Test JSON marshaling
jsonData, err := json.Marshal(request)
require.NoError(t, err)
assert.NotEmpty(t, jsonData)
// Test JSON unmarshaling
var unmarshaled models.CreateCompositeCaseRequest
err = json.Unmarshal(jsonData, &unmarshaled)
require.NoError(t, err)
// Verify complex structure is preserved
assert.Equal(t, request.Name, unmarshaled.Name)
assert.Equal(t, len(request.Steps), len(unmarshaled.Steps))
// Verify flow control fields
step1 := unmarshaled.Steps[0]
assert.Equal(t, successOrder, *step1.SuccessNextStepOrder)
assert.Equal(t, failureOrder, *step1.FailureNextStepOrder)
assert.Contains(t, step1.RunCondition, "requires")
// Verify parameter templating is preserved
assert.Contains(t, step1.ApiTestParameters.Body, "${global.username}")
step2 := unmarshaled.Steps[1]
assert.Contains(t, step2.ApiTestParameters.Headers["Authorization"], "${step.1.json_token}")
// Verify UI test assertions
step3 := unmarshaled.Steps[2]
assert.Equal(t, "verify_text", step3.UiTestParameters.EventType)
assert.Contains(t, step3.UiTestParameters.AssertionValue, "${step.2.json_name}")
}
func TestResponseFormatting(t *testing.T) {
// Test that our response structures work correctly
compositeCase := &models.CompositeCase{
ID: 1,
Name: "Test Case",
Description: "Test Description",
Status: "active",
Steps: []models.CompositeCaseStep{
{
ID: 1,
CompositeCaseID: 1,
StepOrder: 1,
StepName: "Test Step",
StepType: "API_TEST",
ActivityName: "RunApiTest",
IsRequired: true,
ApiTestParameters: &models.ApiTestParameters{
ID: 1,
StepID: 1,
Endpoint: "/api/test",
HttpMethod: "GET",
Headers: `{"Content-Type": "application/json"}`,
Timeout: 30,
},
},
},
}
// Test JSON marshaling for response
jsonData, err := json.Marshal(compositeCase)
require.NoError(t, err)
assert.NotEmpty(t, jsonData)
// Verify the JSON contains expected fields
var jsonMap map[string]interface{}
err = json.Unmarshal(jsonData, &jsonMap)
require.NoError(t, err)
assert.Equal(t, float64(1), jsonMap["id"])
assert.Equal(t, "Test Case", jsonMap["name"])
assert.Contains(t, jsonMap, "steps")
steps, ok := jsonMap["steps"].([]interface{})
assert.True(t, ok)
assert.Len(t, steps, 1)
step, ok := steps[0].(map[string]interface{})
assert.True(t, ok)
assert.Equal(t, "Test Step", step["step_name"])
assert.Equal(t, "API_TEST", step["step_type"])
assert.Contains(t, step, "api_test_parameters")
}

View File

@ -0,0 +1,43 @@
{
"name": "示例复合测试案例",
"description": "这是一个包含API和UI测试的复合案例",
"status": "active",
"steps": [
{
"step_order": 1,
"step_name": "登录获取toekn",
"step_description": "测试用户登录功能",
"step_type": "API",
"activity_name": "RunApiTest",
"run_condition": "{\"initial_step\": true}",
"success_next_step_order": 2,
"api_test_parameters": {
"endpoint": "http://101.89.127.197:9080/api/ucenter/open/access/user_app_login",
"http_method": "POST",
"headers": {
"Content-Type": "application/x-www-form-urlencoded"
},
"request_body": "{\"appKey\":\"1518492248155004929\",\"userAccount\":\"admin\",\"userPassword\":\"/olNQxk4Zzse237xSNY/I9tNg2L2F+AIUUUt59ph0R2Pg1LkAoUgXKYk6XFHqSH64oGBbP3CTc0aPX8kaIS5iu2284V6MI86ntjVvqMBA=\"}"
},
"is_required": true
},
{
"step_order": 2,
"step_name": "获取系统树",
"step_description": "测试用户登录功能",
"step_type": "API",
"activity_name": "RunApiTest",
"run_condition": "{\"previous_step\": \"1\", \"status\": \"success\"}",
"api_test_parameters": {
"test_case_id": "2",
"endpoint": "http://101.89.127.196:11001/api/system/systemproject/list",
"http_method": "POST",
"headers": {
"Content-Type": "application/json",
"Access_token": "eyJhbGciOiJIUzI1NiJ9.ssss.897-L4gSNWDOaTqNWh0V-igKmqRnO6onM8sR2ssG0cM"
}
},
"is_required": true
}
]
}

View File

@ -2,11 +2,12 @@ package services
import (
"beacon/models"
"beacon/pkg/dao/mysql"
dao "beacon/pkg/dao/mysql"
"beacon/utils"
"encoding/json"
"fmt"
"go.uber.org/zap"
"gorm.io/gorm"
)
type CompositeCaseService struct {
@ -57,15 +58,24 @@ func (s *CompositeCaseService) CreateCompositeCase(req *models.CreateCompositeCa
var steps []models.CompositeCaseStep
for _, stepReq := range req.Steps {
activityName, _ := utils.GetActivityName(stepReq.StepType)
// Handle empty run_condition by setting it to null instead of empty string
runCondition := stepReq.RunCondition
if runCondition == "" {
runCondition = "{}"
}
step := models.CompositeCaseStep{
CompositeCaseID: compositeCase.ID,
StepOrder: stepReq.StepOrder,
StepName: stepReq.StepName,
StepDescription: stepReq.StepDescription,
StepType: stepReq.StepType,
ActivityName: activityName,
ParametersJson: fixParametersJson(stepReq.ParametersJson),
IsRequired: stepReq.IsRequired,
CompositeCaseID: compositeCase.ID,
StepOrder: stepReq.StepOrder,
StepName: stepReq.StepName,
StepDescription: stepReq.StepDescription,
StepType: stepReq.StepType,
ActivityName: activityName,
IsRequired: stepReq.IsRequired,
SuccessNextStepOrder: stepReq.SuccessNextStepOrder,
FailureNextStepOrder: stepReq.FailureNextStepOrder,
RunCondition: runCondition,
}
steps = append(steps, step)
}
@ -78,6 +88,18 @@ func (s *CompositeCaseService) CreateCompositeCase(req *models.CreateCompositeCa
return nil, fmt.Errorf("创建复合案例步骤失败: %w", err)
}
// 创建步骤参数
for i, stepReq := range req.Steps {
stepID := steps[i].ID
if err := s.createStepParameters(tx, stepID, stepReq); err != nil {
zap.L().Error("创建步骤参数失败",
zap.Uint("step_id", stepID),
zap.Error(err))
tx.Rollback()
return nil, fmt.Errorf("创建步骤参数失败: %w", err)
}
}
zap.L().Info("复合案例步骤创建成功",
zap.Uint("composite_case_id", compositeCase.ID),
zap.Int("created_steps_count", len(steps)))
@ -181,15 +203,24 @@ func (s *CompositeCaseService) UpdateCompositeCase(id uint, req *models.UpdateCo
var steps []models.CompositeCaseStep
for _, stepReq := range req.Steps {
activityName, _ := utils.GetActivityName(stepReq.StepType)
// Handle empty run_condition by setting it to null instead of empty string
runCondition := stepReq.RunCondition
if runCondition == "" {
runCondition = "{}"
}
step := models.CompositeCaseStep{
CompositeCaseID: id,
StepOrder: stepReq.StepOrder,
StepName: stepReq.StepName,
StepDescription: stepReq.StepDescription,
StepType: stepReq.StepType,
ActivityName: activityName,
ParametersJson: fixParametersJson(stepReq.ParametersJson),
IsRequired: stepReq.IsRequired,
CompositeCaseID: id,
StepOrder: stepReq.StepOrder,
StepName: stepReq.StepName,
StepDescription: stepReq.StepDescription,
StepType: stepReq.StepType,
ActivityName: activityName,
IsRequired: stepReq.IsRequired,
SuccessNextStepOrder: stepReq.SuccessNextStepOrder,
FailureNextStepOrder: stepReq.FailureNextStepOrder,
RunCondition: runCondition,
}
steps = append(steps, step)
}
@ -201,6 +232,18 @@ func (s *CompositeCaseService) UpdateCompositeCase(id uint, req *models.UpdateCo
tx.Rollback()
return nil, fmt.Errorf("创建新步骤失败: %w", err)
}
// 创建步骤参数
for i, stepReq := range req.Steps {
stepID := steps[i].ID
if err := s.updateStepParameters(tx, stepID, stepReq); err != nil {
zap.L().Error("创建步骤参数失败",
zap.Uint("step_id", stepID),
zap.Error(err))
tx.Rollback()
return nil, fmt.Errorf("创建步骤参数失败: %w", err)
}
}
zap.L().Info("新步骤创建成功",
zap.Uint("id", id),
zap.Int("created_steps_count", len(steps)))
@ -214,26 +257,156 @@ 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), &params); err != nil {
// Not a valid json string, return as is.
return jsonStr
}
// createStepParameters 创建步骤参数
func (s *CompositeCaseService) createStepParameters(tx *gorm.DB, stepID uint, stepReq models.CreateCompositeCaseStepRequest) error {
if stepReq.ApiTestParameters != nil {
apiParams := &models.ApiTestParameters{
StepID: stepID,
Endpoint: stepReq.ApiTestParameters.Endpoint,
HttpMethod: stepReq.ApiTestParameters.HttpMethod,
Body: stepReq.ApiTestParameters.Body,
Timeout: stepReq.ApiTestParameters.Timeout,
RetryCount: stepReq.ApiTestParameters.RetryCount,
}
if rb, ok := params["request_body"]; ok {
if rbs, ok := rb.(string); ok && rbs == "{" {
params["request_body"] = "{}"
// 转换headers和query_params为JSON字符串
// 确保总是设置为有效的JSON避免空字符串或null
if stepReq.ApiTestParameters.Headers != nil && len(stepReq.ApiTestParameters.Headers) > 0 {
headersJson, err := json.Marshal(stepReq.ApiTestParameters.Headers)
if err != nil {
apiParams.Headers = "{}"
} else {
apiParams.Headers = string(headersJson)
}
} else {
apiParams.Headers = "{}"
}
if stepReq.ApiTestParameters.QueryParams != nil && len(stepReq.ApiTestParameters.QueryParams) > 0 {
queryParamsJson, err := json.Marshal(stepReq.ApiTestParameters.QueryParams)
if err != nil {
apiParams.QueryParams = "{}"
} else {
apiParams.QueryParams = string(queryParamsJson)
}
} else {
apiParams.QueryParams = "{}"
}
// 最后确认检查,确保不会出现空字符串
if apiParams.Headers == "" || apiParams.Headers == "null" {
apiParams.Headers = "{}"
}
if apiParams.QueryParams == "" || apiParams.QueryParams == "null" {
apiParams.QueryParams = "{}"
}
if err := dao.CreateApiTestParameters(tx, apiParams); err != nil {
return fmt.Errorf("创建API测试参数失败: %w", err)
}
}
fixedJSONBytes, err := json.Marshal(params)
if err != nil {
// Failed to marshal back, return original.
return jsonStr
if stepReq.UiTestParameters != nil {
uiParams := &models.UiTestParameters{
StepID: stepID,
Selector: stepReq.UiTestParameters.Selector,
SelectorType: stepReq.UiTestParameters.SelectorType,
EventType: stepReq.UiTestParameters.EventType,
InputValue: stepReq.UiTestParameters.InputValue,
OffsetX: stepReq.UiTestParameters.OffsetX,
OffsetY: stepReq.UiTestParameters.OffsetY,
WaitTimeSeconds: stepReq.UiTestParameters.WaitTimeSeconds,
ScreenshotName: stepReq.UiTestParameters.ScreenshotName,
AssertionType: stepReq.UiTestParameters.AssertionType,
AssertionValue: stepReq.UiTestParameters.AssertionValue,
}
if err := dao.CreateUiTestParameters(tx, uiParams); err != nil {
return fmt.Errorf("创建UI测试参数失败: %w", err)
}
}
return string(fixedJSONBytes)
return nil
}
// updateStepParameters 更新步骤参数
func (s *CompositeCaseService) updateStepParameters(tx *gorm.DB, stepID uint, stepReq models.UpdateCompositeCaseStepRequest) error {
// 先删除旧参数
if err := dao.DeleteApiTestParametersByStepID(tx, stepID); err != nil {
return fmt.Errorf("删除旧API测试参数失败: %w", err)
}
if err := dao.DeleteUiTestParametersByStepID(tx, stepID); err != nil {
return fmt.Errorf("删除旧UI测试参数失败: %w", err)
}
// 创建新参数
if stepReq.ApiTestParameters != nil {
apiParams := &models.ApiTestParameters{
StepID: stepID,
Endpoint: stepReq.ApiTestParameters.Endpoint,
HttpMethod: stepReq.ApiTestParameters.HttpMethod,
Body: stepReq.ApiTestParameters.Body,
Timeout: stepReq.ApiTestParameters.Timeout,
RetryCount: stepReq.ApiTestParameters.RetryCount,
}
// 确保总是设置为有效的JSON避免空字符串或null
if stepReq.ApiTestParameters.Headers != nil && len(stepReq.ApiTestParameters.Headers) > 0 {
headersJson, err := json.Marshal(stepReq.ApiTestParameters.Headers)
if err != nil {
apiParams.Headers = "{}"
} else {
apiParams.Headers = string(headersJson)
}
} else {
apiParams.Headers = "{}"
}
if stepReq.ApiTestParameters.QueryParams != nil && len(stepReq.ApiTestParameters.QueryParams) > 0 {
queryParamsJson, err := json.Marshal(stepReq.ApiTestParameters.QueryParams)
if err != nil {
apiParams.QueryParams = "{}"
} else {
apiParams.QueryParams = string(queryParamsJson)
}
} else {
apiParams.QueryParams = "{}"
}
// 最后确认检查,确保不会出现空字符串
if apiParams.Headers == "" || apiParams.Headers == "null" {
apiParams.Headers = "{}"
}
if apiParams.QueryParams == "" || apiParams.QueryParams == "null" {
apiParams.QueryParams = "{}"
}
if err := dao.CreateApiTestParameters(tx, apiParams); err != nil {
return fmt.Errorf("创建API测试参数失败: %w", err)
}
}
if stepReq.UiTestParameters != nil {
uiParams := &models.UiTestParameters{
StepID: stepID,
Selector: stepReq.UiTestParameters.Selector,
SelectorType: stepReq.UiTestParameters.SelectorType,
EventType: stepReq.UiTestParameters.EventType,
InputValue: stepReq.UiTestParameters.InputValue,
OffsetX: stepReq.UiTestParameters.OffsetX,
OffsetY: stepReq.UiTestParameters.OffsetY,
WaitTimeSeconds: stepReq.UiTestParameters.WaitTimeSeconds,
ScreenshotName: stepReq.UiTestParameters.ScreenshotName,
AssertionType: stepReq.UiTestParameters.AssertionType,
AssertionValue: stepReq.UiTestParameters.AssertionValue,
}
if err := dao.CreateUiTestParameters(tx, uiParams); err != nil {
return fmt.Errorf("创建UI测试参数失败: %w", err)
}
}
return nil
}
// DeleteCompositeCase 删除复合案例

447
services/composite_test.go Normal file
View File

@ -0,0 +1,447 @@
package services
import (
"beacon/models"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCompositeCaseService_CreateStepParameters(t *testing.T) {
service := &CompositeCaseService{}
tests := []struct {
name string
stepReq models.CreateCompositeCaseStepRequest
wantErr bool
}{
{
name: "create API test parameters",
stepReq: models.CreateCompositeCaseStepRequest{
StepOrder: 1,
StepName: "API Test",
StepType: "API_TEST",
ActivityName: "RunApiTest",
IsRequired: true,
ApiTestParameters: &models.ApiTestParametersRequest{
Endpoint: "/api/test",
HttpMethod: "POST",
Headers: map[string]string{
"Content-Type": "application/json",
},
QueryParams: map[string]string{
"page": "1",
},
Body: `{"test": "data"}`,
Timeout: 30,
RetryCount: 3,
},
},
wantErr: false,
},
{
name: "create UI test parameters",
stepReq: models.CreateCompositeCaseStepRequest{
StepOrder: 1,
StepName: "UI Test",
StepType: "UI_TEST",
ActivityName: "RunUiTest",
IsRequired: true,
UiTestParameters: &models.UiTestParametersRequest{
Selector: "#login-button",
SelectorType: "css",
EventType: "click",
InputValue: "test@example.com",
OffsetX: 10,
OffsetY: 20,
WaitTimeSeconds: 2.5,
ScreenshotName: "login.png",
AssertionType: "element_visible",
AssertionValue: "#success-message",
},
},
wantErr: false,
},
{
name: "no parameters",
stepReq: models.CreateCompositeCaseStepRequest{
StepOrder: 1,
StepName: "No Params Test",
StepType: "OTHER",
ActivityName: "RunOtherTest",
IsRequired: true,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We can't test with a real database transaction here,
// but we can test the parameter validation logic
err := service.validateStepParameters(tt.stepReq)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestCompositeCaseService_ValidateStepParameters(t *testing.T) {
service := &CompositeCaseService{}
tests := []struct {
name string
stepReq models.CreateCompositeCaseStepRequest
wantErr bool
errMsg string
}{
{
name: "valid API parameters",
stepReq: models.CreateCompositeCaseStepRequest{
StepType: "API_TEST",
ApiTestParameters: &models.ApiTestParametersRequest{
Endpoint: "/api/valid",
HttpMethod: "GET",
},
},
wantErr: false,
},
{
name: "invalid API parameters - empty endpoint",
stepReq: models.CreateCompositeCaseStepRequest{
StepType: "API_TEST",
ApiTestParameters: &models.ApiTestParametersRequest{
Endpoint: "",
HttpMethod: "GET",
},
},
wantErr: true,
errMsg: "endpoint is required for API test",
},
{
name: "invalid API parameters - empty method",
stepReq: models.CreateCompositeCaseStepRequest{
StepType: "API_TEST",
ApiTestParameters: &models.ApiTestParametersRequest{
Endpoint: "/api/test",
HttpMethod: "",
},
},
wantErr: true,
errMsg: "http_method is required for API test",
},
{
name: "valid UI parameters",
stepReq: models.CreateCompositeCaseStepRequest{
StepType: "UI_TEST",
UiTestParameters: &models.UiTestParametersRequest{
Selector: "#button",
EventType: "click",
},
},
wantErr: false,
},
{
name: "invalid UI parameters - empty selector",
stepReq: models.CreateCompositeCaseStepRequest{
StepType: "UI_TEST",
UiTestParameters: &models.UiTestParametersRequest{
Selector: "",
EventType: "click",
},
},
wantErr: true,
errMsg: "selector is required for UI test",
},
{
name: "invalid UI parameters - empty event type",
stepReq: models.CreateCompositeCaseStepRequest{
StepType: "UI_TEST",
UiTestParameters: &models.UiTestParametersRequest{
Selector: "#button",
EventType: "",
},
},
wantErr: true,
errMsg: "event_type is required for UI test",
},
{
name: "API test without parameters",
stepReq: models.CreateCompositeCaseStepRequest{
StepType: "API_TEST",
ApiTestParameters: nil,
},
wantErr: true,
errMsg: "API test parameters are required for API_TEST step type",
},
{
name: "UI test without parameters",
stepReq: models.CreateCompositeCaseStepRequest{
StepType: "UI_TEST",
UiTestParameters: nil,
},
wantErr: true,
errMsg: "UI test parameters are required for UI_TEST step type",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := service.validateStepParameters(tt.stepReq)
if tt.wantErr {
assert.Error(t, err)
if tt.errMsg != "" {
assert.Contains(t, err.Error(), tt.errMsg)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestCompositeCaseService_ValidateCreateRequest(t *testing.T) {
service := &CompositeCaseService{}
tests := []struct {
name string
req *models.CreateCompositeCaseRequest
wantErr bool
errMsg string
}{
{
name: "valid request",
req: &models.CreateCompositeCaseRequest{
Name: "Test Case",
Description: "Test Description",
Status: "active",
Steps: []models.CreateCompositeCaseStepRequest{
{
StepOrder: 1,
StepName: "Login",
StepType: "API_TEST",
ActivityName: "RunApiTest",
IsRequired: true,
ApiTestParameters: &models.ApiTestParametersRequest{
Endpoint: "/login",
HttpMethod: "POST",
},
},
},
},
wantErr: false,
},
{
name: "empty name",
req: &models.CreateCompositeCaseRequest{
Name: "",
},
wantErr: true,
errMsg: "name is required",
},
{
name: "duplicate step orders",
req: &models.CreateCompositeCaseRequest{
Name: "Test Case",
Steps: []models.CreateCompositeCaseStepRequest{
{
StepOrder: 1,
StepName: "Step 1",
StepType: "API_TEST",
ApiTestParameters: &models.ApiTestParametersRequest{
Endpoint: "/test1",
HttpMethod: "GET",
},
},
{
StepOrder: 1, // Duplicate order
StepName: "Step 2",
StepType: "API_TEST",
ApiTestParameters: &models.ApiTestParametersRequest{
Endpoint: "/test2",
HttpMethod: "GET",
},
},
},
},
wantErr: true,
errMsg: "duplicate step order",
},
{
name: "invalid step parameters",
req: &models.CreateCompositeCaseRequest{
Name: "Test Case",
Steps: []models.CreateCompositeCaseStepRequest{
{
StepOrder: 1,
StepName: "Invalid Step",
StepType: "API_TEST",
ApiTestParameters: &models.ApiTestParametersRequest{
Endpoint: "", // Invalid
HttpMethod: "GET",
},
},
},
},
wantErr: true,
errMsg: "endpoint is required",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := service.validateCreateRequest(tt.req)
if tt.wantErr {
assert.Error(t, err)
if tt.errMsg != "" {
assert.Contains(t, err.Error(), tt.errMsg)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestCompositeCaseService_ValidateUpdateRequest(t *testing.T) {
service := &CompositeCaseService{}
tests := []struct {
name string
req *models.UpdateCompositeCaseRequest
wantErr bool
errMsg string
}{
{
name: "valid update request",
req: &models.UpdateCompositeCaseRequest{
Name: "Updated Test Case",
Description: "Updated Description",
Status: "inactive",
Steps: []models.UpdateCompositeCaseStepRequest{
{
ID: 1,
StepOrder: 1,
StepName: "Updated Step",
StepType: "API_TEST",
ApiTestParameters: &models.ApiTestParametersRequest{
Endpoint: "/updated",
HttpMethod: "PUT",
},
},
},
},
wantErr: false,
},
{
name: "empty update - should be valid",
req: &models.UpdateCompositeCaseRequest{
Name: "",
},
wantErr: false,
},
{
name: "invalid step in update",
req: &models.UpdateCompositeCaseRequest{
Steps: []models.UpdateCompositeCaseStepRequest{
{
ID: 1,
StepType: "API_TEST",
ApiTestParameters: &models.ApiTestParametersRequest{
Endpoint: "", // Invalid
HttpMethod: "GET",
},
},
},
},
wantErr: true,
errMsg: "endpoint is required",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := service.validateUpdateRequest(tt.req)
if tt.wantErr {
assert.Error(t, err)
if tt.errMsg != "" {
assert.Contains(t, err.Error(), tt.errMsg)
}
} else {
assert.NoError(t, err)
}
})
}
}
// Add the validation methods to the service for testing
func (s *CompositeCaseService) validateStepParameters(stepReq models.CreateCompositeCaseStepRequest) error {
switch stepReq.StepType {
case "API_TEST":
if stepReq.ApiTestParameters == nil {
return fmt.Errorf("API test parameters are required for API_TEST step type")
}
if stepReq.ApiTestParameters.Endpoint == "" {
return fmt.Errorf("endpoint is required for API test")
}
if stepReq.ApiTestParameters.HttpMethod == "" {
return fmt.Errorf("http_method is required for API test")
}
case "UI_TEST":
if stepReq.UiTestParameters == nil {
return fmt.Errorf("UI test parameters are required for UI_TEST step type")
}
if stepReq.UiTestParameters.Selector == "" {
return fmt.Errorf("selector is required for UI test")
}
if stepReq.UiTestParameters.EventType == "" {
return fmt.Errorf("event_type is required for UI test")
}
}
return nil
}
func (s *CompositeCaseService) validateCreateRequest(req *models.CreateCompositeCaseRequest) error {
if req.Name == "" {
return fmt.Errorf("name is required")
}
// Check for duplicate step orders
orderMap := make(map[int]bool)
for _, step := range req.Steps {
if orderMap[step.StepOrder] {
return fmt.Errorf("duplicate step order: %d", step.StepOrder)
}
orderMap[step.StepOrder] = true
if err := s.validateStepParameters(step); err != nil {
return err
}
}
return nil
}
func (s *CompositeCaseService) validateUpdateRequest(req *models.UpdateCompositeCaseRequest) error {
// Update requests can have empty fields (partial updates)
if req.Steps != nil {
for _, step := range req.Steps {
// Convert UpdateRequest to CreateRequest for validation
createReq := models.CreateCompositeCaseStepRequest{
StepOrder: step.StepOrder,
StepName: step.StepName,
StepType: step.StepType,
ApiTestParameters: step.ApiTestParameters,
UiTestParameters: step.UiTestParameters,
}
if err := s.validateStepParameters(createReq); err != nil {
return err
}
}
}
return nil
}