diff --git a/backend/src/main/java/io/metersphere/api/controller/ApiModuleController.java b/backend/src/main/java/io/metersphere/api/controller/ApiModuleController.java new file mode 100644 index 0000000000..cf07d0078b --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/controller/ApiModuleController.java @@ -0,0 +1,56 @@ +package io.metersphere.api.controller; + +import io.metersphere.api.dto.delimit.ApiModuleDTO; +import io.metersphere.api.dto.delimit.DragModuleRequest; +import io.metersphere.api.service.ApiModuleService; +import io.metersphere.base.domain.ApiModule; +import io.metersphere.commons.constants.RoleConstants; +import io.metersphere.service.CheckOwnerService; +import org.apache.shiro.authz.annotation.Logical; +import org.apache.shiro.authz.annotation.RequiresRoles; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; + +@RequestMapping("/api/module") +@RestController +@RequiresRoles(value = {RoleConstants.ADMIN, RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER, RoleConstants.TEST_VIEWER, RoleConstants.ORG_ADMIN}, logical = Logical.OR) +public class ApiModuleController { + + @Resource + ApiModuleService apiModuleService; + @Resource + private CheckOwnerService checkOwnerService; + + @GetMapping("/list/{projectId}") + public List getNodeByProjectId(@PathVariable String projectId,@PathVariable String protocol) { + checkOwnerService.checkProjectOwner(projectId); + return apiModuleService.getNodeTreeByProjectId(projectId); + } + + @PostMapping("/add") + @RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR) + public String addNode(@RequestBody ApiModule node) { + return apiModuleService.addNode(node); + } + + @PostMapping("/edit") + @RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR) + public int editNode(@RequestBody DragModuleRequest node) { + return apiModuleService.editNode(node); + } + + @PostMapping("/delete") + @RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR) + public int deleteNode(@RequestBody List nodeIds) { + //nodeIds 包含删除节点ID及其所有子节点ID + return apiModuleService.deleteNode(nodeIds); + } + + @PostMapping("/drag") + @RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR) + public void dragNode(@RequestBody DragModuleRequest node) { + apiModuleService.dragNode(node); + } +} diff --git a/backend/src/main/java/io/metersphere/api/dto/delimit/ApiDTO.java b/backend/src/main/java/io/metersphere/api/dto/delimit/ApiDTO.java new file mode 100644 index 0000000000..05d8a34009 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/delimit/ApiDTO.java @@ -0,0 +1,14 @@ +package io.metersphere.api.dto.delimit; + +import io.metersphere.base.domain.TestCaseWithBLOBs; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ApiDTO extends TestCaseWithBLOBs { + + private String maintainerName; + private String apiName; + private String performName; +} diff --git a/backend/src/main/java/io/metersphere/api/dto/delimit/ApiModuleDTO.java b/backend/src/main/java/io/metersphere/api/dto/delimit/ApiModuleDTO.java new file mode 100644 index 0000000000..2c7b68f6a7 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/delimit/ApiModuleDTO.java @@ -0,0 +1,16 @@ +package io.metersphere.api.dto.delimit; + +import io.metersphere.base.domain.ApiModule; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class ApiModuleDTO extends ApiModule { + + private String label; + private List children; + +} diff --git a/backend/src/main/java/io/metersphere/api/dto/delimit/DragModuleRequest.java b/backend/src/main/java/io/metersphere/api/dto/delimit/DragModuleRequest.java new file mode 100644 index 0000000000..44445ca5df --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/delimit/DragModuleRequest.java @@ -0,0 +1,15 @@ +package io.metersphere.api.dto.delimit; + +import io.metersphere.base.domain.ApiModule; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class DragModuleRequest extends ApiModule { + + List nodeIds; + ApiModuleDTO nodeTree; +} diff --git a/backend/src/main/java/io/metersphere/api/service/ApiModuleService.java b/backend/src/main/java/io/metersphere/api/service/ApiModuleService.java new file mode 100644 index 0000000000..f1e5550408 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/service/ApiModuleService.java @@ -0,0 +1,240 @@ +package io.metersphere.api.service; + + +import io.metersphere.api.dto.delimit.ApiModuleDTO; +import io.metersphere.api.dto.delimit.DragModuleRequest; +import io.metersphere.base.domain.ApiModule; +import io.metersphere.base.domain.ApiModuleExample; +import io.metersphere.base.domain.TestCaseExample; +import io.metersphere.base.mapper.ApiModuleMapper; +import io.metersphere.base.mapper.TestCaseMapper; +import io.metersphere.base.mapper.ext.ExtTestCaseMapper; +import io.metersphere.commons.constants.TestCaseConstants; +import io.metersphere.commons.exception.MSException; +import io.metersphere.commons.utils.BeanUtils; +import io.metersphere.i18n.Translator; +import io.metersphere.track.dto.TestCaseDTO; +import io.metersphere.track.request.testcase.QueryTestCaseRequest; +import org.apache.commons.lang3.StringUtils; +import org.apache.ibatis.session.ExecutorType; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Transactional(rollbackFor = Exception.class) +public class ApiModuleService { + + @Resource + ApiModuleMapper apiModuleMapper; + @Resource + ExtTestCaseMapper extTestCaseMapper; + @Resource + SqlSessionFactory sqlSessionFactory; + + public List getNodeTreeByProjectId(String projectId) { + ApiModuleExample testCaseNodeExample = new ApiModuleExample(); + testCaseNodeExample.createCriteria().andProjectIdEqualTo(projectId); + testCaseNodeExample.setOrderByClause("create_time asc"); + List nodes = apiModuleMapper.selectByExample(testCaseNodeExample); + return getNodeTrees(nodes); + } + + public List getNodeTrees(List nodes) { + + List nodeTreeList = new ArrayList<>(); + Map> nodeLevelMap = new HashMap<>(); + nodes.forEach(node -> { + Integer level = node.getLevel(); + if (nodeLevelMap.containsKey(level)) { + nodeLevelMap.get(level).add(node); + } else { + List apiModules = new ArrayList<>(); + apiModules.add(node); + nodeLevelMap.put(node.getLevel(), apiModules); + } + }); + List rootNodes = Optional.ofNullable(nodeLevelMap.get(1)).orElse(new ArrayList<>()); + rootNodes.forEach(rootNode -> nodeTreeList.add(buildNodeTree(nodeLevelMap, rootNode))); + return nodeTreeList; + } + + /** + * 递归构建节点树 + * + * @param nodeLevelMap + * @param rootNode + * @return + */ + private ApiModuleDTO buildNodeTree(Map> nodeLevelMap, ApiModule rootNode) { + + ApiModuleDTO nodeTree = new ApiModuleDTO(); + BeanUtils.copyBean(nodeTree, rootNode); + nodeTree.setLabel(rootNode.getName()); + + List lowerNodes = nodeLevelMap.get(rootNode.getLevel() + 1); + if (lowerNodes == null) { + return nodeTree; + } + List children = Optional.ofNullable(nodeTree.getChildren()).orElse(new ArrayList<>()); + lowerNodes.forEach(node -> { + if (node.getParentId() != null && node.getParentId().equals(rootNode.getId())) { + children.add(buildNodeTree(nodeLevelMap, node)); + nodeTree.setChildren(children); + } + }); + + return nodeTree; + } + + + public String addNode(ApiModule node) { + validateNode(node); + node.setCreateTime(System.currentTimeMillis()); + node.setUpdateTime(System.currentTimeMillis()); + node.setId(UUID.randomUUID().toString()); + apiModuleMapper.insertSelective(node); + return node.getId(); + } + + private void validateNode(ApiModule node) { + if (node.getLevel() > TestCaseConstants.MAX_NODE_DEPTH) { + throw new RuntimeException(Translator.get("test_case_node_level_tip") + + TestCaseConstants.MAX_NODE_DEPTH + Translator.get("test_case_node_level")); + } + checkTestCaseNodeExist(node); + } + + private void checkTestCaseNodeExist(ApiModule node) { + if (node.getName() != null) { + ApiModuleExample example = new ApiModuleExample(); + ApiModuleExample.Criteria criteria = example.createCriteria(); + criteria.andNameEqualTo(node.getName()) + .andProjectIdEqualTo(node.getProjectId()); + if (StringUtils.isNotBlank(node.getParentId())) { + criteria.andParentIdEqualTo(node.getParentId()); + } else { + criteria.andParentIdIsNull(); + } + if (StringUtils.isNotBlank(node.getId())) { + criteria.andIdNotEqualTo(node.getId()); + } + if (apiModuleMapper.selectByExample(example).size() > 0) { + MSException.throwException(Translator.get("test_case_module_already_exists")); + } + } + } + + private List QueryTestCaseByNodeIds(List nodeIds) { + QueryTestCaseRequest testCaseRequest = new QueryTestCaseRequest(); + testCaseRequest.setNodeIds(nodeIds); + return extTestCaseMapper.list(testCaseRequest); + } + + public int editNode(DragModuleRequest request) { + request.setUpdateTime(System.currentTimeMillis()); + checkTestCaseNodeExist(request); + List apiModule = QueryTestCaseByNodeIds(request.getNodeIds()); + + apiModule.forEach(testCase -> { + StringBuilder path = new StringBuilder(testCase.getNodePath()); + List pathLists = Arrays.asList(path.toString().split("/")); + pathLists.set(request.getLevel(), request.getName()); + path.delete(0, path.length()); + for (int i = 1; i < pathLists.size(); i++) { + path = path.append("/").append(pathLists.get(i)); + } + testCase.setNodePath(path.toString()); + }); + + batchUpdateTestCase(apiModule); + + return apiModuleMapper.updateByPrimaryKeySelective(request); + } + + public int deleteNode(List nodeIds) { + TestCaseExample testCaseExample = new TestCaseExample(); + testCaseExample.createCriteria().andNodeIdIn(nodeIds); + + ApiModuleExample testCaseNodeExample = new ApiModuleExample(); + testCaseNodeExample.createCriteria().andIdIn(nodeIds); + return apiModuleMapper.deleteByExample(testCaseNodeExample); + } + + private void batchUpdateTestCase(List apiModule) { + SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); + TestCaseMapper testCaseMapper = sqlSession.getMapper(TestCaseMapper.class); + apiModule.forEach((value) -> { + testCaseMapper.updateByPrimaryKey(value); + }); + sqlSession.flushStatements(); + } + + public void dragNode(DragModuleRequest request) { + + checkTestCaseNodeExist(request); + + List nodeIds = request.getNodeIds(); + + List apiModule = QueryTestCaseByNodeIds(nodeIds); + + ApiModuleDTO nodeTree = request.getNodeTree(); + + List updateNodes = new ArrayList<>(); + + buildUpdateTestCase(nodeTree, apiModule, updateNodes, "/", "0", 1); + + updateNodes = updateNodes.stream() + .filter(item -> nodeIds.contains(item.getId())) + .collect(Collectors.toList()); + + batchUpdateTestCaseNode(updateNodes); + + batchUpdateTestCase(apiModule); + } + + private void buildUpdateTestCase(ApiModuleDTO rootNode, List apiModule, + List updateNodes, String rootPath, String pId, int level) { + + rootPath = rootPath + rootNode.getName(); + + if (level > 8) { + MSException.throwException(Translator.get("node_deep_limit")); + } + + ApiModule testCaseNode = new ApiModule(); + testCaseNode.setId(rootNode.getId()); + testCaseNode.setLevel(level); + testCaseNode.setParentId(pId); + updateNodes.add(testCaseNode); + + for (TestCaseDTO item : apiModule) { + if (StringUtils.equals(item.getNodeId(), rootNode.getId())) { + item.setNodePath(rootPath); + } + } + + List children = rootNode.getChildren(); + if (children != null && children.size() > 0) { + for (int i = 0; i < children.size(); i++) { + buildUpdateTestCase(children.get(i), apiModule, updateNodes, rootPath + '/', rootNode.getId(), level + 1); + } + } + } + + private void batchUpdateTestCaseNode(List updateNodes) { + SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); + ApiModuleMapper apiModuleMapper = sqlSession.getMapper(ApiModuleMapper.class); + updateNodes.forEach((value) -> { + apiModuleMapper.updateByPrimaryKeySelective(value); + }); + sqlSession.flushStatements(); + } + + +} diff --git a/backend/src/main/java/io/metersphere/base/domain/ApiModule.java b/backend/src/main/java/io/metersphere/base/domain/ApiModule.java new file mode 100644 index 0000000000..851c52abe2 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/domain/ApiModule.java @@ -0,0 +1,26 @@ +package io.metersphere.base.domain; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class ApiModule implements Serializable { + private String id; + + private String projectId; + + private String name; + + private String parentId; + + private Integer level; + + private String protocol; + + private Long createTime; + + private Long updateTime; + + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/domain/ApiModuleExample.java b/backend/src/main/java/io/metersphere/base/domain/ApiModuleExample.java new file mode 100644 index 0000000000..198251d7f4 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/domain/ApiModuleExample.java @@ -0,0 +1,660 @@ +package io.metersphere.base.domain; + +import java.util.ArrayList; +import java.util.List; + +public class ApiModuleExample { + protected String orderByClause; + + protected boolean distinct; + + protected List oredCriteria; + + public ApiModuleExample() { + 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 andIdIsNull() { + addCriterion("id is null"); + return (Criteria) this; + } + + public Criteria andIdIsNotNull() { + addCriterion("id is not null"); + return (Criteria) this; + } + + public Criteria andIdEqualTo(String value) { + addCriterion("id =", value, "id"); + return (Criteria) this; + } + + public Criteria andIdNotEqualTo(String value) { + addCriterion("id <>", value, "id"); + return (Criteria) this; + } + + public Criteria andIdGreaterThan(String value) { + addCriterion("id >", value, "id"); + return (Criteria) this; + } + + public Criteria andIdGreaterThanOrEqualTo(String value) { + addCriterion("id >=", value, "id"); + return (Criteria) this; + } + + public Criteria andIdLessThan(String value) { + addCriterion("id <", value, "id"); + return (Criteria) this; + } + + public Criteria andIdLessThanOrEqualTo(String value) { + addCriterion("id <=", value, "id"); + return (Criteria) this; + } + + public Criteria andIdLike(String value) { + addCriterion("id like", value, "id"); + return (Criteria) this; + } + + public Criteria andIdNotLike(String value) { + addCriterion("id not like", value, "id"); + return (Criteria) this; + } + + public Criteria andIdIn(List values) { + addCriterion("id in", values, "id"); + return (Criteria) this; + } + + public Criteria andIdNotIn(List values) { + addCriterion("id not in", values, "id"); + return (Criteria) this; + } + + public Criteria andIdBetween(String value1, String value2) { + addCriterion("id between", value1, value2, "id"); + return (Criteria) this; + } + + public Criteria andIdNotBetween(String value1, String value2) { + addCriterion("id not between", value1, value2, "id"); + return (Criteria) this; + } + + public Criteria andProjectIdIsNull() { + addCriterion("project_id is null"); + return (Criteria) this; + } + + public Criteria andProjectIdIsNotNull() { + addCriterion("project_id is not null"); + return (Criteria) this; + } + + public Criteria andProjectIdEqualTo(String value) { + addCriterion("project_id =", value, "projectId"); + return (Criteria) this; + } + + public Criteria andProjectIdNotEqualTo(String value) { + addCriterion("project_id <>", value, "projectId"); + return (Criteria) this; + } + + public Criteria andProjectIdGreaterThan(String value) { + addCriterion("project_id >", value, "projectId"); + return (Criteria) this; + } + + public Criteria andProjectIdGreaterThanOrEqualTo(String value) { + addCriterion("project_id >=", value, "projectId"); + return (Criteria) this; + } + + public Criteria andProjectIdLessThan(String value) { + addCriterion("project_id <", value, "projectId"); + return (Criteria) this; + } + + public Criteria andProjectIdLessThanOrEqualTo(String value) { + addCriterion("project_id <=", value, "projectId"); + return (Criteria) this; + } + + public Criteria andProjectIdLike(String value) { + addCriterion("project_id like", value, "projectId"); + return (Criteria) this; + } + + public Criteria andProjectIdNotLike(String value) { + addCriterion("project_id not like", value, "projectId"); + return (Criteria) this; + } + + public Criteria andProjectIdIn(List values) { + addCriterion("project_id in", values, "projectId"); + return (Criteria) this; + } + + public Criteria andProjectIdNotIn(List values) { + addCriterion("project_id not in", values, "projectId"); + return (Criteria) this; + } + + public Criteria andProjectIdBetween(String value1, String value2) { + addCriterion("project_id between", value1, value2, "projectId"); + return (Criteria) this; + } + + public Criteria andProjectIdNotBetween(String value1, String value2) { + addCriterion("project_id not between", value1, value2, "projectId"); + return (Criteria) this; + } + + public Criteria andNameIsNull() { + addCriterion("name is null"); + return (Criteria) this; + } + + public Criteria andNameIsNotNull() { + addCriterion("name is not null"); + return (Criteria) this; + } + + public Criteria andNameEqualTo(String value) { + addCriterion("name =", value, "name"); + return (Criteria) this; + } + + public Criteria andNameNotEqualTo(String value) { + addCriterion("name <>", value, "name"); + return (Criteria) this; + } + + public Criteria andNameGreaterThan(String value) { + addCriterion("name >", value, "name"); + return (Criteria) this; + } + + public Criteria andNameGreaterThanOrEqualTo(String value) { + addCriterion("name >=", value, "name"); + return (Criteria) this; + } + + public Criteria andNameLessThan(String value) { + addCriterion("name <", value, "name"); + return (Criteria) this; + } + + public Criteria andNameLessThanOrEqualTo(String value) { + addCriterion("name <=", value, "name"); + return (Criteria) this; + } + + public Criteria andNameLike(String value) { + addCriterion("name like", value, "name"); + return (Criteria) this; + } + + public Criteria andNameNotLike(String value) { + addCriterion("name not like", value, "name"); + return (Criteria) this; + } + + public Criteria andNameIn(List values) { + addCriterion("name in", values, "name"); + return (Criteria) this; + } + + public Criteria andNameNotIn(List values) { + addCriterion("name not in", values, "name"); + return (Criteria) this; + } + + public Criteria andNameBetween(String value1, String value2) { + addCriterion("name between", value1, value2, "name"); + return (Criteria) this; + } + + public Criteria andNameNotBetween(String value1, String value2) { + addCriterion("name not between", value1, value2, "name"); + return (Criteria) this; + } + + public Criteria andParentIdIsNull() { + addCriterion("parent_id is null"); + return (Criteria) this; + } + + public Criteria andParentIdIsNotNull() { + addCriterion("parent_id is not null"); + return (Criteria) this; + } + + public Criteria andParentIdEqualTo(String value) { + addCriterion("parent_id =", value, "parentId"); + return (Criteria) this; + } + + public Criteria andParentIdNotEqualTo(String value) { + addCriterion("parent_id <>", value, "parentId"); + return (Criteria) this; + } + + public Criteria andParentIdGreaterThan(String value) { + addCriterion("parent_id >", value, "parentId"); + return (Criteria) this; + } + + public Criteria andParentIdGreaterThanOrEqualTo(String value) { + addCriterion("parent_id >=", value, "parentId"); + return (Criteria) this; + } + + public Criteria andParentIdLessThan(String value) { + addCriterion("parent_id <", value, "parentId"); + return (Criteria) this; + } + + public Criteria andParentIdLessThanOrEqualTo(String value) { + addCriterion("parent_id <=", value, "parentId"); + return (Criteria) this; + } + + public Criteria andParentIdLike(String value) { + addCriterion("parent_id like", value, "parentId"); + return (Criteria) this; + } + + public Criteria andParentIdNotLike(String value) { + addCriterion("parent_id not like", value, "parentId"); + return (Criteria) this; + } + + public Criteria andParentIdIn(List values) { + addCriterion("parent_id in", values, "parentId"); + return (Criteria) this; + } + + public Criteria andParentIdNotIn(List values) { + addCriterion("parent_id not in", values, "parentId"); + return (Criteria) this; + } + + public Criteria andParentIdBetween(String value1, String value2) { + addCriterion("parent_id between", value1, value2, "parentId"); + return (Criteria) this; + } + + public Criteria andParentIdNotBetween(String value1, String value2) { + addCriterion("parent_id not between", value1, value2, "parentId"); + return (Criteria) this; + } + + public Criteria andLevelIsNull() { + addCriterion("level is null"); + return (Criteria) this; + } + + public Criteria andLevelIsNotNull() { + addCriterion("level is not null"); + return (Criteria) this; + } + + public Criteria andLevelEqualTo(Integer value) { + addCriterion("level =", value, "level"); + return (Criteria) this; + } + + public Criteria andLevelNotEqualTo(Integer value) { + addCriterion("level <>", value, "level"); + return (Criteria) this; + } + + public Criteria andLevelGreaterThan(Integer value) { + addCriterion("level >", value, "level"); + return (Criteria) this; + } + + public Criteria andLevelGreaterThanOrEqualTo(Integer value) { + addCriterion("level >=", value, "level"); + return (Criteria) this; + } + + public Criteria andLevelLessThan(Integer value) { + addCriterion("level <", value, "level"); + return (Criteria) this; + } + + public Criteria andLevelLessThanOrEqualTo(Integer value) { + addCriterion("level <=", value, "level"); + return (Criteria) this; + } + + public Criteria andLevelIn(List values) { + addCriterion("level in", values, "level"); + return (Criteria) this; + } + + public Criteria andLevelNotIn(List values) { + addCriterion("level not in", values, "level"); + return (Criteria) this; + } + + public Criteria andLevelBetween(Integer value1, Integer value2) { + addCriterion("level between", value1, value2, "level"); + return (Criteria) this; + } + + public Criteria andLevelNotBetween(Integer value1, Integer value2) { + addCriterion("level not between", value1, value2, "level"); + return (Criteria) this; + } + + public Criteria andCreateTimeIsNull() { + addCriterion("create_time is null"); + return (Criteria) this; + } + + public Criteria andCreateTimeIsNotNull() { + addCriterion("create_time is not null"); + return (Criteria) this; + } + + public Criteria andCreateTimeEqualTo(Long value) { + addCriterion("create_time =", value, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeNotEqualTo(Long value) { + addCriterion("create_time <>", value, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeGreaterThan(Long value) { + addCriterion("create_time >", value, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeGreaterThanOrEqualTo(Long value) { + addCriterion("create_time >=", value, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeLessThan(Long value) { + addCriterion("create_time <", value, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeLessThanOrEqualTo(Long value) { + addCriterion("create_time <=", value, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeIn(List values) { + addCriterion("create_time in", values, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeNotIn(List values) { + addCriterion("create_time not in", values, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeBetween(Long value1, Long value2) { + addCriterion("create_time between", value1, value2, "createTime"); + return (Criteria) this; + } + + public Criteria andCreateTimeNotBetween(Long value1, Long value2) { + addCriterion("create_time not between", value1, value2, "createTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeIsNull() { + addCriterion("update_time is null"); + return (Criteria) this; + } + + public Criteria andUpdateTimeIsNotNull() { + addCriterion("update_time is not null"); + return (Criteria) this; + } + + public Criteria andUpdateTimeEqualTo(Long value) { + addCriterion("update_time =", value, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeNotEqualTo(Long value) { + addCriterion("update_time <>", value, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeGreaterThan(Long value) { + addCriterion("update_time >", value, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeGreaterThanOrEqualTo(Long value) { + addCriterion("update_time >=", value, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeLessThan(Long value) { + addCriterion("update_time <", value, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeLessThanOrEqualTo(Long value) { + addCriterion("update_time <=", value, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeIn(List values) { + addCriterion("update_time in", values, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeNotIn(List values) { + addCriterion("update_time not in", values, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeBetween(Long value1, Long value2) { + addCriterion("update_time between", value1, value2, "updateTime"); + return (Criteria) this; + } + + public Criteria andUpdateTimeNotBetween(Long value1, Long value2) { + addCriterion("update_time not between", value1, value2, "updateTime"); + 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/ApiModuleMapper.java b/backend/src/main/java/io/metersphere/base/mapper/ApiModuleMapper.java new file mode 100644 index 0000000000..65000c3129 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/ApiModuleMapper.java @@ -0,0 +1,34 @@ +package io.metersphere.base.mapper; + +import io.metersphere.base.domain.ApiModule; +import io.metersphere.base.domain.ApiModuleExample; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface ApiModuleMapper { + + long countByExample(ApiModuleExample example); + + int deleteByExample(ApiModuleExample example); + + int deleteByPrimaryKey(String id); + + int insert(ApiModule record); + + int insertBatch(@Param("records") List records); + + int insertSelective(ApiModule record); + + List selectByExample(ApiModuleExample example); + + ApiModule selectByPrimaryKey(String id); + + int updateByExampleSelective(@Param("record") ApiModule record, @Param("example") ApiModuleExample example); + + int updateByExample(@Param("record") ApiModule record, @Param("example") ApiModuleExample example); + + int updateByPrimaryKeySelective(ApiModule record); + + int updateByPrimaryKey(ApiModule record); +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/base/mapper/ApiModuleMapper.xml b/backend/src/main/java/io/metersphere/base/mapper/ApiModuleMapper.xml new file mode 100644 index 0000000000..d938186b38 --- /dev/null +++ b/backend/src/main/java/io/metersphere/base/mapper/ApiModuleMapper.xml @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + + + + + + + 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} + + + + + + + + + + + id, project_id, name, parent_id, level, create_time, update_time + + + + + delete from api_module + where id = #{id,jdbcType=VARCHAR} + + + delete from api_module + + + + + + + insert into api_module (id, project_id, name,protocol + parent_id, level, create_time, + update_time) + values + + (#{emp.id,jdbcType=VARCHAR}, #{emp.projectId,jdbcType=VARCHAR}, + #{emp.name,jdbcType=VARCHAR},#{emp.protocol,jdbcType=VARCHAR}, + #{emp.parentId,jdbcType=VARCHAR}, #{emp.level,jdbcType=INTEGER}, #{emp.createTime,jdbcType=BIGINT}, + #{emp.updateTime,jdbcType=BIGINT}) + + + + + insert into api_module (id, project_id, name,protocol + parent_id, level, create_time, + update_time) + values (#{id,jdbcType=VARCHAR}, #{projectId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR},#{protocol,jdbcType=VARCHAR}, + #{parentId,jdbcType=VARCHAR}, #{level,jdbcType=INTEGER}, #{createTime,jdbcType=BIGINT}, + #{updateTime,jdbcType=BIGINT}) + + + + insert into api_module + + + id, + + + project_id, + + + name, + + + protocol, + + + parent_id, + + + level, + + + create_time, + + + update_time, + + + + + #{id,jdbcType=VARCHAR}, + + + #{projectId,jdbcType=VARCHAR}, + + + #{name,jdbcType=VARCHAR}, + + + #{protocol,jdbcType=VARCHAR}, + + + #{parentId,jdbcType=VARCHAR}, + + + #{level,jdbcType=INTEGER}, + + + #{createTime,jdbcType=BIGINT}, + + + #{updateTime,jdbcType=BIGINT}, + + + + + + update api_module + + + id = #{record.id,jdbcType=VARCHAR}, + + + project_id = #{record.projectId,jdbcType=VARCHAR}, + + + name = #{record.name,jdbcType=VARCHAR}, + + + protocol = #{record.protocol,jdbcType=VARCHAR}, + + + + parent_id = #{record.parentId,jdbcType=VARCHAR}, + + + level = #{record.level,jdbcType=INTEGER}, + + + create_time = #{record.createTime,jdbcType=BIGINT}, + + + update_time = #{record.updateTime,jdbcType=BIGINT}, + + + + + + + + update api_module + set id = #{record.id,jdbcType=VARCHAR}, + project_id = #{record.projectId,jdbcType=VARCHAR}, + name = #{record.name,jdbcType=VARCHAR}, + protocol = #{protocol,jdbcType=VARCHAR}, + parent_id = #{record.parentId,jdbcType=VARCHAR}, + level = #{record.level,jdbcType=INTEGER}, + create_time = #{record.createTime,jdbcType=BIGINT}, + update_time = #{record.updateTime,jdbcType=BIGINT} + + + + + + update api_module + + + project_id = #{projectId,jdbcType=VARCHAR}, + + + name = #{name,jdbcType=VARCHAR}, + + + protocol = #{protocol,jdbcType=VARCHAR}, + + + + parent_id = #{parentId,jdbcType=VARCHAR}, + + + level = #{level,jdbcType=INTEGER}, + + + create_time = #{createTime,jdbcType=BIGINT}, + + + update_time = #{updateTime,jdbcType=BIGINT}, + + + where id = #{id,jdbcType=VARCHAR} + + + update api_module + set project_id = #{projectId,jdbcType=VARCHAR}, + name = #{name,jdbcType=VARCHAR}, + protocol = #{protocol,jdbcType=VARCHAR}, + parent_id = #{parentId,jdbcType=VARCHAR}, + level = #{level,jdbcType=INTEGER}, + create_time = #{createTime,jdbcType=BIGINT}, + update_time = #{updateTime,jdbcType=BIGINT} + where id = #{id,jdbcType=VARCHAR} + + \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V40_api_delimit.sql b/backend/src/main/resources/db/migration/V40_api_delimit.sql new file mode 100644 index 0000000000..df34f15dda --- /dev/null +++ b/backend/src/main/resources/db/migration/V40_api_delimit.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS `api_module` ( + `id` varchar(50) NOT NULL COMMENT 'Test case node ID', + `project_id` varchar(50) NOT NULL COMMENT 'Project ID this node belongs to', + `name` varchar(64) NOT NULL COMMENT 'Node name', + `protocol` varchar(64) NOT NULL COMMENT 'Node protocol', + `parent_id` varchar(50) DEFAULT NULL COMMENT 'Parent node ID', + `level` int(10) DEFAULT 1 COMMENT 'Node level', + `create_time` bigint(13) NOT NULL COMMENT 'Create timestamp', + `update_time` bigint(13) NOT NULL COMMENT 'Update timestamp', + PRIMARY KEY (`id`) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; \ No newline at end of file diff --git a/frontend/src/business/components/api/delimit/ApiDelimit.vue b/frontend/src/business/components/api/delimit/ApiDelimit.vue new file mode 100644 index 0000000000..b955b4f8cb --- /dev/null +++ b/frontend/src/business/components/api/delimit/ApiDelimit.vue @@ -0,0 +1,219 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/ApiAdvancedConfig.vue b/frontend/src/business/components/api/delimit/components/ApiAdvancedConfig.vue new file mode 100644 index 0000000000..e832ff7704 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/ApiAdvancedConfig.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/ApiCaseList.vue b/frontend/src/business/components/api/delimit/components/ApiCaseList.vue new file mode 100644 index 0000000000..586e530945 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/ApiCaseList.vue @@ -0,0 +1,305 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/ApiConfig.vue b/frontend/src/business/components/api/delimit/components/ApiConfig.vue new file mode 100644 index 0000000000..4b0f2ac7e2 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/ApiConfig.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/ApiKeyValue.vue b/frontend/src/business/components/api/delimit/components/ApiKeyValue.vue new file mode 100644 index 0000000000..f1362e6322 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/ApiKeyValue.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/ApiList.vue b/frontend/src/business/components/api/delimit/components/ApiList.vue new file mode 100644 index 0000000000..94aa899dea --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/ApiList.vue @@ -0,0 +1,258 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/ApiModule.vue b/frontend/src/business/components/api/delimit/components/ApiModule.vue new file mode 100644 index 0000000000..b1c299df96 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/ApiModule.vue @@ -0,0 +1,399 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/ApiVariable.vue b/frontend/src/business/components/api/delimit/components/ApiVariable.vue new file mode 100644 index 0000000000..cd36f59bf1 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/ApiVariable.vue @@ -0,0 +1,216 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/ApiVariableInput.vue b/frontend/src/business/components/api/delimit/components/ApiVariableInput.vue new file mode 100644 index 0000000000..f9319f713b --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/ApiVariableInput.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/BottomContainer.vue b/frontend/src/business/components/api/delimit/components/BottomContainer.vue new file mode 100644 index 0000000000..b91056fd06 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/BottomContainer.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/assertion/ApiAssertionDuration.vue b/frontend/src/business/components/api/delimit/components/assertion/ApiAssertionDuration.vue new file mode 100644 index 0000000000..c2de8c0adf --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/assertion/ApiAssertionDuration.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/assertion/ApiAssertionJsonPath.vue b/frontend/src/business/components/api/delimit/components/assertion/ApiAssertionJsonPath.vue new file mode 100644 index 0000000000..56b3a2408a --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/assertion/ApiAssertionJsonPath.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/assertion/ApiAssertionRegex.vue b/frontend/src/business/components/api/delimit/components/assertion/ApiAssertionRegex.vue new file mode 100644 index 0000000000..40a06e7ddd --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/assertion/ApiAssertionRegex.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/assertion/ApiAssertionText.vue b/frontend/src/business/components/api/delimit/components/assertion/ApiAssertionText.vue new file mode 100644 index 0000000000..d3c6c28761 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/assertion/ApiAssertionText.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/assertion/ApiAssertions.vue b/frontend/src/business/components/api/delimit/components/assertion/ApiAssertions.vue new file mode 100644 index 0000000000..3d17ed2b34 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/assertion/ApiAssertions.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/assertion/ApiAssertionsEdit.vue b/frontend/src/business/components/api/delimit/components/assertion/ApiAssertionsEdit.vue new file mode 100644 index 0000000000..722aae8eb4 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/assertion/ApiAssertionsEdit.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/assertion/ApiJsonpathSuggestList.vue b/frontend/src/business/components/api/delimit/components/assertion/ApiJsonpathSuggestList.vue new file mode 100644 index 0000000000..418e9635f4 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/assertion/ApiJsonpathSuggestList.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/basis/AddBasisHttpApi.vue b/frontend/src/business/components/api/delimit/components/basis/AddBasisHttpApi.vue new file mode 100644 index 0000000000..71ea9d52cc --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/basis/AddBasisHttpApi.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/body/ApiBody.vue b/frontend/src/business/components/api/delimit/components/body/ApiBody.vue new file mode 100644 index 0000000000..4c20e0bd8d --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/body/ApiBody.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/body/ApiBodyFileUpload.vue b/frontend/src/business/components/api/delimit/components/body/ApiBodyFileUpload.vue new file mode 100644 index 0000000000..c86aa57fd5 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/body/ApiBodyFileUpload.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/collapse/ApiCollapse.vue b/frontend/src/business/components/api/delimit/components/collapse/ApiCollapse.vue new file mode 100644 index 0000000000..7e38dd82d0 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/collapse/ApiCollapse.vue @@ -0,0 +1,73 @@ + + diff --git a/frontend/src/business/components/api/delimit/components/collapse/ApiCollapseItem.vue b/frontend/src/business/components/api/delimit/components/collapse/ApiCollapseItem.vue new file mode 100644 index 0000000000..2dee4ef51f --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/collapse/ApiCollapseItem.vue @@ -0,0 +1,134 @@ + + + + diff --git a/frontend/src/business/components/api/delimit/components/collapse/ApiRequestMethodSelect.vue b/frontend/src/business/components/api/delimit/components/collapse/ApiRequestMethodSelect.vue new file mode 100644 index 0000000000..ffda071ffe --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/collapse/ApiRequestMethodSelect.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/complete/AddCompleteHttpApi.vue b/frontend/src/business/components/api/delimit/components/complete/AddCompleteHttpApi.vue new file mode 100644 index 0000000000..2792c7f1c1 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/complete/AddCompleteHttpApi.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/debug/DebugHttpPage.vue b/frontend/src/business/components/api/delimit/components/debug/DebugHttpPage.vue new file mode 100644 index 0000000000..9b0d672ead --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/debug/DebugHttpPage.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/extract/ApiExtract.vue b/frontend/src/business/components/api/delimit/components/extract/ApiExtract.vue new file mode 100644 index 0000000000..54ce088051 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/extract/ApiExtract.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/extract/ApiExtractCommon.vue b/frontend/src/business/components/api/delimit/components/extract/ApiExtractCommon.vue new file mode 100644 index 0000000000..0413286773 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/extract/ApiExtractCommon.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/extract/ApiExtractEdit.vue b/frontend/src/business/components/api/delimit/components/extract/ApiExtractEdit.vue new file mode 100644 index 0000000000..ae08694e8e --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/extract/ApiExtractEdit.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/processor/Jsr233Processor.vue b/frontend/src/business/components/api/delimit/components/processor/Jsr233Processor.vue new file mode 100644 index 0000000000..a1af2e7e68 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/processor/Jsr233Processor.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/request/ApiHttpRequestForm.vue b/frontend/src/business/components/api/delimit/components/request/ApiHttpRequestForm.vue new file mode 100644 index 0000000000..b4ffb221db --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/request/ApiHttpRequestForm.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/request/ApiRequestForm.vue b/frontend/src/business/components/api/delimit/components/request/ApiRequestForm.vue new file mode 100644 index 0000000000..b89238ad6d --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/request/ApiRequestForm.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/response/AssertionResults.vue b/frontend/src/business/components/api/delimit/components/response/AssertionResults.vue new file mode 100644 index 0000000000..f33a998781 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/response/AssertionResults.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/response/ResponseText.vue b/frontend/src/business/components/api/delimit/components/response/ResponseText.vue new file mode 100644 index 0000000000..d949f0d665 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/response/ResponseText.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/components/runtest/RunTestHttpPage.vue b/frontend/src/business/components/api/delimit/components/runtest/RunTestHttpPage.vue new file mode 100644 index 0000000000..6acaaf4ee1 --- /dev/null +++ b/frontend/src/business/components/api/delimit/components/runtest/RunTestHttpPage.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/frontend/src/business/components/api/delimit/model/EnvironmentModel.js b/frontend/src/business/components/api/delimit/model/EnvironmentModel.js new file mode 100644 index 0000000000..1b0c414805 --- /dev/null +++ b/frontend/src/business/components/api/delimit/model/EnvironmentModel.js @@ -0,0 +1,127 @@ +import {BaseConfig, DatabaseConfig, KeyValue} from "./ScenarioModel"; +import {TCPConfig} from "@/business/components/api/test/model/ScenarioModel"; + +export class Environment extends BaseConfig { + constructor(options = {}) { + + super(); + + this.projectId = undefined; + this.name = undefined; + this.id = undefined; + this.config = undefined; + + this.set(options); + this.sets({}, options); + } + + initOptions(options = {}) { + this.config = new Config(options.config); + return options; + } +} + +export class Config extends BaseConfig { + constructor(options = {}) { + super(); + this.commonConfig = undefined; + this.httpConfig = undefined; + this.databaseConfigs = []; + this.tcpConfig = undefined; + + this.set(options); + this.sets({databaseConfigs: DatabaseConfig}, options); + } + + initOptions(options = {}) { + this.commonConfig = new CommonConfig(options.commonConfig); + this.httpConfig = new HttpConfig(options.httpConfig); + options.databaseConfigs = options.databaseConfigs || []; + options.tcpConfig = new TCPConfig(options.tcpConfig); + return options; + } +} + +export class CommonConfig extends BaseConfig { + constructor(options = {}) { + super(); + this.variables = []; + this.enableHost = false; + this.hosts = []; + + this.set(options); + this.sets({variables: KeyValue, hosts: Host}, options); + } + + initOptions(options = {}) { + options.variables = options.variables || [new KeyValue()]; + options.hosts = options.hosts || []; + return options; + } +} + +export class HttpConfig extends BaseConfig { + constructor(options = {}) { + super(); + + this.socket = undefined; + this.domain = undefined; + this.headers = []; + this.protocol = 'https'; + this.port = undefined; + + this.set(options); + this.sets({headers: KeyValue}, options); + } + + initOptions(options = {}) { + options.headers = options.headers || [new KeyValue()]; + return options; + } +} + +export class Host extends BaseConfig { + constructor(options = {}) { + super(); + + this.ip = undefined; + this.domain = undefined; + this.status = undefined; + this.annotation = undefined; + this.uuid = undefined; + + this.set(options); + } +} + + +/* ---------- Functions ------- */ + +export function compatibleWithEnvironment(environment) { + //兼容旧版本 + if (!environment.config) { + let config = new Config(); + if (!(environment.variables instanceof Array)) { + config.commonConfig.variables = JSON.parse(environment.variables); + } + if (environment.hosts && !(environment.hosts instanceof Array)) { + config.commonConfig.hosts = JSON.parse(environment.hosts); + config.commonConfig.enableHost = true; + } + if (!(environment.headers instanceof Array)) { + config.httpConfig.headers = JSON.parse(environment.headers); + } + config.httpConfig.port = environment.port; + config.httpConfig.protocol = environment.protocol; + config.httpConfig.domain = environment.domain; + config.httpConfig.socket = environment.socket; + environment.config = JSON.stringify(config); + } +} + +export function parseEnvironment(environment) { + compatibleWithEnvironment(environment); + if (!(environment.config instanceof Config)) { + environment.config = new Config(JSON.parse(environment.config)); + } +} diff --git a/frontend/src/business/components/api/delimit/model/JMX.js b/frontend/src/business/components/api/delimit/model/JMX.js new file mode 100644 index 0000000000..3a10643de3 --- /dev/null +++ b/frontend/src/business/components/api/delimit/model/JMX.js @@ -0,0 +1,679 @@ +const INDENT = ' '; // 缩进2空格 + +export class Element { + constructor(name, attributes, value) { + this.indent = ''; + this.name = name; // 标签名 + this.attributes = attributes || {}; // 属性 + this.value = undefined; // 基础类型的内容 + this.elements = []; // 子节点 + + if (value instanceof Element) { + this.elements.push(value); + } else { + this.value = value; + } + } + + set(value) { + this.elements = []; + this.value = value; + } + + add(element) { + if (element instanceof Element) { + this.value = undefined; + this.elements.push(element); + return element; + } + } + + getDefault(value, defaultValue) { + return value === undefined ? defaultValue : value; + } + + commonValue(tag, name, value, defaultValue) { + let v = this.getDefault(value, defaultValue); + return this.add(new Element(tag, {name: name}, v)); + } + + boolProp(name, value, defaultValue) { + return this.commonValue('boolProp', name, value, defaultValue); + } + + intProp(name, value, defaultValue) { + return this.commonValue('intProp', name, value, defaultValue); + } + + longProp(name, value, defaultValue) { + return this.commonValue('longProp', name, value, defaultValue); + } + + stringProp(name, value, defaultValue) { + return this.commonValue('stringProp', name, value, defaultValue); + } + + collectionProp(name) { + return this.commonValue('collectionProp', name); + } + + elementProp(name, elementType) { + return this.add(new Element('elementProp', {name: name, elementType: elementType})); + } + + isEmptyValue() { + return this.value === undefined || this.value === ''; + } + + isEmptyElement() { + return this.elements.length === 0; + } + + isEmpty() { + return this.isEmptyValue() && this.isEmptyElement(); + } + + replace(str) { + if (!str || !(typeof str === 'string')) return str; + return str.replace(/&/g, "&").replace(//g, ">").replace(/'/g, "'").replace(/"/g, """); + } + + toXML(indent) { + if (indent) { + this.indent = indent; + } + + let str = this.start(); + str += this.content(); + str += this.end(); + return str; + } + + start() { + let str = this.indent + '<' + this.replace(this.name); + for (let key in this.attributes) { + if (this.attributes.hasOwnProperty(key)) { + str += ' ' + this.replace(key) + '="' + this.replace(this.attributes[key]) + '"'; + } + } + if (this.isEmpty()) { + str += '/>'; + } else { + str += '>'; + } + return str; + } + + content() { + if (!this.isEmptyValue()) { + return this.replace(this.value); + } + + let str = ''; + let parent = this; + if (this.elements.length > 0) { + str += '\n'; + this.elements.forEach(e => { + e.indent += parent.indent + INDENT; + str += e.toXML(); + }); + } + return str; + } + + end() { + if (this.isEmpty()) { + return '\n'; + } + let str = '\n'; + if (!this.isEmptyValue()) { + return str; + } + if (!this.isEmptyElement()) { + return this.indent + str; + } + } +} + +// HashTree, 只能添加TestElement的子元素,没有基础类型内容 +export class HashTree extends Element { + constructor() { + super('hashTree'); + } + + add(te) { + if (te instanceof TestElement) { + super.add(te); + } + } +} + +// TestElement包含2部分,Element 和 HashTree +export class TestElement extends Element { + constructor(name, attributes, value) { + // Element, 只能添加Element + super(name, attributes, value); + // HashTree, 只能添加TestElement + this.hashTree = new HashTree(); + } + + put(te) { + this.hashTree.add(te); + } + + toXML() { + let str = super.toXML(); + str += this.hashTree.toXML(this.indent); + return str; + } +} + +export class DefaultTestElement extends TestElement { + constructor(tag, guiclass, testclass, testname, enabled) { + super(tag, { + guiclass: guiclass, + testclass: testclass, + testname: testname === undefined ? tag + ' Name' : testname, + enabled: enabled || true + }); + } +} + +export class TestPlan extends DefaultTestElement { + constructor(testName, props) { + super('TestPlan', 'TestPlanGui', 'TestPlan', testName); + + props = props || {}; + this.boolProp("TestPlan.functional_mode", props.mode, false); + this.boolProp("TestPlan.serialize_threadgroups", props.stg, true); + this.boolProp("TestPlan.tearDown_on_shutdown", props.tos, true); + this.stringProp("TestPlan.comments", props.comments); + this.stringProp("TestPlan.user_define_classpath", props.classpath); + this.add(new ElementArguments(props.args, "TestPlan.user_defined_variables", "User Defined Variables")); + } +} + +export class ThreadGroup extends DefaultTestElement { + constructor(testName, props) { + super('ThreadGroup', 'ThreadGroupGui', 'ThreadGroup', testName); + + props = props || {}; + this.intProp("ThreadGroup.num_threads", props.threads, 1); + this.intProp("ThreadGroup.ramp_time", props.ramp, 1); + this.longProp("ThreadGroup.delay", props.delay, 0); + this.longProp("ThreadGroup.duration", props.delay, 0); + this.stringProp("ThreadGroup.on_sample_error", props.error, "continue"); + this.boolProp("ThreadGroup.scheduler", props.scheduler, false); + + let loopAttrs = { + name: "ThreadGroup.main_controller", + elementType: "LoopController", + guiclass: "LoopControlPanel", + testclass: "LoopController", + testname: "Loop Controller", + enabled: "true" + }; + let loopProps = props.loopProps || {}; + let loopController = this.add(new Element('elementProp', loopAttrs)); + loopController.boolProp('LoopController.continue_forever', loopProps.continue, false); + loopController.stringProp('LoopController.loops', loopProps.loops, 1); + } +} + +export class DubboSample extends DefaultTestElement { + constructor(testName, request = {}) { + super('io.github.ningyu.jmeter.plugin.dubbo.sample.DubboSample', + 'io.github.ningyu.jmeter.plugin.dubbo.gui.DubboSampleGui', + 'io.github.ningyu.jmeter.plugin.dubbo.sample.DubboSample', testName); + this.request = request; + + this.stringProp("FIELD_DUBBO_CONFIG_CENTER_PROTOCOL", this.request.configCenter.protocol); + this.stringProp("FIELD_DUBBO_CONFIG_CENTER_GROUP", this.request.configCenter.group); + this.stringProp("FIELD_DUBBO_CONFIG_CENTER_NAMESPACE", this.request.configCenter.namespace); + this.stringProp("FIELD_DUBBO_CONFIG_CENTER_USER_NAME", this.request.configCenter.username); + this.stringProp("FIELD_DUBBO_CONFIG_CENTER_PASSWORD", this.request.configCenter.password); + this.stringProp("FIELD_DUBBO_CONFIG_CENTER_ADDRESS", this.request.configCenter.address); + this.stringProp("FIELD_DUBBO_CONFIG_CENTER_TIMEOUT", this.request.configCenter.timeout); + + this.stringProp("FIELD_DUBBO_REGISTRY_PROTOCOL", this.request.registryCenter.protocol); + this.stringProp("FIELD_DUBBO_REGISTRY_GROUP", this.request.registryCenter.group); + this.stringProp("FIELD_DUBBO_REGISTRY_USER_NAME", this.request.registryCenter.username); + this.stringProp("FIELD_DUBBO_REGISTRY_PASSWORD", this.request.registryCenter.password); + this.stringProp("FIELD_DUBBO_ADDRESS", this.request.registryCenter.address); + this.stringProp("FIELD_DUBBO_REGISTRY_TIMEOUT", this.request.registryCenter.timeout); + + this.stringProp("FIELD_DUBBO_TIMEOUT", this.request.consumerAndService.timeout); + this.stringProp("FIELD_DUBBO_VERSION", this.request.consumerAndService.version); + this.stringProp("FIELD_DUBBO_RETRIES", this.request.consumerAndService.retries); + this.stringProp("FIELD_DUBBO_GROUP", this.request.consumerAndService.group); + this.stringProp("FIELD_DUBBO_CONNECTIONS", this.request.consumerAndService.connections); + this.stringProp("FIELD_DUBBO_LOADBALANCE", this.request.consumerAndService.loadBalance); + this.stringProp("FIELD_DUBBO_ASYNC", this.request.consumerAndService.async); + this.stringProp("FIELD_DUBBO_CLUSTER", this.request.consumerAndService.cluster); + + this.stringProp("FIELD_DUBBO_RPC_PROTOCOL", this.request.protocol); + this.stringProp("FIELD_DUBBO_INTERFACE", this.request.interface); + this.stringProp("FIELD_DUBBO_METHOD", this.request.method); + + this.intProp("FIELD_DUBBO_METHOD_ARGS_SIZE", this.request.args.length); + this.intProp("FIELD_DUBBO_ATTACHMENT_ARGS_SIZE", this.request.attachmentArgs.length); + this.request.args.forEach((arg, i) => { + if (!!arg.name || !!arg.value) { + let index = i + 1; + this.stringProp("FIELD_DUBBO_METHOD_ARGS_PARAM_TYPE" + index, arg.name); + this.stringProp("FIELD_DUBBO_METHOD_ARGS_PARAM_VALUE" + index, arg.value); + } + }) + this.request.attachmentArgs.forEach((arg, i) => { + if (!!arg.name || !!arg.value) { + let index = i + 1; + this.stringProp("FIELD_DUBBO_ATTACHMENT_ARGS_KEY" + index, arg.name); + this.stringProp("FIELD_DUBBO_ATTACHMENT_ARGS_VALUE" + index, arg.value); + } + }) + } +} + +export class JDBCSampler extends DefaultTestElement { + constructor(testName, request = {}) { + super('JDBCSampler', 'TestBeanGUI', 'JDBCSampler', testName); + + this.stringProp("dataSource", request.dataSource); + this.stringProp("query", request.query); + this.stringProp("queryTimeout", request.queryTimeout); + this.stringProp("resultVariable", request.resultVariable); + this.stringProp("variableNames", request.variableNames); + this.stringProp("queryArguments"); + this.stringProp("queryArgumentsTypes"); + this.stringProp("resultSetMaxRows"); + this.stringProp("resultSetHandler", 'Store as String'); + this.stringProp("queryType", 'Callable Statement'); + } +} + +export class TCPSampler extends DefaultTestElement { + constructor(testName, request = {}) { + super('TCPSampler', 'TCPSamplerGui', 'TCPSampler', testName); + + this.stringProp("TCPSampler.classname", request.classname); + this.stringProp("TCPSampler.server", request.server); + this.stringProp("TCPSampler.port", request.port); + this.stringProp("TCPSampler.ctimeout", request.ctimeout); + this.stringProp("TCPSampler.timeout", request.timeout); + this.boolProp("TCPSampler.reUseConnection", request.reUseConnection); + this.boolProp("TCPSampler.nodelay", request.nodelay); + this.boolProp("TCPSampler.closeConnection", request.closeConnection); + this.stringProp("TCPSampler.soLinger", request.soLinger); + this.stringProp("TCPSampler.EolByte", request.eolByte); + this.stringProp("TCPSampler.request", request.request); + this.stringProp("ConfigTestElement.username", request.username); + this.stringProp("ConfigTestElement.password", request.password); + } +} + +export class HTTPSamplerProxy extends DefaultTestElement { + constructor(testName, options = {}) { + super('HTTPSamplerProxy', 'HttpTestSampleGui', 'HTTPSamplerProxy', testName); + + this.stringProp("HTTPSampler.domain", options.domain); + this.stringProp("HTTPSampler.protocol", options.protocol); + this.stringProp("HTTPSampler.path", options.path); + + this.stringProp("HTTPSampler.method", options.method); + this.stringProp("HTTPSampler.contentEncoding", options.encoding, "UTF-8"); + if (!options.port) { + this.stringProp("HTTPSampler.port", ""); + } else { + this.stringProp("HTTPSampler.port", options.port); + } + if (options.connectTimeout) { + this.stringProp('HTTPSampler.connect_timeout', options.connectTimeout); + } + if (options.responseTimeout) { + this.stringProp('HTTPSampler.response_timeout', options.responseTimeout); + } + if (options.followRedirects) { + this.boolProp('HTTPSampler.follow_redirects', options.followRedirects, true); + } + + this.boolProp("HTTPSampler.use_keepalive", options.keepalive, true); + this.boolProp("HTTPSampler.DO_MULTIPART_POST", options.doMultipartPost, false); + } +} + +// 这是一个Element +export class HTTPSamplerArguments extends Element { + constructor(args) { + super('elementProp', { + name: "HTTPsampler.Arguments", // s必须小写 + elementType: "Arguments", + guiclass: "HTTPArgumentsPanel", + testclass: "Arguments", + enabled: "true" + }); + + this.args = args || []; + + let collectionProp = this.collectionProp('Arguments.arguments'); + this.args.forEach(arg => { + if (arg.enable === true || arg.enable === undefined) { // 非禁用的条件加入执行 + let elementProp = collectionProp.elementProp(arg.name, 'HTTPArgument'); + elementProp.boolProp('HTTPArgument.always_encode', arg.encode, true); + elementProp.boolProp('HTTPArgument.use_equals', arg.equals, true); + if (arg.name) { + elementProp.stringProp('Argument.name', arg.name); + } + elementProp.stringProp('Argument.value', arg.value); + elementProp.stringProp('Argument.metadata', arg.metadata || "="); + if (arg.contentType) { + elementProp.stringProp('HTTPArgument.content_type', arg.contentType, ""); + } + } + }); + } +} + +export class HTTPsamplerFiles extends Element { + constructor(args) { + super('elementProp', { + name: "HTTPsampler.Files", + elementType: "HTTPFileArgs", + }); + + this.args = args || {}; + + let collectionProp = this.collectionProp('HTTPFileArgs.files'); + this.args.forEach(arg => { + let elementProp = collectionProp.elementProp(arg.value, 'HTTPFileArg'); + elementProp.stringProp('File.path', arg.value); + elementProp.stringProp('File.paramname', arg.name); + elementProp.stringProp('File.mimetype', arg.contentType || "application/octet-stream"); + }); + } +} + +export class CookieManager extends DefaultTestElement { + constructor(testName) { + super('CookieManager', 'CookiePanel', 'CookieManager', testName); + this.collectionProp('CookieManager.cookies'); + this.boolProp('CookieManager.clearEachIteration', false, false); + this.boolProp('CookieManager.controlledByThreadGroup', false, false); + } +} + +export class DurationAssertion extends DefaultTestElement { + constructor(testName, duration) { + super('DurationAssertion', 'DurationAssertionGui', 'DurationAssertion', testName); + this.duration = duration || 0; + this.stringProp('DurationAssertion.duration', this.duration); + } +} + +export class ResponseAssertion extends DefaultTestElement { + constructor(testName, assertion) { + super('ResponseAssertion', 'AssertionGui', 'ResponseAssertion', testName); + this.assertion = assertion || {}; + + this.stringProp('Assertion.test_field', this.assertion.field); + this.boolProp('Assertion.assume_success', this.assertion.assumeSuccess); + this.intProp('Assertion.test_type', this.assertion.type); + this.stringProp('Assertion.custom_message', this.assertion.message); + + let collectionProp = this.collectionProp('Asserion.test_strings'); + let random = Math.floor(Math.random() * 10000); + collectionProp.stringProp(random, this.assertion.value); + } +} + +export class JSONPathAssertion extends DefaultTestElement { + constructor(testName, jsonPath) { + super('JSONPathAssertion', 'JSONPathAssertionGui', 'JSONPathAssertion', testName); + this.jsonPath = jsonPath || {}; + + this.stringProp('JSON_PATH', this.jsonPath.expression); + this.stringProp('EXPECTED_VALUE', this.jsonPath.expect); + this.boolProp('JSONVALIDATION', true); + this.boolProp('EXPECT_NULL', false); + this.boolProp('INVERT', false); + this.boolProp('ISREGEX', true); + } +} + +export class ResponseCodeAssertion extends ResponseAssertion { + constructor(testName, type, value, assumeSuccess, message) { + let assertion = { + field: 'Assertion.response_code', + type: type, + value: value, + assumeSuccess: assumeSuccess, + message: message, + } + super(testName, assertion) + } +} + +export class ResponseDataAssertion extends ResponseAssertion { + constructor(testName, type, value, assumeSuccess, message) { + let assertion = { + field: 'Assertion.response_data', + type: type, + value: value, + assumeSuccess: assumeSuccess, + message: message, + } + super(testName, assertion) + } +} + +export class ResponseHeadersAssertion extends ResponseAssertion { + constructor(testName, type, value, assumeSuccess, message) { + let assertion = { + field: 'Assertion.response_headers', + type: type, + value: value, + assumeSuccess: assumeSuccess, + message: message, + } + super(testName, assertion) + } +} + +export class BeanShellProcessor extends DefaultTestElement { + constructor(tag, guiclass, testclass, testname, processor) { + super(tag, guiclass, testclass, testname); + this.processor = processor || {}; + this.boolProp('resetInterpreter', false); + this.stringProp('parameters'); + this.stringProp('filename'); + this.stringProp('script', processor.script); + } +} + +export class JSR223Processor extends DefaultTestElement { + constructor(tag, guiclass, testclass, testname, processor) { + super(tag, guiclass, testclass, testname); + this.processor = processor || {}; + this.stringProp('cacheKey', 'true'); + this.stringProp('filename'); + this.stringProp('parameters'); + this.stringProp('script', this.processor.script); + this.stringProp('scriptLanguage', this.processor.language); + } +} + +export class JSR223PreProcessor extends JSR223Processor { + constructor(testName, processor) { + super('JSR223PreProcessor', 'TestBeanGUI', 'JSR223PreProcessor', testName, processor) + } +} + +export class JSR223PostProcessor extends JSR223Processor { + constructor(testName, processor) { + super('JSR223PostProcessor', 'TestBeanGUI', 'JSR223PostProcessor', testName, processor) + } +} + +export class BeanShellPreProcessor extends BeanShellProcessor { + constructor(testName, processor) { + super('BeanShellPreProcessor', 'TestBeanGUI', 'BeanShellPreProcessor', testName, processor) + } +} + +export class BeanShellPostProcessor extends BeanShellProcessor { + constructor(testName, processor) { + super('BeanShellPostProcessor', 'TestBeanGUI', 'BeanShellPostProcessor', testName, processor) + } +} + +export class IfController extends DefaultTestElement { + constructor(testName, controller = {}) { + super('IfController', 'IfControllerPanel', 'IfController', testName); + + this.stringProp('IfController.comments', controller.comments); + this.stringProp('IfController.condition', controller.condition); + this.boolProp('IfController.evaluateAll', controller.evaluateAll, false); + this.boolProp('IfController.useExpression', controller.useExpression, true); + } +} + +export class ConstantTimer extends DefaultTestElement { + constructor(testName, timer = {}) { + super('ConstantTimer', 'ConstantTimerGui', 'ConstantTimer', testName); + + this.stringProp('ConstantTimer.delay', timer.delay); + } +} + +export class HeaderManager extends DefaultTestElement { + constructor(testName, headers) { + super('HeaderManager', 'HeaderPanel', 'HeaderManager', testName); + this.headers = headers || []; + + let collectionProp = this.collectionProp('HeaderManager.headers'); + this.headers.forEach(header => { + if (header.enable === true || header.enable === undefined) { + let elementProp = collectionProp.elementProp('', 'Header'); + elementProp.stringProp('Header.name', header.name); + elementProp.stringProp('Header.value', header.value); + } + }); + } +} + +export class DNSCacheManager extends DefaultTestElement { + constructor(testName, hosts) { + super('DNSCacheManager', 'DNSCachePanel', 'DNSCacheManager', testName); + let collectionPropServers = this.collectionProp('DNSCacheManager.servers'); + let collectionPropHosts = this.collectionProp('DNSCacheManager.hosts'); + + hosts.forEach(host => { + let elementProp = collectionPropHosts.elementProp(host.domain, 'StaticHost'); + elementProp.stringProp('StaticHost.Name', host.domain); + elementProp.stringProp('StaticHost.Address', host.ip); + }); + + let boolProp = this.boolProp('DNSCacheManager.isCustomResolver', true); + } +} + +export class JDBCDataSource extends DefaultTestElement { + constructor(testName, datasource) { + super('JDBCDataSource', 'TestBeanGUI', 'JDBCDataSource', testName); + + this.boolProp('autocommit', true); + this.boolProp('keepAlive', true); + this.boolProp('preinit', false); + this.stringProp('dataSource', datasource.name); + this.stringProp('dbUrl', datasource.dbUrl); + this.stringProp('driver', datasource.driver); + this.stringProp('username', datasource.username); + this.stringProp('password', datasource.password); + this.stringProp('poolMax', datasource.poolMax); + this.stringProp('timeout', datasource.timeout); + this.stringProp('connectionAge', '5000'); + this.stringProp('trimInterval', '60000'); + this.stringProp('transactionIsolation', 'DEFAULT'); + this.stringProp('checkQuery'); + this.stringProp('initQuery'); + this.stringProp('connectionProperties'); + } +} + +export class Arguments extends DefaultTestElement { + constructor(testName, args) { + super('Arguments', 'ArgumentsPanel', 'Arguments', testName); + this.args = args || []; + + let collectionProp = this.collectionProp('Arguments.arguments'); + + this.args.forEach(arg => { + if (arg.enable === true || arg.enable === undefined) { // 非禁用的条件加入执行 + let elementProp = collectionProp.elementProp(arg.name, 'Argument'); + elementProp.stringProp('Argument.name', arg.name); + elementProp.stringProp('Argument.value', arg.value); + elementProp.stringProp('Argument.desc', arg.desc); + elementProp.stringProp('Argument.metadata', arg.metadata, "="); + } + }); + } +} + +export class ElementArguments extends Element { + constructor(args, name, testName) { + super('elementProp', { + name: name || "arguments", + elementType: "Arguments", + guiclass: "ArgumentsPanel", + testclass: "Arguments", + testname: testName || "", + enabled: "true" + }); + + let collectionProp = this.collectionProp('Arguments.arguments'); + if (args) { + args.forEach(arg => { + if (arg.enable === true || arg.enable === undefined) { // 非禁用的条件加入执行 + let elementProp = collectionProp.elementProp(arg.name, 'Argument'); + elementProp.stringProp('Argument.name', arg.name); + elementProp.stringProp('Argument.value', arg.value); + elementProp.stringProp('Argument.metadata', arg.metadata, "="); + } + }); + } + } +} + +export class RegexExtractor extends DefaultTestElement { + constructor(testName, props) { + super('RegexExtractor', 'RegexExtractorGui', 'RegexExtractor', testName); + this.props = props || {} + this.stringProp('RegexExtractor.useHeaders', props.headers); + this.stringProp('RegexExtractor.refname', props.name); + this.stringProp('RegexExtractor.regex', props.expression); + this.stringProp('RegexExtractor.template', props.template); + this.stringProp('RegexExtractor.default', props.default); + this.stringProp('RegexExtractor.match_number', props.match); + } +} + +export class JSONPostProcessor extends DefaultTestElement { + constructor(testName, props) { + super('JSONPostProcessor', 'JSONPostProcessorGui', 'JSONPostProcessor', testName); + this.props = props || {} + this.stringProp('JSONPostProcessor.referenceNames', props.name); + this.stringProp('JSONPostProcessor.jsonPathExprs', props.expression); + this.stringProp('JSONPostProcessor.match_numbers', props.match); + } +} + +export class XPath2Extractor extends DefaultTestElement { + constructor(testName, props) { + super('XPath2Extractor', 'XPath2ExtractorGui', 'XPath2Extractor', testName); + this.props = props || {} + this.stringProp('XPathExtractor2.default', props.default); + this.stringProp('XPathExtractor2.refname', props.name); + this.stringProp('XPathExtractor2.xpathQuery', props.expression); + this.stringProp('XPathExtractor2.namespaces', props.namespaces); + this.stringProp('XPathExtractor2.matchNumber', props.match); + } +} diff --git a/frontend/src/business/components/api/delimit/model/ScenarioModel.js b/frontend/src/business/components/api/delimit/model/ScenarioModel.js new file mode 100644 index 0000000000..9d239dbf64 --- /dev/null +++ b/frontend/src/business/components/api/delimit/model/ScenarioModel.js @@ -0,0 +1,1499 @@ +import { + Arguments, + CookieManager, + DNSCacheManager, + DubboSample, + DurationAssertion, + Element, + HashTree, + HeaderManager, + HTTPSamplerArguments, + HTTPsamplerFiles, + HTTPSamplerProxy, + JDBCDataSource, + JDBCSampler, + JSONPathAssertion, + JSONPostProcessor, + JSR223PostProcessor, + JSR223PreProcessor, + RegexExtractor, + ResponseCodeAssertion, + ResponseDataAssertion, + ResponseHeadersAssertion, + TestElement, + TestPlan, + ThreadGroup, + XPath2Extractor, + IfController as JMXIfController, + ConstantTimer as JMXConstantTimer, TCPSampler, +} from "./JMX"; +import Mock from "mockjs"; +import {funcFilters} from "@/common/js/func-filter"; + +export const uuid = function () { + let d = new Date().getTime() + let d2 = (performance && performance.now && (performance.now() * 1000)) || 0; + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + let r = Math.random() * 16; + if (d > 0) { + r = (d + r) % 16 | 0; + d = Math.floor(d / 16); + } else { + r = (d2 + r) % 16 | 0; + d2 = Math.floor(d2 / 16); + } + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); +} + +export const BODY_FILE_DIR = "/opt/metersphere/data/body"; //存放body文件上传目录 + +export const calculate = function (itemValue) { + if (!itemValue) { + return; + } + try { + if (itemValue.trim().startsWith("${")) { + // jmeter 内置函数不做处理 + return itemValue; + } + let funcs = itemValue.split("|"); + let value = Mock.mock(funcs[0].trim()); + if (funcs.length === 1) { + return value; + } + for (let i = 1; i < funcs.length; i++) { + let func = funcs[i].trim(); + let args = func.split(":"); + let strings = []; + if (args[1]) { + strings = args[1].split(","); + } + value = funcFilters[args[0].trim()](value, ...strings); + } + return value; + } catch (e) { + return itemValue; + } +} + +export const BODY_TYPE = { + KV: "KeyValue", + FORM_DATA: "Form Data", + RAW: "Raw" +} + +export const BODY_FORMAT = { + TEXT: "text", + JSON: "json", + XML: "xml", + HTML: "html", +} + +export const ASSERTION_TYPE = { + TEXT: "Text", + REGEX: "Regex", + JSON_PATH: "JSON", + DURATION: "Duration" +} + +export const ASSERTION_REGEX_SUBJECT = { + RESPONSE_CODE: "Response Code", + RESPONSE_HEADERS: "Response Headers", + RESPONSE_DATA: "Response Data" +} + +export const EXTRACT_TYPE = { + REGEX: "Regex", + JSON_PATH: "JSONPath", + XPATH: "XPath" +} + +export class BaseConfig { + + set(options, notUndefined) { + options = this.initOptions(options) + for (let name in options) { + if (options.hasOwnProperty(name)) { + if (!(this[name] instanceof Array)) { + if (notUndefined === true) { + this[name] = options[name] === undefined ? this[name] : options[name]; + } else { + this[name] = options[name]; + } + } + } + } + } + + sets(types, options) { + options = this.initOptions(options) + if (types) { + for (let name in types) { + if (types.hasOwnProperty(name) && options.hasOwnProperty(name)) { + options[name].forEach(o => { + this[name].push(new types[name](o)); + }) + } + } + } + } + + initOptions(options) { + return options || {}; + } + + isValid() { + return true; + } +} + +export class Test extends BaseConfig { + constructor(options) { + super(); + this.type = "MS API CONFIG"; + this.version = '1.4.0'; + this.id = uuid(); + this.name = undefined; + this.projectId = undefined; + this.scenarioDefinition = []; + this.schedule = {}; + + this.set(options); + this.sets({scenarioDefinition: Scenario}, options); + } + + export() { + let obj = { + type: this.type, + version: this.version, + scenarios: this.scenarioDefinition + }; + + return JSON.stringify(obj); + } + + initOptions(options) { + options = options || {}; + options.scenarioDefinition = options.scenarioDefinition || [new Scenario()]; + return options; + } + + isValid() { + for (let i = 0; i < this.scenarioDefinition.length; i++) { + let validator = this.scenarioDefinition[i].isValid(); + if (!validator.isValid) { + return validator; + } + } + if (!this.projectId) { + return { + isValid: false, + info: 'api_test.select_project' + } + } else if (!this.name) { + return { + isValid: false, + info: 'api_test.input_name' + } + } + return {isValid: true}; + } + + toJMX() { + return { + name: this.name + '.jmx', + xml: new JMXGenerator(this).toXML() + }; + } +} + +export class Scenario extends BaseConfig { + constructor(options = {}) { + super(); + this.id = undefined; + this.name = undefined; + this.url = undefined; + this.variables = []; + this.headers = []; + this.requests = []; + this.environmentId = undefined; + this.dubboConfig = undefined; + this.environment = undefined; + this.enableCookieShare = false; + this.enable = true; + this.databaseConfigs = []; + this.tcpConfig = undefined; + + this.set(options); + this.sets({ + variables: KeyValue, + headers: KeyValue, + requests: RequestFactory, + databaseConfigs: DatabaseConfig + }, options); + } + + initOptions(options = {}) { + options.id = options.id || uuid(); + options.requests = options.requests || [new RequestFactory()]; + options.databaseConfigs = options.databaseConfigs || []; + options.dubboConfig = new DubboConfig(options.dubboConfig); + options.tcpConfig = new TCPConfig(options.tcpConfig); + return options; + } + + clone() { + let clone = new Scenario(this); + clone.id = uuid(); + return clone; + } + + isValid() { + if (this.enable) { + for (let i = 0; i < this.requests.length; i++) { + let validator = this.requests[i].isValid(this.environmentId, this.environment); + if (!validator.isValid) { + return validator; + } + } + } + return {isValid: true}; + } + + isReference() { + return this.id.indexOf("#") !== -1 + } +} + +class DubboConfig extends BaseConfig { + constructor(options = {}) { + super(); + this.configCenter = new ConfigCenter(options.configCenter) + this.registryCenter = new RegistryCenter(options.registryCenter) + if (options.consumerAndService === undefined) { + options.consumerAndService = { + timeout: undefined, + version: undefined, + retries: undefined, + cluster: undefined, + group: undefined, + connections: undefined, + async: undefined, + loadBalance: undefined + } + } + this.consumerAndService = new ConsumerAndService(options.consumerAndService) + } +} + +export class RequestFactory { + static TYPES = { + HTTP: "HTTP", + DUBBO: "DUBBO", + SQL: "SQL", + TCP: "TCP", + } + + constructor(options = {}) { + options.type = options.type || RequestFactory.TYPES.HTTP + switch (options.type) { + case RequestFactory.TYPES.DUBBO: + return new DubboRequest(options); + case RequestFactory.TYPES.SQL: + return new SqlRequest(options); + case RequestFactory.TYPES.TCP: + return new TCPRequest(options); + default: + return new HttpRequest(options); + } + } +} + +export class Request extends BaseConfig { + constructor(type, options = {}) { + super(); + this.type = type; + this.id = options.id || uuid(); + this.name = options.name; + this.enable = options.enable === undefined ? true : options.enable; + this.assertions = new Assertions(options.assertions); + this.extract = new Extract(options.extract); + this.jsr223PreProcessor = new JSR223Processor(options.jsr223PreProcessor); + this.jsr223PostProcessor = new JSR223Processor(options.jsr223PostProcessor); + this.timer = new ConstantTimer(options.timer); + this.controller = new IfController(options.controller); + } + + showType() { + return this.type; + } + + showMethod() { + return ""; + } +} + +export class HttpRequest extends Request { + constructor(options) { + super(RequestFactory.TYPES.HTTP, options); + this.url = options.url; + this.path = options.path; + this.method = options.method || "GET"; + this.parameters = []; + this.headers = []; + this.body = new Body(options.body); + this.environment = options.environment; + this.useEnvironment = options.useEnvironment; + this.debugReport = undefined; + this.doMultipartPost = options.doMultipartPost; + this.connectTimeout = options.connectTimeout || 60 * 1000; + this.responseTimeout = options.responseTimeout; + this.followRedirects = options.followRedirects === undefined ? true : options.followRedirects; + + this.sets({parameters: KeyValue, headers: KeyValue}, options); + } + + isValid(environmentId, environment) { + if (this.enable) { + if (this.useEnvironment) { + if (!environmentId) { + return { + isValid: false, + info: 'api_test.request.please_configure_environment_in_scenario' + } + } + if (!environment.config.httpConfig.socket) { + return { + isValid: false, + info: 'api_test.request.please_configure_socket_in_environment' + } + } + } else { + if (!this.url) { + return { + isValid: false, + info: 'api_test.request.input_url' + } + } + try { + new URL(this.url) + } catch (e) { + return { + isValid: false, + info: 'api_test.request.url_invalid' + } + } + } + } + return { + isValid: true + } + } + + showType() { + return this.type; + } + + showMethod() { + return this.method.toUpperCase(); + } + +} + +export class DubboRequest extends Request { + static PROTOCOLS = { + DUBBO: "dubbo://", + RMI: "rmi://", + } + + constructor(options = {}) { + super(RequestFactory.TYPES.DUBBO, options); + this.protocol = options.protocol || DubboRequest.PROTOCOLS.DUBBO; + this.interface = options.interface; + this.method = options.method; + this.configCenter = new ConfigCenter(options.configCenter); + this.registryCenter = new RegistryCenter(options.registryCenter); + this.consumerAndService = new ConsumerAndService(options.consumerAndService); + this.args = []; + this.attachmentArgs = []; + // Scenario.dubboConfig + this.dubboConfig = undefined; + this.debugReport = undefined; + + this.sets({args: KeyValue, attachmentArgs: KeyValue}, options); + } + + isValid() { + if (this.enable) { + if (!this.interface) { + return { + isValid: false, + info: 'api_test.request.dubbo.input_interface' + } + } + if (!this.method) { + return { + isValid: false, + info: 'api_test.request.dubbo.input_method' + } + } + if (!this.registryCenter.isValid()) { + return { + isValid: false, + info: 'api_test.request.dubbo.input_registry_center' + } + } + if (!this.consumerAndService.isValid()) { + return { + isValid: false, + info: 'api_test.request.dubbo.input_consumer_service' + } + } + } + return { + isValid: true + } + } + + showType() { + return "RPC"; + } + + showMethod() { + // dubbo:// -> DUBBO + return this.protocol.substr(0, this.protocol.length - 3).toUpperCase(); + } + + clone() { + return new DubboRequest(this); + } +} + +export class SqlRequest extends Request { + + constructor(options = {}) { + super(RequestFactory.TYPES.SQL, options); + this.useEnvironment = options.useEnvironment; + this.resultVariable = options.resultVariable; + this.variableNames = options.variableNames; + this.variables = []; + this.debugReport = undefined; + this.dataSource = options.dataSource; + this.query = options.query; + // this.queryType = options.queryType; + this.queryTimeout = options.queryTimeout || 60000; + + this.sets({args: KeyValue, attachmentArgs: KeyValue, variables: KeyValue}, options); + } + + isValid() { + if (this.enable) { + if (!this.name) { + return { + isValid: false, + info: 'api_test.request.sql.name_cannot_be_empty' + } + } + if (!this.dataSource) { + return { + isValid: false, + info: 'api_test.request.sql.dataSource_cannot_be_empty' + } + } + } + return { + isValid: true + } + } + + showType() { + return "SQL"; + } + + showMethod() { + return "SQL"; + } + + clone() { + return new SqlRequest(this); + } +} + +export class TCPConfig extends BaseConfig { + static CLASSES = ["TCPClientImpl", "BinaryTCPClientImpl", "LengthPrefixedBinaryTCPClientImpl"] + + constructor(options = {}) { + super(); + this.classname = options.classname || TCPConfig.CLASSES[0]; + this.server = options.server; + this.port = options.port; + this.ctimeout = options.ctimeout; // Connect + this.timeout = options.timeout; // Response + + this.reUseConnection = options.reUseConnection === undefined ? true : options.reUseConnection; + this.nodelay = options.nodelay === undefined ? false : options.nodelay; + this.closeConnection = options.closeConnection === undefined ? false : options.closeConnection; + this.soLinger = options.soLinger; + this.eolByte = options.eolByte; + + this.username = options.username; + this.password = options.password; + } +} + +export class TCPRequest extends Request { + constructor(options = {}) { + super(RequestFactory.TYPES.TCP, options); + this.useEnvironment = options.useEnvironment; + this.debugReport = undefined; + + //设置TCPConfig的属性 + this.set(new TCPConfig(options)); + + this.request = options.request; + } + + isValid() { + return { + isValid: true + } + } + + showType() { + return "TCP"; + } + + showMethod() { + return "TCP"; + } + + clone() { + return new TCPRequest(this); + } +} + + +export class ConfigCenter extends BaseConfig { + static PROTOCOLS = ["zookeeper", "nacos", "apollo"]; + + constructor(options) { + super(); + this.protocol = undefined; + this.group = undefined; + this.namespace = undefined; + this.username = undefined; + this.address = undefined; + this.password = undefined; + this.timeout = undefined; + + this.set(options); + } + + isValid() { + return !!this.protocol || !!this.group || !!this.namespace || !!this.username || !!this.address || !!this.password || !!this.timeout; + } +} + +export class DatabaseConfig extends BaseConfig { + static DRIVER_CLASS = ["com.mysql.jdbc.Driver", "com.microsoft.sqlserver.jdbc.SQLServerDriver", "org.postgresql.Driver", "oracle.jdbc.OracleDriver"]; + + constructor(options) { + super(); + this.id = undefined; + this.name = undefined; + this.poolMax = undefined; + this.timeout = undefined; + this.driver = undefined; + this.dbUrl = undefined; + this.username = undefined; + this.password = undefined; + + this.set(options); + } + + initOptions(options = {}) { + // options.id = options.id || uuid(); + return options; + } + + isValid() { + return !!this.name || !!this.poolMax || !!this.timeout || !!this.driver || !!this.dbUrl || !!this.username || !!this.password; + } +} + +export class RegistryCenter extends BaseConfig { + static PROTOCOLS = ["none", "zookeeper", "nacos", "apollo", "multicast", "redis", "simple"]; + + constructor(options) { + super(); + this.protocol = undefined; + this.group = undefined; + this.username = undefined; + this.address = undefined; + this.password = undefined; + this.timeout = undefined; + + this.set(options); + } + + isValid() { + return !!this.protocol || !!this.group || !!this.username || !!this.address || !!this.password || !!this.timeout; + } +} + +export class ConsumerAndService extends BaseConfig { + static ASYNC_OPTIONS = ["sync", "async"]; + static LOAD_BALANCE_OPTIONS = ["random", "roundrobin", "leastactive", "consistenthash"]; + + constructor(options) { + super(); + this.timeout = "1000"; + this.version = "1.0"; + this.retries = "0"; + this.cluster = "failfast"; + this.group = undefined; + this.connections = "100"; + this.async = "sync"; + this.loadBalance = "random"; + + this.set(options); + } + + isValid() { + return !!this.timeout || !!this.version || !!this.retries || !!this.cluster || !!this.group || !!this.connections || !!this.async || !!this.loadBalance; + } +} + +export class Body extends BaseConfig { + constructor(options) { + super(); + this.type = undefined; + this.raw = undefined; + this.kvs = []; + + this.set(options); + this.sets({kvs: KeyValue}, options); + } + + isValid() { + if (this.isKV()) { + return this.kvs.some(kv => { + return kv.isValid(); + }) + } else { + return !!this.raw; + } + } + + isKV() { + return this.type === BODY_TYPE.KV; + } +} + +export class KeyValue extends BaseConfig { + constructor(options) { + options = options || {}; + options.enable = options.enable === undefined ? true : options.enable; + + super(); + this.name = undefined; + this.value = undefined; + this.type = undefined; + this.files = undefined; + this.enable = undefined; + this.uuid = undefined; + this.contentType = undefined; + this.set(options); + } + + isValid() { + return (!!this.name || !!this.value) && this.type !== 'file'; + } + + isFile() { + return (!!this.name || !!this.value) && this.type === 'file'; + } +} + +export class Assertions extends BaseConfig { + constructor(options) { + super(); + this.text = []; + this.regex = []; + this.jsonPath = []; + this.duration = undefined; + + this.set(options); + this.sets({text: Text, regex: Regex, jsonPath: JSONPath}, options); + } + + initOptions(options) { + options = options || {}; + options.duration = new Duration(options.duration); + return options; + } +} + +export class AssertionType extends BaseConfig { + constructor(type) { + super(); + this.type = type; + } +} + +export class BeanShellProcessor extends BaseConfig { + constructor(options) { + super(); + this.script = undefined; + this.set(options); + } +} + + +export class JSR223Processor extends BaseConfig { + constructor(options) { + super(); + this.script = undefined; + this.language = "beanshell"; + this.set(options); + } +} + +export class Text extends AssertionType { + constructor(options) { + super(ASSERTION_TYPE.TEXT); + this.subject = undefined; + this.condition = undefined; + this.value = undefined; + + this.set(options); + } +} + +export class Regex extends AssertionType { + constructor(options) { + super(ASSERTION_TYPE.REGEX); + this.subject = undefined; + this.expression = undefined; + this.description = undefined; + this.assumeSuccess = false; + + this.set(options); + } + + isValid() { + return !!this.subject && !!this.expression; + } +} + +export class JSONPath extends AssertionType { + constructor(options) { + super(ASSERTION_TYPE.JSON_PATH); + this.expression = undefined; + this.expect = undefined; + this.description = undefined; + + this.set(options); + } + + setJSONPathDescription() { + this.description = this.expression + " expect: " + (this.expect ? this.expect : ''); + } + + isValid() { + return !!this.expression; + } +} + +export class Duration extends AssertionType { + constructor(options) { + super(ASSERTION_TYPE.DURATION); + this.value = undefined; + + this.set(options); + } + + isValid() { + return !!this.value; + } +} + +export class Extract extends BaseConfig { + constructor(options) { + super(); + this.regex = []; + this.json = []; + this.xpath = []; + + this.set(options); + let types = { + json: ExtractJSONPath, + xpath: ExtractXPath, + regex: ExtractRegex + } + this.sets(types, options); + } +} + +export class ExtractType extends BaseConfig { + constructor(type) { + super(); + this.type = type; + } +} + +export class ExtractCommon extends ExtractType { + constructor(type, options) { + super(type); + this.variable = undefined; + this.useHeaders = undefined; + this.value = ""; // ${variable} + this.expression = undefined; + this.description = undefined; + this.multipleMatching = undefined; + + this.set(options); + } + + isValid() { + return !!this.variable && !!this.expression; + } +} + +export class ExtractRegex extends ExtractCommon { + constructor(options) { + super(EXTRACT_TYPE.REGEX, options); + } +} + +export class ExtractJSONPath extends ExtractCommon { + constructor(options) { + super(EXTRACT_TYPE.JSON_PATH, options); + } +} + +export class ExtractXPath extends ExtractCommon { + constructor(options) { + super(EXTRACT_TYPE.XPATH, options); + } +} + +export class Controller extends BaseConfig { + static TYPES = { + IF_CONTROLLER: "If Controller", + } + + constructor(type, options = {}) { + super(); + this.type = type + options.id = options.id || uuid(); + options.enable = options.enable === undefined ? true : options.enable; + } +} + +export class IfController extends Controller { + constructor(options = {}) { + super(Controller.TYPES.IF_CONTROLLER, options); + this.variable; + this.operator; + this.value; + + this.set(options); + } + + isValid() { + if (!!this.operator && this.operator.indexOf("empty") > 0) { + return !!this.variable && !!this.operator; + } + return !!this.variable && !!this.operator && !!this.value; + } + + label() { + if (this.isValid()) { + let label = this.variable; + if (this.operator) label += " " + this.operator; + if (this.value) label += " " + this.value; + return label; + } + return ""; + } +} + +export class Timer extends BaseConfig { + static TYPES = { + CONSTANT_TIMER: "Constant Timer", + } + + constructor(type, options = {}) { + super(); + this.type = type; + options.id = options.id || uuid(); + options.enable = options.enable === undefined ? true : options.enable; + } +} + +export class ConstantTimer extends Timer { + constructor(options = {}) { + super(Timer.TYPES.CONSTANT_TIMER, options); + this.delay; + + this.set(options); + } + + isValid() { + return this.delay > 0; + } + + label() { + if (this.isValid()) { + return this.delay + " ms"; + } + return ""; + } +} + +/** ------------------------------------------------------------------------ **/ +const JMX_ASSERTION_CONDITION = { + MATCH: 1, + CONTAINS: 1 << 1, + NOT: 1 << 2, + EQUALS: 1 << 3, + SUBSTRING: 1 << 4, + OR: 1 << 5 +} + +class JMXHttpRequest { + constructor(request, environment) { + if (request && request instanceof HttpRequest) { + this.useEnvironment = request.useEnvironment; + this.method = request.method; + if (!request.useEnvironment) { + if (!request.url.startsWith("http://") && !request.url.startsWith("https://")) { + request.url = 'http://' + request.url; + } + let url = new URL(request.url); + this.domain = decodeURIComponent(url.hostname); + this.port = url.port; + this.protocol = url.protocol.split(":")[0]; + this.path = this.getPostQueryParameters(request, decodeURIComponent(url.pathname)); + } else { + this.domain = environment.config.httpConfig.domain; + this.port = environment.config.httpConfig.port; + this.protocol = environment.config.httpConfig.protocol; + let url = new URL(environment.config.httpConfig.protocol + "://" + environment.config.httpConfig.socket); + this.path = this.getPostQueryParameters(request, decodeURIComponent(url.pathname + (request.path ? request.path : ''))); + } + this.connectTimeout = request.connectTimeout; + this.responseTimeout = request.responseTimeout; + this.followRedirects = request.followRedirects; + this.doMultipartPost = request.doMultipartPost; + } + } + + getPostQueryParameters(request, path) { + if (this.method.toUpperCase() !== "GET") { + let parameters = []; + request.parameters.forEach(parameter => { + if (parameter.name && parameter.value && parameter.enable === true) { + parameters.push(parameter); + } + }); + if (parameters.length > 0) { + path += '?'; + } + for (let i = 0; i < parameters.length; i++) { + let parameter = parameters[i]; + path += (parameter.name + '=' + parameter.value); + if (i !== parameters.length - 1) { + path += '&'; + } + } + } + return path; + } +} + +class JMXDubboRequest { + constructor(request, dubboConfig) { + // Request 复制 + let obj = request.clone(); + // 去掉无效的kv + obj.args = obj.args.filter(arg => { + return arg.isValid(); + }); + obj.attachmentArgs = obj.attachmentArgs.filter(arg => { + return arg.isValid(); + }); + + // Scenario DubboConfig复制 + this.copy(obj.configCenter, dubboConfig.configCenter); + this.copy(obj.registryCenter, dubboConfig.registryCenter); + this.copy(obj.consumerAndService, dubboConfig.consumerAndService); + + return obj; + } + + copy(target, source) { + for (let key in source) { + if (source.hasOwnProperty(key)) { + if (source[key] !== undefined && !target[key]) { + target[key] = source[key]; + } + } + } + } +} + +class JMXTCPRequest { + constructor(request, scenario) { + let obj = request.clone(); + if (request.useEnvironment) { + obj.set(scenario.environment.config.tcpConfig, true); + return obj; + } + + this.copy(this, scenario.tcpConfig); + + return obj; + } + + copy(target, source) { + for (let key in source) { + if (source.hasOwnProperty(key)) { + if (source[key] !== undefined && !target[key]) { + target[key] = source[key]; + } + } + } + } +} + +class JMeterTestPlan extends Element { + constructor() { + super('jmeterTestPlan', { + version: "1.2", properties: "5.0", jmeter: "5.2.1" + }); + + this.add(new HashTree()); + } + + put(te) { + if (te instanceof TestElement) { + this.elements[0].add(te); + } + } +} + +class JMXGenerator { + constructor(test) { + if (!test || !test.id || !(test instanceof Test)) return undefined; + + let testPlan = new TestPlan(test.name); + this.addScenarios(testPlan, test.id, test.scenarioDefinition); + + this.jmeterTestPlan = new JMeterTestPlan(); + this.jmeterTestPlan.put(testPlan); + } + + addScenarios(testPlan, testId, scenarios) { + scenarios.forEach(s => { + + if (s.enable) { + let scenario = s.clone(); + + let threadGroup = new ThreadGroup(scenario.name || ""); + + this.addScenarioVariables(threadGroup, scenario); + + this.addScenarioHeaders(threadGroup, scenario); + + this.addScenarioCookieManager(threadGroup, scenario); + + this.addJDBCDataSources(threadGroup, scenario); + scenario.requests.forEach(request => { + if (request.enable) { + if (!request.isValid()) return; + let sampler; + if (request instanceof DubboRequest) { + sampler = new DubboSample(request.name || "", new JMXDubboRequest(request, scenario.dubboConfig)); + } else if (request instanceof HttpRequest) { + sampler = new HTTPSamplerProxy(request.name || "", new JMXHttpRequest(request, scenario.environment)); + this.addRequestHeader(sampler, request); + this.addRequestArguments(sampler, request); + this.addRequestBody(sampler, request, testId); + } else if (request instanceof SqlRequest) { + request.dataSource = scenario.databaseConfigMap.get(request.dataSource); + sampler = new JDBCSampler(request.name || "", request); + this.addRequestVariables(sampler, request); + } else if (request instanceof TCPRequest) { + sampler = new TCPSampler(request.name || "", new JMXTCPRequest(request, scenario)); + } + + this.addDNSCacheManager(sampler, scenario.environment, request.useEnvironment); + + this.addRequestExtractor(sampler, request); + + this.addRequestAssertion(sampler, request); + + this.addJSR223PreProcessor(sampler, request); + + this.addConstantsTimer(sampler, request); + + if (request.controller && request.controller.isValid() && request.controller.enable) { + if (request.controller instanceof IfController) { + let controller = this.getController(sampler, request); + threadGroup.put(controller); + } + } else { + threadGroup.put(sampler); + } + } + }) + testPlan.put(threadGroup); + } + + }) + } + + addEnvironments(environments, target) { + let keys = new Set(); + target.forEach(item => { + keys.add(item.name); + }); + let envArray = environments; + if (!(envArray instanceof Array)) { + envArray = JSON.parse(environments); + } + envArray.forEach(item => { + if (item.name && !keys.has(item.name)) { + target.push(new KeyValue({name: item.name, value: item.value})); + } + }) + } + + addScenarioVariables(threadGroup, scenario) { + if (scenario.environment) { + let config = scenario.environment.config; + if (!(scenario.environment.config instanceof Object)) { + config = JSON.parse(scenario.environment.config); + } + this.addEnvironments(config.commonConfig.variables, scenario.variables) + } + let args = this.filterKV(scenario.variables); + if (args.length > 0) { + let name = scenario.name + " Variables"; + threadGroup.put(new Arguments(name, args)); + } + } + + addRequestVariables(httpSamplerProxy, request) { + let name = request.name + " Variables"; + let variables = this.filterKV(request.variables); + if (variables && variables.length > 0) { + httpSamplerProxy.put(new Arguments(name, variables)); + } + } + + addScenarioCookieManager(threadGroup, scenario) { + if (scenario.enableCookieShare) { + threadGroup.put(new CookieManager(scenario.name)); + } + } + + addDNSCacheManager(httpSamplerProxy, environment, useEnv) { + if (environment && useEnv === true) { + let commonConfig = environment.config.commonConfig; + let hosts = commonConfig.hosts; + if (commonConfig.enableHost && hosts.length > 0) { + let name = " DNSCacheManager"; + // 强化判断,如果未匹配到合适的host则不开启DNSCache + let domain = environment.config.httpConfig.domain; + let validHosts = []; + hosts.forEach(item => { + if (item.domain !== undefined && domain !== undefined) { + let d = item.domain.trim().replace("http://", "").replace("https://", ""); + if (d === domain.trim()) { + item.domain = d; // 域名去掉协议 + validHosts.push(item); + } + } + }); + if (validHosts.length > 0) { + httpSamplerProxy.put(new DNSCacheManager(name, validHosts)); + } + } + } + } + + addJDBCDataSources(threadGroup, scenario) { + let names = new Set(); + let databaseConfigMap = new Map(); + scenario.databaseConfigs.forEach(config => { + let name = config.name + "JDBCDataSource"; + threadGroup.put(new JDBCDataSource(name, config)); + names.add(name); + databaseConfigMap.set(config.id, config.name); + }); + if (scenario.environment) { + let config = scenario.environment.config; + if (!(scenario.environment.config instanceof Object)) { + config = JSON.parse(scenario.environment.config); + } + config.databaseConfigs.forEach(config => { + if (!names.has(config.name)) { + let name = config.name + "JDBCDataSource"; + threadGroup.put(new JDBCDataSource(name, config)); + databaseConfigMap.set(config.id, config.name); + } + }); + } + scenario.databaseConfigMap = databaseConfigMap; + } + + addScenarioHeaders(threadGroup, scenario) { + if (scenario.environment) { + let config = scenario.environment.config; + if (!(scenario.environment.config instanceof Object)) { + config = JSON.parse(scenario.environment.config); + } + this.addEnvironments(config.httpConfig.headers, scenario.headers) + } + let headers = this.filterKV(scenario.headers); + if (headers.length > 0) { + let name = scenario.name + " Headers"; + threadGroup.put(new HeaderManager(name, headers)); + } + } + + addRequestHeader(httpSamplerProxy, request) { + let name = request.name + " Headers"; + this.addBodyFormat(request); + let headers = this.filterKV(request.headers); + if (headers.length > 0) { + httpSamplerProxy.put(new HeaderManager(name, headers)); + } + } + + addJSR223PreProcessor(sampler, request) { + let name = request.name; + if (request.jsr223PreProcessor && request.jsr223PreProcessor.script) { + sampler.put(new JSR223PreProcessor(name, request.jsr223PreProcessor)); + } + if (request.jsr223PostProcessor && request.jsr223PostProcessor.script) { + sampler.put(new JSR223PostProcessor(name, request.jsr223PostProcessor)); + } + } + + addConstantsTimer(sampler, request) { + if (request.timer && request.timer.isValid() && request.timer.enable) { + sampler.put(new JMXConstantTimer(request.timer.label(), request.timer)); + } + } + + getController(sampler, request) { + if (request.controller.isValid() && request.controller.enable) { + if (request.controller instanceof IfController) { + let name = request.controller.label(); + let variable = "\"" + request.controller.variable + "\""; + let operator = request.controller.operator; + let value = "\"" + request.controller.value + "\""; + + if (operator === "=~" || operator === "!~") { + value = "\".*" + request.controller.value + ".*\""; + } + + if (operator === "is empty") { + variable = "empty(" + variable + ")"; + operator = ""; + value = ""; + } + + if (operator === "is not empty") { + variable = "!empty(" + variable + ")"; + operator = ""; + value = ""; + } + + let condition = "${__jexl3(" + variable + operator + value + ")}"; + let controller = new JMXIfController(name, {condition: condition}); + controller.put(sampler); + return controller; + } + } + } + + addBodyFormat(request) { + let bodyFormat = request.body.format; + if (!request.body.isKV() && bodyFormat) { + switch (bodyFormat) { + case BODY_FORMAT.JSON: + this.addContentType(request, 'application/json'); + break; + case BODY_FORMAT.HTML: + this.addContentType(request, 'text/html'); + break; + case BODY_FORMAT.XML: + this.addContentType(request, 'text/xml'); + break; + default: + break; + } + } + } + + addContentType(request, type) { + for (let index in request.headers) { + if (request.headers.hasOwnProperty(index)) { + if (request.headers[index].name === 'Content-Type') { + request.headers.splice(index, 1); + break; + } + } + } + request.headers.push(new KeyValue({name: 'Content-Type', value: type})); + } + + addRequestArguments(httpSamplerProxy, request) { + let args = this.filterKV(request.parameters); + if (args.length > 0) { + httpSamplerProxy.add(new HTTPSamplerArguments(args)); + } + } + + addRequestBody(httpSamplerProxy, request, testId) { + let body = []; + if (request.body.isKV()) { + body = this.filterKV(request.body.kvs); + this.addRequestBodyFile(httpSamplerProxy, request, testId); + } else { + httpSamplerProxy.boolProp('HTTPSampler.postBodyRaw', true); + body.push({name: '', value: request.body.raw, encode: false, enable: true}); + } + + if (request.method !== 'GET') { + httpSamplerProxy.add(new HTTPSamplerArguments(body)); + } + } + + addRequestBodyFile(httpSamplerProxy, request, testId) { + let files = []; + let kvs = this.filterKVFile(request.body.kvs); + kvs.forEach(kv => { + if ((kv.enable !== false) && kv.files) { + kv.files.forEach(file => { + let arg = {}; + arg.name = kv.name; + arg.value = BODY_FILE_DIR + '/' + testId + '/' + file.id + '_' + file.name; + files.push(arg); + }); + } + }); + httpSamplerProxy.add(new HTTPsamplerFiles(files)); + } + + addRequestAssertion(httpSamplerProxy, request) { + let assertions = request.assertions; + if (assertions.regex.length > 0) { + assertions.regex.filter(this.filter).forEach(regex => { + httpSamplerProxy.put(this.getResponseAssertion(regex)); + }) + } + + if (assertions.jsonPath.length > 0) { + assertions.jsonPath.filter(this.filter).forEach(item => { + httpSamplerProxy.put(this.getJSONPathAssertion(item)); + }) + } + + if (assertions.duration.isValid()) { + let name = "Response In Time: " + assertions.duration.value + httpSamplerProxy.put(new DurationAssertion(name, assertions.duration.value)); + } + } + + getJSONPathAssertion(jsonPath) { + let name = jsonPath.description; + return new JSONPathAssertion(name, jsonPath); + } + + getResponseAssertion(regex) { + let name = regex.description; + let type = JMX_ASSERTION_CONDITION.CONTAINS; // 固定用Match,自己写正则 + let value = regex.expression; + let assumeSuccess = regex.assumeSuccess; + switch (regex.subject) { + case ASSERTION_REGEX_SUBJECT.RESPONSE_CODE: + return new ResponseCodeAssertion(name, type, value, assumeSuccess); + case ASSERTION_REGEX_SUBJECT.RESPONSE_DATA: + return new ResponseDataAssertion(name, type, value, assumeSuccess); + case ASSERTION_REGEX_SUBJECT.RESPONSE_HEADERS: + return new ResponseHeadersAssertion(name, type, value, assumeSuccess); + } + } + + addRequestExtractor(httpSamplerProxy, request) { + let extract = request.extract; + if (extract.regex.length > 0) { + extract.regex.filter(this.filter).forEach(regex => { + httpSamplerProxy.put(this.getExtractor(regex)); + }) + } + + if (extract.json.length > 0) { + extract.json.filter(this.filter).forEach(json => { + httpSamplerProxy.put(this.getExtractor(json)); + }) + } + + if (extract.xpath.length > 0) { + extract.xpath.filter(this.filter).forEach(xpath => { + httpSamplerProxy.put(this.getExtractor(xpath)); + }) + } + } + + getExtractor(extractCommon) { + let props = { + name: extractCommon.variable, + expression: extractCommon.expression, + match: extractCommon.multipleMatching ? -1 : undefined + } + let testName = props.name + switch (extractCommon.type) { + case EXTRACT_TYPE.REGEX: + testName += " RegexExtractor"; + props.headers = extractCommon.useHeaders; // 对应jMeter body + props.template = "$1$"; + return new RegexExtractor(testName, props); + case EXTRACT_TYPE.JSON_PATH: + testName += " JSONExtractor"; + return new JSONPostProcessor(testName, props); + case EXTRACT_TYPE.XPATH: + testName += " XPath2Evaluator"; + return new XPath2Extractor(testName, props); + } + } + + filter(config) { + return config.isValid(); + } + + filterKV(kvs) { + return kvs.filter(this.filter); + } + + filterKVFile(kvs) { + return kvs.filter(kv => { + return kv.isFile(); + }); + } + + toXML() { + let xml = '\n'; + xml += this.jmeterTestPlan.toXML(); + return xml; + } +} + + diff --git a/frontend/src/business/components/api/head/ApiHeaderMenus.vue b/frontend/src/business/components/api/head/ApiHeaderMenus.vue index 5e1e6f1d1f..c2e74b4993 100644 --- a/frontend/src/business/components/api/head/ApiHeaderMenus.vue +++ b/frontend/src/business/components/api/head/ApiHeaderMenus.vue @@ -7,6 +7,10 @@ {{ $t("i18n.home") }} + + {{ $t("i18n.delimit") }} + + diff --git a/frontend/src/business/components/api/router.js b/frontend/src/business/components/api/router.js index 672d9faf7e..687d2585e2 100644 --- a/frontend/src/business/components/api/router.js +++ b/frontend/src/business/components/api/router.js @@ -38,6 +38,11 @@ export default { path: "report/view/:reportId", name: "ApiReportView", component: () => import('@/business/components/api/report/ApiReportView'), + }, + { + path: "delimit", + name: "ApiDelimit", + component: () => import('@/business/components/api/delimit/ApiDelimit'), } ] } diff --git a/frontend/src/i18n/en-US.js b/frontend/src/i18n/en-US.js index 6f9023742d..fabf29d998 100644 --- a/frontend/src/i18n/en-US.js +++ b/frontend/src/i18n/en-US.js @@ -1,6 +1,6 @@ export default { commons: { - comment:'comment', + comment: 'comment', examples: 'examples', help_documentation: 'Help documentation', delete_cancelled: 'Delete cancelled', @@ -216,8 +216,8 @@ export default { select: 'Select Organization', service_integration: 'Service integration', defect_manage: 'Defect management platform', - message_settings:'Message settings', - message:{ + message_settings: 'Message settings', + message: { jenkins_task_notification: 'Jenkins task notification', test_plan_task_notification: 'Test plan task notification', test_review_task_notice: 'Test review task notice', @@ -454,6 +454,33 @@ export default { file_exist: "The name already exists in the project", upload_limit_size: "Upload file size cannot exceed 30MB!", }, + delimit: { + api_title: "Api test", + api_name: "Api name", + api_status: "Api status", + api_type: "Api type", + api_path: "Api path", + api_principal: "Api principal", + api_last_time: "Last update time", + api_case_number: "Number use case", + api_case_status: "Ise case status", + api_case_passing_rate: "Use case pass rate", + request: { + grade_info: "From high to low", + run_env: "Operating environment", + select_case: "Search use cases", + case: "Case", + title: "Create api", + path_info:"Please enter the URL of the interface, such as /api/demo/#{id}, where id is the path parameter", + fast_debug: "Fast debug", + close_all_label: "close all label", + save_as: "Save as new interface", + load_case: "Load use case", + save_as_case: "Save as new use case", + update_api: "Update interface", + + } + }, environment: { name: "Environment Name", socket: "Socket", @@ -984,7 +1011,8 @@ export default { }, i18n: { - home: 'Home' + home: 'Home', + delimit: 'ApiDelimit', }, ldap: { url: 'LDAP URL', diff --git a/frontend/src/i18n/zh-CN.js b/frontend/src/i18n/zh-CN.js index 61d57cdd58..af91a68cbf 100644 --- a/frontend/src/i18n/zh-CN.js +++ b/frontend/src/i18n/zh-CN.js @@ -1,6 +1,6 @@ export default { commons: { - comment:'评论', + comment: '评论', examples: '示例', help_documentation: '帮助文档', delete_cancelled: '已取消删除', @@ -217,8 +217,8 @@ export default { delete_warning: '删除该组织将同步删除该组织下所有相关工作空间和相关工作空间下的所有项目,以及项目中的所有用例、接口测试、性能测试等,确定要删除吗?', service_integration: '服务集成', defect_manage: '缺陷管理平台', - message_settings:'消息设置', - message:{ + message_settings: '消息设置', + message: { jenkins_task_notification: 'Jenkins接口调用任务通知', test_plan_task_notification: '测试计划任务通知', test_review_task_notice: '测试评审任务通知', @@ -454,6 +454,33 @@ export default { file_exist: "该项目下已存在改jar包", upload_limit_size: "上传文件大小不能超过 30MB!", }, + delimit: { + api_title: "接口列表", + api_name: "接口名称", + api_status: "接口状态", + api_type: "请求类型", + api_path: "路径", + api_principal: "负责人", + api_last_time: "最后更新时间", + api_case_number: "用例数", + api_case_status: "用例状态", + api_case_passing_rate: "用例通过率", + request: { + grade_info: "按等级从高到低", + run_env: "运行环境", + select_case: "搜索用例", + case: "用例", + responsible: "责任人", + title: "创建接口", + path_info: "请输入接口的URL,如/api/demo/#{id},其中id为路径参数", + fast_debug: "快捷调试", + close_all_label: "关闭所有标签", + save_as: "另存为新接口", + load_case: "加载用例", + save_as_case: "另存为新用例", + update_api: "更新接口", + } + }, environment: { name: "环境名称", socket: "环境域名", @@ -983,7 +1010,8 @@ export default { account: '账户不能为空', }, i18n: { - home: '首页' + home: '首页', + delimit: '接口定义', }, ldap: { url: 'LDAP地址', diff --git a/frontend/src/i18n/zh-TW.js b/frontend/src/i18n/zh-TW.js index 491e41cfc5..6cd1620dd8 100644 --- a/frontend/src/i18n/zh-TW.js +++ b/frontend/src/i18n/zh-TW.js @@ -1,6 +1,6 @@ export default { commons: { - comment:'評論', + comment: '評論', examples: '示例', help_documentation: '幫助文檔', delete_cancelled: '已取消刪除', @@ -217,8 +217,8 @@ export default { delete_warning: '刪除該組織將同步刪除該組織下所有相關工作空間和相關工作空間下的所有項目,以及項目中的所有用例、接口測試、性能測試等,確定要刪除嗎?', service_integration: '服務集成', defect_manage: '缺陷管理平臺', - message_settings:'消息設置', - message:{ + message_settings: '消息設置', + message: { jenkins_task_notification: 'Jenkins接口調用任務通知', test_plan_task_notification: '測試計劃任務通知', test_review_task_notice: '測試評審任務通知', @@ -230,8 +230,8 @@ export default { nail_robot: '釘釘機器人', enterprise_wechat_robot: '企業微信機器人', notes: '註意: 1.事件,接收方式,接收人為必填項;\n' + - ' 2.接收方式除郵件外webhook為必填;\n' + - ' 3.機器人選擇為群機器人,安全驗證選擇“自定義關鍵詞” :"任務通知"', + ' 2.接收方式除郵件外webhook為必填;\n' + + ' 3.機器人選擇為群機器人,安全驗證選擇“自定義關鍵詞” :"任務通知"', message: '事件,接收人,接收方式為必填項', message_webhook: '接收方式為釘釘和企業機器人時,webhook為必填項' @@ -454,6 +454,35 @@ export default { file_exist: "該項目下已存在改jar包", upload_limit_size: "上傳文件大小不能超過 30MB!", }, + delimit: { + api_title: "接口列表", + api_name: "接口名稱", + api_status: "接口狀態", + api_type: "請求類型", + api_path: "路徑", + api_principal: "負責人", + api_last_time: "最後更新時間", + api_case_number: "用例數", + api_case_status: "用例狀態", + api_case_passing_rate: "用例通過率", + request: { + grade_info: "按等級從高到低", + run_env: "運行環境", + select_case: "搜索用例", + case: "用例", + responsible: "责任人", + title: "创建接口", + path_info:"請輸入接口的URL,如/api/demo/#{id},其中id為路徑參數", + fast_debug: "快捷調試", + close_all_label: "關閉所有標簽", + save_as: "另存為新接口", + load_case: "加载用例", + save_as_case: "另存為新用例", + update_api: "更新接口", + + } + + }, environment: { name: "環境名稱", socket: "環境域名", @@ -983,7 +1012,8 @@ export default { account: '賬戶不能為空', }, i18n: { - home: '首頁' + home: '首頁', + delimit: '接口定義', }, ldap: { url: 'LDAP地址',