diff --git a/README.md b/README.md index 85c7bd0b8a..8dbea3c9e7 100755 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ v1.1.0 是 v1.0.0 之后的功能版本。 - + @@ -92,9 +92,22 @@ v1.1.0 是 v1.0.0 之后的功能版本。 + + + + - + + + + + + + + + + @@ -110,8 +123,17 @@ v1.1.0 是 v1.0.0 之后的功能版本。 - - + + + + + + + + + + + @@ -123,19 +145,56 @@ v1.1.0 是 v1.0.0 之后的功能版本。 + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + @@ -150,6 +209,16 @@ v1.1.0 是 v1.0.0 之后的功能版本。 + + + + + + + + + + @@ -164,27 +233,37 @@ v1.1.0 是 v1.0.0 之后的功能版本。 - - + + - + + + + + + + + - + + + +
测试跟踪测试跟踪 项目管理 多项目支持,测试用例、测试计划与项目关联
快速导入用例到系统
测试用例评审基于已有用例发起评审
测试计划跟踪在线更新评审结果
支持多人在线添加评审评论
灵活的评审人分配形式
测试计划跟踪 基于已有用例发起测试计划
与平台中的接口测试、性能测试功能结合,自动更新关联用例的结果
接口测试测试脚本记录测试用例关联的缺陷
缺陷记录支持关联到 Jira/TAPD 平台
测试报告支持分享、导出
接口测试测试脚本 在线编辑接口测试内容
支持多接口的场景化测试
测试场景复用
测试场景支持引用已有环境信息
测试环境信息管理
通过浏览器插件快速录制测试脚本
测试报告测试执行后自动生成测试报告支持前后置 BeanShell/Python 脚本
上传并引用自定义 Jar 包
多协议支持,支持 HTTP、Dubbo、SQL、TCP 类型请求
支持等待时间、条件判断等逻辑控制功能
测试执行内置定时任务支持
通过 Jenkins 插件触发测试执行
多个接口测试一键合并执行
一键创建性能测试
测试报告测试执行后自动生成动态实时测试报告
测试报告导出
性能测试测试脚本通过邮件、IM 工具等通知执行结果
性能测试测试脚本 完全兼容 JMeter 脚本
通过浏览器插件快速录制测试脚本
多协议支持
测试执行内置定时任务支持
通过 Jenkins 插件触发测试执行
测试报告 测试执行后自动生成测试报告查看测试日志详情
系统管理租户管理系统管理用户租户管理 支持多级租户体系
支持多种租户角色
测试资源管理LDAP 认证对接
测试资源管理 性能测试资源池管理
消息通知配置IM 工具通知(如企业微信、钉钉)
邮件通知配置
集成与扩展集成与扩展 完善的 API 列表
支持对接 Jenkins 等持续集成工具
支持对接 Jira/TAPD 等缺陷管理工具
diff --git a/backend/src/main/java/io/metersphere/api/controller/ApiDatabaseController.java b/backend/src/main/java/io/metersphere/api/controller/ApiDatabaseController.java new file mode 100644 index 0000000000..f3b316e703 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/controller/ApiDatabaseController.java @@ -0,0 +1,25 @@ +package io.metersphere.api.controller; + +import io.metersphere.api.dto.scenario.DatabaseConfig; +import io.metersphere.api.service.APIDatabaseService; +import io.metersphere.commons.constants.RoleConstants; +import org.apache.shiro.authz.annotation.Logical; +import org.apache.shiro.authz.annotation.RequiresRoles; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +@RestController +@RequestMapping(value = "/api/database") +@RequiresRoles(value = {RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER}, logical = Logical.OR) +public class ApiDatabaseController { + + @Resource + APIDatabaseService apiDatabaseService; + + @PostMapping("/validate") + public void validate(@RequestBody DatabaseConfig databaseConfig) { + apiDatabaseService.validate(databaseConfig); + } + +} diff --git a/backend/src/main/java/io/metersphere/api/dto/scenario/request/HttpRequest.java b/backend/src/main/java/io/metersphere/api/dto/scenario/request/HttpRequest.java index f43fcc41c7..74b497d07c 100644 --- a/backend/src/main/java/io/metersphere/api/dto/scenario/request/HttpRequest.java +++ b/backend/src/main/java/io/metersphere/api/dto/scenario/request/HttpRequest.java @@ -35,4 +35,6 @@ public class HttpRequest extends Request { private Long responseTimeout; @JSONField(ordinal = 16) private Boolean followRedirects; + @JSONField(ordinal = 17) + private Boolean doMultipartPost; } diff --git a/backend/src/main/java/io/metersphere/api/dto/scenario/request/SqlRequest.java b/backend/src/main/java/io/metersphere/api/dto/scenario/request/SqlRequest.java index d48c75ac3a..b116326049 100644 --- a/backend/src/main/java/io/metersphere/api/dto/scenario/request/SqlRequest.java +++ b/backend/src/main/java/io/metersphere/api/dto/scenario/request/SqlRequest.java @@ -2,9 +2,12 @@ package io.metersphere.api.dto.scenario.request; import com.alibaba.fastjson.annotation.JSONField; import com.alibaba.fastjson.annotation.JSONType; +import io.metersphere.api.dto.scenario.KeyValue; import lombok.Data; import lombok.EqualsAndHashCode; +import java.util.List; + @EqualsAndHashCode(callSuper = true) @Data @JSONType(typeName = RequestType.SQL) @@ -25,4 +28,6 @@ public class SqlRequest extends Request { private String resultVariable; @JSONField(ordinal = 14) private String variableNames; + @JSONField(ordinal = 15) + private List variables; } diff --git a/backend/src/main/java/io/metersphere/api/jmeter/APIBackendListenerClient.java b/backend/src/main/java/io/metersphere/api/jmeter/APIBackendListenerClient.java index 67173ef791..9f23b8c526 100644 --- a/backend/src/main/java/io/metersphere/api/jmeter/APIBackendListenerClient.java +++ b/backend/src/main/java/io/metersphere/api/jmeter/APIBackendListenerClient.java @@ -204,7 +204,7 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl ResponseAssertionResult responseAssertionResult = new ResponseAssertionResult(); responseAssertionResult.setMessage(assertionResult.getFailureMessage()); responseAssertionResult.setName(assertionResult.getName()); - responseAssertionResult.setPass(!assertionResult.isFailure()); + responseAssertionResult.setPass(!assertionResult.isFailure() && !assertionResult.isError()); return responseAssertionResult; } diff --git a/backend/src/main/java/io/metersphere/api/service/APIDatabaseService.java b/backend/src/main/java/io/metersphere/api/service/APIDatabaseService.java new file mode 100644 index 0000000000..939c27ff99 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/service/APIDatabaseService.java @@ -0,0 +1,23 @@ +package io.metersphere.api.service; + +import io.metersphere.api.dto.scenario.DatabaseConfig; +import io.metersphere.commons.exception.MSException; +import io.metersphere.commons.utils.LogUtil; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.sql.DriverManager; + +@Service +@Transactional(rollbackFor = Exception.class) +public class APIDatabaseService { + + public void validate(DatabaseConfig databaseConfig) { + try { + DriverManager.getConnection(databaseConfig.getDbUrl(), databaseConfig.getUsername(), databaseConfig.getPassword()); + } catch (Exception e) { + LogUtil.error(e.getMessage(), e); + MSException.throwException(e.getMessage()); + } + } +} diff --git a/backend/src/main/java/io/metersphere/base/domain/TestCaseFile.java b/backend/src/main/java/io/metersphere/base/domain/TestCaseFile.java new file mode 100644 index 0000000000..a0791bee07 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/domain/TestCaseFile.java @@ -0,0 +1,13 @@ +package io.metersphere.base.domain; + +import java.io.Serializable; +import lombok.Data; + +@Data +public class TestCaseFile implements Serializable { + private String caseId; + + private String fileId; + + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/domain/TestCaseFileExample.java b/backend/src/main/java/io/metersphere/base/domain/TestCaseFileExample.java new file mode 100644 index 0000000000..ed55a15442 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/domain/TestCaseFileExample.java @@ -0,0 +1,340 @@ +package io.metersphere.base.domain; + +import java.util.ArrayList; +import java.util.List; + +public class TestCaseFileExample { + protected String orderByClause; + + protected boolean distinct; + + protected List oredCriteria; + + public TestCaseFileExample() { + oredCriteria = new ArrayList(); + } + + public void setOrderByClause(String orderByClause) { + this.orderByClause = orderByClause; + } + + public String getOrderByClause() { + return orderByClause; + } + + public void setDistinct(boolean distinct) { + this.distinct = distinct; + } + + public boolean isDistinct() { + return distinct; + } + + public List getOredCriteria() { + return oredCriteria; + } + + public void or(Criteria criteria) { + oredCriteria.add(criteria); + } + + public Criteria or() { + Criteria criteria = createCriteriaInternal(); + oredCriteria.add(criteria); + return criteria; + } + + public Criteria createCriteria() { + Criteria criteria = createCriteriaInternal(); + if (oredCriteria.size() == 0) { + oredCriteria.add(criteria); + } + return criteria; + } + + protected Criteria createCriteriaInternal() { + Criteria criteria = new Criteria(); + return criteria; + } + + public void clear() { + oredCriteria.clear(); + orderByClause = null; + distinct = false; + } + + protected abstract static class GeneratedCriteria { + protected List criteria; + + protected GeneratedCriteria() { + super(); + criteria = new ArrayList(); + } + + public boolean isValid() { + return criteria.size() > 0; + } + + public List getAllCriteria() { + return criteria; + } + + public List getCriteria() { + return criteria; + } + + protected void addCriterion(String condition) { + if (condition == null) { + throw new RuntimeException("Value for condition cannot be null"); + } + criteria.add(new Criterion(condition)); + } + + protected void addCriterion(String condition, Object value, String property) { + if (value == null) { + throw new RuntimeException("Value for " + property + " cannot be null"); + } + criteria.add(new Criterion(condition, value)); + } + + protected void addCriterion(String condition, Object value1, Object value2, String property) { + if (value1 == null || value2 == null) { + throw new RuntimeException("Between values for " + property + " cannot be null"); + } + criteria.add(new Criterion(condition, value1, value2)); + } + + public Criteria andCaseIdIsNull() { + addCriterion("case_id is null"); + return (Criteria) this; + } + + public Criteria andCaseIdIsNotNull() { + addCriterion("case_id is not null"); + return (Criteria) this; + } + + public Criteria andCaseIdEqualTo(String value) { + addCriterion("case_id =", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdNotEqualTo(String value) { + addCriterion("case_id <>", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdGreaterThan(String value) { + addCriterion("case_id >", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdGreaterThanOrEqualTo(String value) { + addCriterion("case_id >=", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdLessThan(String value) { + addCriterion("case_id <", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdLessThanOrEqualTo(String value) { + addCriterion("case_id <=", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdLike(String value) { + addCriterion("case_id like", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdNotLike(String value) { + addCriterion("case_id not like", value, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdIn(List values) { + addCriterion("case_id in", values, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdNotIn(List values) { + addCriterion("case_id not in", values, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdBetween(String value1, String value2) { + addCriterion("case_id between", value1, value2, "caseId"); + return (Criteria) this; + } + + public Criteria andCaseIdNotBetween(String value1, String value2) { + addCriterion("case_id not between", value1, value2, "caseId"); + return (Criteria) this; + } + + public Criteria andFileIdIsNull() { + addCriterion("file_id is null"); + return (Criteria) this; + } + + public Criteria andFileIdIsNotNull() { + addCriterion("file_id is not null"); + return (Criteria) this; + } + + public Criteria andFileIdEqualTo(String value) { + addCriterion("file_id =", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdNotEqualTo(String value) { + addCriterion("file_id <>", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdGreaterThan(String value) { + addCriterion("file_id >", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdGreaterThanOrEqualTo(String value) { + addCriterion("file_id >=", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdLessThan(String value) { + addCriterion("file_id <", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdLessThanOrEqualTo(String value) { + addCriterion("file_id <=", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdLike(String value) { + addCriterion("file_id like", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdNotLike(String value) { + addCriterion("file_id not like", value, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdIn(List values) { + addCriterion("file_id in", values, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdNotIn(List values) { + addCriterion("file_id not in", values, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdBetween(String value1, String value2) { + addCriterion("file_id between", value1, value2, "fileId"); + return (Criteria) this; + } + + public Criteria andFileIdNotBetween(String value1, String value2) { + addCriterion("file_id not between", value1, value2, "fileId"); + return (Criteria) this; + } + } + + public static class Criteria extends GeneratedCriteria { + + protected Criteria() { + super(); + } + } + + public static class Criterion { + private String condition; + + private Object value; + + private Object secondValue; + + private boolean noValue; + + private boolean singleValue; + + private boolean betweenValue; + + private boolean listValue; + + private String typeHandler; + + public String getCondition() { + return condition; + } + + public Object getValue() { + return value; + } + + public Object getSecondValue() { + return secondValue; + } + + public boolean isNoValue() { + return noValue; + } + + public boolean isSingleValue() { + return singleValue; + } + + public boolean isBetweenValue() { + return betweenValue; + } + + public boolean isListValue() { + return listValue; + } + + public String getTypeHandler() { + return typeHandler; + } + + protected Criterion(String condition) { + super(); + this.condition = condition; + this.typeHandler = null; + this.noValue = true; + } + + protected Criterion(String condition, Object value, String typeHandler) { + super(); + this.condition = condition; + this.value = value; + this.typeHandler = typeHandler; + if (value instanceof List) { + this.listValue = true; + } else { + this.singleValue = true; + } + } + + protected Criterion(String condition, Object value) { + this(condition, value, null); + } + + protected Criterion(String condition, Object value, Object secondValue, String typeHandler) { + super(); + this.condition = condition; + this.value = value; + this.secondValue = secondValue; + this.typeHandler = typeHandler; + this.betweenValue = true; + } + + protected Criterion(String condition, Object value, Object secondValue) { + this(condition, value, secondValue, null); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/TestCaseFileMapper.java b/backend/src/main/java/io/metersphere/base/mapper/TestCaseFileMapper.java new file mode 100644 index 0000000000..b7eb9c0df4 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/TestCaseFileMapper.java @@ -0,0 +1,22 @@ +package io.metersphere.base.mapper; + +import io.metersphere.base.domain.TestCaseFile; +import io.metersphere.base.domain.TestCaseFileExample; +import java.util.List; +import org.apache.ibatis.annotations.Param; + +public interface TestCaseFileMapper { + long countByExample(TestCaseFileExample example); + + int deleteByExample(TestCaseFileExample example); + + int insert(TestCaseFile record); + + int insertSelective(TestCaseFile record); + + List selectByExample(TestCaseFileExample example); + + int updateByExampleSelective(@Param("record") TestCaseFile record, @Param("example") TestCaseFileExample example); + + int updateByExample(@Param("record") TestCaseFile record, @Param("example") TestCaseFileExample example); +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/TestCaseFileMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/TestCaseFileMapper.xml new file mode 100644 index 0000000000..70ef1820a9 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/TestCaseFileMapper.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + and ${criterion.condition} + + + and ${criterion.condition} #{criterion.value} + + + and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} + + + and ${criterion.condition} + + #{listItem} + + + + + + + + + + + + + + + + + + and ${criterion.condition} + + + and ${criterion.condition} #{criterion.value} + + + and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} + + + and ${criterion.condition} + + #{listItem} + + + + + + + + + + + case_id, file_id + + + + delete from test_case_file + + + + + + insert into test_case_file (case_id, file_id) + values (#{caseId,jdbcType=VARCHAR}, #{fileId,jdbcType=VARCHAR}) + + + insert into test_case_file + + + case_id, + + + file_id, + + + + + #{caseId,jdbcType=VARCHAR}, + + + #{fileId,jdbcType=VARCHAR}, + + + + + + update test_case_file + + + case_id = #{record.caseId,jdbcType=VARCHAR}, + + + file_id = #{record.fileId,jdbcType=VARCHAR}, + + + + + + + + update test_case_file + set case_id = #{record.caseId,jdbcType=VARCHAR}, + file_id = #{record.fileId,jdbcType=VARCHAR} + + + + + \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/commons/constants/FileType.java b/backend/src/main/java/io/metersphere/commons/constants/FileType.java index db8236c2ed..2172e2e78e 100644 --- a/backend/src/main/java/io/metersphere/commons/constants/FileType.java +++ b/backend/src/main/java/io/metersphere/commons/constants/FileType.java @@ -1,7 +1,7 @@ package io.metersphere.commons.constants; public enum FileType { - JMX(".jmx"), CSV(".csv"), JSON(".json"); + JMX(".jmx"), CSV(".csv"), JSON(".json"), PDF(".pdf"), JPG(".jpg"), PNG(".png"); // 保存后缀 private String suffix; diff --git a/backend/src/main/java/io/metersphere/controller/TestController.java b/backend/src/main/java/io/metersphere/controller/TestController.java index c3b7c033af..9783d98572 100644 --- a/backend/src/main/java/io/metersphere/controller/TestController.java +++ b/backend/src/main/java/io/metersphere/controller/TestController.java @@ -25,6 +25,15 @@ public class TestController { return jsonObject; } + @PostMapping(value = "/multipart", consumes = {"multipart/form-data"}) + public Object testMultipart(@RequestPart(value = "id") String id, @RequestPart(value = "user") User user, @RequestParam(value = "name") String name) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("id", id); + jsonObject.put("user", user.getName()); + jsonObject.put("name", name); + return jsonObject; + } + @GetMapping(value = "/{str}") public Object getString(@PathVariable String str) throws InterruptedException { if (StringUtils.equals("error", str)) { diff --git a/backend/src/main/java/io/metersphere/service/FileService.java b/backend/src/main/java/io/metersphere/service/FileService.java index b82c2f784d..ff2801320e 100644 --- a/backend/src/main/java/io/metersphere/service/FileService.java +++ b/backend/src/main/java/io/metersphere/service/FileService.java @@ -4,6 +4,7 @@ import io.metersphere.base.domain.*; import io.metersphere.base.mapper.FileContentMapper; import io.metersphere.base.mapper.FileMetadataMapper; import io.metersphere.base.mapper.LoadTestFileMapper; +import io.metersphere.base.mapper.TestCaseFileMapper; import io.metersphere.commons.constants.FileType; import io.metersphere.commons.exception.MSException; import org.springframework.stereotype.Service; @@ -24,6 +25,8 @@ public class FileService { private LoadTestFileMapper loadTestFileMapper; @Resource private FileContentMapper fileContentMapper; + @Resource + private TestCaseFileMapper testCaseFileMapper; public byte[] loadFileAsBytes(String id) { FileContent fileContent = fileContentMapper.selectByPrimaryKey(id); @@ -66,6 +69,19 @@ public class FileService { loadTestFileMapper.deleteByExample(example3); } + public void deleteFileRelatedByIds(List ids) { + if (CollectionUtils.isEmpty(ids)) { + return; + } + FileMetadataExample example = new FileMetadataExample(); + example.createCriteria().andIdIn(ids); + fileMetadataMapper.deleteByExample(example); + + FileContentExample example2 = new FileContentExample(); + example2.createCriteria().andFileIdIn(ids); + fileContentMapper.deleteByExample(example2); + } + public FileMetadata saveFile(MultipartFile file) { final FileMetadata fileMetadata = new FileMetadata(); fileMetadata.setId(UUID.randomUUID().toString()); @@ -109,4 +125,19 @@ public class FileService { String type = filename.substring(s); return FileType.valueOf(type.toUpperCase()); } + + public List getFileMetadataByCaseId(String caseId) { + TestCaseFileExample testCaseFileExample = new TestCaseFileExample(); + testCaseFileExample.createCriteria().andCaseIdEqualTo(caseId); + final List testCaseFiles = testCaseFileMapper.selectByExample(testCaseFileExample); + + if (CollectionUtils.isEmpty(testCaseFiles)) { + return null; + } + + List fileIds = testCaseFiles.stream().map(TestCaseFile::getFileId).collect(Collectors.toList()); + FileMetadataExample example = new FileMetadataExample(); + example.createCriteria().andIdIn(fileIds); + return fileMetadataMapper.selectByExample(example); + } } \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/track/controller/TestCaseController.java b/backend/src/main/java/io/metersphere/track/controller/TestCaseController.java index f9770e3ab9..1d5d86db88 100644 --- a/backend/src/main/java/io/metersphere/track/controller/TestCaseController.java +++ b/backend/src/main/java/io/metersphere/track/controller/TestCaseController.java @@ -2,6 +2,7 @@ package io.metersphere.track.controller; import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; +import io.metersphere.base.domain.FileMetadata; import io.metersphere.base.domain.Project; import io.metersphere.base.domain.TestCase; import io.metersphere.base.domain.TestCaseWithBLOBs; @@ -11,12 +12,18 @@ import io.metersphere.commons.utils.Pager; import io.metersphere.commons.utils.SessionUtils; import io.metersphere.excel.domain.ExcelResponse; import io.metersphere.service.CheckOwnerService; +import io.metersphere.service.FileService; import io.metersphere.track.dto.TestCaseDTO; +import io.metersphere.track.request.testcase.EditTestCaseRequest; import io.metersphere.track.request.testcase.QueryTestCaseRequest; import io.metersphere.track.request.testcase.TestCaseBatchRequest; +import io.metersphere.track.request.testplan.FileOperationRequest; import io.metersphere.track.service.TestCaseService; import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.RequiresRoles; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -33,6 +40,8 @@ public class TestCaseController { TestCaseService testCaseService; @Resource private CheckOwnerService checkOwnerService; + @Resource + private FileService fileService; @PostMapping("/list/{goPage}/{pageSize}") public Pager> list(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody QueryTestCaseRequest request) { @@ -96,16 +105,16 @@ public class TestCaseController { return testCaseService.getProjectByTestCaseId(testCaseId); } - @PostMapping("/add") + @PostMapping(value = "/add", consumes = {"multipart/form-data"}) @RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR) - public void addTestCase(@RequestBody TestCaseWithBLOBs testCase) { - testCaseService.addTestCase(testCase); + public void addTestCase(@RequestPart("request") TestCaseWithBLOBs testCase, @RequestPart(value = "file") List files) { + testCaseService.save(testCase, files); } - @PostMapping("/edit") + @PostMapping(value = "/edit", consumes = {"multipart/form-data"}) @RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR) - public void editTestCase(@RequestBody TestCaseWithBLOBs testCase) { - testCaseService.editTestCase(testCase); + public void editTestCase(@RequestPart("request") EditTestCaseRequest request, @RequestPart(value = "file") List files) { + testCaseService.edit(request, files); } @PostMapping("/delete/{testCaseId}") @@ -152,4 +161,18 @@ public class TestCaseController { testCaseService.deleteTestCaseBath(request); } + @GetMapping("/file/metadata/{caseId}") + public List getFileMetadata(@PathVariable String caseId) { + return fileService.getFileMetadataByCaseId(caseId); + } + + @PostMapping("/file/download") + public ResponseEntity downloadJmx(@RequestBody FileOperationRequest fileOperationRequest) { + byte[] bytes = fileService.loadFileAsBytes(fileOperationRequest.getId()); + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType("application/octet-stream")) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileOperationRequest.getName() + "\"") + .body(bytes); + } + } diff --git a/backend/src/main/java/io/metersphere/track/request/testcase/EditTestCaseRequest.java b/backend/src/main/java/io/metersphere/track/request/testcase/EditTestCaseRequest.java new file mode 100644 index 0000000000..3cdc82df3a --- /dev/null +++ b/backend/src/main/java/io/metersphere/track/request/testcase/EditTestCaseRequest.java @@ -0,0 +1,14 @@ +package io.metersphere.track.request.testcase; + +import io.metersphere.base.domain.FileMetadata; +import io.metersphere.base.domain.TestCaseWithBLOBs; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class EditTestCaseRequest extends TestCaseWithBLOBs { + private List updatedFileList; +} diff --git a/backend/src/main/java/io/metersphere/track/service/TestCaseService.java b/backend/src/main/java/io/metersphere/track/service/TestCaseService.java index dd289df044..6310cfbd97 100644 --- a/backend/src/main/java/io/metersphere/track/service/TestCaseService.java +++ b/backend/src/main/java/io/metersphere/track/service/TestCaseService.java @@ -26,10 +26,13 @@ import io.metersphere.excel.listener.EasyExcelListener; import io.metersphere.excel.listener.TestCaseDataListener; import io.metersphere.excel.utils.EasyExcelExporter; import io.metersphere.i18n.Translator; +import io.metersphere.service.FileService; import io.metersphere.track.dto.TestCaseDTO; +import io.metersphere.track.request.testcase.EditTestCaseRequest; import io.metersphere.track.request.testcase.QueryTestCaseRequest; import io.metersphere.track.request.testcase.TestCaseBatchRequest; import io.metersphere.xmind.XmindCaseParser; +import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.session.ExecutorType; import org.apache.ibatis.session.SqlSession; @@ -83,8 +86,12 @@ public class TestCaseService { TestCaseReviewTestCaseMapper testCaseReviewTestCaseMapper; @Resource TestCaseCommentService testCaseCommentService; + @Resource + FileService fileService; + @Resource + TestCaseFileMapper testCaseFileMapper; - public void addTestCase(TestCaseWithBLOBs testCase) { + private TestCaseWithBLOBs addTestCase(TestCaseWithBLOBs testCase) { testCase.setName(testCase.getName()); checkTestCaseExist(testCase); testCase.setId(UUID.randomUUID().toString()); @@ -93,6 +100,7 @@ public class TestCaseService { testCase.setNum(getNextNum(testCase.getProjectId())); testCase.setReviewStatus(TestCaseReviewStatus.Prepare.name()); testCaseMapper.insert(testCase); + return testCase; } public List getTestCaseByNodeId(List nodeIds) { @@ -574,4 +582,55 @@ public class TestCaseService { return false; } + + public String save(TestCaseWithBLOBs testCase, List files) { + if (files == null) { + throw new IllegalArgumentException(Translator.get("file_cannot_be_null")); + } + final TestCaseWithBLOBs testCaseWithBLOBs = addTestCase(testCase); + files.forEach(file -> { + final FileMetadata fileMetadata = fileService.saveFile(file); + TestCaseFile testCaseFile = new TestCaseFile(); + testCaseFile.setCaseId(testCaseWithBLOBs.getId()); + testCaseFile.setFileId(fileMetadata.getId()); + testCaseFileMapper.insert(testCaseFile); + }); + return testCaseWithBLOBs.getId(); + } + + public String edit(EditTestCaseRequest request, List files) { + TestCaseWithBLOBs testCaseWithBLOBs = testCaseMapper.selectByPrimaryKey(request.getId()); + if (testCaseWithBLOBs == null) { + MSException.throwException(Translator.get("edit_load_test_not_found") + request.getId()); + } + + // 新选择了一个文件,删除原来的文件 + List updatedFiles = request.getUpdatedFileList(); + List originFiles = fileService.getFileMetadataByCaseId(request.getId()); + List updatedFileIds = updatedFiles.stream().map(FileMetadata::getId).collect(Collectors.toList()); + List originFileIds = originFiles.stream().map(FileMetadata::getId).collect(Collectors.toList()); + // 相减 + List deleteFileIds = ListUtils.subtract(originFileIds, updatedFileIds); + fileService.deleteFileRelatedByIds(deleteFileIds); + + if (!CollectionUtils.isEmpty(deleteFileIds)) { + TestCaseFileExample testCaseFileExample = new TestCaseFileExample(); + testCaseFileExample.createCriteria().andFileIdIn(deleteFileIds); + testCaseFileMapper.deleteByExample(testCaseFileExample); + } + + + if (files != null) { + files.forEach(file -> { + final FileMetadata fileMetadata = fileService.saveFile(file); + TestCaseFile testCaseFile = new TestCaseFile(); + testCaseFile.setFileId(fileMetadata.getId()); + testCaseFile.setCaseId(request.getId()); + testCaseFileMapper.insert(testCaseFile); + }); + } + + editTestCase(request); + return request.getId(); + } } diff --git a/backend/src/main/resources/db/migration/V30__test_case_file.sql b/backend/src/main/resources/db/migration/V30__test_case_file.sql new file mode 100644 index 0000000000..f4b2c4a3d4 --- /dev/null +++ b/backend/src/main/resources/db/migration/V30__test_case_file.sql @@ -0,0 +1,7 @@ +create table if not exists test_case_file +( + case_id varchar(64) null, + file_id varchar(64) null, + constraint test_case_file_unique_key + unique (case_id, file_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/frontend/src/business/components/api/test/components/ApiScenarioVariables.vue b/frontend/src/business/components/api/test/components/ApiScenarioVariables.vue index 236b3b2555..3500c4145f 100644 --- a/frontend/src/business/components/api/test/components/ApiScenarioVariables.vue +++ b/frontend/src/business/components/api/test/components/ApiScenarioVariables.vue @@ -11,7 +11,7 @@ - @@ -45,6 +45,10 @@ type: Boolean, default: true }, + showCopy: { + type: Boolean, + default: true + }, }, data() { return { diff --git a/frontend/src/business/components/api/test/components/ApiVariableInput.vue b/frontend/src/business/components/api/test/components/ApiVariableInput.vue index 7d95810ed6..0cbd17338f 100644 --- a/frontend/src/business/components/api/test/components/ApiVariableInput.vue +++ b/frontend/src/business/components/api/test/components/ApiVariableInput.vue @@ -2,8 +2,8 @@
-
{{variable}}
- +
{{variable}}
+
@@ -25,6 +25,10 @@ type: Boolean, default: true }, + showCopy: { + type: Boolean, + default: true + }, }, data() { diff --git a/frontend/src/business/components/api/test/components/body/ApiBodyFileUpload.vue b/frontend/src/business/components/api/test/components/body/ApiBodyFileUpload.vue index 9d1a03730c..909b314ea9 100644 --- a/frontend/src/business/components/api/test/components/body/ApiBodyFileUpload.vue +++ b/frontend/src/business/components/api/test/components/body/ApiBodyFileUpload.vue @@ -46,7 +46,7 @@ handleRemove(file) { this.$refs.upload.handleRemove(file); for (let i = 0; i < this.parameter.files.length; i++) { - if (file.name === this.parameter.files[i].name) { + if (file.file.name === this.parameter.files[i].file.name) { this.parameter.files.splice(i, 1); this.$refs.upload.handleRemove(file); break; diff --git a/frontend/src/business/components/api/test/components/environment/EnvironmentCommonConfig.vue b/frontend/src/business/components/api/test/components/environment/EnvironmentCommonConfig.vue index 2dd5c442c4..3803db4ea2 100644 --- a/frontend/src/business/components/api/test/components/environment/EnvironmentCommonConfig.vue +++ b/frontend/src/business/components/api/test/components/environment/EnvironmentCommonConfig.vue @@ -3,7 +3,7 @@ {{$t('api_test.environment.globalVariable')}} - + diff --git a/frontend/src/business/components/api/test/components/request/ApiHttpRequestForm.vue b/frontend/src/business/components/api/test/components/request/ApiHttpRequestForm.vue index 4999ba3842..3e0e43b21a 100644 --- a/frontend/src/business/components/api/test/components/request/ApiHttpRequestForm.vue +++ b/frontend/src/business/components/api/test/components/request/ApiHttpRequestForm.vue @@ -39,6 +39,7 @@ :active-text="$t('api_test.request.refer_to_environment')" @change="useEnvironmentChange"> + {{$t('api_test.request.do_multipart_post')}} diff --git a/frontend/src/business/components/api/test/components/request/ApiRequestConfig.vue b/frontend/src/business/components/api/test/components/request/ApiRequestConfig.vue index f192c009cc..76d44cf4d2 100644 --- a/frontend/src/business/components/api/test/components/request/ApiRequestConfig.vue +++ b/frontend/src/business/components/api/test/components/request/ApiRequestConfig.vue @@ -9,7 +9,7 @@
{{ request.showType() }}
-
+
{{ request.showMethod() }}
@@ -85,7 +85,13 @@ export default { selected: 0, visible: false, types: RequestFactory.TYPES, - type: "" + type: "", + methodColorMap: new Map([ + ['GET', "#61AFFE"], ['POST', '#49CC90'], ['PUT', '#fca130'], + ['PATCH', '#E2EE11'], ['DELETE', '#f93e3d'], ['OPTIONS', '#0EF5DA'], + ['HEAD', '#8E58E7'], ['CONNECT', '#90AFAE'], + ['DUBBO', '#C36EEF'],['SQL', '#0AEAD4'],['TCP', '#0A52DF'], + ]) } }, @@ -156,6 +162,11 @@ export default { break; } }, + getColor(enable, method) { + if (enable) { + return this.methodColorMap.get(method); + } + }, select(request) { if (!request) { return; diff --git a/frontend/src/business/components/api/test/components/request/ApiSqlRequestForm.vue b/frontend/src/business/components/api/test/components/request/ApiSqlRequestForm.vue index 92212cfffa..7282fe9d53 100644 --- a/frontend/src/business/components/api/test/components/request/ApiSqlRequestForm.vue +++ b/frontend/src/business/components/api/test/components/request/ApiSqlRequestForm.vue @@ -41,6 +41,10 @@ {{$t('api_test.request.debug')}} + + +
@@ -74,10 +78,12 @@ import MsDubboConsumerService from "@/business/components/api/test/components/request/dubbo/ConsumerAndService"; import MsJsr233Processor from "../processor/Jsr233Processor"; import MsCodeEdit from "../../../../common/components/MsCodeEdit"; + import MsApiScenarioVariables from "../ApiScenarioVariables"; export default { name: "MsApiSqlRequestForm", components: { + MsApiScenarioVariables, MsCodeEdit, MsJsr233Processor, MsDubboConsumerService, @@ -96,7 +102,7 @@ data() { return { - activeName: "sql", + activeName: "variables", databaseConfigsOptions: [], rules: { name: [ diff --git a/frontend/src/business/components/api/test/components/request/database/DatabaseConfig.vue b/frontend/src/business/components/api/test/components/request/database/DatabaseConfig.vue index 1dc77469b2..7542ed13df 100644 --- a/frontend/src/business/components/api/test/components/request/database/DatabaseConfig.vue +++ b/frontend/src/business/components/api/test/components/request/database/DatabaseConfig.vue @@ -27,6 +27,11 @@ currentConfig: new DatabaseConfig() } }, + watch: { + configs() { + this.currentConfig = new DatabaseConfig(); + } + }, methods: { saveConfig(config) { for (let item of this.configs) { diff --git a/frontend/src/business/components/api/test/components/request/database/DatabaseFrom.vue b/frontend/src/business/components/api/test/components/request/database/DatabaseFrom.vue index 89919ab28b..93c14e1258 100644 --- a/frontend/src/business/components/api/test/components/request/database/DatabaseFrom.vue +++ b/frontend/src/business/components/api/test/components/request/database/DatabaseFrom.vue @@ -1,5 +1,5 @@