From 316394076bf83348b4516b771f3ef400d6a22eda Mon Sep 17 00:00:00 2001 From: fit2-zhao Date: Thu, 4 Nov 2021 12:54:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=8E=A5=E5=8F=A3=E5=AE=9A=E4=B9=89):=20?= =?UTF-8?q?=E6=96=AD=E8=A8=80-=E6=96=87=E6=A1=A3=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E5=9F=BA=E6=9C=AC=E5=8A=9F=E8=83=BD=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pom.xml | 6 + .../controller/ApiDefinitionController.java | 17 +- .../request/assertions/MsAssertions.java | 25 + .../assertions/document/Condition.java | 18 + .../request/assertions/document/Document.java | 170 +++++++ .../assertions/document/DocumentElement.java | 101 ++++ .../assertions/document/ElementCondition.java | 24 + .../document/MsAssertionDocument.java | 9 + .../api/service/ApiDefinitionService.java | 55 ++- .../json/JSONSchemaToDocumentUtils.java | 213 ++++++++ .../commons/json/JSONToDocumentUtils.java | 110 +++++ .../jmeter/assertions/JSONPathAssertion.java | 70 ++- .../jmeter/assertions/XMLAssertion.java | 254 ++++++++++ .../components/assertion/ApiAssertions.vue | 89 +++- .../assertion/ApiAssertionsEdit.vue | 199 ++++---- .../assertion/document/DocumentBody.vue | 461 ++++++++++++++++++ .../assertion/document/DocumentHeader.vue | 96 ++++ .../assertion/document/DocumentImport.vue | 117 +++++ .../definition/components/body/ApiBody.vue | 2 + .../components/runtest/RunTestDubboPage.vue | 2 +- .../components/runtest/RunTestHTTPPage.vue | 2 +- .../components/runtest/RunTestSQLPage.vue | 2 +- .../components/runtest/RunTestTCPPage.vue | 2 +- .../definition/components/step/JmxStep.vue | 2 + .../api/definition/model/ApiTestModel.js | 9 +- frontend/src/i18n/en-US.js | 1 + frontend/src/i18n/zh-CN.js | 1 + frontend/src/i18n/zh-TW.js | 1 + 28 files changed, 1929 insertions(+), 129 deletions(-) create mode 100644 backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/Condition.java create mode 100644 backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/Document.java create mode 100644 backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/DocumentElement.java create mode 100644 backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/ElementCondition.java create mode 100644 backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/MsAssertionDocument.java create mode 100644 backend/src/main/java/io/metersphere/commons/json/JSONSchemaToDocumentUtils.java create mode 100644 backend/src/main/java/io/metersphere/commons/json/JSONToDocumentUtils.java create mode 100644 backend/src/main/java/org/apache/jmeter/assertions/XMLAssertion.java create mode 100644 frontend/src/business/components/api/definition/components/assertion/document/DocumentBody.vue create mode 100644 frontend/src/business/components/api/definition/components/assertion/document/DocumentHeader.vue create mode 100644 frontend/src/business/components/api/definition/components/assertion/document/DocumentImport.vue diff --git a/backend/pom.xml b/backend/pom.xml index 8f94e67f1f..a6f9b95bfb 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -457,6 +457,12 @@ generex 1.0.2 + + + net.sf.json-lib + json-lib + 2.4 + diff --git a/backend/src/main/java/io/metersphere/api/controller/ApiDefinitionController.java b/backend/src/main/java/io/metersphere/api/controller/ApiDefinitionController.java index 11c00685dd..b6ac8bd99e 100644 --- a/backend/src/main/java/io/metersphere/api/controller/ApiDefinitionController.java +++ b/backend/src/main/java/io/metersphere/api/controller/ApiDefinitionController.java @@ -8,6 +8,8 @@ import io.metersphere.api.dto.automation.ApiScenarioRequest; import io.metersphere.api.dto.automation.ReferenceDTO; import io.metersphere.api.dto.definition.*; import io.metersphere.api.dto.definition.parse.ApiDefinitionImport; +import io.metersphere.api.dto.definition.request.assertions.document.DocumentElement; +import io.metersphere.api.dto.scenario.Body; import io.metersphere.api.dto.swaggerurl.SwaggerTaskResult; import io.metersphere.api.dto.swaggerurl.SwaggerUrlRequest; import io.metersphere.api.service.ApiDefinitionService; @@ -21,6 +23,7 @@ import io.metersphere.commons.constants.NoticeConstants; import io.metersphere.commons.constants.OperLogConstants; import io.metersphere.commons.constants.PermissionConstants; import io.metersphere.commons.json.JSONSchemaGenerator; +import io.metersphere.commons.json.JSONToDocumentUtils; import io.metersphere.commons.utils.PageUtils; import io.metersphere.commons.utils.Pager; import io.metersphere.controller.request.ResetOrderRequest; @@ -61,6 +64,7 @@ public class ApiDefinitionController { Page page = PageHelper.startPage(goPage, pageSize, true); return PageUtils.setPageInfo(page, apiDefinitionService.list(request)); } + @PostMapping("/week/list/{goPage}/{pageSize}") @RequiresPermissions("PROJECT_API_DEFINITION:READ") public Pager> weekList(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody ApiDefinitionRequest request) { @@ -319,7 +323,7 @@ public class ApiDefinitionController { } @PostMapping("/relationship/relate/{goPage}/{pageSize}") - public Pager< List> getRelationshipRelateList(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody ApiDefinitionRequest request) { + public Pager> getRelationshipRelateList(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody ApiDefinitionRequest request) { return apiDefinitionService.getRelationshipRelateList(request, goPage, pageSize); } @@ -327,4 +331,15 @@ public class ApiDefinitionController { public List getFollows(@PathVariable String definitionId) { return apiDefinitionService.getFollows(definitionId); } + + @GetMapping("/getDocument/{id}/{type}") + public List getDocument(@PathVariable String id,@PathVariable String type) { + return apiDefinitionService.getDocument(id,type); + } + + @PostMapping("/jsonGenerator") + public List jsonGenerator(@RequestBody Body body) { + return JSONToDocumentUtils.getDocument(body.getRaw(),body.getType()); + } + } diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/MsAssertions.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/MsAssertions.java index af4f7851f6..a0dc1077a9 100644 --- a/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/MsAssertions.java +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/MsAssertions.java @@ -2,6 +2,9 @@ package io.metersphere.api.dto.definition.request.assertions; import com.alibaba.fastjson.annotation.JSONType; import io.metersphere.api.dto.definition.request.ParameterConfig; +import io.metersphere.api.dto.definition.request.assertions.document.MsAssertionDocument; +import io.metersphere.api.service.ApiDefinitionService; +import io.metersphere.commons.utils.CommonBeanFactory; import io.metersphere.plugin.core.MsParameter; import io.metersphere.plugin.core.MsTestElement; import lombok.Data; @@ -27,6 +30,7 @@ public class MsAssertions extends MsTestElement { private List xpath2; private MsAssertionDuration duration; private String type = "Assertions"; + private MsAssertionDocument document; @Override public void toHashTree(HashTree tree, List hashTree, MsParameter msParameter) { @@ -39,6 +43,27 @@ public class MsAssertions extends MsTestElement { } private void addAssertions(HashTree hashTree) { + // 增加JSON文档结构校验 + if (this.getDocument() != null && this.getDocument().getType().equals("JSON")) { + if (StringUtils.isNotEmpty(this.getDocument().getData().getJsonFollowAPI())) { + ApiDefinitionService apiDefinitionService = CommonBeanFactory.getBean(ApiDefinitionService.class); + this.getDocument().getData().setJson(apiDefinitionService.getDocument(this.getDocument().getData().getJsonFollowAPI(), "JSON")); + } + if (CollectionUtils.isNotEmpty(this.getDocument().getData().getJson())) { + this.getDocument().getData().parseJson(hashTree, this.getName()); + } + } + // 增加XML文档结构校验 + if (this.getDocument() != null && this.getDocument().getType().equals("XML") && CollectionUtils.isNotEmpty(this.getDocument().getData().getXml())) { + if (StringUtils.isNotEmpty(this.getDocument().getData().getXmlFollowAPI())) { + ApiDefinitionService apiDefinitionService = CommonBeanFactory.getBean(ApiDefinitionService.class); + this.getDocument().getData().setXml(apiDefinitionService.getDocument(this.getDocument().getData().getXmlFollowAPI(), "XML")); + } + if (CollectionUtils.isNotEmpty(this.getDocument().getData().getXml())) { + this.getDocument().getData().parseXml(hashTree, this.getName()); + } + } + if (CollectionUtils.isNotEmpty(this.getRegex())) { this.getRegex().stream().filter(MsAssertionRegex::isValid).forEach(assertion -> hashTree.add(responseAssertion(assertion)) diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/Condition.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/Condition.java new file mode 100644 index 0000000000..8a4743cc24 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/Condition.java @@ -0,0 +1,18 @@ +package io.metersphere.api.dto.definition.request.assertions.document; + +import lombok.Data; + +@Data +public class Condition { + private String key; + private Object value; + + public Condition() { + + } + + public Condition(String key, Object value) { + this.key = key; + this.value = value; + } +} diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/Document.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/Document.java new file mode 100644 index 0000000000..8ccfa03951 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/Document.java @@ -0,0 +1,170 @@ +package io.metersphere.api.dto.definition.request.assertions.document; + +import com.alibaba.fastjson.JSON; +import lombok.Data; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.jmeter.assertions.JSONPathAssertion; +import org.apache.jmeter.assertions.XMLAssertion; +import org.apache.jmeter.save.SaveService; +import org.apache.jmeter.testelement.TestElement; +import org.apache.jorphan.collections.HashTree; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +@Data +public class Document { + private String jsonFollowAPI; + private String xmlFollowAPI; + private List json; + private List xml; + private String assertionName; + + public void parseJson(HashTree hashTree, String name) { + this.assertionName = name; + // 提取出合并的权限 + Map conditionMap = new HashMap<>(); + conditions(this.getJson(), conditionMap); + // 数据处理生成断言条件 + List list = new LinkedList<>(); + formatting(this.getJson(), list, null, conditionMap); + + if (CollectionUtils.isNotEmpty(list)) { + hashTree.add(list); + } + } + + public void parseXml(HashTree hashTree, String name) { + this.assertionName = name; + // 提取出合并的权限 + Map conditionMap = new HashMap<>(); + conditions(this.getXml(), conditionMap); + // 数据处理生成断言条件 + List list = new LinkedList<>(); + xmlFormatting(this.getXml(), list, null, conditionMap); + + if (CollectionUtils.isNotEmpty(list)) { + hashTree.add(list); + } + } + + private void conditions(List dataList, Map conditionMap) { + dataList.forEach(item -> { + if (StringUtils.isEmpty(item.getGroupId())) { + conditionMap.put(item.getId(), + new ElementCondition(item.isInclude(), item.isTypeVerification(), item.isArrayVerification(), + new LinkedList() {{ + this.add(new Condition(item.getContentVerification(), item.getExpectedOutcome())); + }})); + } else { + if (conditionMap.containsKey(item.getGroupId())) { + conditionMap.get(item.getGroupId()).getConditions().add(new Condition(item.getContentVerification(), item.getExpectedOutcome())); + } else { + conditionMap.put(item.getGroupId(), + new ElementCondition(item.isInclude(), item.isTypeVerification(), item.isArrayVerification(), + new LinkedList() {{ + this.add(new Condition(item.getContentVerification(), item.getExpectedOutcome())); + }})); + } + } + if (CollectionUtils.isNotEmpty(item.getChildren())) { + conditions(item.getChildren(), conditionMap); + } + }); + } + + public void formatting(List dataList, List list, DocumentElement parentNode, Map conditionMap) { + for (DocumentElement item : dataList) { + if (StringUtils.isEmpty(item.getGroupId())) { + if (!item.getId().equals("root")) { + if (parentNode != null) { + item.setJsonPath(parentNode.getJsonPath() + "." + item.getName()); + } else { + item.setJsonPath("$." + item.getName()); + } + if (!StringUtils.equalsAny(item.getContentVerification(), "none", null) || item.isInclude()) { + list.add(newJSONPathAssertion(item, conditionMap.get(item.getId()))); + } + if (CollectionUtils.isNotEmpty(item.getChildren())) { + formatting(item.getChildren(), list, item, conditionMap); + } + } else { + if (CollectionUtils.isNotEmpty(item.getChildren())) { + formatting(item.getChildren(), list, null, conditionMap); + } + } + } + } + } + + public void xmlFormatting(List dataList, List list, DocumentElement parentNode, Map conditionMap) { + for (DocumentElement item : dataList) { + if (StringUtils.isEmpty(item.getGroupId())) { + if (parentNode != null) { + item.setJsonPath(parentNode.getJsonPath() + "." + item.getName()); + } else { + item.setJsonPath("$." + item.getName()); + } + if (!StringUtils.equalsAny(item.getContentVerification(), "none", null) || item.isInclude()) { + list.add(newXMLAssertion(item, conditionMap.get(item.getId()))); + } + if (CollectionUtils.isNotEmpty(item.getChildren())) { + xmlFormatting(item.getChildren(), list, item, conditionMap); + } + } + } + } + + private String getConditionStr(DocumentElement item, ElementCondition elementCondition) { + StringBuilder conditionStr = new StringBuilder(); + if (elementCondition != null && CollectionUtils.isNotEmpty(elementCondition.getConditions())) { + elementCondition.getConditions().forEach(condition -> { + conditionStr.append(item.getLabel(item.getContentVerification()).replace("'%'", (item.getExpectedOutcome() != null ? item.getExpectedOutcome().toString() : ""))); + conditionStr.append(" and "); + }); + } + String label = ""; + if (StringUtils.isNotEmpty(conditionStr.toString())) { + label = conditionStr.toString().substring(0, conditionStr.toString().length() - 4); + } + return label; + } + + private JSONPathAssertion newJSONPathAssertion(DocumentElement item, ElementCondition elementCondition) { + JSONPathAssertion assertion = new JSONPathAssertion(); + assertion.setEnabled(true); + assertion.setJsonValidationBool(true); + assertion.setExpectNull(false); + assertion.setInvert(false); + + assertion.setName((StringUtils.isNotEmpty(assertionName) ? assertionName : "DocumentAssertion") + ("==" + item.getJsonPath() + " " + getConditionStr(item, elementCondition))); + assertion.setProperty(TestElement.TEST_CLASS, JSONPathAssertion.class.getName()); + assertion.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("JSONPathAssertionGui")); + assertion.setJsonPath(item.getJsonPath()); + assertion.setExpectedValue(item.getExpectedOutcome() != null ? item.getExpectedOutcome().toString() : ""); + assertion.setProperty("ASS_OPTION", "DOCUMENT"); + assertion.setProperty("ElementCondition", JSON.toJSONString(elementCondition)); + + if (StringUtils.isEmpty(item.getContentVerification()) || "regular".equals(item.getContentVerification())) { + assertion.setIsRegex(true); + } else { + assertion.setIsRegex(false); + } + return assertion; + } + + private XMLAssertion newXMLAssertion(DocumentElement item, ElementCondition elementCondition) { + XMLAssertion assertion = new XMLAssertion(); + assertion.setEnabled(true); + assertion.setName((StringUtils.isNotEmpty(assertionName) ? assertionName : "XMLAssertion") + "==" + (item.getJsonPath() + " " + getConditionStr(item, elementCondition))); + assertion.setProperty(TestElement.TEST_CLASS, XMLAssertion.class.getName()); + assertion.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("XMLAssertionGui")); + assertion.setProperty("XML_PATH", item.getJsonPath()); + assertion.setProperty("EXPECTED_VALUE", item.getExpectedOutcome() != null ? item.getExpectedOutcome().toString() : ""); + assertion.setProperty("ElementCondition", JSON.toJSONString(elementCondition)); + return assertion; + } +} diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/DocumentElement.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/DocumentElement.java new file mode 100644 index 0000000000..b1d710fa64 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/DocumentElement.java @@ -0,0 +1,101 @@ +package io.metersphere.api.dto.definition.request.assertions.document; + +import lombok.Data; +import org.apache.commons.lang3.StringUtils; + +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +@Data +public class DocumentElement { + private String id; + private String name; + private boolean include; + private String status; + private boolean typeVerification; + private String type; + private String groupId; + private int rowspan; + private boolean arrayVerification; + private String contentVerification; + private Object expectedOutcome; + + private List children; + + // 候补两个属性,在执行时组装数据用 + private String jsonPath; + List conditions; + + public DocumentElement() { + + } + + public DocumentElement(String name, String type, Object expectedOutcome, List children) { + this.id = UUID.randomUUID().toString(); + this.name = name; + this.expectedOutcome = expectedOutcome; + this.type = type; + this.children = children == null ? this.children = new LinkedList<>() : children; + this.rowspan = 1; + this.contentVerification = "value_eq"; + if (StringUtils.equalsAny(type, "object", "array")) { + this.contentVerification = "none"; + } else if (expectedOutcome == null || StringUtils.isEmpty(expectedOutcome.toString())) { + this.contentVerification = "none"; + } + } + + public DocumentElement(String id, String name, String type, Object expectedOutcome, List children) { + this.id = id; + this.name = name; + this.expectedOutcome = expectedOutcome; + this.type = type; + this.children = children == null ? this.children = new LinkedList<>() : children; + this.rowspan = 1; + this.contentVerification = "value_eq"; + if (StringUtils.equalsAny(type, "object", "array")) { + this.contentVerification = "none"; + } else if (expectedOutcome == null || StringUtils.isEmpty(expectedOutcome.toString())) { + this.contentVerification = "none"; + } + } + + public DocumentElement newRoot(String type, List children) { + return new DocumentElement("root", "root", type, "", children); + } + + public String getLabel(String value) { + String label = ""; + switch (value) { + case "value_eq": + label = "值-等于[value='%']"; + break; + case "value_not_eq": + label = "值-不等于[value!='%']"; + break; + case "value_in": + label = "值-包含[include='%']"; + break; + case "length_eq": + label = "长度-等于[length='%']"; + break; + case "length_not_eq": + label = "长度-不等于[length!='%']"; + break; + case "length_gt": + label = "长度-大于[length>'%']"; + break; + case "length_lt": + label = "长度-小于[length<'%']"; + break; + case "regular": + label = "正则匹配"; + break; + default: + label = "不校验[]"; + break; + } + return label; + } +} diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/ElementCondition.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/ElementCondition.java new file mode 100644 index 0000000000..389d1e2f78 --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/ElementCondition.java @@ -0,0 +1,24 @@ +package io.metersphere.api.dto.definition.request.assertions.document; + +import lombok.Data; + +import java.util.List; + +@Data +public class ElementCondition { + private boolean include; + private boolean typeVerification; + private boolean arrayVerification; + List conditions; + + public ElementCondition() { + + } + + public ElementCondition(boolean include, boolean typeVerification, boolean arrayVerification, List conditions) { + this.include = include; + this.typeVerification = typeVerification; + this.arrayVerification = arrayVerification; + this.conditions = conditions; + } +} diff --git a/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/MsAssertionDocument.java b/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/MsAssertionDocument.java new file mode 100644 index 0000000000..92b34ed4dd --- /dev/null +++ b/backend/src/main/java/io/metersphere/api/dto/definition/request/assertions/document/MsAssertionDocument.java @@ -0,0 +1,9 @@ +package io.metersphere.api.dto.definition.request.assertions.document; + +import lombok.Data; + +@Data +public class MsAssertionDocument { + private String type; + private Document data; +} diff --git a/backend/src/main/java/io/metersphere/api/service/ApiDefinitionService.java b/backend/src/main/java/io/metersphere/api/service/ApiDefinitionService.java index 75b96904f3..9009eec070 100644 --- a/backend/src/main/java/io/metersphere/api/service/ApiDefinitionService.java +++ b/backend/src/main/java/io/metersphere/api/service/ApiDefinitionService.java @@ -14,6 +14,7 @@ import io.metersphere.api.dto.definition.parse.ApiDefinitionImport; import io.metersphere.api.dto.definition.parse.ApiDefinitionImportParserFactory; import io.metersphere.api.dto.definition.parse.Swagger3Parser; import io.metersphere.api.dto.definition.request.ParameterConfig; +import io.metersphere.api.dto.definition.request.assertions.document.DocumentElement; import io.metersphere.api.dto.definition.request.sampler.MsHTTPSamplerProxy; import io.metersphere.api.dto.definition.request.sampler.MsTCPSampler; import io.metersphere.api.dto.mockconfig.MockConfigImportDTO; @@ -32,6 +33,8 @@ import io.metersphere.base.mapper.*; import io.metersphere.base.mapper.ext.*; import io.metersphere.commons.constants.*; import io.metersphere.commons.exception.MSException; +import io.metersphere.commons.json.JSONSchemaToDocumentUtils; +import io.metersphere.commons.json.JSONToDocumentUtils; import io.metersphere.commons.utils.*; import io.metersphere.controller.request.ResetOrderRequest; import io.metersphere.controller.request.ScheduleRequest; @@ -725,11 +728,11 @@ public class ApiDefinitionService { private void reSetImportMocksApiId(List mocks, String originId, String newId, int apiNum) { if (CollectionUtils.isNotEmpty(mocks)) { int index = 1; - for(MockConfigImportDTO item : mocks){ + for (MockConfigImportDTO item : mocks) { if (StringUtils.equals(item.getApiId(), originId)) { item.setApiId(newId); } - item.setExpectNum(apiNum+"_"+index); + item.setExpectNum(apiNum + "_" + index); index++; } } @@ -901,7 +904,7 @@ public class ApiDefinitionService { if (request.getConfig() != null && StringUtils.isNotBlank(request.getConfig().getResourcePoolId())) { jMeterService.runTest(request.getId(), request.getId(), runMode, null, request.getConfig()); } else { - jMeterService.runLocal(request.getId(),request.getConfig(), hashTree, request.getReportId(), runMode); + jMeterService.runLocal(request.getId(), request.getConfig(), hashTree, request.getReportId(), runMode); } return request.getId(); } @@ -1015,10 +1018,10 @@ public class ApiDefinitionService { apiImport = (ApiDefinitionImport) Objects.requireNonNull(apiImportParser).parse(file == null ? null : file.getInputStream(), request); } catch (Exception e) { LogUtil.error(e.getMessage(), e); - String returnThrowException = e.getMessage(); - if(StringUtils.contains(returnThrowException,"模块树最大深度为")){ + String returnThrowException = e.getMessage(); + if (StringUtils.contains(returnThrowException, "模块树最大深度为")) { MSException.throwException(returnThrowException); - }else { + } else { MSException.throwException(Translator.get("parse_data_error")); } // 发送通知 @@ -1207,8 +1210,8 @@ public class ApiDefinitionService { } public List selectApiDefinitionBydIds(List ids) { - if(CollectionUtils.isEmpty(ids)){ - return new ArrayList<>(); + if (CollectionUtils.isEmpty(ids)) { + return new ArrayList<>(); } ApiDefinitionExample example = new ApiDefinitionExample(); example.createCriteria().andIdIn(ids); @@ -1493,7 +1496,7 @@ public class ApiDefinitionService { } for (ApiDefinition api : apiList) { String path = api.getPath(); - if(StringUtils.isEmpty(path)){ + if (StringUtils.isEmpty(path)) { continue; } if (path.startsWith("/")) { @@ -1729,4 +1732,38 @@ public class ApiDefinitionService { List follows = apiDefinitionFollowMapper.selectByExample(example); return follows.stream().map(ApiDefinitionFollow::getFollowId).distinct().collect(Collectors.toList()); } + + public List getDocument(String id, String type) { + ApiDefinitionWithBLOBs bloBs = apiDefinitionMapper.selectByPrimaryKey(id); + List list = new LinkedList<>(); + if (bloBs != null && StringUtils.isNotEmpty(bloBs.getResponse())) { + JSONObject object = JSON.parseObject(bloBs.getResponse()); + JSONObject body = (JSONObject) object.get("body"); + if (body != null) { + if (StringUtils.equals(type, "JSON")) { + String jsonSchema = body.getString("jsonSchema"); + String dataType = body.getString("type"); + if (StringUtils.equalsAny(dataType, "JSON", "JSON-SCHEMA") && StringUtils.isNotEmpty(jsonSchema)) { + JSONObject obj = (JSONObject) body.get("jsonSchema"); + if (StringUtils.equals(obj.getString("type"), "array")) { + list.add(new DocumentElement().newRoot("array", JSONSchemaToDocumentUtils.getDocument(jsonSchema))); + } else { + list.add(new DocumentElement().newRoot("object", JSONSchemaToDocumentUtils.getDocument(jsonSchema))); + } + } else { + list.add(new DocumentElement().newRoot("object", null)); + } + } else { + String xml = body.getString("raw"); + String dataType = body.getString("type"); + if (StringUtils.equals(dataType, "XML") && StringUtils.isNotEmpty(xml)) { + list = JSONToDocumentUtils.getDocument(xml, type); + } else { + list.add(new DocumentElement().newRoot("root", null)); + } + } + } + } + return list; + } } diff --git a/backend/src/main/java/io/metersphere/commons/json/JSONSchemaToDocumentUtils.java b/backend/src/main/java/io/metersphere/commons/json/JSONSchemaToDocumentUtils.java new file mode 100644 index 0000000000..dc27a13e15 --- /dev/null +++ b/backend/src/main/java/io/metersphere/commons/json/JSONSchemaToDocumentUtils.java @@ -0,0 +1,213 @@ +package io.metersphere.commons.json; + +import com.google.gson.*; +import io.metersphere.api.dto.definition.request.assertions.document.DocumentElement; +import io.metersphere.jmeter.utils.ScriptEngineUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map.Entry; + +public class JSONSchemaToDocumentUtils { + + private static void analyzeRootSchemaElement(JsonObject rootElement, List roots) { + if (rootElement.has("type") || rootElement.has("allOf")) { + analyzeObject(rootElement, roots); + } + } + + private static void analyzeObject(JsonObject object, List roots) { + if (object.has("allOf")) { + JsonArray allOfArray = object.get("allOf").getAsJsonArray(); + for (JsonElement allOfElement : allOfArray) { + JsonObject allOfElementObj = allOfElement.getAsJsonObject(); + if (allOfElementObj.has("properties")) { + // Properties elements will become the attributes/references of the element + JsonObject propertiesObj = allOfElementObj.get("properties").getAsJsonObject(); + for (Entry entry : propertiesObj.entrySet()) { + String propertyKey = entry.getKey(); + JsonObject propertyObj = propertiesObj.get(propertyKey).getAsJsonObject(); + analyzeProperty(roots, propertyKey, propertyObj); + } + } + } + } else if (object.has("properties")) { + JsonObject propertiesObj = object.get("properties").getAsJsonObject(); + for (Entry entry : propertiesObj.entrySet()) { + String propertyKey = entry.getKey(); + JsonObject propertyObj = propertiesObj.get(propertyKey).getAsJsonObject(); + analyzeProperty(roots, propertyKey, propertyObj); + } + } else if (object.has("type") && object.get("type").getAsString().equals("array")) { + analyzeProperty(roots, "MS-OBJECT", object); + } else if (object.has("type") && !object.get("type").getAsString().equals("object")) { + analyzeProperty(roots, object.getAsString(), object); + } + } + + private static void analyzeProperty(List concept, + String propertyName, JsonObject object) { + if (object.has("type")) { + String propertyObjType = null; + if (object.get("type") instanceof JsonPrimitive) { + propertyObjType = object.get("type").getAsString(); + } else if (object.get("type") instanceof JsonArray) { + JsonArray typeArray = (JsonArray) object.get("type").getAsJsonArray(); + propertyObjType = typeArray.get(0).getAsString(); + } + if (object.has("default")) { + concept.add(new DocumentElement(propertyName, propertyObjType, object.get("default") != null ? object.get("default").toString() : "", null)); + } else if (object.has("enum")) { + try { + if (object.has("mock") && object.get("mock").getAsJsonObject() != null && StringUtils.isNotEmpty(object.get("mock").getAsJsonObject().get("mock").getAsString())) { + Object value = object.get("mock").getAsJsonObject().get("mock"); + concept.add(new DocumentElement(propertyName, propertyObjType, value.toString(), null)); + } else { + List list = analyzeEnumProperty(object); + if (list.size() > 0) { + int index = (int) (Math.random() * list.size()); + concept.add(new DocumentElement(propertyName, propertyObjType, list.get(index).toString(), null)); + } + } + } catch (Exception e) { + concept.add(new DocumentElement(propertyName, propertyObjType, "", null)); + } + } else if (propertyObjType.equals("string")) { + // 先设置空值 + String value = ""; + if (object.has("default")) { + value = object.get("default") != null ? object.get("default").toString() : ""; + } + if (object.has("mock") && object.get("mock").getAsJsonObject() != null && StringUtils.isNotEmpty(object.get("mock").getAsJsonObject().get("mock").getAsString()) && StringUtils.isNotEmpty(object.get("mock").getAsJsonObject().get("mock").getAsString())) { + value = ScriptEngineUtils.buildFunctionCallString(object.get("mock").getAsJsonObject().get("mock").getAsString()); + } + concept.add(new DocumentElement(propertyName, propertyObjType, value, null)); + } else if (propertyObjType.equals("integer")) { + Object value = null; + if (object.has("default")) { + value = object.get("default"); + } + if (object.has("mock") && object.get("mock").getAsJsonObject() != null && StringUtils.isNotEmpty(object.get("mock").getAsJsonObject().get("mock").getAsString())) { + try { + value = object.get("mock").getAsJsonObject().get("mock").getAsInt(); + } catch (Exception e) { + value = ScriptEngineUtils.buildFunctionCallString(object.get("mock").getAsJsonObject().get("mock").getAsString()); + } + } + concept.add(new DocumentElement(propertyName, propertyObjType, value, null)); + } else if (propertyObjType.equals("number")) { + Object value = null; + if (object.has("default")) { + value = object.get("default"); + } + if (object.has("mock") && object.get("mock").getAsJsonObject() != null && StringUtils.isNotEmpty(object.get("mock").getAsJsonObject().get("mock").getAsString())) { + try { + value = object.get("mock").getAsJsonObject().get("mock").getAsNumber(); + } catch (Exception e) { + value = ScriptEngineUtils.buildFunctionCallString(object.get("mock").getAsJsonObject().get("mock").getAsString()); + } + } + concept.add(new DocumentElement(propertyName, propertyObjType, value, null)); + } else if (propertyObjType.equals("boolean")) { + Object value = null; + if (object.has("default")) { + value = object.get("default"); + } + if (object.has("mock") && object.get("mock").getAsJsonObject() != null && StringUtils.isNotEmpty(object.get("mock").getAsJsonObject().get("mock").getAsString())) { + try { + value = ScriptEngineUtils.buildFunctionCallString(object.get("mock").getAsJsonObject().get("mock").toString()); + } catch (Exception e) { + } + } + concept.add(new DocumentElement(propertyName, propertyObjType, value, null)); + } else if (propertyObjType.equals("array")) { + analyzeArray(propertyObjType, propertyName, object); + } else if (propertyObjType.equals("object")) { + List list = new LinkedList<>(); + concept.add(new DocumentElement(propertyName, propertyObjType, "", list)); + analyzeObject(object, list); + } + } + } + + private static void analyzeArray(String propertyObjType, String propertyName, JsonObject object) { + // 先设置空值 + List array = new LinkedList<>(); + JsonArray jsonArray = new JsonArray(); + if (object.has("items") && object.get("items").isJsonArray()) { + jsonArray = object.get("items").getAsJsonArray(); + } else { + JsonObject itemsObject = object.get("items").getAsJsonObject(); + array.add(new DocumentElement(propertyName, propertyObjType, itemsObject, null)); + } + for (int i = 0; i < jsonArray.size(); i++) { + JsonObject itemsObject = jsonArray.get(i).getAsJsonObject(); + if (object.has("items")) { + Object value = null; + if (itemsObject.has("mock") && itemsObject.get("mock").getAsJsonObject() != null && StringUtils.isNotEmpty(itemsObject.get("mock").getAsJsonObject().get("mock").getAsString())) { + try { + value = itemsObject.get("mock").getAsJsonObject().get("mock").getAsInt(); + } catch (Exception e) { + value = ScriptEngineUtils.buildFunctionCallString(itemsObject.get("mock").getAsJsonObject().get("mock").getAsString()); + } + } else if (itemsObject.has("type") && itemsObject.get("type").getAsString().equals("string")) { + if (itemsObject.has("default")) { + value = itemsObject.get("default"); + } else if (itemsObject.has("mock") && itemsObject.get("mock").getAsJsonObject() != null && StringUtils.isNotEmpty(itemsObject.get("mock").getAsJsonObject().get("mock").getAsString())) { + value = ScriptEngineUtils.buildFunctionCallString(itemsObject.get("mock").getAsJsonObject().get("mock").getAsString()); + } + } else if (itemsObject.has("type") && itemsObject.get("type").getAsString().equals("number")) { + if (itemsObject.has("default")) { + value = itemsObject.get("default"); + } else if (itemsObject.has("mock") && itemsObject.get("mock").getAsJsonObject() != null && StringUtils.isNotEmpty(itemsObject.get("mock").getAsJsonObject().get("mock").getAsString())) { + value = ScriptEngineUtils.buildFunctionCallString(itemsObject.get("mock").getAsJsonObject().get("mock").getAsString()); + } + } else if (itemsObject.has("properties")) { + List propertyConcept = new LinkedList<>(); + JsonObject propertiesObj = itemsObject.get("properties").getAsJsonObject(); + for (Entry entry : propertiesObj.entrySet()) { + String propertyKey = entry.getKey(); + JsonObject propertyObj = propertiesObj.get(propertyKey).getAsJsonObject(); + analyzeProperty(propertyConcept, propertyKey, propertyObj); + } + } + array.add(new DocumentElement(propertyName, itemsObject.get("type").getAsString(), value, null)); + } else if (object.has("items") && object.get("items").isJsonArray()) { + JsonArray itemsObjectArray = object.get("items").getAsJsonArray(); + array.add(new DocumentElement(propertyName, itemsObject.get("type").getAsString(), itemsObjectArray, null)); + } + } + } + + private static List analyzeEnumProperty(JsonObject object) { + List list = new LinkedList<>(); + String jsonStr = null; + try { + JsonArray enumValues = object.get("enum").getAsJsonArray(); + for (JsonElement enumValueElem : enumValues) { + String enumValue = enumValueElem.getAsString(); + list.add(enumValue); + } + } catch (Exception e) { + jsonStr = object.get("enum").getAsString(); + } + if (jsonStr != null && list.isEmpty()) { + String[] arrays = jsonStr.split("\n"); + for (String str : arrays) { + list.add(str); + } + } + return list; + } + + public static List getDocument(String jsonSchema) { + Gson gson = new Gson(); + JsonElement element = gson.fromJson(jsonSchema, JsonElement.class); + JsonObject rootElement = element.getAsJsonObject(); + List roots = new LinkedList<>(); + analyzeRootSchemaElement(rootElement, roots); + return roots; + } + +} diff --git a/backend/src/main/java/io/metersphere/commons/json/JSONToDocumentUtils.java b/backend/src/main/java/io/metersphere/commons/json/JSONToDocumentUtils.java new file mode 100644 index 0000000000..5993535a14 --- /dev/null +++ b/backend/src/main/java/io/metersphere/commons/json/JSONToDocumentUtils.java @@ -0,0 +1,110 @@ +package io.metersphere.commons.json; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import io.metersphere.api.dto.definition.request.assertions.document.DocumentElement; +import io.metersphere.commons.utils.LogUtil; +import org.apache.commons.lang3.StringUtils; +import org.dom4j.Document; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.LinkedList; +import java.util.List; + +public class JSONToDocumentUtils { + + public static void jsonDataFormatting(JSONArray array, List children) { + for (int i = 0; i < array.size(); i++) { + JSONObject element = array.getJSONObject(i); + jsonDataFormatting(element, children); + } + } + + public static void jsonDataFormatting(JSONObject object, List children) { + for (String key : object.keySet()) { + Object value = object.get(key); + if (value instanceof JSONObject) { + List childrenElements = new LinkedList<>(); + children.add(new DocumentElement(key, "object", "", childrenElements)); + jsonDataFormatting((JSONObject) value, childrenElements); + } else if (value instanceof JSONArray) { + List childrenElements = new LinkedList<>(); + children.add(new DocumentElement(key, "array", "", childrenElements)); + jsonDataFormatting((JSONArray) value, childrenElements); + } else { + children.add(new DocumentElement(key, "string", value, null)); + } + } + } + + + public static List getDocument(String json, String type) { + try { + if (StringUtils.equals(type, "JSON")) { + List roots = new LinkedList<>(); + List children = new LinkedList<>(); + roots.add(new DocumentElement("root", "root", "object", "", children)); + JSONObject object = JSON.parseObject(json); + jsonDataFormatting(object, children); + return roots; + } else { + return getXmlDocument(json); + } + } catch (Exception e) { + LogUtil.error(e); + return new LinkedList<>(); + } + } + + + /** + * 从指定节点开始,递归遍历所有子节点 + */ + public static void getNodes(Element node, List children) { + //递归遍历当前节点所有的子节点 + List listElement = node.elements(); + if (listElement.isEmpty()) { + children.add(new DocumentElement(node.getName(), "string", node.getTextTrim(), null)); + } + for (Element element : listElement) {//遍历所有一级子节点 + List elementNodes = element.elements(); + if (elementNodes.size() > 0) { + List elements = new LinkedList<>(); + children.add(new DocumentElement(element.getName(), "object", element.getTextTrim(), elements)); + getNodes(element, elements);//递归 + } else { + getNodes(element, children);//递归 + } + } + } + + public static List getXmlDocument(String xml) { + List roots = new LinkedList<>(); + try { + InputStream is = new ByteArrayInputStream(xml.getBytes("UTF-8")); + // 创建saxReader对象 + SAXReader reader = new SAXReader(); + // 通过read方法读取一个文件 转换成Document对象 + Document document = reader.read(is); + //获取根节点元素对象 + getNodes(document.getRootElement(), roots); + // 未能处理root补偿root 节点 + if (roots.size() > 1) { + Element node = document.getRootElement(); + List newRoots = new LinkedList<>(); + newRoots.add(new DocumentElement("root", node.getName(), "object", node.getTextTrim(), roots)); + return newRoots; + } else if (roots.size() == 1) { + roots.get(0).setId("root"); + } + return roots; + } catch (Exception ex) { + LogUtil.error(ex); + return roots; + } + } +} diff --git a/backend/src/main/java/org/apache/jmeter/assertions/JSONPathAssertion.java b/backend/src/main/java/org/apache/jmeter/assertions/JSONPathAssertion.java index ef459e7711..ad2c0daec8 100644 --- a/backend/src/main/java/org/apache/jmeter/assertions/JSONPathAssertion.java +++ b/backend/src/main/java/org/apache/jmeter/assertions/JSONPathAssertion.java @@ -5,10 +5,14 @@ package org.apache.jmeter.assertions; +import com.alibaba.fastjson.JSON; import com.google.gson.Gson; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.Predicate; +import io.metersphere.api.dto.definition.request.assertions.document.Condition; +import io.metersphere.api.dto.definition.request.assertions.document.ElementCondition; import net.minidev.json.JSONArray; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.jmeter.samplers.SampleResult; import org.apache.jmeter.testelement.AbstractTestElement; @@ -47,6 +51,10 @@ public class JSONPathAssertion extends AbstractTestElement implements Serializab return getPropertyAsString("ASS_OPTION"); } + public String getElementCondition() { + return getPropertyAsString("ElementCondition"); + } + public String getJsonPath() { return this.getPropertyAsString("JSON_PATH"); } @@ -132,6 +140,9 @@ public class JSONPathAssertion extends AbstractTestElement implements Serializab case "LT": msg = "Value < '%s', but found '%s'"; break; + case "DOCUMENT": + msg = (StringUtils.isNotEmpty(this.getName()) ? this.getName().split("==")[1] : "") + "校验失败,返回数据:" + (value != null ? value.toString() : ""); + break; } } else { msg = "Value expected to be '%s', but found '%s'"; @@ -202,13 +213,70 @@ public class JSONPathAssertion extends AbstractTestElement implements Serializab case "LT": refFlag = isLt(str, getExpectedValue()); break; - + case "DOCUMENT": + refFlag = isDocument(str); + break; } return refFlag; } return str.equals(this.getExpectedValue()); } } + private String ifValue(Object value) { + if (value != null) { + return value.toString(); + } + return ""; + } + + private boolean isDocument(String resValue) { + String condition = this.getElementCondition(); + if (StringUtils.isNotEmpty(condition)) { + ElementCondition elementCondition = JSON.parseObject(condition, ElementCondition.class); + boolean isTrue = true; + if (CollectionUtils.isNotEmpty(elementCondition.getConditions())) { + for (Condition item : elementCondition.getConditions()) { + String expectedValue = ifValue(item.getValue()); + switch (item.getKey()) { + case "value_eq": + isTrue = StringUtils.equals(resValue, expectedValue); + break; + case "value_not_eq": + isTrue = !StringUtils.equals(resValue, expectedValue); + break; + case "value_in": + isTrue = StringUtils.contains(resValue, expectedValue); + break; + case "length_eq": + isTrue = (StringUtils.isNotEmpty(resValue) && StringUtils.isNotEmpty(expectedValue) && resValue.length() == expectedValue.length()) + || (StringUtils.isEmpty(resValue) && StringUtils.isEmpty(expectedValue)); + break; + case "length_not_eq": + isTrue = (StringUtils.isNotEmpty(resValue) && StringUtils.isNotEmpty(expectedValue) && resValue.length() != expectedValue.length()) + || (StringUtils.isEmpty(resValue) || StringUtils.isEmpty(expectedValue)); + break; + case "length_gt": + isTrue = (StringUtils.isNotEmpty(resValue) && StringUtils.isNotEmpty(expectedValue) && resValue.length() > expectedValue.length()) + || (StringUtils.isNotEmpty(resValue) && StringUtils.isEmpty(expectedValue)); + break; + case "length_lt": + isTrue = (StringUtils.isNotEmpty(resValue) && StringUtils.isNotEmpty(expectedValue) && resValue.length() < expectedValue.length()) + || (StringUtils.isEmpty(resValue) || StringUtils.isEmpty(expectedValue)); + break; + case "regular": + Pattern pattern = JMeterUtils.getPatternCache().getPattern(this.getExpectedValue()); + isTrue = JMeterUtils.getMatcher().matches(resValue, pattern); + break; + } + if (!isTrue) { + break; + } + } + } + return isTrue; + } + return true; + } public AssertionResult getResult(SampleResult samplerResult) { AssertionResult result = new AssertionResult(this.getName()); diff --git a/backend/src/main/java/org/apache/jmeter/assertions/XMLAssertion.java b/backend/src/main/java/org/apache/jmeter/assertions/XMLAssertion.java new file mode 100644 index 0000000000..736871342d --- /dev/null +++ b/backend/src/main/java/org/apache/jmeter/assertions/XMLAssertion.java @@ -0,0 +1,254 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jmeter.assertions; + +import com.alibaba.fastjson.JSON; +import com.google.gson.Gson; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.Predicate; +import io.metersphere.api.dto.definition.request.assertions.document.Condition; +import io.metersphere.api.dto.definition.request.assertions.document.ElementCondition; +import net.minidev.json.JSONArray; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jmeter.testelement.AbstractTestElement; +import org.apache.jmeter.testelement.ThreadListener; +import org.apache.jmeter.util.JMeterUtils; +import org.apache.oro.text.regex.Pattern; +import org.json.JSONObject; +import org.json.XML; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; +import java.io.IOException; +import java.io.Serializable; +import java.io.StringReader; +import java.text.DecimalFormat; +import java.util.Map; + +/** + * Checks if the result is a well-formed XML content using {@link XMLReader} + */ +public class XMLAssertion extends AbstractTestElement implements Serializable, Assertion, ThreadListener { + private static final Logger log = LoggerFactory.getLogger(XMLAssertion.class); + private static ThreadLocal decimalFormatter = ThreadLocal.withInitial(XMLAssertion::createDecimalFormat); + + private static final long serialVersionUID = 242L; + + public String getXmlPath() { + return this.getPropertyAsString("XML_PATH"); + } + + public String getExpectedValue() { + return this.getPropertyAsString("EXPECTED_VALUE"); + } + + public String getCondition() { + return getPropertyAsString("ElementCondition"); + } + + private static DecimalFormat createDecimalFormat() { + DecimalFormat decimalFormatter = new DecimalFormat("#.#"); + decimalFormatter.setMaximumFractionDigits(340); + decimalFormatter.setMinimumFractionDigits(1); + return decimalFormatter; + } + + // one builder for all requests in a thread + private static final ThreadLocal XML_READER = new ThreadLocal() { + @Override + protected XMLReader initialValue() { + try { + XMLReader reader = SAXParserFactory.newInstance() + .newSAXParser() + .getXMLReader(); + reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + return reader; + } catch (SAXException | ParserConfigurationException e) { + log.error("Error initializing XMLReader in XMLAssertion", e); + return null; + } + } + }; + + /** + * Returns the result of the Assertion. + * Here it checks whether the Sample data is XML. + * If so an AssertionResult containing a FailureMessage will be returned. + * Otherwise the returned AssertionResult will reflect the success of the Sample. + */ + @Override + public AssertionResult getResult(SampleResult response) { + // no error as default + AssertionResult result = new AssertionResult(getName()); + String resultData = response.getResponseDataAsString(); + if (resultData.length() == 0) { + return result.setResultForNull(); + } + result.setFailure(false); + XMLReader builder = XML_READER.get(); + if (builder != null) { + try { + builder.setErrorHandler(new LogErrorHandler()); + builder.parse(new InputSource(new StringReader(resultData))); + try { + JSONObject xmlJSONObj = XML.toJSONObject(resultData); + String jsonPrettyPrintString = xmlJSONObj.toString(4); + doAssert(jsonPrettyPrintString); + } catch (Exception e) { + result.setError(true); + result.setFailure(true); + result.setFailureMessage(e.getMessage()); + } + } catch (SAXException | IOException e) { + result.setError(true); + result.setFailure(true); + result.setFailureMessage(e.getMessage()); + } + } else { + result.setError(true); + result.setFailureMessage("Cannot initialize XMLReader in element:" + getName() + ", check jmeter.log file"); + } + return result; + } + + + private void doAssert(String jsonString) { + Object value = JsonPath.read(jsonString, this.getXmlPath(), new Predicate[0]); + if (value instanceof JSONArray) { + if (this.arrayMatched((JSONArray) value)) { + return; + } + } + if (!this.isEquals(value)) { + String msg = (StringUtils.isNotEmpty(this.getName()) ? this.getName().split("==")[1] : "") + "校验失败,返回数据:" + (value != null ? value.toString() : ""); + throw new IllegalStateException(String.format(msg, this.getExpectedValue(), objectToString(value))); + } + } + + + private boolean isEquals(Object subj) { + String str = objectToString(subj); + return isDocument(str); + } + + private boolean arrayMatched(JSONArray value) { + if (value.isEmpty() && "[]".equals(this.getExpectedValue())) { + return true; + } else { + Object[] var2 = value.toArray(); + int var3 = var2.length; + + for (int var4 = 0; var4 < var3; ++var4) { + Object subj = var2[var4]; + if (subj == null || this.isEquals(subj)) { + return true; + } + } + + return this.isEquals(value); + } + } + + private String ifValue(Object value) { + if (value != null) { + return value.toString(); + } + return ""; + } + + private boolean isDocument(String resValue) { + String condition = this.getCondition(); + if (StringUtils.isNotEmpty(condition)) { + ElementCondition elementCondition = JSON.parseObject(condition, ElementCondition.class); + boolean isTrue = true; + if (CollectionUtils.isNotEmpty(elementCondition.getConditions())) { + for (Condition item : elementCondition.getConditions()) { + String expectedValue = ifValue(item.getValue()); + switch (item.getKey()) { + case "value_eq": + isTrue = StringUtils.equals(resValue, expectedValue); + break; + case "value_not_eq": + isTrue = !StringUtils.equals(resValue, expectedValue); + break; + case "value_in": + isTrue = StringUtils.contains(resValue, expectedValue); + break; + case "length_eq": + isTrue = (StringUtils.isNotEmpty(resValue) && StringUtils.isNotEmpty(expectedValue) && resValue.length() == expectedValue.length()) + || (StringUtils.isEmpty(resValue) && StringUtils.isEmpty(expectedValue)); + break; + case "length_not_eq": + isTrue = (StringUtils.isNotEmpty(resValue) && StringUtils.isNotEmpty(expectedValue) && resValue.length() != expectedValue.length()) + || (StringUtils.isEmpty(resValue) || StringUtils.isEmpty(expectedValue)); + break; + case "length_gt": + isTrue = (StringUtils.isNotEmpty(resValue) && StringUtils.isNotEmpty(expectedValue) && resValue.length() > expectedValue.length()) + || (StringUtils.isNotEmpty(resValue) && StringUtils.isEmpty(expectedValue)); + break; + case "length_lt": + isTrue = (StringUtils.isNotEmpty(resValue) && StringUtils.isNotEmpty(expectedValue) && resValue.length() < expectedValue.length()) + || (StringUtils.isEmpty(resValue) || StringUtils.isEmpty(expectedValue)); + break; + case "regular": + Pattern pattern = JMeterUtils.getPatternCache().getPattern(this.getExpectedValue()); + isTrue = JMeterUtils.getMatcher().matches(resValue, pattern); + break; + } + if (!isTrue) { + break; + } + } + } + return isTrue; + } + return true; + } + + public static String objectToString(Object subj) { + String str; + if (subj == null) { + str = "null"; + } else if (subj instanceof Map) { + str = new Gson().toJson(subj); + } else if (!(subj instanceof Double) && !(subj instanceof Float)) { + str = subj.toString(); + } else { + str = ((DecimalFormat) decimalFormatter.get()).format(subj); + } + + return str; + } + + @Override + public void threadStarted() { + // nothing to do on thread start + } + + @Override + public void threadFinished() { + XML_READER.remove(); + } +} diff --git a/frontend/src/business/components/api/definition/components/assertion/ApiAssertions.vue b/frontend/src/business/components/api/definition/components/assertion/ApiAssertions.vue index 2d6111c3fc..8a2fc2e7a2 100644 --- a/frontend/src/business/components/api/definition/components/assertion/ApiAssertions.vue +++ b/frontend/src/business/components/api/definition/components/assertion/ApiAssertions.vue @@ -15,31 +15,61 @@
- + + - - - - - - - + + + + + + + {{ $t('api_test.request.assertions.add') }} @@ -47,12 +77,23 @@
- + - + - + @@ -71,6 +112,7 @@ import {getUUID} from "@/common/js/utils"; import ApiJsonPathSuggestButton from "./ApiJsonPathSuggestButton"; import MsApiJsonpathSuggest from "./ApiJsonpathSuggest"; import ApiBaseComponent from "../../../automation/scenario/common/ApiBaseComponent"; +import MsApiAssertionDocument from "./document/DocumentHeader"; export default { name: "MsApiAssertions", @@ -82,7 +124,11 @@ export default { MsApiAssertionJsr223, MsApiJsonpathSuggestList, MsApiAssertionJsonPath, - MsApiAssertionsEdit, MsApiAssertionDuration, MsApiAssertionRegex, MsApiAssertionText + MsApiAssertionsEdit, + MsApiAssertionDuration, + MsApiAssertionRegex, + MsApiAssertionText, + MsApiAssertionDocument, }, props: { draggable: { @@ -100,6 +146,7 @@ export default { assertions: {}, node: {}, request: {}, + apiId: String, response: {}, customizeStyle: { type: String, diff --git a/frontend/src/business/components/api/definition/components/assertion/ApiAssertionsEdit.vue b/frontend/src/business/components/api/definition/components/assertion/ApiAssertionsEdit.vue index bb17c23403..385003c1fd 100644 --- a/frontend/src/business/components/api/definition/components/assertion/ApiAssertionsEdit.vue +++ b/frontend/src/business/components/api/definition/components/assertion/ApiAssertionsEdit.vue @@ -1,135 +1,156 @@ diff --git a/frontend/src/business/components/api/definition/components/assertion/document/DocumentBody.vue b/frontend/src/business/components/api/definition/components/assertion/document/DocumentBody.vue new file mode 100644 index 0000000000..d25a6872f6 --- /dev/null +++ b/frontend/src/business/components/api/definition/components/assertion/document/DocumentBody.vue @@ -0,0 +1,461 @@ + + + + + diff --git a/frontend/src/business/components/api/definition/components/assertion/document/DocumentHeader.vue b/frontend/src/business/components/api/definition/components/assertion/document/DocumentHeader.vue new file mode 100644 index 0000000000..631f7e96bb --- /dev/null +++ b/frontend/src/business/components/api/definition/components/assertion/document/DocumentHeader.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/frontend/src/business/components/api/definition/components/assertion/document/DocumentImport.vue b/frontend/src/business/components/api/definition/components/assertion/document/DocumentImport.vue new file mode 100644 index 0000000000..2209b992b2 --- /dev/null +++ b/frontend/src/business/components/api/definition/components/assertion/document/DocumentImport.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/frontend/src/business/components/api/definition/components/body/ApiBody.vue b/frontend/src/business/components/api/definition/components/body/ApiBody.vue index cfafc13fcb..209b4fa9b4 100644 --- a/frontend/src/business/components/api/definition/components/body/ApiBody.vue +++ b/frontend/src/business/components/api/definition/components/body/ApiBody.vue @@ -137,6 +137,8 @@ export default { let data = MsConvert.format(JSON.parse(this.body.raw)); if (this.body.jsonSchema) { this.body.jsonSchema = this.deepAssign(this.body.jsonSchema, data); + }else{ + this.body.jsonSchema = data; } } catch (ex) { this.body.jsonSchema = ""; diff --git a/frontend/src/business/components/api/definition/components/runtest/RunTestDubboPage.vue b/frontend/src/business/components/api/definition/components/runtest/RunTestDubboPage.vue index 6ebdd9d9c7..f4ee9b1f1e 100644 --- a/frontend/src/business/components/api/definition/components/runtest/RunTestDubboPage.vue +++ b/frontend/src/business/components/api/definition/components/runtest/RunTestDubboPage.vue @@ -29,7 +29,7 @@ - + diff --git a/frontend/src/business/components/api/definition/components/runtest/RunTestHTTPPage.vue b/frontend/src/business/components/api/definition/components/runtest/RunTestHTTPPage.vue index 53074119b7..8cf0c95ff9 100644 --- a/frontend/src/business/components/api/definition/components/runtest/RunTestHTTPPage.vue +++ b/frontend/src/business/components/api/definition/components/runtest/RunTestHTTPPage.vue @@ -55,7 +55,7 @@ - + diff --git a/frontend/src/business/components/api/definition/components/runtest/RunTestSQLPage.vue b/frontend/src/business/components/api/definition/components/runtest/RunTestSQLPage.vue index a36ecffcd5..80c5b014a9 100644 --- a/frontend/src/business/components/api/definition/components/runtest/RunTestSQLPage.vue +++ b/frontend/src/business/components/api/definition/components/runtest/RunTestSQLPage.vue @@ -27,7 +27,7 @@ - + diff --git a/frontend/src/business/components/api/definition/components/runtest/RunTestTCPPage.vue b/frontend/src/business/components/api/definition/components/runtest/RunTestTCPPage.vue index b92b06881b..be5a4a1efa 100644 --- a/frontend/src/business/components/api/definition/components/runtest/RunTestTCPPage.vue +++ b/frontend/src/business/components/api/definition/components/runtest/RunTestTCPPage.vue @@ -47,7 +47,7 @@ - +

{{$t('api_test.definition.request.res_param')}}

diff --git a/frontend/src/business/components/api/definition/components/step/JmxStep.vue b/frontend/src/business/components/api/definition/components/step/JmxStep.vue index 2f6f0d3a04..ae528cfe69 100644 --- a/frontend/src/business/components/api/definition/components/step/JmxStep.vue +++ b/frontend/src/business/components/api/definition/components/step/JmxStep.vue @@ -55,6 +55,7 @@ @copyRow="copyRow" @remove="remove" :response="response" + :apiId="apiId" :is-read-only="isReadOnly" :assertions="row"/>
@@ -97,6 +98,7 @@ export default { props: { request: {}, response: {}, + apiId: String, showScript: { type: Boolean, default: true, diff --git a/frontend/src/business/components/api/definition/model/ApiTestModel.js b/frontend/src/business/components/api/definition/model/ApiTestModel.js index 9b646d177f..fc9abf4155 100644 --- a/frontend/src/business/components/api/definition/model/ApiTestModel.js +++ b/frontend/src/business/components/api/definition/model/ApiTestModel.js @@ -103,6 +103,7 @@ export const ASSERTION_TYPE = { DURATION: "Duration", JSR223: "JSR223", XPATH2: "XPath2", + DOCUMENT: "Document", } export const ASSERTION_REGEX_SUBJECT = { @@ -126,8 +127,7 @@ export class BaseConfig { if (!(this[name] instanceof Array)) { if (notUndefined === true) { this[name] = options[name] === undefined ? this[name] : options[name]; - } - else { + } else { this[name] = options[name]; } } @@ -782,6 +782,7 @@ export class Assertions extends BaseConfig { this.xpath2 = []; this.duration = undefined; this.enable = true; + this.document = {type: "JSON", data: {xmlFollowAPI: false, jsonFollowAPI: false, json: [], xml: []}}; this.set(options); this.sets({text: Text, regex: Regex, jsonPath: JSONPath, jsr223: AssertionJSR223, xpath2: XPath2}, options); } @@ -846,7 +847,7 @@ export class JSR223Processor extends BaseConfig { this.resourceId = uuid(); this.active = false; this.type = "JSR223Processor"; - this.label=""; + this.label = ""; this.script = undefined; this.scriptLanguage = "beanshell"; this.enable = true; @@ -1115,7 +1116,7 @@ export class TransactionController extends Controller { label() { if (this.isValid()) { let label = this.$t('api_test.automation.transcation_controller'); - if(this.name != null && this.name !== ""){ + if (this.name != null && this.name !== "") { label = this.name; } return label; diff --git a/frontend/src/i18n/en-US.js b/frontend/src/i18n/en-US.js index d7b9d29211..9231d35f8b 100644 --- a/frontend/src/i18n/en-US.js +++ b/frontend/src/i18n/en-US.js @@ -955,6 +955,7 @@ export default { add_data: "Add Data" }, request: { + document_structure: "Document structure verification", grade_info: "Filter by rank", grade_order_asc: "from low to high by use case level", grade_order_desc: "from high to low by use case level", diff --git a/frontend/src/i18n/zh-CN.js b/frontend/src/i18n/zh-CN.js index ac245a95b9..5f534d78a5 100644 --- a/frontend/src/i18n/zh-CN.js +++ b/frontend/src/i18n/zh-CN.js @@ -963,6 +963,7 @@ export default { add_data: "去添加" }, request: { + document_structure: "文档结构校验", grade_info: "按等级筛选", grade_order_asc: "按用例等级从低到高", grade_order_desc: "按用例等级从高到低", diff --git a/frontend/src/i18n/zh-TW.js b/frontend/src/i18n/zh-TW.js index e9ba25e14d..173946e844 100644 --- a/frontend/src/i18n/zh-TW.js +++ b/frontend/src/i18n/zh-TW.js @@ -961,6 +961,7 @@ export default { add_data: "去添加" }, request: { + document_structure: "文檔結構校驗", grade_info: "按等級篩選", grade_order_asc: "按用例等級從低到高", grade_order_desc: "按用例等級從高到低",