暂存
This commit is contained in:
parent
27f0bdbe8f
commit
a2fc0f4067
102
CLAUDE.md
Normal file
102
CLAUDE.md
Normal 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.
|
@ -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
|
||||
}
|
||||
|
209
docs/composite_case_examples.md
Normal file
209
docs/composite_case_examples.md
Normal 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
|
||||
```
|
225
docs/create_composite_case_sample.json
Normal file
225
docs/create_composite_case_sample.json
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
44
docs/create_composite_case_simple.json
Normal file
44
docs/create_composite_case_simple.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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 '重试次数',
|
||||
|
@ -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
351
models/composite_test.go
Normal 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"])
|
||||
}
|
@ -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()
|
||||
|
@ -44,7 +44,7 @@ func (h *CompositeCaseHandler) CreateCompositeCase(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "创建复合案例成功",
|
||||
"data": "compositeCase",
|
||||
"data": compositeCase,
|
||||
})
|
||||
}
|
||||
|
||||
|
386
routers/handlers/composite_test.go
Normal file
386
routers/handlers/composite_test.go
Normal 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")
|
||||
}
|
43
routers/handlers/test.json
Normal file
43
routers/handlers/test.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
@ -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), ¶ms); 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
447
services/composite_test.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user